Skip to content

Commit

Permalink
feat: Refine types, implement skipFields and renameFields, create mul…
Browse files Browse the repository at this point in the history
…tiple dynamic routes from mock data
  • Loading branch information
danielptv committed Apr 18, 2024
1 parent 83538b6 commit 1f70c9a
Show file tree
Hide file tree
Showing 6 changed files with 197 additions and 41 deletions.
47 changes: 47 additions & 0 deletions src/app/dataset/[resourceId]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import DataTable from '@/components/visualization/DataTable';
import { Resource } from '@/types/visualization';
import { transformJson } from '@/transform';

const mockData = [
{
id: '1',
name: 'Bevölkerung mit Hauptwohnung',
endpoint: 'https://transparenz.karlsruhe.de/datastore/dump/d8be5f4a-0788-4ee3-abe5-b36313ce3799?format=json',
skipFields: '^_id$',
renameFields: {
'mannlich (%)': 'Männlich (%)',
'weiblich (%)': 'Weiblich (%)',
},
},
{
id: '2',
name: 'Bevölkerung mit Hauptwohnung',
endpoint:
'https://transparenz.karlsruhe.de/api/3/action/datastore_search?resource_id=d8be5f4a-0788-4ee3-abe5-b36313ce3799&limit=450',
},
] as Resource[];

export default async function Page({ params: { resourceId } }: { params: { resourceId: string } }) {
const resource = mockData.find((item) => item.id === resourceId);
const data = await getData(resource?.endpoint ?? '', resource?.skipFields, resource?.renameFields);
if (data === undefined) {
return <></>;
}
return (
<>
<DataTable key={resourceId} data={data}></DataTable>
</>
);
}

async function getData(endpoint: string, skipFields?: string, renameFields?: Record<string, string>) {
const res = await fetch(endpoint);
const json = (await res.json()) as unknown;
return transformJson(json, skipFields, renameFields);
}

export function generateStaticParams() {
return mockData.map((data) => ({
resourceId: data.id,
}));
}
25 changes: 0 additions & 25 deletions src/app/dataset/page.tsx

This file was deleted.

19 changes: 4 additions & 15 deletions src/components/visualization/DataTable.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,17 @@
import { DataRecord } from '@/types/visualization';
import Table from 'react-bootstrap/Table';

type DataSet = Record<string, never>;

interface Props {
data: JsonDictionary;
}

interface JsonDictionary {
fields: [{ type: string; id: string }];
records: DataSet[];
data: DataRecord;
}

const getHeaderNames = (jsonData: JsonDictionary) => {
return jsonData.fields.map((field) => field.id);
};

export default function DataTable({ data }: Props) {
const headers = getHeaderNames(data);
return (
<Table striped bordered hover responsive>
<thead>
<tr>
{headers.map((header) => (
<th key={header}>{header}</th>
{data.fields.map((field) => (
<th key={field}>{field}</th>
))}
</tr>
</thead>
Expand Down
116 changes: 116 additions & 0 deletions src/transform.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { DataRecord, JsonSourceArrays, JsonSourceObjects, JsonSourceStandard } from '@/types/visualization';

export function transformJson(json: unknown, skipFieldsRegex?: string, renameFieldsObj?: Record<string, string>) {
const transformedJson = transformJsonData(json);
if (transformedJson !== undefined && skipFieldsRegex !== undefined) {
const skippedFieldsJson = skipFields(transformedJson, skipFieldsRegex);
if (renameFieldsObj !== undefined) {
renameFields(skippedFieldsJson, renameFieldsObj);
}
return skippedFieldsJson;
}
return transformedJson;
}

function transformJsonData(json: unknown) {
if (isJsonSourceStandard(json)) {
if (json.length === 0) {
return;
}
const objectsArray = json as Record<string, never>[];
return {
fields: Object.keys(objectsArray[0]),
records: objectsArray.map((obj) => Object.values(obj)),
} as DataRecord;
}

if (isJsonSourceObjects(json)) {
if (json.result.records.length === 0) {
return;
}
return {
fields: Object.keys(json.result.records[0]),
records: json.result.records.map((obj) => Object.values(obj)),
} as DataRecord;
}

if (isJsonSourceArrays(json)) {
if (json.records.length === 0) {
return;
}
return {
fields: json.fields.map((field) => field.id),
records: json.records.map((obj) => Object.values(obj)),
} as DataRecord;
}
}

function isJsonSourceStandard(json: unknown): json is JsonSourceStandard {
return Array.isArray(json) && json.every((item) => typeof item === 'object');
}

function isJsonSourceObjects(json: unknown): json is JsonSourceObjects {
return (
json !== null &&
typeof json === 'object' &&
'result' in json &&
json.result !== null &&
typeof json.result === 'object' &&
'records_format' in json.result &&
json.result.records_format === 'objects' &&
'fields' in json.result &&
Array.isArray(json.result.fields) &&
json.result.fields.every((field) => typeof field === 'object' && 'type' in field && 'id' in field) &&
'records' in json.result &&
Array.isArray(json.result.records)
);
}

function isJsonSourceArrays(json: unknown): json is JsonSourceArrays {
return (
json !== null &&
typeof json === 'object' &&
'fields' in json &&
Array.isArray(json.fields) &&
json.fields.every((field) => typeof field === 'object' && 'type' in field && 'id' in field) &&
'records' in json &&
Array.isArray(json.records)
);
}

function skipFields(record: DataRecord, skipFieldsRegex: string) {
const regex = new RegExp(skipFieldsRegex, 'u');
const indicesToRemove: number[] = [];
const remainingFields: string[] = [];
const recordsWithoutSkippedFields = record.records.map((arr) => [...arr]);

record.fields.forEach((field, index) => {
if (regex.test(field)) {
indicesToRemove.push(index);
} else {
remainingFields.push(field);
}
});

indicesToRemove.sort((a, b) => b - a);
recordsWithoutSkippedFields.forEach((records) => {
indicesToRemove.forEach((index) => {
if (index >= 0 && index < records.length) {
records.splice(index, 1);
}
});
});
return {
fields: remainingFields,
records: recordsWithoutSkippedFields,
};
}

function renameFields(record: DataRecord, renameFieldsObj: Record<string, string>) {
record.fields.forEach((fieldName, index) => {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (renameFieldsObj[fieldName] !== undefined) {
record.fields[index] = renameFieldsObj[fieldName];
}
});
}
29 changes: 29 additions & 0 deletions src/types/visualization.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
export type JsonSourceStandard = Record<string, never>[];

export interface JsonSourceObjects {
result: {
records_format: 'objects';
fields: [{ type: string; id: string }];
records: Record<string, never>[];
};
}

export interface JsonSourceArrays {
fields: [{ type: string; id: string }];
records: Record<string, never>[];
}

export interface DataRecord {
fields: string[];
records: never[][];
}

export interface Resource {
id: string;
name: string;
description?: string;
endpoint: string;
type: 'JSON' | 'CSV';
skipFields?: string;
renameFields?: Record<string, string>;
}
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,6 @@
"@/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "*.mjs"],
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "*.mjs", "*.d.ts"],
"exclude": ["node_modules"]
}

0 comments on commit 1f70c9a

Please sign in to comment.