Skip to content

Example solution for Advanced Filtering Challenge #30

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/advanced-filtering/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,6 @@ npm run build

## 🌟 Example solution

As this is an advanced challenge, there is no single correct solution. However, you can find an example solution in the `advanced-filtering-solution` branch.
As this is an advanced challenge, there is no single correct solution. However, you can find an example solution in the [Pull Request: Example solution for Advanced Filtering Challenge #30](https://github.com/przeprogramowani/typescript-challenges/pull/30).

Before checking the solution, try to solve the challenge on your own! Use sorting-related code as a reference and try to apply similar typing techniques to filtering-related code.
72 changes: 36 additions & 36 deletions src/advanced-filtering/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,50 +1,50 @@
import {useMemo, useState} from "react";
import {LogFilter} from "./components/LogFilter";
import {LogLevelBadge} from "./components/LogLevelBadge";
import {LogSortingHeader} from "./components/LogSortingHeader";
import { useMemo, useState } from 'react';
import { LogFilter } from './components/LogFilter';
import { LogLevelBadge } from './components/LogLevelBadge';
import { LogSortingHeader } from './components/LogSortingHeader';
import {
LogEntryFields,
LogLevelMap,
LogLevelOrder,
LogSourceMap,
} from "./constants";
} from './constants';
import {
advancedFilter,
advancedSort,
NO_FILTER,
simpleLogFilter,
} from "./services/logProcessing";
} from './services/logProcessing';
import {
FilterCriteria,
LogEntry,
LogLevel,
SimpleLogFilterCriteria,
SortCriteria,
UpdateLogFilterFunction,
} from "./types";
} from './types';

interface LogDashboardProps {
logs: LogEntry[];
}

function LogDashboard({logs}: LogDashboardProps) {
function LogDashboard({ logs }: LogDashboardProps) {
const [sortCriteria, setSortCriteria] = useState<SortCriteria<LogEntry>>({
timestamp: {direction: "desc"},
timestamp: { direction: 'desc' },
});
const [filterCriteria, setFilterCriteria] = useState<SimpleLogFilterCriteria>(
{}
);
const [filterCriteria, setFilterCriteria] = useState<
FilterCriteria<LogEntry>
>({});

const compareLogLevels = (a: LogLevel, b: LogLevel) =>
LogLevelOrder[a] - LogLevelOrder[b];

const toggleSort = (key: keyof LogEntry) => {
setSortCriteria((prev) => {
const direction = prev[key]?.direction === "asc" ? "desc" : "asc";
const baseCriteria = {direction};
const direction = prev[key]?.direction === 'asc' ? 'desc' : 'asc';
const baseCriteria = { direction };

return {
[key]:
key === "level"
? {...baseCriteria, customCompare: compareLogLevels}
key === 'level'
? { ...baseCriteria, customCompare: compareLogLevels }
: baseCriteria,
};
});
Expand All @@ -53,36 +53,36 @@ function LogDashboard({logs}: LogDashboardProps) {
const updateFilter: UpdateLogFilterFunction = (key, value) => {
setFilterCriteria((prev) => ({
...prev,
[key]: value === "" ? NO_FILTER : value,
[key]: value === '' ? NO_FILTER : value,
}));
};

const filteredAndSortedLogs = useMemo(() => {
const filteredLogs = simpleLogFilter(logs, filterCriteria);
const filteredLogs = advancedFilter(logs, filterCriteria);
return advancedSort(filteredLogs, sortCriteria);
}, [logs, filterCriteria, sortCriteria]);

return (
<div className='container p-6 mx-auto'>
<div className='flex mb-4 space-x-4'>
<div className="container p-6 mx-auto">
<div className="flex mb-4 space-x-4">
<LogFilter
updateFilter={updateFilter}
filterKey='level'
filterKey="level"
options={Object.values(LogLevelMap)}
allLabel='All levels'
allLabel="All levels"
/>
<LogFilter
updateFilter={updateFilter}
filterKey='source'
filterKey="source"
options={Object.values(LogSourceMap)}
allLabel='All sources'
allLabel="All sources"
/>
</div>
<div className='overflow-x-auto shadow-md sm:rounded-lg'>
<table className='w-full mx-auto text-sm text-left text-gray-500'>
<thead className='text-xs text-gray-700 uppercase bg-gray-50'>
<div className="overflow-x-auto shadow-md sm:rounded-lg">
<table className="w-full mx-auto text-sm text-left text-gray-500">
<thead className="text-xs text-gray-700 uppercase bg-gray-50">
<tr>
{LogEntryFields.map(({key, label, className}) => (
{LogEntryFields.map(({ key, label, className }) => (
<LogSortingHeader
key={key}
field={key}
Expand All @@ -96,16 +96,16 @@ function LogDashboard({logs}: LogDashboardProps) {
</thead>
<tbody>
{filteredAndSortedLogs.map((log, index) => (
<tr key={index} className='bg-white border-b hover:bg-gray-50'>
<td className='px-6 py-4'>
<tr key={index} className="bg-white border-b hover:bg-gray-50">
<td className="px-6 py-4">
{log.timestamp.toISOString().slice(0, 19)}
</td>
<td className='px-6 py-4'>
<td className="px-6 py-4">
<LogLevelBadge level={log.level} />
</td>
<td className='px-6 py-4'>{log.source}</td>
<td className='px-6 py-4'>{log.message}</td>
<td className='px-6 py-4'>{log.userId}</td>
<td className="px-6 py-4">{log.source}</td>
<td className="px-6 py-4">{log.message}</td>
<td className="px-6 py-4">{log.userId}</td>
</tr>
))}
</tbody>
Expand Down
28 changes: 14 additions & 14 deletions src/advanced-filtering/src/components/LogFilter.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,25 @@
import {
SimpleFilterableLogValue,
SimpleLogFilterKey,
FilterableLogValue,
LogFilterKey,
UpdateLogFilterFunction,
} from "../types";
} from '../types';

const SelectArrow = () => (
<div className='absolute inset-y-0 right-0 flex items-center px-2 text-gray-700 pointer-events-none'>
<div className="absolute inset-y-0 right-0 flex items-center px-2 text-gray-700 pointer-events-none">
<svg
className='w-4 h-4 fill-current'
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 20 20'
className="w-4 h-4 fill-current"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
>
<path d='M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z' />
<path d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" />
</svg>
</div>
);

interface LogFilterProps {
updateFilter: UpdateLogFilterFunction;
filterKey: SimpleLogFilterKey;
options: readonly SimpleFilterableLogValue[];
filterKey: LogFilterKey;
options: readonly FilterableLogValue[];
allLabel: string;
}

Expand All @@ -29,14 +29,14 @@ export const LogFilter = ({
options,
allLabel,
}: LogFilterProps) => (
<div className='relative inline-block w-full'>
<div className="relative inline-block w-full">
<select
onChange={(e) =>
updateFilter(filterKey, e.target.value as SimpleFilterableLogValue | "")
updateFilter(filterKey, e.target.value as FilterableLogValue | '')
}
className='w-full px-4 py-2 pr-10 text-gray-700 bg-white border rounded-lg appearance-none focus:outline-none focus:ring-2 focus:ring-blue-500'
className="w-full px-4 py-2 pr-10 text-gray-700 bg-white border rounded-lg appearance-none focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value=''>{allLabel}</option>
<option value="">{allLabel}</option>
{options.map((option) => (
<option key={option} value={option}>
{option}
Expand Down
22 changes: 11 additions & 11 deletions src/advanced-filtering/src/components/LogLevelBadge.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,28 @@
import React from "react";
import {LogLevel} from "../types";
import React from 'react';
import { LogLevel } from '../types';

interface LogLevelBadgeProps {
level: LogLevel;
}

export const LogLevelBadge: React.FC<LogLevelBadgeProps> = ({level}) => {
export const LogLevelBadge: React.FC<LogLevelBadgeProps> = ({ level }) => {
const getBadgeColor = (level: LogLevel) => {
switch (level) {
case "INFO":
return "bg-blue-100 text-blue-800";
case "WARNING":
return "bg-yellow-100 text-yellow-800";
case "ERROR":
return "bg-red-100 text-red-800";
case 'INFO':
return 'bg-blue-100 text-blue-800';
case 'WARNING':
return 'bg-yellow-100 text-yellow-800';
case 'ERROR':
return 'bg-red-100 text-red-800';
default:
return "bg-gray-100 text-gray-800";
return 'bg-gray-100 text-gray-800';
}
};

return (
<span
className={`px-2 py-1 rounded-full text-xs font-medium ${getBadgeColor(
level
level,
)}`}
>
{level}
Expand Down
6 changes: 3 additions & 3 deletions src/advanced-filtering/src/components/LogSortingHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {LogEntry, SortCriteria} from "../types";
import { LogEntry, SortCriteria } from '../types';

interface LogSortingHeaderProps {
field: keyof LogEntry;
Expand All @@ -13,10 +13,10 @@ export function LogSortingHeader({
label,
sortCriteria,
toggleSort,
className = "",
className = '',
}: LogSortingHeaderProps) {
const direction = sortCriteria[field]?.direction;
const arrow = direction === "asc" ? "↑" : direction === "desc" ? "↓" : "";
const arrow = direction === 'asc' ? '↑' : direction === 'desc' ? '↓' : '';

return (
<th
Expand Down
32 changes: 16 additions & 16 deletions src/advanced-filtering/src/constants/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {LogEntry, LogLevel, LogSource} from "../types";
import { LogEntry, LogLevel, LogSource } from '../types';

export const LogLevelOrder: Record<LogLevel, number> = {
ERROR: 3,
Expand All @@ -11,23 +11,23 @@ export const LogEntryFields: {
label: string;
className?: string;
}[] = [
{key: "timestamp", label: "Timestamp"},
{key: "level", label: "Level"},
{key: "source", label: "Source", className: "min-w-44"},
{key: "message", label: "Message", className: "min-w-80"},
{key: "userId", label: "User ID", className: "min-w-24"},
{ key: 'timestamp', label: 'Timestamp' },
{ key: 'level', label: 'Level' },
{ key: 'source', label: 'Source', className: 'min-w-44' },
{ key: 'message', label: 'Message', className: 'min-w-80' },
{ key: 'userId', label: 'User ID', className: 'min-w-24' },
];

export const LogLevelMap: {[Key in LogLevel]: Key} = {
INFO: "INFO",
WARNING: "WARNING",
ERROR: "ERROR",
export const LogLevelMap: { [Key in LogLevel]: Key } = {
INFO: 'INFO',
WARNING: 'WARNING',
ERROR: 'ERROR',
};

export const LogSourceMap: {[Key in LogSource]: Key} = {
"user-service": "user-service",
"auth-service": "auth-service",
"payment-service": "payment-service",
"notification-service": "notification-service",
"api-gateway": "api-gateway",
export const LogSourceMap: { [Key in LogSource]: Key } = {
'user-service': 'user-service',
'auth-service': 'auth-service',
'payment-service': 'payment-service',
'notification-service': 'notification-service',
'api-gateway': 'api-gateway',
};
Loading