From cd951234de21f0793051e3fe58c743e33d95e802 Mon Sep 17 00:00:00 2001 From: Artyom Zaporozhets Date: Sat, 30 Aug 2025 14:18:51 +0300 Subject: [PATCH 1/8] #4: add eslint and fix its problems --- .github/workflows/lint.yml | 29 ++++++++++++++++++++++ eslint.config.mjs | 9 +++++++ makefile | 6 +++++ package.json | 10 +++----- src/components/ui/aladin.tsx | 15 +++++++++++- src/components/ui/common-table.tsx | 10 +++++--- src/pages/TableDetails.tsx | 39 ++++++++++++++++++------------ 7 files changed, 92 insertions(+), 26 deletions(-) create mode 100644 .github/workflows/lint.yml create mode 100644 eslint.config.mjs diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..cc5f94c --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,29 @@ +name: Check code for smells and potential bugs + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + eslint: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install dependencies + run: yarn install + + - name: Generate code + run: make gen + + - name: Run check + run: make check diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..0fcbde3 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,9 @@ +// @ts-check + +import eslint from '@eslint/js'; +import tseslint from 'typescript-eslint'; + +export default tseslint.config( + eslint.configs.recommended, + tseslint.configs.recommended, +); diff --git a/makefile b/makefile index 307f74a..9b9c434 100644 --- a/makefile +++ b/makefile @@ -6,6 +6,12 @@ run: build: yarn build +check: + yarn eslint . --ignore-pattern 'src/clients/*' + +fix: + yarn eslint --fix . --ignore-pattern 'src/clients/*' + gen: yarn run openapi-ts -i http://dm2.sao.ru:81/api/openapi.json -o ./src/clients/backend yarn run openapi-ts -i http://dm2.sao.ru:81/admin/api/openapi.json -o ./src/clients/admin diff --git a/package.json b/package.json index 7b4bd59..89593ec 100644 --- a/package.json +++ b/package.json @@ -20,23 +20,21 @@ "react": "^19.0.0", "react-dom": "^19.0.0", "react-icons": "^5.5.0", - "react-markdown": "^10.0.1", "react-router-dom": "^7.2.0", - "remark-gfm": "^4.0.1", "serve": "^14.2.4", "tailwindcss": "^4.0.9" }, "devDependencies": { - "@eslint/js": "^9.21.0", + "@eslint/js": "^9.34.0", "@types/react": "^19.0.10", "@types/react-dom": "^19.0.4", "@vitejs/plugin-react-swc": "^3.8.0", - "eslint": "^9.21.0", + "eslint": "^9.34.0", "eslint-plugin-react-hooks": "^5.1.0", "eslint-plugin-react-refresh": "^0.4.19", "globals": "^15.15.0", - "typescript": "~5.7.2", - "typescript-eslint": "^8.24.1", + "typescript": "^5.9.2", + "typescript-eslint": "^8.41.0", "vite": "^6.2.0" } } diff --git a/src/components/ui/aladin.tsx b/src/components/ui/aladin.tsx index eedacaf..c2a5ebf 100644 --- a/src/components/ui/aladin.tsx +++ b/src/components/ui/aladin.tsx @@ -51,6 +51,19 @@ export function AladinViewer({ declare global { interface Window { - A: any; + A: { + aladin: (element: HTMLElement, options?: { + survey?: string; + fov?: number; + showReticle?: boolean; + showZoomControl?: boolean; + showFullscreenControl?: boolean; + showLayersControl?: boolean; + showCooGridControl?: boolean; + }) => { + gotoObject: (target: string) => void; + gotoRaDec: (ra: number, dec: number) => void; + }; + }; } } diff --git a/src/components/ui/common-table.tsx b/src/components/ui/common-table.tsx index b314e04..6978123 100644 --- a/src/components/ui/common-table.tsx +++ b/src/components/ui/common-table.tsx @@ -1,16 +1,18 @@ -import React, { ReactElement } from "react"; +import React, { ReactElement, ReactNode } from "react"; import classNames from "classnames"; import { Hint } from "./hint"; +export type CellPrimitive = ReactElement | string | number + export interface Column { name: string; - renderCell?: (value: any) => React.ReactNode; + renderCell?: (value: CellPrimitive) => ReactNode; hint?: ReactElement; } interface CommonTableProps { columns: Column[]; - data: Record[]; + data: Record[]; className?: string; tableClassName?: string; headerClassName?: string; @@ -29,7 +31,7 @@ export const CommonTable: React.FC = ({ cellClassName = "text-gray-200", children, }) => { - const renderCell = (value: any, column: Column): React.ReactNode => { + const renderCell = (value: CellPrimitive, column: Column): React.ReactNode => { if (column.renderCell) { return column.renderCell(value); } diff --git a/src/pages/TableDetails.tsx b/src/pages/TableDetails.tsx index 39995c3..e21a911 100644 --- a/src/pages/TableDetails.tsx +++ b/src/pages/TableDetails.tsx @@ -2,12 +2,12 @@ import React, { ReactElement, useEffect, useState } from "react"; import { GetTableResponse, HttpValidationError, Bibliography } from "../clients/admin/types.gen"; import { getTableAdminApiV1TableGet } from "../clients/admin/sdk.gen"; import { useNavigate, useParams } from "react-router-dom"; -import { CommonTable, Column } from "../components/ui/common-table"; +import { CommonTable, Column, CellPrimitive } from "../components/ui/common-table"; import { CopyButton } from "../components/ui/copy-button"; import { Link } from "../components/ui/link"; function renderBibliography(bib: Bibliography): ReactElement { - var authors = "" + let authors = "" if (bib.authors.length >= 1) { authors += bib.authors[0] @@ -31,12 +31,12 @@ function renderTime(time: string): string { return dt.toString() } -function renderUCD(ucd: string | undefined | null): ReactElement { - if (!ucd) { +function renderUCD(ucd: CellPrimitive): ReactElement { + if (!(typeof ucd == "string")) { return
} - var words: ReactElement[] = [] + const words: ReactElement[] = [] ucd.split(";").forEach((word, index) => { words.push( @@ -49,8 +49,8 @@ function renderUCD(ucd: string | undefined | null): ReactElement { } -function renderColumnName(name: string): ReactElement { - return +function renderColumnName(name: CellPrimitive): ReactElement { + return

{name}

} @@ -61,7 +61,7 @@ const renderTableDetails = (tableName: string, table: GetTableResponse) => { { name: "Value" } ] - const infoValues = [ + const infoValues: Record[] = [ { Parameter: "Table ID", Value: table.id, @@ -76,7 +76,7 @@ const renderTableDetails = (tableName: string, table: GetTableResponse) => { }, { Parameter: "Type of data", - Value: table.meta.datatype + Value: String(table.meta.datatype) }, { Parameter: "Modification time", @@ -98,15 +98,24 @@ const renderTableDetails = (tableName: string, table: GetTableResponse) => { }, ] - var columnInfoValues: any[] = [] + const columnInfoValues: Record[] = [] table.column_info.forEach(col => { - columnInfoValues.push({ + const colValue: Record = { Name: col.name, - Description: col.description, - Unit: col.unit, - UCD: col.ucd, - }) + } + + if (col.description) { + colValue.Description = col.description + } + if (col.unit) { + colValue.Unit = col.unit + } + if (col.ucd) { + colValue.UCD = col.ucd + } + + columnInfoValues.push(colValue) }); return
From affd626442f89a633e40051bac96af35917e94cf Mon Sep 17 00:00:00 2001 From: Artyom Zaporozhets Date: Sat, 30 Aug 2025 14:46:12 +0300 Subject: [PATCH 2/8] add some more rules to linter --- eslint.config.js | 28 ------------------------- eslint.config.mjs | 9 -------- eslint.config.ts | 28 +++++++++++++++++++++++++ makefile | 4 ++-- package.json | 1 + src/components/ui/button.tsx | 30 ++++++++------------------- src/components/ui/card.tsx | 40 +++++++++++++----------------------- src/components/ui/hint.tsx | 22 +++++++++----------- 8 files changed, 64 insertions(+), 98 deletions(-) delete mode 100644 eslint.config.js delete mode 100644 eslint.config.mjs create mode 100644 eslint.config.ts diff --git a/eslint.config.js b/eslint.config.js deleted file mode 100644 index 092408a..0000000 --- a/eslint.config.js +++ /dev/null @@ -1,28 +0,0 @@ -import js from '@eslint/js' -import globals from 'globals' -import reactHooks from 'eslint-plugin-react-hooks' -import reactRefresh from 'eslint-plugin-react-refresh' -import tseslint from 'typescript-eslint' - -export default tseslint.config( - { ignores: ['dist'] }, - { - extends: [js.configs.recommended, ...tseslint.configs.recommended], - files: ['**/*.{ts,tsx}'], - languageOptions: { - ecmaVersion: 2020, - globals: globals.browser, - }, - plugins: { - 'react-hooks': reactHooks, - 'react-refresh': reactRefresh, - }, - rules: { - ...reactHooks.configs.recommended.rules, - 'react-refresh/only-export-components': [ - 'warn', - { allowConstantExport: true }, - ], - }, - }, -) diff --git a/eslint.config.mjs b/eslint.config.mjs deleted file mode 100644 index 0fcbde3..0000000 --- a/eslint.config.mjs +++ /dev/null @@ -1,9 +0,0 @@ -// @ts-check - -import eslint from '@eslint/js'; -import tseslint from 'typescript-eslint'; - -export default tseslint.config( - eslint.configs.recommended, - tseslint.configs.recommended, -); diff --git a/eslint.config.ts b/eslint.config.ts new file mode 100644 index 0000000..ff7c265 --- /dev/null +++ b/eslint.config.ts @@ -0,0 +1,28 @@ +import eslint from '@eslint/js'; +import tseslint from 'typescript-eslint' + +export default tseslint.config( + { + extends: [ + eslint.configs.recommended, + tseslint.configs.recommended, + ], + ignores: ['src/clients/**'], + rules: { + '@typescript-eslint/array-type': 'error', + 'array-callback-return': 'error', + 'no-await-in-loop': 'error', + 'no-constructor-return': 'error', + 'no-inner-declarations': 'error', + 'no-promise-executor-return': 'error', + 'no-self-compare': 'error', + 'no-template-curly-in-string': 'error', + 'no-unassigned-vars': 'error', + 'no-unreachable-loop': 'error', + 'no-use-before-define': 'error', + 'no-useless-assignment': 'error', + 'require-atomic-updates': 'error', + 'arrow-body-style': 'error' + } + }, +) diff --git a/makefile b/makefile index 9b9c434..e64f726 100644 --- a/makefile +++ b/makefile @@ -7,10 +7,10 @@ build: yarn build check: - yarn eslint . --ignore-pattern 'src/clients/*' + yarn eslint src fix: - yarn eslint --fix . --ignore-pattern 'src/clients/*' + yarn eslint --fix src gen: yarn run openapi-ts -i http://dm2.sao.ru:81/api/openapi.json -o ./src/clients/backend diff --git a/package.json b/package.json index 89593ec..0d0a524 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "eslint-plugin-react-hooks": "^5.1.0", "eslint-plugin-react-refresh": "^0.4.19", "globals": "^15.15.0", + "jiti": "^2.5.1", "typescript": "^5.9.2", "typescript-eslint": "^8.41.0", "vite": "^6.2.0" diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx index 4781feb..75e30a4 100644 --- a/src/components/ui/button.tsx +++ b/src/components/ui/button.tsx @@ -9,24 +9,12 @@ interface ButtonProps { disabled?: boolean; } -export const Button: React.FC = ({ - children, - onClick, - className, - type = "button", - disabled = false, -}) => { - return ( - - ); -}; +export const Button: React.FC = (params) => ; diff --git a/src/components/ui/card.tsx b/src/components/ui/card.tsx index 907fa83..ade9b44 100644 --- a/src/components/ui/card.tsx +++ b/src/components/ui/card.tsx @@ -8,35 +8,23 @@ interface CardProps { title?: string; } -export const Card: React.FC = ({ - children, - className, - onClick, - title, -}) => { - return ( -
- {title &&

{title}

} - {children} -
- ); -}; +export const Card: React.FC = (params) =>
+ {params.title &&

{params.title}

} + {params.children} +
; interface CardContentProps { children: React.ReactNode; className?: string; } -export const CardContent: React.FC = ({ - children, - className, -}) => { - return
{children}
; -}; +export const CardContent: React.FC = (params) =>
+ {params.children} +
; diff --git a/src/components/ui/hint.tsx b/src/components/ui/hint.tsx index 93331ac..4992112 100644 --- a/src/components/ui/hint.tsx +++ b/src/components/ui/hint.tsx @@ -8,15 +8,13 @@ interface HintProps { className?: string; } -export const Hint: React.FC = ({ children, hintContent, className = "" }) => { - return ( -
-
{children}
-
- - - -
-
- ); -}; +export const Hint: React.FC = ({ children, hintContent, className = "" }) => ( +
+
{children}
+
+ + + +
+
+); From 45323d9e9a007d0b54541ec6dbef748c3b69631a Mon Sep 17 00:00:00 2001 From: Artyom Zaporozhets Date: Sat, 30 Aug 2025 23:31:19 +0300 Subject: [PATCH 3/8] add some opinionated suggestions to linter --- eslint.config.ts | 15 +- src/components/ui/button.tsx | 23 +- src/components/ui/card.tsx | 34 +-- src/components/ui/common-table.tsx | 10 +- src/components/ui/copy-button.tsx | 8 +- src/components/ui/footer.tsx | 2 +- src/components/ui/hint.tsx | 12 +- src/components/ui/link.tsx | 8 +- src/components/ui/searchbar.tsx | 13 +- src/pages/Home.tsx | 16 +- src/pages/ObjectDetails.tsx | 350 +++++++++++++++-------------- src/pages/SearchResults.tsx | 52 ++--- src/pages/TableDetails.tsx | 34 +-- 13 files changed, 299 insertions(+), 278 deletions(-) diff --git a/eslint.config.ts b/eslint.config.ts index ff7c265..f8b713a 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -7,7 +7,7 @@ export default tseslint.config( eslint.configs.recommended, tseslint.configs.recommended, ], - ignores: ['src/clients/**'], + ignores: ['src/clients/**'], // code-generated rules: { '@typescript-eslint/array-type': 'error', 'array-callback-return': 'error', @@ -22,7 +22,18 @@ export default tseslint.config( 'no-use-before-define': 'error', 'no-useless-assignment': 'error', 'require-atomic-updates': 'error', - 'arrow-body-style': 'error' + + // suggestion enforcements to make TypeScript a sane language + 'arrow-body-style': 'error', + 'block-scoped-var': 'error', + 'camelcase': ['error', { properties: 'never' }], // code-generated client structures might not always adhere to camelcase + 'consistent-return': 'error', + 'default-case': 'error', + 'default-case-last': 'error', + 'default-param-last': 'error', + 'eqeqeq': 'error', + 'func-name-matching': 'error', + 'func-style': ["error", "declaration"] } }, ) diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx index 75e30a4..b229767 100644 --- a/src/components/ui/button.tsx +++ b/src/components/ui/button.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { ReactElement } from "react"; import classNames from "classnames"; interface ButtonProps { @@ -9,12 +9,15 @@ interface ButtonProps { disabled?: boolean; } -export const Button: React.FC = (params) => ; +export function Button(props: ButtonProps): ReactElement { + return ; + +} \ No newline at end of file diff --git a/src/components/ui/card.tsx b/src/components/ui/card.tsx index ade9b44..b3f3331 100644 --- a/src/components/ui/card.tsx +++ b/src/components/ui/card.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { ReactElement } from "react"; import classNames from "classnames"; interface CardProps { @@ -8,23 +8,27 @@ interface CardProps { title?: string; } -export const Card: React.FC = (params) =>
- {params.title &&

{params.title}

} - {params.children} -
; +export function Card(props: CardProps): ReactElement { + return
+ {props.title &&

{props.title}

} + {props.children} +
; +} interface CardContentProps { children: React.ReactNode; className?: string; } -export const CardContent: React.FC = (params) =>
- {params.children} -
; +export function CardContent(props: CardContentProps): ReactElement { + return
+ {props.children} +
; +} diff --git a/src/components/ui/common-table.tsx b/src/components/ui/common-table.tsx index 6978123..f81f9a1 100644 --- a/src/components/ui/common-table.tsx +++ b/src/components/ui/common-table.tsx @@ -1,4 +1,4 @@ -import React, { ReactElement, ReactNode } from "react"; +import React, { ReactElement } from "react"; import classNames from "classnames"; import { Hint } from "./hint"; @@ -6,7 +6,7 @@ export type CellPrimitive = ReactElement | string | number export interface Column { name: string; - renderCell?: (value: CellPrimitive) => ReactNode; + renderCell?: (value: CellPrimitive) => ReactElement; hint?: ReactElement; } @@ -21,7 +21,7 @@ interface CommonTableProps { children?: React.ReactNode; } -export const CommonTable: React.FC = ({ +export function CommonTable({ columns, data, className = "", @@ -30,8 +30,8 @@ export const CommonTable: React.FC = ({ columnHeaderClassName = "bg-gray-600 text-white", cellClassName = "text-gray-200", children, -}) => { - const renderCell = (value: CellPrimitive, column: Column): React.ReactNode => { +}: CommonTableProps): ReactElement { + function renderCell(value: CellPrimitive, column: Column): ReactElement { if (column.renderCell) { return column.renderCell(value); } diff --git a/src/components/ui/copy-button.tsx b/src/components/ui/copy-button.tsx index e885d78..94acd12 100644 --- a/src/components/ui/copy-button.tsx +++ b/src/components/ui/copy-button.tsx @@ -7,12 +7,12 @@ interface CopyButtonProps { textToCopy: string; } -export const CopyButton: React.FC = ({ children, textToCopy }) => { +export function CopyButton(props: CopyButtonProps): ReactElement { const [copied, setCopied] = useState(false); - const handleCopy = async () => { + async function handleCopy() { try { - await navigator.clipboard.writeText(textToCopy); + await navigator.clipboard.writeText(props.textToCopy); setCopied(true); setTimeout(() => setCopied(false), 1000); } catch (err) { @@ -22,7 +22,7 @@ export const CopyButton: React.FC = ({ children, textToCopy }) return (
-
{children}
+
{props.children}
); -export const HomePage: React.FC = () => { - const navigate = useNavigate(); - - const handleSearch = (query: string) => { +function searchHandler(navigate: NavigateFunction) { + return function f(query: string) { navigate(`/query?q=${encodeURIComponent(query)}`); - }; + } +}; + +export function HomePage(): ReactElement { + const navigate = useNavigate(); return (
- +
{homePageHint}
diff --git a/src/pages/ObjectDetails.tsx b/src/pages/ObjectDetails.tsx index baa7602..8e1cc60 100644 --- a/src/pages/ObjectDetails.tsx +++ b/src/pages/ObjectDetails.tsx @@ -1,5 +1,5 @@ -import { useEffect, useState } from "react"; -import { useNavigate, useParams } from "react-router-dom"; +import { ReactElement, useEffect, useState } from "react"; +import { NavigateFunction, useNavigate, useParams } from "react-router-dom"; import { SearchBar } from "../components/ui/searchbar"; import { Button } from "../components/ui/button"; import { AladinViewer } from "../components/ui/aladin"; @@ -7,7 +7,178 @@ import { CommonTable } from "../components/ui/common-table"; import { querySimpleApiV1QuerySimpleGet } from "../clients/backend/sdk.gen" import { PgcObject, Schema } from "../clients/backend/types.gen" -export const ObjectDetailsPage: React.FC = () => { +function backToResultsHandler(navigate: NavigateFunction) { + return function f() { + navigate(-1); + } +} + +function searchHandler(navigate: NavigateFunction) { + return function f(query: string) { + navigate(`/query?q=${encodeURIComponent(query)}`); + } +}; + + +function renderNotFound(navigate: NavigateFunction) { + return
+ +

Object not found.

+
+}; + + +function renderObjectDetails(object: PgcObject, schema: Schema | null): ReactElement { + if (!object || !schema) return
; + + const coordinatesColumns = [ + { name: "Parameter" }, + { name: "Value" }, + { name: "Unit" }, + { name: "Error" }, + { name: "Error unit" }, + ]; + + const coordinatesData = [ + { + Parameter: "Right ascension", + Value: object.catalogs?.coordinates?.equatorial?.ra?.toFixed(6) || "NULL", + Unit: schema.units.coordinates?.equatorial?.ra || "NULL", + Error: object.catalogs?.coordinates?.equatorial?.e_ra?.toFixed(6) || "NULL", + "Error unit": schema.units.coordinates?.equatorial?.e_ra || "NULL", + }, + { + Parameter: "Declination", + Value: object.catalogs?.coordinates?.equatorial?.dec?.toFixed(6) || "NULL", + Unit: schema.units.coordinates?.equatorial?.dec || "NULL", + Error: object.catalogs?.coordinates?.equatorial?.e_dec?.toFixed(6) || "NULL", + "Error unit": schema.units.coordinates?.equatorial?.e_dec || "NULL", + }, + { + Parameter: "Galactic longitude", + Value: object.catalogs?.coordinates?.galactic?.lon?.toFixed(6) || "NULL", + Unit: schema.units.coordinates?.galactic?.lon || "NULL", + Error: object.catalogs?.coordinates?.galactic?.e_lon?.toFixed(6) || "NULL", + "Error unit": schema.units.coordinates?.galactic?.e_lon || "NULL", + }, + { + Parameter: "Galactic latitude", + Value: object.catalogs?.coordinates?.galactic?.lat?.toFixed(6) || "NULL", + Unit: schema.units.coordinates?.galactic?.lat || "NULL", + Error: object.catalogs?.coordinates?.galactic?.e_lat?.toFixed(6) || "NULL", + "Error unit": schema.units.coordinates?.galactic?.e_lat || "NULL", + }, + ]; + + const redshiftColumns = [ + { name: "Parameter" }, + { name: "Value" }, + { name: "Error" }, + ]; + + const redshiftData = [ + { + Parameter: "z", + Value: object.catalogs?.redshift?.z?.toFixed(6) || "NULL", + Error: object.catalogs?.redshift?.e_z?.toFixed(6) || "NULL", + }, + ]; + + const velocityColumns = [ + { name: "Parameter" }, + { name: "Value" }, + { name: "Unit" }, + { name: "Error" }, + { name: "Error unit" }, + ]; + + const velocityData = [ + { + Parameter: "Heliocentric", + Value: object.catalogs?.velocity?.heliocentric?.v?.toFixed(2) || "NULL", + Unit: schema.units.velocity?.heliocentric?.v || "NULL", + Error: object.catalogs?.velocity?.heliocentric?.e_v?.toFixed(2) || "NULL", + "Error unit": schema.units.velocity?.heliocentric?.e_v || "NULL", + }, + { + Parameter: "Local Group", + Value: object.catalogs?.velocity?.local_group?.v?.toFixed(2) || "NULL", + Unit: schema.units.velocity?.local_group?.v || "NULL", + Error: object.catalogs?.velocity?.local_group?.e_v?.toFixed(2) || "NULL", + "Error unit": schema.units.velocity?.local_group?.e_v || "NULL", + }, + { + Parameter: "CMB (old)", + Value: object.catalogs?.velocity?.cmb_old?.v?.toFixed(2) || "NULL", + Unit: schema.units.velocity?.cmb_old?.v || "NULL", + Error: object.catalogs?.velocity?.cmb_old?.e_v?.toFixed(2) || "NULL", + "Error unit": schema.units.velocity?.cmb_old?.e_v || "NULL", + }, + { + Parameter: "CMB", + Value: object.catalogs?.velocity?.cmb?.v?.toFixed(2) || "NULL", + Unit: schema.units.velocity?.cmb?.v || "NULL", + Error: object.catalogs?.velocity?.cmb?.e_v?.toFixed(2) || "NULL", + "Error unit": schema.units.velocity?.cmb?.e_v || "NULL", + }, + ]; + + return ( +
+
+ {object.catalogs?.coordinates?.equatorial && ( + + )} +
+

+ {object.catalogs?.designation?.name || `PGC ${object.pgc}`} +

+

PGC: {object.pgc}

+
+
+ + {object.catalogs?.coordinates && ( + +

Coordinates

+

Celestial coordinates of the object

+
+ )} + + {object.catalogs?.redshift && ( + +

Redshift

+

Redshift measurements

+
+ )} + + {object.catalogs?.velocity && ( + +

Velocity

+

Velocity measurements with respect to different apexes

+
+ )} +
+ ); +}; + +export function ObjectDetailsPage(): ReactElement { const { pgcId } = useParams<{ pgcId: string }>(); const [object, setObject] = useState(null); const [schema, setSchema] = useState(null); @@ -15,7 +186,7 @@ export const ObjectDetailsPage: React.FC = () => { const navigate = useNavigate(); useEffect(() => { - const fetchObjectDetails = async () => { + async function fetchObjectDetails() { if (!pgcId || isNaN(Number(pgcId))) { navigate("/"); return; @@ -49,183 +220,18 @@ export const ObjectDetailsPage: React.FC = () => { fetchObjectDetails(); }, [pgcId, navigate]); - const handleBackToResults = () => { - navigate(-1); - }; - - const handleSearch = (query: string) => { - navigate(`/query?q=${encodeURIComponent(query)}`); - }; - - const renderObjectDetails = () => { - if (!object || !schema) return null; - - const coordinatesColumns = [ - { name: "Parameter" }, - { name: "Value" }, - { name: "Unit" }, - { name: "Error" }, - { name: "Error unit" }, - ]; - - const coordinatesData = [ - { - Parameter: "Right ascension", - Value: object.catalogs?.coordinates?.equatorial?.ra?.toFixed(6) || "NULL", - Unit: schema.units.coordinates?.equatorial?.ra || "NULL", - Error: object.catalogs?.coordinates?.equatorial?.e_ra?.toFixed(6) || "NULL", - "Error unit": schema.units.coordinates?.equatorial?.e_ra || "NULL", - }, - { - Parameter: "Declination", - Value: object.catalogs?.coordinates?.equatorial?.dec?.toFixed(6) || "NULL", - Unit: schema.units.coordinates?.equatorial?.dec || "NULL", - Error: object.catalogs?.coordinates?.equatorial?.e_dec?.toFixed(6) || "NULL", - "Error unit": schema.units.coordinates?.equatorial?.e_dec || "NULL", - }, - { - Parameter: "Galactic longitude", - Value: object.catalogs?.coordinates?.galactic?.lon?.toFixed(6) || "NULL", - Unit: schema.units.coordinates?.galactic?.lon || "NULL", - Error: object.catalogs?.coordinates?.galactic?.e_lon?.toFixed(6) || "NULL", - "Error unit": schema.units.coordinates?.galactic?.e_lon || "NULL", - }, - { - Parameter: "Galactic latitude", - Value: object.catalogs?.coordinates?.galactic?.lat?.toFixed(6) || "NULL", - Unit: schema.units.coordinates?.galactic?.lat || "NULL", - Error: object.catalogs?.coordinates?.galactic?.e_lat?.toFixed(6) || "NULL", - "Error unit": schema.units.coordinates?.galactic?.e_lat || "NULL", - }, - ]; - - const redshiftColumns = [ - { name: "Parameter" }, - { name: "Value" }, - { name: "Error" }, - ]; - - const redshiftData = [ - { - Parameter: "z", - Value: object.catalogs?.redshift?.z?.toFixed(6) || "NULL", - Error: object.catalogs?.redshift?.e_z?.toFixed(6) || "NULL", - }, - ]; - - const velocityColumns = [ - { name: "Parameter" }, - { name: "Value" }, - { name: "Unit" }, - { name: "Error" }, - { name: "Error unit" }, - ]; - - const velocityData = [ - { - Parameter: "Heliocentric", - Value: object.catalogs?.velocity?.heliocentric?.v?.toFixed(2) || "NULL", - Unit: schema.units.velocity?.heliocentric?.v || "NULL", - Error: object.catalogs?.velocity?.heliocentric?.e_v?.toFixed(2) || "NULL", - "Error unit": schema.units.velocity?.heliocentric?.e_v || "NULL", - }, - { - Parameter: "Local Group", - Value: object.catalogs?.velocity?.local_group?.v?.toFixed(2) || "NULL", - Unit: schema.units.velocity?.local_group?.v || "NULL", - Error: object.catalogs?.velocity?.local_group?.e_v?.toFixed(2) || "NULL", - "Error unit": schema.units.velocity?.local_group?.e_v || "NULL", - }, - { - Parameter: "CMB (old)", - Value: object.catalogs?.velocity?.cmb_old?.v?.toFixed(2) || "NULL", - Unit: schema.units.velocity?.cmb_old?.v || "NULL", - Error: object.catalogs?.velocity?.cmb_old?.e_v?.toFixed(2) || "NULL", - "Error unit": schema.units.velocity?.cmb_old?.e_v || "NULL", - }, - { - Parameter: "CMB", - Value: object.catalogs?.velocity?.cmb?.v?.toFixed(2) || "NULL", - Unit: schema.units.velocity?.cmb?.v || "NULL", - Error: object.catalogs?.velocity?.cmb?.e_v?.toFixed(2) || "NULL", - "Error unit": schema.units.velocity?.cmb?.e_v || "NULL", - }, - ]; - - return ( -
-
- {object.catalogs?.coordinates?.equatorial && ( - - )} -
-

- {object.catalogs?.designation?.name || `PGC ${object.pgc}`} -

-

PGC: {object.pgc}

-
-
- - {object.catalogs?.coordinates && ( - -

Coordinates

-

Celestial coordinates of the object

-
- )} - - {object.catalogs?.redshift && ( - -

Redshift

-

Redshift measurements

-
- )} - - {object.catalogs?.velocity && ( - -

Velocity

-

Velocity measurements with respect to different apexes

-
- )} -
- ); - }; - - const renderNotFound = () => ( -
- -

Object not found.

-
- ); - return (
- + {loading ? (

Loading...

) : object ? ( - renderObjectDetails() + renderObjectDetails(object, schema) ) : ( - renderNotFound() + renderNotFound(navigate) )}
); diff --git a/src/pages/SearchResults.tsx b/src/pages/SearchResults.tsx index de1f447..96cae99 100644 --- a/src/pages/SearchResults.tsx +++ b/src/pages/SearchResults.tsx @@ -1,11 +1,29 @@ -import { useEffect, useState } from "react"; -import { useNavigate, useSearchParams } from "react-router-dom"; +import { ReactElement, useEffect, useState } from "react"; +import { NavigateFunction, useNavigate, useSearchParams } from "react-router-dom"; import { backendClient, SearchPGCObject } from "../clients/backend"; import { SearchBar } from "../components/ui/searchbar"; import { AladinViewer } from "../components/ui/aladin"; import { Card, CardContent } from "../components/ui/card"; -export const SearchResultsPage: React.FC = () => { +function objectClickHandler(navigate: NavigateFunction, object: SearchPGCObject) { + navigate(`/object/${object.pgc}`); +}; + +function searchHandler(navigate: NavigateFunction) { + return function f(query: string) { + navigate(`/query?q=${encodeURIComponent(query)}`); + } +}; + +function pageChangeHandler(navigate: NavigateFunction, query: string, pageSize: number, newPage: number) { + navigate( + `/query?q=${encodeURIComponent( + query + )}&page=${newPage}&pagesize=${pageSize}` + ); +}; + +export function SearchResultsPage(): ReactElement { const [searchParams] = useSearchParams(); const [results, setResults] = useState([]); const [loading, setLoading] = useState(true); @@ -15,7 +33,7 @@ export const SearchResultsPage: React.FC = () => { const pageSize = parseInt(searchParams.get("pagesize") || "10"); useEffect(() => { - const fetchResults = async () => { + async function fetchResults() { if (!query.trim()) { navigate("/"); return; @@ -35,29 +53,11 @@ export const SearchResultsPage: React.FC = () => { fetchResults(); }, [query, navigate, pageSize, page]); - const handleObjectClick = (object: SearchPGCObject) => { - navigate(`/object/${object.pgc}`); - }; - - const handleSearch = (newQuery: string) => { - if (newQuery.trim()) { - navigate(`/query?q=${encodeURIComponent(newQuery)}`); - } - }; - - const handlePageChange = (newPage: number) => { - navigate( - `/query?q=${encodeURIComponent( - query - )}&page=${newPage}&pagesize=${pageSize}` - ); - }; - return (
@@ -79,7 +79,7 @@ export const SearchResultsPage: React.FC = () => { handleObjectClick(object)} + onClick={() => objectClickHandler(navigate, object)} >

PGC {object.pgc}

@@ -100,7 +100,7 @@ export const SearchResultsPage: React.FC = () => { ))}
Page {page}
} -export const TableDetailsPage: React.FC = () => { +function renderNotFound(): ReactElement { + return
+

Table not found.

+
+}; + +function renderError(error: HttpValidationError): ReactElement { + return
+

{error.detail?.toString()}

+
+}; + +export function TableDetailsPage(): ReactElement { const { tableName } = useParams<{ tableName: string }>(); const [table, setTable] = useState(null); const [error, setError] = useState(null); @@ -137,7 +149,7 @@ export const TableDetailsPage: React.FC = () => { const navigate = useNavigate(); useEffect(() => { - const fetchData = async () => { + async function fetchData() { if (!tableName) { navigate("/"); return; @@ -163,18 +175,6 @@ export const TableDetailsPage: React.FC = () => { fetchData(); }, [tableName, navigate]) - const renderNotFound = () => ( -
-

Table not found.

-
- ); - - const renderError = (error: HttpValidationError) => ( -
-

{error.detail?.toString()}

-
- ); - return (
{loading ? ( From 813617eb62484964a8f61bd40e1884dd1359e4fc Mon Sep 17 00:00:00 2001 From: Artyom Zaporozhets Date: Sun, 31 Aug 2025 00:07:03 +0300 Subject: [PATCH 4/8] some more rules --- eslint.config.ts | 27 +++++++++++++++++++++++++-- src/App.tsx | 2 +- src/pages/SearchResults.tsx | 2 +- src/pages/TableDetails.tsx | 4 ++-- 4 files changed, 29 insertions(+), 6 deletions(-) diff --git a/eslint.config.ts b/eslint.config.ts index f8b713a..0d95f91 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -23,7 +23,7 @@ export default tseslint.config( 'no-useless-assignment': 'error', 'require-atomic-updates': 'error', - // suggestion enforcements to make TypeScript a sane language + // opinionated suggestion enforcements to make TypeScript a sane language 'arrow-body-style': 'error', 'block-scoped-var': 'error', 'camelcase': ['error', { properties: 'never' }], // code-generated client structures might not always adhere to camelcase @@ -33,7 +33,30 @@ export default tseslint.config( 'default-param-last': 'error', 'eqeqeq': 'error', 'func-name-matching': 'error', - 'func-style': ["error", "declaration"] + 'func-style': ["error", "declaration"], + 'init-declarations': ["error", "always"], + 'no-array-constructor': 'error', + 'no-caller': 'error', + 'no-delete-var': 'error', + 'no-else-return': 'error', + 'no-empty': 'error', + 'no-invalid-this': 'error', + 'no-label-var': 'error', + 'no-lone-blocks': 'error', + 'no-lonely-if': 'error', + 'no-loop-func': 'error', + 'no-octal': 'error', + 'no-octal-escape': 'error', + 'no-return-assign': 'error', + 'no-sequences': 'error', + 'no-throw-literal': 'error', + 'no-unneeded-ternary': 'error', + 'no-useless-rename': 'error', + 'no-useless-return': 'error', + 'no-var': 'error', + 'prefer-const': 'error', + 'require-await': 'error', + // 'sort-imports': 'error' // TODO: add code formatter } }, ) diff --git a/src/App.tsx b/src/App.tsx index 34ca67b..fe5ebd8 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,4 @@ -import { BrowserRouter, Routes, Route } from "react-router-dom"; +import { BrowserRouter, Route, Routes } from "react-router-dom"; import { Footer } from "./components/ui/footer"; import { HomePage } from "./pages/Home"; import { SearchResultsPage } from "./pages/SearchResults"; diff --git a/src/pages/SearchResults.tsx b/src/pages/SearchResults.tsx index 96cae99..75f55e4 100644 --- a/src/pages/SearchResults.tsx +++ b/src/pages/SearchResults.tsx @@ -1,6 +1,6 @@ import { ReactElement, useEffect, useState } from "react"; import { NavigateFunction, useNavigate, useSearchParams } from "react-router-dom"; -import { backendClient, SearchPGCObject } from "../clients/backend"; +import { SearchPGCObject, backendClient } from "../clients/backend"; import { SearchBar } from "../components/ui/searchbar"; import { AladinViewer } from "../components/ui/aladin"; import { Card, CardContent } from "../components/ui/card"; diff --git a/src/pages/TableDetails.tsx b/src/pages/TableDetails.tsx index d25ada9..27a3fe9 100644 --- a/src/pages/TableDetails.tsx +++ b/src/pages/TableDetails.tsx @@ -1,8 +1,8 @@ import { ReactElement, useEffect, useState } from "react"; -import { GetTableResponse, HttpValidationError, Bibliography } from "../clients/admin/types.gen"; +import { Bibliography, GetTableResponse, HttpValidationError } from "../clients/admin/types.gen"; import { getTableAdminApiV1TableGet } from "../clients/admin/sdk.gen"; import { useNavigate, useParams } from "react-router-dom"; -import { CommonTable, Column, CellPrimitive } from "../components/ui/common-table"; +import { CellPrimitive, Column, CommonTable } from "../components/ui/common-table"; import { CopyButton } from "../components/ui/copy-button"; import { Link } from "../components/ui/link"; From afd818607e3bba570cb84657eda2fc7d74be7109 Mon Sep 17 00:00:00 2001 From: Artyom Zaporozhets Date: Sun, 31 Aug 2025 00:25:50 +0300 Subject: [PATCH 5/8] add prettier --- .gitignore | 5 + eslint.config.ts | 114 +++++----- makefile | 2 + package.json | 5 +- src/.gitignore | 2 - src/App.tsx | 3 +- src/clients/backend.tsx | 34 ++- src/components/ui/aladin.tsx | 25 ++- src/components/ui/button.tsx | 25 ++- src/components/ui/card.tsx | 34 +-- src/components/ui/common-table.tsx | 189 ++++++++-------- src/components/ui/copy-button.tsx | 56 ++--- src/components/ui/footer.tsx | 38 ++-- src/components/ui/hint.tsx | 31 ++- src/components/ui/link.tsx | 19 +- src/components/ui/searchbar.tsx | 14 +- src/index.css | 2 +- src/main.tsx | 12 +- src/pages/Home.tsx | 15 +- src/pages/NotFound.tsx | 41 ++-- src/pages/ObjectDetails.tsx | 91 ++++---- src/pages/SearchResults.tsx | 42 ++-- src/pages/TableDetails.tsx | 342 ++++++++++++++++------------- 23 files changed, 620 insertions(+), 521 deletions(-) delete mode 100644 src/.gitignore diff --git a/.gitignore b/.gitignore index ea43704..b7e757e 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,8 @@ dist-ssr *.sw? yarn.lock + +# code-generated files +src/clients/backend +src/clients/admin + diff --git a/eslint.config.ts b/eslint.config.ts index 0d95f91..85f2e1c 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -1,62 +1,58 @@ -import eslint from '@eslint/js'; -import tseslint from 'typescript-eslint' +import eslint from "@eslint/js"; +import tseslint from "typescript-eslint"; +import eslintConfigPrettier from "eslint-config-prettier/flat"; -export default tseslint.config( - { - extends: [ - eslint.configs.recommended, - tseslint.configs.recommended, - ], - ignores: ['src/clients/**'], // code-generated - rules: { - '@typescript-eslint/array-type': 'error', - 'array-callback-return': 'error', - 'no-await-in-loop': 'error', - 'no-constructor-return': 'error', - 'no-inner-declarations': 'error', - 'no-promise-executor-return': 'error', - 'no-self-compare': 'error', - 'no-template-curly-in-string': 'error', - 'no-unassigned-vars': 'error', - 'no-unreachable-loop': 'error', - 'no-use-before-define': 'error', - 'no-useless-assignment': 'error', - 'require-atomic-updates': 'error', +export default tseslint.config(eslintConfigPrettier, { + extends: [eslint.configs.recommended, tseslint.configs.recommended], + ignores: ["src/clients/**"], // code-generated + rules: { + "@typescript-eslint/array-type": "error", + "array-callback-return": "error", + "no-await-in-loop": "error", + "no-constructor-return": "error", + "no-inner-declarations": "error", + "no-promise-executor-return": "error", + "no-self-compare": "error", + "no-template-curly-in-string": "error", + "no-unassigned-vars": "error", + "no-unreachable-loop": "error", + "no-use-before-define": "error", + "no-useless-assignment": "error", + "require-atomic-updates": "error", - // opinionated suggestion enforcements to make TypeScript a sane language - 'arrow-body-style': 'error', - 'block-scoped-var': 'error', - 'camelcase': ['error', { properties: 'never' }], // code-generated client structures might not always adhere to camelcase - 'consistent-return': 'error', - 'default-case': 'error', - 'default-case-last': 'error', - 'default-param-last': 'error', - 'eqeqeq': 'error', - 'func-name-matching': 'error', - 'func-style': ["error", "declaration"], - 'init-declarations': ["error", "always"], - 'no-array-constructor': 'error', - 'no-caller': 'error', - 'no-delete-var': 'error', - 'no-else-return': 'error', - 'no-empty': 'error', - 'no-invalid-this': 'error', - 'no-label-var': 'error', - 'no-lone-blocks': 'error', - 'no-lonely-if': 'error', - 'no-loop-func': 'error', - 'no-octal': 'error', - 'no-octal-escape': 'error', - 'no-return-assign': 'error', - 'no-sequences': 'error', - 'no-throw-literal': 'error', - 'no-unneeded-ternary': 'error', - 'no-useless-rename': 'error', - 'no-useless-return': 'error', - 'no-var': 'error', - 'prefer-const': 'error', - 'require-await': 'error', - // 'sort-imports': 'error' // TODO: add code formatter - } + // opinionated suggestion enforcements to make TypeScript a sane language + "arrow-body-style": "error", + "block-scoped-var": "error", + camelcase: ["error", { properties: "never" }], // code-generated client structures might not always adhere to camelcase + "consistent-return": "error", + "default-case": "error", + "default-case-last": "error", + "default-param-last": "error", + eqeqeq: "error", + "func-name-matching": "error", + "func-style": ["error", "declaration"], + "init-declarations": ["error", "always"], + "no-array-constructor": "error", + "no-caller": "error", + "no-delete-var": "error", + "no-else-return": "error", + "no-empty": "error", + "no-invalid-this": "error", + "no-label-var": "error", + "no-lone-blocks": "error", + "no-lonely-if": "error", + "no-loop-func": "error", + "no-octal": "error", + "no-octal-escape": "error", + "no-return-assign": "error", + "no-sequences": "error", + "no-throw-literal": "error", + "no-unneeded-ternary": "error", + "no-useless-rename": "error", + "no-useless-return": "error", + "no-var": "error", + "prefer-const": "error", + "require-await": "error", + // 'sort-imports': 'error' // TODO: add code formatter }, -) +}); diff --git a/makefile b/makefile index e64f726..93a0fbf 100644 --- a/makefile +++ b/makefile @@ -7,9 +7,11 @@ build: yarn build check: + yarn run prettier --check src yarn eslint src fix: + yarn run prettier --write src yarn eslint --fix src gen: diff --git a/package.json b/package.json index 0d0a524..b060b85 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,8 @@ "build": "tsc -b && vite build", "lint": "eslint .", "preview": "vite preview", - "openapi-ts": "openapi-ts" + "openapi-ts": "openapi-ts", + "prettier": "prettier" }, "dependencies": { "@hey-api/openapi-ts": "^0.80.10", @@ -30,10 +31,12 @@ "@types/react-dom": "^19.0.4", "@vitejs/plugin-react-swc": "^3.8.0", "eslint": "^9.34.0", + "eslint-config-prettier": "^10.1.8", "eslint-plugin-react-hooks": "^5.1.0", "eslint-plugin-react-refresh": "^0.4.19", "globals": "^15.15.0", "jiti": "^2.5.1", + "prettier": "3.6.2", "typescript": "^5.9.2", "typescript-eslint": "^8.41.0", "vite": "^6.2.0" diff --git a/src/.gitignore b/src/.gitignore deleted file mode 100644 index 1d760ce..0000000 --- a/src/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -clients/backend -clients/admin diff --git a/src/App.tsx b/src/App.tsx index fe5ebd8..3a84019 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -56,7 +56,8 @@ function App() { element={ - } + + } /> diff --git a/src/clients/backend.tsx b/src/clients/backend.tsx index 3e37cdf..f0b1569 100644 --- a/src/clients/backend.tsx +++ b/src/clients/backend.tsx @@ -130,7 +130,7 @@ export class HyperLEDAClient { }, }); - private constructor() { } + private constructor() {} public static getInstance(): HyperLEDAClient { if (!HyperLEDAClient.instance) { @@ -142,7 +142,7 @@ export class HyperLEDAClient { public async query( queryString: string, page: number = 0, - pageSize: number = 25 + pageSize: number = 25, ): Promise { try { const response = await this.axiosInstance.get>( @@ -153,7 +153,7 @@ export class HyperLEDAClient { page: page, page_size: pageSize, }, - } + }, ); return { objects: response.data.data.objects || [], @@ -176,10 +176,9 @@ export class HyperLEDAClient { page_size?: number; }): Promise { try { - const response = await this.axiosInstance.get>( - "/api/v1/query/simple", - { params } - ); + const response = await this.axiosInstance.get< + APIResponse + >("/api/v1/query/simple", { params }); return response.data.data; } catch (error) { console.error("Error in querySimple:", error); @@ -190,19 +189,18 @@ export class HyperLEDAClient { public async queryByPGC( pgcNumbers: number[], page: number = 0, - pageSize: number = 25 + pageSize: number = 25, ): Promise { try { - const response = await this.axiosInstance.get>( - "/api/v1/query/simple", - { - params: { - pgcs: pgcNumbers, - page: page, - page_size: pageSize, - }, - } - ); + const response = await this.axiosInstance.get< + APIResponse + >("/api/v1/query/simple", { + params: { + pgcs: pgcNumbers, + page: page, + page_size: pageSize, + }, + }); return response.data.data; } catch (error) { console.error("Error in queryByPGC:", error); diff --git a/src/components/ui/aladin.tsx b/src/components/ui/aladin.tsx index c2a5ebf..b1bad85 100644 --- a/src/components/ui/aladin.tsx +++ b/src/components/ui/aladin.tsx @@ -44,23 +44,24 @@ export function AladinViewer({ } }, [ra, dec, fov, survey, target]); - return ( -
- ); + return
; } declare global { interface Window { A: { - aladin: (element: HTMLElement, options?: { - survey?: string; - fov?: number; - showReticle?: boolean; - showZoomControl?: boolean; - showFullscreenControl?: boolean; - showLayersControl?: boolean; - showCooGridControl?: boolean; - }) => { + aladin: ( + element: HTMLElement, + options?: { + survey?: string; + fov?: number; + showReticle?: boolean; + showZoomControl?: boolean; + showFullscreenControl?: boolean; + showLayersControl?: boolean; + showCooGridControl?: boolean; + }, + ) => { gotoObject: (target: string) => void; gotoRaDec: (ra: number, dec: number) => void; }; diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx index b229767..317445d 100644 --- a/src/components/ui/button.tsx +++ b/src/components/ui/button.tsx @@ -10,14 +10,17 @@ interface ButtonProps { } export function Button(props: ButtonProps): ReactElement { - return ; - -} \ No newline at end of file + return ( + + ); +} diff --git a/src/components/ui/card.tsx b/src/components/ui/card.tsx index b3f3331..dbd4068 100644 --- a/src/components/ui/card.tsx +++ b/src/components/ui/card.tsx @@ -9,17 +9,21 @@ interface CardProps { } export function Card(props: CardProps): ReactElement { - return
- {props.title &&

{props.title}

} - {props.children} -
; + return ( +
+ {props.title && ( +

{props.title}

+ )} + {props.children} +
+ ); } interface CardContentProps { @@ -28,7 +32,7 @@ interface CardContentProps { } export function CardContent(props: CardContentProps): ReactElement { - return
- {props.children} -
; -} + return ( +
{props.children}
+ ); +} diff --git a/src/components/ui/common-table.tsx b/src/components/ui/common-table.tsx index f81f9a1..b8df151 100644 --- a/src/components/ui/common-table.tsx +++ b/src/components/ui/common-table.tsx @@ -2,110 +2,115 @@ import React, { ReactElement } from "react"; import classNames from "classnames"; import { Hint } from "./hint"; -export type CellPrimitive = ReactElement | string | number +export type CellPrimitive = ReactElement | string | number; export interface Column { - name: string; - renderCell?: (value: CellPrimitive) => ReactElement; - hint?: ReactElement; + name: string; + renderCell?: (value: CellPrimitive) => ReactElement; + hint?: ReactElement; } interface CommonTableProps { - columns: Column[]; - data: Record[]; - className?: string; - tableClassName?: string; - headerClassName?: string; - columnHeaderClassName?: string; - cellClassName?: string; - children?: React.ReactNode; + columns: Column[]; + data: Record[]; + className?: string; + tableClassName?: string; + headerClassName?: string; + columnHeaderClassName?: string; + cellClassName?: string; + children?: React.ReactNode; } export function CommonTable({ - columns, - data, - className = "", - tableClassName = "", - headerClassName = "bg-gray-700 border-gray-600", - columnHeaderClassName = "bg-gray-600 text-white", - cellClassName = "text-gray-200", - children, + columns, + data, + className = "", + tableClassName = "", + headerClassName = "bg-gray-700 border-gray-600", + columnHeaderClassName = "bg-gray-600 text-white", + cellClassName = "text-gray-200", + children, }: CommonTableProps): ReactElement { - function renderCell(value: CellPrimitive, column: Column): ReactElement { - if (column.renderCell) { - return column.renderCell(value); - } + function renderCell(value: CellPrimitive, column: Column): ReactElement { + if (column.renderCell) { + return column.renderCell(value); + } - if (value === undefined || value === null) { - return
; - } + if (value === undefined || value === null) { + return
; + } - if (React.isValidElement(value)) { - return value; - } + if (React.isValidElement(value)) { + return value; + } - return {String(value)}; - }; + return {String(value)}; + } - return ( -
- {children && ( -
- {children} -
- )} + return ( +
+ {children && ( +
+ {children} +
+ )} -
- - - - {columns.map((column) => ( - - ))} - - +
+
- {column.hint ? ( - - {column.name} - - ) : ( - column.name - )} -
+ + + {columns.map((column) => ( + + ))} + + - - {data.map((row, rowIndex) => ( - - {columns.map((column) => { - const cellValue = row[column.name]; - return ( - - ); - })} - - ))} - -
+ {column.hint ? ( + + {column.name} + + ) : ( + column.name + )} +
- {renderCell(cellValue, column)} -
-
-
- ); -}; + + {data.map((row, rowIndex) => ( + + {columns.map((column) => { + const cellValue = row[column.name]; + return ( + + {renderCell(cellValue, column)} + + ); + })} + + ))} + + +
+
+ ); +} diff --git a/src/components/ui/copy-button.tsx b/src/components/ui/copy-button.tsx index 94acd12..b90fab9 100644 --- a/src/components/ui/copy-button.tsx +++ b/src/components/ui/copy-button.tsx @@ -3,36 +3,36 @@ import { Button } from "./button"; import { MdCheck, MdContentCopy } from "react-icons/md"; interface CopyButtonProps { - children: ReactElement; - textToCopy: string; + children: ReactElement; + textToCopy: string; } export function CopyButton(props: CopyButtonProps): ReactElement { - const [copied, setCopied] = useState(false); + const [copied, setCopied] = useState(false); - async function handleCopy() { - try { - await navigator.clipboard.writeText(props.textToCopy); - setCopied(true); - setTimeout(() => setCopied(false), 1000); - } catch (err) { - console.error('Failed to copy text: ', err); - } - }; + async function handleCopy() { + try { + await navigator.clipboard.writeText(props.textToCopy); + setCopied(true); + setTimeout(() => setCopied(false), 1000); + } catch (err) { + console.error("Failed to copy text: ", err); + } + } - return ( -
-
{props.children}
- -
- ); -}; + return ( +
+
{props.children}
+ +
+ ); +} diff --git a/src/components/ui/footer.tsx b/src/components/ui/footer.tsx index 8ade8ae..52fe08f 100644 --- a/src/components/ui/footer.tsx +++ b/src/components/ui/footer.tsx @@ -4,44 +4,50 @@ import { Button } from "./button"; import { Link } from "./link"; import { MdKeyboardArrowDown, MdKeyboardArrowUp } from "react-icons/md"; -const footerContent =
-
Information:
-
Old version:
-
+const footerContent = ( +
+
+ Information: +
+
+ Old version: +
+
+); export function Footer() { const [isCollapsed, setIsCollapsed] = useState(true); function toggleCollapse() { setIsCollapsed(!isCollapsed); - }; + } return ( <>