diff --git a/apps/web/Makefile b/apps/web/Makefile index 00443a9..03475a8 100644 --- a/apps/web/Makefile +++ b/apps/web/Makefile @@ -104,7 +104,7 @@ format-write: ## lint lint: - pnpm eslint --ignore-path .gitignore --ext .mjs,.tsx,.ts --color && pnpm knip + pnpm eslint . --color && pnpm knip ## typecheck typecheck: diff --git a/apps/web/eslint.config.mjs b/apps/web/eslint.config.mjs new file mode 100644 index 0000000..47c906c --- /dev/null +++ b/apps/web/eslint.config.mjs @@ -0,0 +1,42 @@ +import process from 'process'; + +// eslint-disable-next-line import/no-extraneous-dependencies +import { includeIgnoreFile } from '@eslint/compat'; +// eslint-disable-next-line import/no-extraneous-dependencies +import eslint from '@eslint/js'; +import { node, next } from '@poolofdeath20/eslint-config'; +import tseslint from 'typescript-eslint'; + +const allowedFor = ['InternalLink', 'Image', 'Link']; + +export default tseslint.config( + includeIgnoreFile(`${process.cwd()}/.gitignore`), + eslint.configs.recommended, + ...tseslint.configs.recommendedTypeChecked, + ...tseslint.configs.strict, + ...tseslint.configs.stylistic, + node, + { + ...next, + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + rules: { + ...next.rules, + 'react/forbid-component-props': [ + 'error', + { + forbid: [ + { + propName: 'style', + allowedFor, + message: `Props "style" is forbidden for all components except ${allowedFor + .map((component) => { + return `"${component}"`; + }) + .join(', ')}`, + }, + ], + }, + ], + }, + } +); diff --git a/apps/web/knip.ts b/apps/web/knip.ts index fec89e9..7901900 100644 --- a/apps/web/knip.ts +++ b/apps/web/knip.ts @@ -9,17 +9,20 @@ const config: KnipConfig = { 'script/**/*.ts', ], ignore: ['next-sitemap.config.js', 'next/**.mjs', 'test/**/**.ts'], - ignoreBinaries: ['make'], ignoreDependencies: [ - 'vite-node', - 'next-sitemap', - 'eslint', - '@periotable/data', '@ducanh2912/next-pwa', + '@periotable/data', '@types/jest-image-snapshot', + 'eslint', 'jest-image-snapshot', + 'next-sitemap', 'puppeteer', + 'vite-node', + '@eslint/compat', + '@eslint/js', + 'typescript-eslint', ], + ignoreBinaries: ['make'], }; export default config; diff --git a/apps/web/next-sitemap.config.js b/apps/web/next-sitemap.config.js index 7c13c85..94dbb54 100644 --- a/apps/web/next-sitemap.config.js +++ b/apps/web/next-sitemap.config.js @@ -1,17 +1,13 @@ -const config = () => { - const url = process.env.NEXT_PUBLIC_ORIGIN; +// eslint-disable-next-line no-undef +const url = process.env.NEXT_PUBLIC_ORIGIN; - /** @type {import('next-sitemap').IConfig} */ - const config = { - siteUrl: url, - generateRobotsTxt: true, // (optional) - exclude: ['/server-sitemap.xml'], - robotsTxtOptions: { - additionalSitemaps: [`${url}/server-sitemap.xml`], - }, - }; - - return config; +/** @type {import('next-sitemap').IConfig} */ +// eslint-disable-next-line no-undef, import/no-commonjs +module.exports = { + siteUrl: url, + generateRobotsTxt: true, // (optional) + exclude: ['/server-sitemap.xml'], + robotsTxtOptions: { + additionalSitemaps: [`${url}/server-sitemap.xml`], + }, }; - -module.exports = config(); diff --git a/apps/web/next/web.mjs b/apps/web/next/web.mjs index 616f963..f422891 100644 --- a/apps/web/next/web.mjs +++ b/apps/web/next/web.mjs @@ -1,3 +1,5 @@ +import process from 'process'; + import withPWAInit from '@ducanh2912/next-pwa'; const withPWA = withPWAInit({ diff --git a/apps/web/package.json b/apps/web/package.json index 9fdcdf5..3336609 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -28,18 +28,14 @@ }, "devDependencies": { "@ducanh2912/next-pwa": "^10.2.8", - "@poolofdeath20/eslint-config": "^0.3.4", - "@poolofdeath20/tsconfig": "^0.1.0", + "@poolofdeath20/eslint-config": "^0.4.0", + "@poolofdeath20/tsconfig": "^0.1.1", "@types/jest-image-snapshot": "^6.4.0", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", - "@typescript-eslint/eslint-plugin": "^7.16.1", - "@typescript-eslint/parser": "^7.16.1", "axios": "^1.7.2", "ci-info": "^4.0.0", - "eslint": "^8.57.0", - "eslint-plugin-jsx-a11y": "^6.9.0", - "eslint-plugin-react": "^7.35.0", + "eslint": "^9.9.1", "gen-env-type-def": "^0.0.4", "jest-image-snapshot": "^6.4.0", "knip": "^5.26.0", diff --git a/apps/web/pages/_app.tsx b/apps/web/pages/_app.tsx index 4668647..6461f14 100644 --- a/apps/web/pages/_app.tsx +++ b/apps/web/pages/_app.tsx @@ -1,5 +1,3 @@ -import React from 'react'; - import type { AppProps } from 'next/app'; import { @@ -7,13 +5,16 @@ import { CssVarsProvider, extendTheme, } from '@mui/joy/styles'; +import Script from 'next/script'; +import React from 'react'; import '@fontsource-variable/jetbrains-mono/wght-italic.css'; +// eslint-disable-next-line import/no-unassigned-import import '@fontsource-variable/jetbrains-mono'; +import BackToTop from '../src/web/components/button/back-to-top'; import ErrorBoundary from '../src/web/components/error/boundary'; import Layout from '../src/web/components/layout'; -import BackToTop from '../src/web/components/button/back-to-top'; const App = (props: AppProps) => { const font = 'JetBrains Mono Variable'; @@ -27,15 +28,15 @@ const App = (props: AppProps) => { return ( - + - + type="module" + /> diff --git a/apps/web/pages/_document.tsx b/apps/web/pages/_document.tsx index c1b7725..f21f98a 100644 --- a/apps/web/pages/_document.tsx +++ b/apps/web/pages/_document.tsx @@ -1,17 +1,11 @@ -import React from 'react'; - -import Document, { - type DocumentContext, - Head, - Main, - NextScript, - Html, -} from 'next/document'; +import type { DocumentContext } from 'next/document'; -import { getInitColorSchemeScript } from '@mui/joy/styles'; +import InitColorSchemeScript from '@mui/joy/InitColorSchemeScript'; +import Document, { Head, Main, NextScript, Html } from 'next/document'; +import React from 'react'; export default class Doc extends Document { - static getInitialProps = async (context: DocumentContext) => { + static override getInitialProps = async (context: DocumentContext) => { const { renderPage: originalRenderPage } = context; // Run the React rendering logic synchronously @@ -32,11 +26,11 @@ export default class Doc extends Document { return await Document.getInitialProps(context); }; - render = () => { + override render() { return ( - + - + - {getInitColorSchemeScript({ defaultMode: 'dark' })} +
); - }; + } } diff --git a/apps/web/pages/classifications/[classification].tsx b/apps/web/pages/classifications/[classification].tsx index 50d8bb7..0e31e8b 100644 --- a/apps/web/pages/classifications/[classification].tsx +++ b/apps/web/pages/classifications/[classification].tsx @@ -1,10 +1,9 @@ -import type { GetStaticPaths, GetStaticProps } from 'next'; - +import type { Classification } from '../../src/common/classfication'; import type { Argument } from '@poolofdeath20/util'; +import type { GetStaticPaths, GetStaticProps } from 'next'; -import Index from '../'; +import Index from '..'; import classifications, { - type Classification, transformCategory, } from '../../src/common/classfication'; import { parseQueryParam } from '../../src/common/string'; diff --git a/apps/web/pages/compounds/index.tsx b/apps/web/pages/compounds/index.tsx index ca45840..9985e02 100644 --- a/apps/web/pages/compounds/index.tsx +++ b/apps/web/pages/compounds/index.tsx @@ -1,15 +1,12 @@ -import React from 'react'; +import type { Compounds } from '../../src/web/components/compounds'; import Box from '@mui/joy/Box'; - -import { Optional } from '@poolofdeath20/util'; - import data from '@periotable/data'; +import { Optional } from '@poolofdeath20/util'; +import React from 'react'; +import ListOfCompounds from '../../src/web/components/compounds'; import Seo from '../../src/web/components/seo'; -import ListOfCompounds, { - type Compounds, -} from '../../src/web/components/compounds'; const Compounds = () => { const compounds = data.flatMap((value) => { @@ -17,10 +14,8 @@ const Compounds = () => { }) as Compounds; return ( - + { 'names', 'articles', ]} + title={Optional.some('Compounds')} + url="/compounds" /> diff --git a/apps/web/pages/elements/[name]/[section].tsx b/apps/web/pages/elements/[name]/[section].tsx index b441540..8b49313 100644 --- a/apps/web/pages/elements/[name]/[section].tsx +++ b/apps/web/pages/elements/[name]/[section].tsx @@ -1,16 +1,13 @@ import type { GetStaticPaths, GetStaticProps } from 'next'; -import { Defined } from '@poolofdeath20/util'; - import data from '@periotable/data'; - -import Element, { - listOfPropertiesTitle, - titleToId, - getStaticPaths as getStaticPathsIndex, -} from './'; +import { Defined } from '@poolofdeath20/util'; import { parseQueryParam } from '../../../src/common/string'; +import { titleToId } from '../../../src/web/components/elements/properties'; +import { listOfPropertiesTitle } from '../../../src/web/components/elements/properties-list'; + +import Element, { getStaticPaths as getStaticPathsIndex } from '.'; const getStaticPaths = (() => { const result = getStaticPathsIndex(); @@ -32,12 +29,12 @@ const getStaticPaths = (() => { }; }) satisfies GetStaticPaths; -const getStaticProps = (async (context) => { - const name = parseQueryParam(context.params?.name); +const getStaticProps = ((context) => { + const name = parseQueryParam(context.params?.['name']); - const section = parseQueryParam(context.params?.section); + const section = parseQueryParam(context.params?.['section']); - return { + return Promise.resolve({ props: Defined.parse( data.find((element) => { return element.name_en.toLowerCase() === name; @@ -53,7 +50,7 @@ const getStaticProps = (async (context) => { }; }) .orThrow(`Element not found: ${name}`), - }; + }); }) satisfies GetStaticProps; const ElementWithSection = Element; diff --git a/apps/web/pages/elements/[name]/index.tsx b/apps/web/pages/elements/[name]/index.tsx index 410eaa0..07bb2a0 100644 --- a/apps/web/pages/elements/[name]/index.tsx +++ b/apps/web/pages/elements/[name]/index.tsx @@ -1,7 +1,3 @@ -import React from 'react'; - -import Image from 'next/image'; - import type { GetStaticPaths, GetStaticProps, @@ -12,40 +8,24 @@ import Box from '@mui/joy/Box'; import Grid from '@mui/joy/Grid'; import Stack from '@mui/joy/Stack'; import Typography from '@mui/joy/Typography'; -import Tooltip from '@mui/joy/Tooltip'; - -import { parse, string, union, number } from 'valibot'; - -import { - Optional, - type Argument, - type DeepReadonly, - capitalize, - Defined, -} from '@poolofdeath20/util'; - import data from '@periotable/data'; - -import Seo from '../../../src/web/components/seo'; -import BohrTwoDimensional from '../../../src/web/components/bohr/two-dimensional'; -import BohrThreeDimensional from '../../../src/web/components/bohr/three-dimensional'; -import InternalLink from '../../../src/web/components/link/internal'; -import ExternalLink from '../../../src/web/components/link/external'; -import ListOfCompounds, { - type Compounds, -} from '../../../src/web/components/compounds'; -import { BigTile } from '../../../src/web/components/table/element'; -import useBreakpoint from '../../../src/web/hooks/break-point'; -import { useHeaderHeight } from '../../../src/web/components/common/header'; -import { obtainNameFromUrl } from '../../../src/web/util/asset'; -import constants from '../../../src/web/constant'; +import { Optional, Defined } from '@poolofdeath20/util'; +import React from 'react'; import classifications, { transformCategory, } from '../../../src/common/classfication'; -import { parseQueryParam, spaceToDash } from '../../../src/common/string'; - -type Properties = Record; +import { parseQueryParam } from '../../../src/common/string'; +import { useHeaderHeight } from '../../../src/web/components/common/header'; +import Properties, { + filterProperties, + titleToId, +} from '../../../src/web/components/elements/properties'; +import listOfProperties from '../../../src/web/components/elements/properties-list'; +import InternalLink from '../../../src/web/components/link/internal'; +import Seo from '../../../src/web/components/seo'; +import { BigTile } from '../../../src/web/components/table/element'; +import useBreakpoint from '../../../src/web/hooks/break-point'; type GetStaticPropsType = InferGetStaticPropsType; @@ -86,780 +66,7 @@ const getStaticProps = ((context) => { }) .orThrow(`Element not found: ${name}`), }; -}) satisfies GetStaticProps< - Readonly<{ - section: string; - element: (typeof data)[number] | undefined; - }> ->; - -const titleToId = (name: string) => { - return spaceToDash(name).toLowerCase(); -}; - -const filterProperties = (properties: Properties) => { - return Object.entries(properties).filter(([_, value]) => { - return value && value !== 'NULL'; - }); -}; - -const Property = ( - props: Readonly<{ - name: string; - value: React.ReactNode; - }> -) => { - const Value = () => { - switch (typeof props.value) { - case 'object': { - return props.value; - } - case 'string': - case 'number': { - return ( - - {props.value === '' ? 'N/A' : props.value} - - ); - } - default: { - return null; - } - } - }; - - return ( - - {props.name === 'None' ? null : ( - - {props.name.replace(/_/g, ' ')} - - )} - - - ); -}; - -const Properties = ( - props: DeepReadonly<{ - title: string; - properties: Properties; - top: number; - noGrid?: true; - }> -) => { - const properties = filterProperties(props.properties); - - switch (properties.length) { - case 0: { - return null; - } - default: { - const isBohrModel = props.title === 'Bohr Model'; - - return ( - - {props.title} - {props.noGrid ? ( - - {properties.map(([key, value]) => { - return ( - - ); - })} - - ) : ( - - {properties.map(([key, value]) => { - return ( - - - - ); - })} - - )} - - ); - } - } -}; - -const Color = (color: string | number) => { - if (!color) { - return null; - } - - return ( - - - {`#${typeof color === 'number' ? color : color.toUpperCase()}`} - - ); -}; - -const generatePostfix = () => { - const valueWithSpace = ( - prop: Readonly<{ - value: string | number; - postfix: string; - }> - ) => { - return valueWithoutSpace({ - value: prop.value, - postfix: ` ${prop.postfix}`, - }); - }; - - const valueWithoutSpace = ( - prop: Readonly<{ - value: string | number; - postfix: string; - }> - ) => { - if (!prop.value) { - return ''; - } - - return `${prop.value}${prop.postfix}`; - }; - - const temperature = (value: string | number) => { - return valueWithSpace({ - value, - postfix: '°K', - }); - }; - - const density = (value: string | number) => { - return valueWithSpace({ - value, - postfix: 'kg/cm³', - }); - }; - - const heat = (value: string | number) => { - return valueWithSpace({ - value, - postfix: 'kJ/mol', - }); - }; - - const heatjoule = (value: string | number) => { - return valueWithSpace({ - value, - postfix: 'J/gK', - }); - }; - - const elastic = (value: string | number) => { - return valueWithSpace({ - value, - postfix: 'GPa', - }); - }; - - const speed = (value: string | number) => { - return valueWithSpace({ - value, - postfix: 'm/s', - }); - }; - - const electrical = (value: string | number) => { - return valueWithSpace({ - value, - postfix: 'nΩm', - }); - }; - - return { - valueWithSpace, - valueWithoutSpace, - temperature, - density, - heat, - heatjoule, - elastic, - speed, - electrical, - }; -}; - -const Compounds = ({ element }: GetStaticPropsType) => { - if (!element.compounds.length) { - return There are no known compound; - } - - return ( - - ); -}; - -const listOfPropertiesTitle = () => { - return [ - 'Basic Information', - 'Bohr Model', - 'Descriptive Numbers', - 'Mass', - 'Periodic Position', - 'Classification', - 'Abundance', - 'Color', - 'Atomic Radius', - 'Temperature', - 'Density', - 'Heat', - 'Speed of Sound', - 'Electrical Resistance', - 'Magnetic Properties', - 'Elasticity', - 'Hardness', - 'Etymology', - 'Discovery & Isolation', - 'Production & Use', - 'Radioactivity', - 'Electron Affinity', - 'Dipole Polarity', - 'Lattice', - 'Electron & Quantum', - 'List of Compounds', - ] as const; -}; - -const listOfProperties = (props: GetStaticPropsType) => { - const { element } = props; - - const postfix = generatePostfix(); - - const titles = listOfPropertiesTitle(); - - const stringOrNumber = (value: unknown) => { - return parse(union([string(), number()]), value); - }; - - return [ - { - title: titles[0], - properties: { - Name: element.name_en, - Alternative_Name: element.alternate_name, - Atomic_Number: element.number, - Gas_Phase: element.gas_phase, - Allotropes: element.allotrope_names, - Appearance: capitalize(element.appearance), - Refractive_Index: element.refractive_index, - Phase_At_STP: element.phase_at_stp, - Spectrum_Image: !element.spectrum ? null : ( - {`Spectrum - ), - Source: ( - - - Wikipedia - - - ), - }, - }, - { - title: titles[1], - properties: { - Static: ( - - {!element.bohrModel.static ? null : ( - - )} - - ), - Interactive: ( - - {!element.bohrModel.interactive ? null : ( - - )} - - ), - }, - }, - { - title: titles[2], - properties: { - CAS_Number: element.cas_number, - CID_Number: element.cid_number, - DOT_Number: element.dot_number, - RTECS_Number: element.rtecs_number, - Mendeleev_Number: element.mendeleev_number, - Pettifor_Number: element.pettifor_number, - Eu_Number: element.eu_number, - Space_Group_Number: element.space_group_number, - Glawe_Number: element.glawe_number, - }, - }, - { - title: titles[3], - properties: { - Atomic_Mass: `${element.atomic_mass} Da`, - Uncertainty: element.atomic_mass_uncertainty, - }, - }, - { - title: titles[4], - properties: { - X_Position: element.xpos, - Y_Position: element.ypos, - Period: element.period, - Group: element.group, - }, - }, - { - title: titles[5], - properties: { - Block: element.block, - Category: element.category_code - .split('_') - .map(capitalize) - .join(' '), - Geochemical: element.geochemical_class, - Goldschmidt: element.goldschmidt_class, - Electrical_Type: element.electrical_type, - }, - }, - { - title: titles[6], - properties: { - Urban_Soil: postfix.valueWithSpace({ - value: element.abundance_urban_soil, - postfix: 'mg/kg', - }), - Seawater: postfix.valueWithSpace({ - value: element.abundance_seawater_w1, - postfix: 'kg/L', - }), - Sun: postfix.valueWithSpace({ - value: element.abundance_sun_s1, - postfix: 'mole ratio to silicon', - }), - Earth_Crust: postfix.valueWithSpace({ - value: element.abundance_in_earth_crust_c1, - postfix: 'g', - }), - Human_Body: postfix.valueWithSpace({ - value: element.abundance_humans, - postfix: '%', - }), - Solar_System: postfix.valueWithSpace({ - value: element.abundance_solar_system_y1, - postfix: 'mole ratio to silicon', - }), - Meteorites: postfix.valueWithSpace({ - value: element.abundance_meteorite, - postfix: '%', - }), - }, - }, - { - title: titles[7], - properties: { - Jmol: Color(element.jmol_color), - Molcas_Gv: Color(element.molcas_gv_color), - CPK: Color(element.cpk_color), - }, - }, - { - title: titles[8], - properties: { - Empirical: element.atomic_radius_empirical, - Calculated: element.atomic_radius_calculated, - Van_Der_Waals: element.atomic_radius_vanderwaals, - Bonding: element.vdw_radius_bondi, - Room_Temperature: element.vdw_radius_rt, - Batsanov: element.vdw_radius_batsanov, - Rahm: element.atomic_radius_rahm, - Dreiding: element.vdw_radius_dreiding, - Uff: element.vdw_radius_uff, - Mm3: element.vdw_radius_mm3, - Alvarez: element.vdw_radius_alvarez, - Bragg: element.covalent_radius_bragg, - Truhlar: element.vdw_radius_truhlar, - 'Covalent (Single Bound)': - element.atomic_radius_covalent_single_bond, - 'Covalent (Triple Bound)': - element.atomic_radius_covalent_triple_bond, - 'Covalent (Cordero)': element.covalent_radius_cordero, - 'Covalent (Pyykko)': element.covalent_radius_pyykko, - 'Covalent (Pyykko Double)': - element.covalent_radius_pyykko_double, - 'Covalent (Pyykko Triple)': - element.covalent_radius_pyykko_triple, - Mendeleev: element.metallic_radius_mendeleev, - C12: element.metallic_radius_c12, - Metallic: element.atomic_radius_metallic, - }, - }, - { - title: titles[9], - properties: { - 'Melting/Freeze (USE)': postfix.temperature(element.melt_use), - 'Melting/Freeze (WEL)': postfix.temperature(element.melt_WEL), - 'Melting/Freeze (CRC)': postfix.temperature(element.melt_CRC), - 'Melting/Freeze (LNG)': postfix.temperature(element.melt_LNG), - 'Boiling/Density (USE)': postfix.temperature(element.boil_use), - 'Boiling/Density (WEL)': postfix.temperature(element.boil_WEL), - 'Boiling/Density (CRC)': postfix.temperature(element.boil_CRC), - 'Boiling/Density (LNG)': postfix.temperature(element.boil_LNG), - 'Boiling/Density (Zhang)': postfix.temperature( - element.boil_Zhang - ), - Curie_Point: postfix.valueWithSpace({ - value: element.curie_point, - postfix: 'Tc', - }), - Superconducting_Point: postfix.temperature( - element.superconducting_point - ), - 'Critical Temperature': postfix.temperature( - element.critical_temperature - ), - Flashpoint: postfix.temperature(element.flashpoint), - Autoignition_Point: postfix.temperature( - element.autoignition_point - ), - Critical_Pressure: postfix.valueWithSpace({ - value: element.critical_pressure, - postfix: 'MPa', - }), - }, - }, - { - title: titles[10], - properties: { - STP: postfix.density(element.density_rt), - 'Solid (WEL)': postfix.density(element.density_solid_WEL), - 'Solid (CRC)': postfix.density(element.density_solid_CRC), - 'Solid (LNG)': postfix.density(element.density_solid_LNG), - 'Liquid (CR2)': postfix.density(element.density_liquid_cr2), - Gas: postfix.density(element.density_gas), - }, - }, - { - title: titles[11], - properties: { - Molar_Volume: postfix.valueWithSpace({ - value: element.molar_volume, - postfix: 'cm³/mol', - }), - Atomic_Volume: postfix.valueWithSpace({ - value: element.atomic_volume, - postfix: 'cm³', - }), - Heat_Of_Fusion_USE: postfix.heat(element.enthalpy_of_fusion), - Heat_Of_Fusion_CRC: postfix.heat(element.heat_of_fusion_crc), - Heat_Of_Fusion_LNG: postfix.heat(element.heat_of_fusion_lng), - Heat_Of_Fusion_WEL: postfix.heat(element.heat_of_fusion_wel), - Evaporation_USE: postfix.heat(element.evaporation_heat), - Evaporation_CRC: postfix.heat(element.heat_of_vaporization_crc), - Evaporation_LNG: postfix.heat(element.heat_of_vaporization_lng), - Evaporation_WEL: postfix.heat(element.heat_of_vaporization_wel), - Evaporation_Zhang: postfix.heat( - element.heat_of_vaporization_zhang - ), - Combustion: postfix.valueWithSpace({ - value: element.heat_of_combustion, - postfix: 'kJ/mol', - }), - Molar_Heat: postfix.valueWithSpace({ - value: element.molar_heat, - postfix: 'J/molK', - }), - Heat_Capacity_USE: postfix.heatjoule( - element.specific_heat_capacity - ), - Heat_Capacity_CRC: postfix.heatjoule(element.specific_heat_crc), - Heat_Capacity_LNG: postfix.heatjoule(element.specific_heat_lng), - Heat_Capacity_WEL: postfix.heatjoule(element.specific_heat_wel), - Thermal_Conductivity: postfix.valueWithSpace({ - value: element.thermal_conductivity, - postfix: 'W/m*K', - }), - Thermal_Expansion: postfix.valueWithSpace({ - value: element.thermal_expansion, - postfix: '1/K', - }), - Adiabatic_Index: element.adiabatic_index, - }, - }, - { - title: titles[12], - properties: { - Longitudinal: postfix.speed( - element.speed_of_sound_longitudinal - ), - Transversal: postfix.speed(element.speed_of_sound_transversal), - Extensional: postfix.speed(element.speed_of_sound_extensional), - Fluid: postfix.speed(element.speed_of_sound_fluid), - }, - }, - { - title: titles[13], - properties: { - '80k': postfix.electrical(element.electrical_resistivity_80K), - '273k': postfix.electrical(element.electrical_resistivity_273K), - '293k': postfix.electrical(element.electrical_resistivity_293K), - '298k': postfix.electrical(element.electrical_resistivity_298K), - '300k': postfix.electrical(element.electrical_resistivity_300K), - '500k': postfix.electrical(element.electrical_resistivity_500K), - }, - }, - { - title: titles[14], - properties: { - Order: element.magnetic_ordering, - Neel_Point: postfix.valueWithSpace({ - value: element.neel_point, - postfix: 'Tn', - }), - Susceptibility: postfix.valueWithSpace({ - value: element.magnetic_susceptibility, - postfix: 'm3/kg', - }), - }, - }, - { - title: titles[15], - properties: { - Shear_Modulus: postfix.elastic(element.shear_modulus), - Bulk_Modulus: postfix.elastic(element.bulk_modulus), - Poisson_Ratio: postfix.valueWithSpace({ - value: element.poisson_ratio, - postfix: 'ν', - }), - Youngs_Modulus: postfix.elastic(element.youngs_modulus), - }, - }, - { - title: titles[16], - properties: { - Mohs: element.mohs_hardness, - Brinell: element.brinell_hardness, - Vickers: element.vickers_hardness, - }, - }, - { - title: titles[17], - noGrid: true, - properties: { - Description: element.description, - Language_Of_Origin: element.language_of_origin, - Origin_Of_Word: element.origin_of_word, - Meaning: element.meaning, - Symbol_Origin: element.symbol_origin, - Etymological_Description: element.etymological_description, - }, - }, - { - title: titles[18], - properties: { - 'Observed/Predicted By': element.observed_predicted_by, - 'Observed/Discovery Year': - element.observation_or_discovery_year, - Discovery_Location: !element.countries.length ? null : ( - - {element.countries.map((country) => { - return ( - - {country.name} - - ); - })} - - ), - Isolated_Sample_By: element.isolated_sampled_by, - Isolated_Sample_Year: element.isolation_sample_year, - Named_By: element.named_by, - }, - }, - { - title: titles[19], - noGrid: true, - properties: { - Sources: element.sources, - Uses: element.uses, - }, - }, - { - title: titles[20], - properties: { - Half_Life: element.half_life, - Lifetime: element.lifetime, - Decay_Mode: element.decay_mode, - Neutron_Mass_Absorption: element.neutron_mass_absorption, - Neutron_Cross_Section: element.neutron_cross_section, - }, - }, - { - title: titles[21], - properties: { - Proton_Affinity: element.proton_affinity, - 'Electron_Affinity (eV)': element.electron_affinity_eV, - 'Electron_Affinity (kJ/mol)': element.electron_affinity_kJmol, - 'Electron_Affinity (pauling)': - element.electronegativity_pauling, - 'Electron_Affinity (allen)': element.electronegativity_allen, - 'Electron_Affinity (ghosh)': element.electronegativity_ghosh, - }, - }, - { - title: titles[22], - properties: { - Accepted: element.dipole_polarizability, - Uncertainty: element.dipole_polarizability_unc, - 'C6 GB': element.c6_gb, - 'C6 Coefficient': element.c6_coeff, - }, - }, - { - title: titles[23], - properties: { - Constant_Internal_Default_Radius: - element.lattice_constant_internal_default_radii, - Constant: element.lattice_constant, - Strucutre: element.lattice_structure, - Angles: element.lattice_angles, - }, - }, - { - title: titles[24], - properties: { - Oxidation_States: element.oxidation_states, - Electron_Configuration: element.electron_configuration, - Quantum_Number: element.quantum_number, - Electron_Configuration_Semantic: - element.electron_configuration_semantic, - ...Array.from({ length: 8 }, (_, index) => { - return { - [`Shells-${index}`]: stringOrNumber( - // @ts-expect-error: length of 8 will generate 0 to 7, which is within the range of `shells-number` - element[`shells-${index}`] - ), - }; - }).reduce((accumulator, value) => { - return { - ...accumulator, - ...value, - }; - }, {}), - ...Array.from({ length: 30 }, (_, index) => { - return { - [`Ionization Energies-${index}`]: stringOrNumber( - // @ts-expect-error: length of 30 will be within 30, which is within the range of `ionization_energies-number` - element[`ionization_energies-${index}`] - ), - }; - }).reduce((accumulator, value) => { - return { - ...accumulator, - ...value, - }; - }, {}), - }, - }, - { - title: titles[25], - noGrid: true, - properties: { - None: , - }, - }, - ] as const satisfies ReadonlyArray< - Omit, 'top'> - >; -}; +}) satisfies GetStaticProps; const Element = (props: GetStaticPropsType) => { const { element, section } = props; @@ -904,15 +111,14 @@ const Element = (props: GetStaticPropsType) => { .getElementById(section) ?.style.setProperty('scroll-margin-top', ''); } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [height, element]); const url = `/elements/${element.name_en.toLowerCase()}`; return ( - + { element.period, element.atomic_mass, ]} + title={Optional.some(element.name_en)} + url={sectionValid ? url : `${url}/${section}`} /> { }} > { {properties @@ -975,8 +183,8 @@ const Element = (props: GetStaticPropsType) => { return ( { { return { borderTop: isSmall @@ -1045,7 +253,8 @@ const Element = (props: GetStaticPropsType) => { ); }; -export { listOfPropertiesTitle, titleToId }; export { getStaticProps, getStaticPaths }; +export type { GetStaticPropsType }; + export default Element; diff --git a/apps/web/pages/subshells/[subshell].tsx b/apps/web/pages/subshells/[subshell].tsx index 7e10a20..04d1877 100644 --- a/apps/web/pages/subshells/[subshell].tsx +++ b/apps/web/pages/subshells/[subshell].tsx @@ -1,10 +1,10 @@ -import type { GetStaticPaths, GetStaticProps } from 'next'; - +import type { Subshell } from '../../src/common/subshell'; import type { Argument } from '@poolofdeath20/util'; +import type { GetStaticPaths, GetStaticProps } from 'next'; -import Index from '../'; -import subshells, { type Subshell } from '../../src/common/subshell'; +import Index from '..'; import { parseQueryParam } from '../../src/common/string'; +import subshells from '../../src/common/subshell'; type SubshellProps = Readonly<{ subshell: Subshell | undefined; diff --git a/apps/web/script/assets/countries.ts b/apps/web/script/assets/countries.ts index 66530bd..f3aff4f 100644 --- a/apps/web/script/assets/countries.ts +++ b/apps/web/script/assets/countries.ts @@ -1,8 +1,7 @@ import fs from 'fs'; -import axios from 'axios'; - import data from '@periotable/data'; +import axios from 'axios'; import constants from '../../src/web/constant'; @@ -17,16 +16,22 @@ const element = () => { data.flatMap((data) => { return data.countries; - }).forEach(async (country) => { - const response = await axios.get(country.link, { - responseType: 'arraybuffer', - }); - - fs.writeFile(`${path}/${country.svg}`, response.data, (error) => { - if (error) { - throw error; - } - }); + }).forEach((country) => { + void axios + .get(country.link, { + responseType: 'arraybuffer', + }) + .then(({ data }) => { + if (!Buffer.isBuffer(data)) { + throw new Error('Data is not a buffer'); + } + + fs.writeFile(`${path}/${country.svg}`, data, (error) => { + if (error) { + throw error; + } + }); + }); }); }; diff --git a/apps/web/script/assets/element.ts b/apps/web/script/assets/element.ts index 7ef5438..a0f7b10 100644 --- a/apps/web/script/assets/element.ts +++ b/apps/web/script/assets/element.ts @@ -1,10 +1,8 @@ import fs from 'fs'; -import axios from 'axios'; - -import { isNotUndefined } from '@poolofdeath20/util'; - import data from '@periotable/data'; +import { isNotUndefined } from '@poolofdeath20/util'; +import axios from 'axios'; import constants from '../../src/web/constant'; import { obtainNameFromUrl } from '../../src/web/util/asset'; @@ -54,16 +52,22 @@ const element = () => { url: newSpectrum, }, ].filter(isNotUndefined); - }).forEach(async (props) => { - const response = await axios.get(props.url, { - responseType: 'arraybuffer', - }); + }).forEach((props) => { + void axios + .get(props.url, { + responseType: 'arraybuffer', + }) + .then(({ data }) => { + if (!Buffer.isBuffer(data)) { + throw new Error('Data is not a buffer'); + } - fs.writeFile(props.name, response.data, (error) => { - if (error) { - throw error; - } - }); + fs.writeFile(props.name, data, (error) => { + if (error) { + throw error; + } + }); + }); }); }; diff --git a/apps/web/script/assets/images.ts b/apps/web/script/assets/images.ts index 99ee622..ac37b6d 100644 --- a/apps/web/script/assets/images.ts +++ b/apps/web/script/assets/images.ts @@ -1,5 +1,5 @@ -import element from './element'; import countries from './countries'; +import element from './element'; const main = () => { countries(); diff --git a/apps/web/script/seo/schema.ts b/apps/web/script/seo/schema.ts index 9ff6785..adfb51a 100644 --- a/apps/web/script/seo/schema.ts +++ b/apps/web/script/seo/schema.ts @@ -11,6 +11,7 @@ const main = async () => { `const paths = [] as ReadonlyArray\n; export default paths;` ); + // eslint-disable-next-line import/dynamic-import-chunkname const paths = await import('../../test/snapshot/data').then((data) => { return data.generatePaths(); }); @@ -23,4 +24,4 @@ const main = async () => { process.exit(0); }; -main(); +void main(); diff --git a/apps/web/script/test/snapshot.ts b/apps/web/script/test/snapshot.ts index 3a574f7..e8a1738 100644 --- a/apps/web/script/test/snapshot.ts +++ b/apps/web/script/test/snapshot.ts @@ -1,21 +1,23 @@ +import type { DeepReadonly } from '@poolofdeath20/util'; + import fs from 'fs'; import ci from 'ci-info'; -import type { DeepReadonly } from '@poolofdeath20/util'; - import { generatePaths } from '../../test/snapshot/data'; const code = ( props: DeepReadonly<{ index: number; - paths: string[]; + paths: Array; }> ) => { return [ `import { testSnapshot } from '.'`, `testSnapshot(${props.index}, [${props.paths - .map((path) => `'${path}'`) + .map((path) => { + return `'${path}'`; + }) .join(', ')}]);`, ].join('\n'); }; diff --git a/apps/web/src/common/type.ts b/apps/web/src/common/type.ts deleted file mode 100644 index c15a5a9..0000000 --- a/apps/web/src/common/type.ts +++ /dev/null @@ -1,15 +0,0 @@ -type DeepReadonly = T extends (infer R)[] - ? ReadonlyArray> - : T extends Set - ? ReadonlySet> - : T extends Function - ? T - : T extends Object - ? DeepReadonlyObject - : T; - -type DeepReadonlyObject = { - readonly [P in keyof T]: DeepReadonly; -}; - -export type { DeepReadonly, DeepReadonlyObject }; diff --git a/apps/web/src/web/components/bohr/three-dimensional.tsx b/apps/web/src/web/components/bohr/three-dimensional.tsx index 9c39fc2..4005654 100644 --- a/apps/web/src/web/components/bohr/three-dimensional.tsx +++ b/apps/web/src/web/components/bohr/three-dimensional.tsx @@ -9,24 +9,24 @@ const BohrThreeDimensional = ( return ( // @ts-expect-error: model-viewer is injected by Google ); }; diff --git a/apps/web/src/web/components/bohr/two-dimensional.tsx b/apps/web/src/web/components/bohr/two-dimensional.tsx index c735b23..209e767 100644 --- a/apps/web/src/web/components/bohr/two-dimensional.tsx +++ b/apps/web/src/web/components/bohr/two-dimensional.tsx @@ -1,6 +1,5 @@ -import React from 'react'; - import Image from 'next/image'; +import React from 'react'; const BohrTwoDimensional = ( props: Readonly<{ @@ -12,11 +11,11 @@ const BohrTwoDimensional = ( return ( {`A ); }; diff --git a/apps/web/src/web/components/button/back-to-top/index.tsx b/apps/web/src/web/components/button/back-to-top/index.tsx index 5bc1a0f..4baf4db 100644 --- a/apps/web/src/web/components/button/back-to-top/index.tsx +++ b/apps/web/src/web/components/button/back-to-top/index.tsx @@ -1,7 +1,5 @@ -import React from 'react'; - import IconButton from '@mui/joy/IconButton'; - +import React from 'react'; import { FaArrowUp } from 'react-icons/fa6'; import { useHeight } from '../../../hooks/dimension'; @@ -12,14 +10,13 @@ const BackToTop = () => { return height <= 500 ? null : ( { window.scrollTo({ top: 0, behavior: 'smooth', }); }} - variant="soft" + size="lg" sx={(theme) => { return { zIndex: 3, @@ -29,6 +26,7 @@ const BackToTop = () => { border: `1px solid ${theme.palette.background.level2}`, }; }} + variant="soft" > diff --git a/apps/web/src/web/components/common/footer/index.tsx b/apps/web/src/web/components/common/footer/index.tsx index cd381ad..17b7e53 100644 --- a/apps/web/src/web/components/common/footer/index.tsx +++ b/apps/web/src/web/components/common/footer/index.tsx @@ -1,20 +1,15 @@ -import React from 'react'; - -import Image from 'next/image'; - +import Divider from '@mui/joy/Divider'; import IconButton from '@mui/joy/IconButton'; -import Typography from '@mui/joy/Typography'; import Stack from '@mui/joy/Stack'; -import Divider from '@mui/joy/Divider'; - +import Typography from '@mui/joy/Typography'; +import Image from 'next/image'; +import React from 'react'; import { SiMui, SiNextdotjs, SiTypescript } from 'react-icons/si'; -import useBreakpoint from '../../../hooks/break-point'; - import constants from '../../../constant'; - -import InternalLink from '../../link/internal'; +import useBreakpoint from '../../../hooks/break-point'; import ExternalLink from '../../link/external'; +import InternalLink from '../../link/internal'; const Footer = () => { const breakpoint = useBreakpoint(); @@ -24,26 +19,26 @@ const Footer = () => { const size = isSmall ? 36 : 64; return ( - - + + {breakpoint === 'xs' ? null : ( - + logo - + {[ { section: 'Report a bug', @@ -61,8 +56,8 @@ const Footer = () => { return ( { )} { CC BY-NC-SA 4.0 @@ -135,7 +130,7 @@ const Footer = () => { 2024 © Gervin Fung - + {[ { link: 'nextjs.org', @@ -157,15 +152,15 @@ const Footer = () => { return ( { const [height, setHeight] = React.useState(0); @@ -42,31 +29,11 @@ const Header = () => { const height = useHeight(); - const breakpoint = useBreakpoint(); - - const links = [ - { - name: 'Home', - href: '/', - isExternal: false, - }, - { - name: 'Compounds', - href: '/compounds', - isExternal: false, - }, - { - href: constants.repo, - isExternal: true, - }, - ]; - return ( { return { position: { @@ -84,13 +51,14 @@ const Header = () => { : undefined, }; }} + width="100%" > { }, }} > - + logo - {breakpoint === 'xs' ? ( - - - - - - {links.map((link) => { - switch (link.isExternal) { - case false: { - return ( - - - - {link.name} - - - - ); - } - case true: { - return ( - - - - Github - - - - ); - } - } - })} - - - ) : ( - - {links.map((link) => { - switch (link.isExternal) { - case false: { - return ( - - {link.name} - - ); - } - case true: { - return ( - - - - - - ); - } - } - })} - - )} + ); diff --git a/apps/web/src/web/components/common/header/menu.tsx b/apps/web/src/web/components/common/header/menu.tsx new file mode 100644 index 0000000..d7fca1c --- /dev/null +++ b/apps/web/src/web/components/common/header/menu.tsx @@ -0,0 +1,136 @@ +import Dropdown from '@mui/joy/Dropdown'; +import IconButton from '@mui/joy/IconButton'; +import Menu from '@mui/joy/Menu'; +import MenuButton from '@mui/joy/MenuButton'; +import MenuItem from '@mui/joy/MenuItem'; +import Stack from '@mui/joy/Stack'; +import Typography from '@mui/joy/Typography'; +import React from 'react'; +import { CiMenuBurger } from 'react-icons/ci'; +import { SiGithub } from 'react-icons/si'; + +import constants from '../../../constant'; +import useBreakpoint from '../../../hooks/break-point'; +import ExternalLink from '../../link/external'; +import InternalLink from '../../link/internal'; + +const links = [ + { + name: 'Home', + href: '/', + isExternal: false, + }, + { + name: 'Compounds', + href: '/compounds', + isExternal: false, + }, + { + href: constants.repo, + isExternal: true, + }, +]; + +const DropdownMenu = () => { + return ( + + + + + + {links.map((link) => { + switch (link.isExternal) { + case false: { + return ( + + + + {link.name} + + + + ); + } + case true: { + return ( + + + + Github + + + + ); + } + } + })} + + + ); +}; + +const StackMenu = () => { + return ( + + {links.map((link) => { + switch (link.isExternal) { + case false: { + return ( + + {link.name} + + ); + } + case true: { + return ( + + + + + + ); + } + } + })} + + ); +}; + +const HeaderMenu = () => { + const breakpoint = useBreakpoint(); + + return breakpoint === 'xs' ? : ; +}; + +export default HeaderMenu; diff --git a/apps/web/src/web/components/common/input/index.tsx b/apps/web/src/web/components/common/input/index.tsx index 380351d..ff01c21 100644 --- a/apps/web/src/web/components/common/input/index.tsx +++ b/apps/web/src/web/components/common/input/index.tsx @@ -1,9 +1,9 @@ -import React from 'react'; +import type { DeepReadonly } from '@poolofdeath20/util'; import Box from '@mui/joy/Box'; import Input from '@mui/joy/Input'; - -import { type DeepReadonly, Optional } from '@poolofdeath20/util'; +import { Optional } from '@poolofdeath20/util'; +import React from 'react'; const SearchBar = ( props: DeepReadonly<{ @@ -17,7 +17,11 @@ const SearchBar = ( return ( { + const { value } = event.target; + + props.search.setValue(Optional.some(value)); + }} placeholder={props.placeholder} sx={{ width: { @@ -26,11 +30,7 @@ const SearchBar = ( }, }} value={props.search.value.unwrapOrGet('')} - onChange={(event) => { - const { value } = event.target; - - props.search.setValue(Optional.some(value)); - }} + variant="outlined" /> ); diff --git a/apps/web/src/web/components/compounds/index.tsx b/apps/web/src/web/components/compounds/index.tsx index 120dcf7..ec7040a 100644 --- a/apps/web/src/web/components/compounds/index.tsx +++ b/apps/web/src/web/components/compounds/index.tsx @@ -1,57 +1,36 @@ -import React from 'react'; - -import { useRouter } from 'next/router'; +import type { DeepReadonly, Optional } from '@poolofdeath20/util'; -import Button from '@mui/joy/Button'; import Stack from '@mui/joy/Stack'; import Table from '@mui/joy/Table'; import Typography from '@mui/joy/Typography'; -import IconButton from '@mui/joy/IconButton'; -import FormControl from '@mui/joy/FormControl'; -import FormLabel from '@mui/joy/FormLabel'; -import Select from '@mui/joy/Select'; -import Option from '@mui/joy/Option'; - -import { MdOutlineChevronLeft, MdOutlineChevronRight } from 'react-icons/md'; - -import { - type DeepReadonly, - Defined, - type Return, - formQueryParamStringFromRecord, - type Optional, -} from '@poolofdeath20/util'; - +import { formQueryParamStringFromRecord } from '@poolofdeath20/util'; +import { useRouter } from 'next/router'; +import React from 'react'; import { useDebounce } from 'use-debounce'; -import InternalLink from '../link/internal'; -import ExternalLink from '../link/external'; -import SearchBar from '../common/input'; +import { spaceToUnderscore } from '../../../common/string'; +import useBreakpoint from '../../hooks/break-point'; import { useCurrentPage, usePagination, useRowsPerPage, } from '../../hooks/pagination'; import useSearchQuery from '../../hooks/search'; -import useBreakpoint from '../../hooks/break-point'; - -import { spaceToUnderscore } from '../../../common/string'; - -type Query = () => Readonly>; +import SearchBar from '../common/input'; +import ExternalLink from '../link/external'; -type QueryValue = ( - props: Readonly<{ - old: number; - new: number; - }> -) => Return; +import { + DirectionPaginationButton, + PaginationButton, +} from './pagination-button'; +import RowsSelect from './row-select'; type Compounds = DeepReadonly< - { + Array<{ molecularformula: string; - allnames: string[]; - articles: string[]; - }[] + allnames: Array; + articles: Array; + }> >; const getMaxFrom = (compounds: Compounds) => { @@ -102,142 +81,18 @@ const useCompounds = ( return props.compounds; }) ); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [props.search]); return compounds; }; -const DirectionPaginationButton = ( +const CompoundName = ( props: Readonly<{ - direction: 'left' | 'right'; - path: string; - isLimit: boolean; - query: Query; + name: string; }> ) => { - const Direction = - props.direction === 'left' - ? MdOutlineChevronLeft - : MdOutlineChevronRight; - - const Button = ( - nestProps: Readonly<{ - isDisabled?: true; - }> - ) => { - return ( - - - - ); - }; - - if (props.isLimit) { - return - - ); -}; - -const RowsSelect = ( - props: Readonly<{ - path: string; - query: QueryValue; - rows: number; - }> -) => { - const router = useRouter(); - - return ( - - Rows per page: - - - ); + return {props.name}; }; const ListOfCompounds = ( @@ -292,7 +147,7 @@ const ListOfCompounds = ( if (oldSearch !== search) { switch (props.useNativeRouter) { case false: { - router.push( + void router.push( { pathname: props.path, query: { @@ -331,6 +186,7 @@ const ListOfCompounds = ( } } }); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [debounceSearch]); return ( @@ -355,11 +211,11 @@ const ListOfCompounds = ( md: 'row', xs: 'column', }} + justifyContent="space-between" spacing={{ md: 0, xs: 4, }} - justifyContent="space-between" > { // ref: https://ux.stackexchange.com/a/87617 const first = (current - 1) * result.old; @@ -393,6 +248,7 @@ const ListOfCompounds = ( rows: result.new, }; }} + rows={rows} /> {!compounds.length ? null : ( @@ -429,30 +285,29 @@ const ListOfCompounds = ( } ); - const Name = () => { + if (!article) { return ( - - {name} - + ); - }; - - if (!article) { - return ; } return ( - + ); })} @@ -475,8 +330,8 @@ const ListOfCompounds = ( > { return { rows, @@ -485,13 +340,12 @@ const ListOfCompounds = ( }; }} /> - {pagination.map((page, index) => { + {pagination.map((page) => { return ( { return { rows, @@ -499,13 +353,14 @@ const ListOfCompounds = ( search: search.unwrapOrGet(''), }; }} + value={page} /> ); })} { return { rows, diff --git a/apps/web/src/web/components/compounds/pagination-button.tsx b/apps/web/src/web/components/compounds/pagination-button.tsx new file mode 100644 index 0000000..469c196 --- /dev/null +++ b/apps/web/src/web/components/compounds/pagination-button.tsx @@ -0,0 +1,110 @@ +import type { Argument } from '@poolofdeath20/util'; + +import Button from '@mui/joy/Button'; +import IconButton from '@mui/joy/IconButton'; +import Typography from '@mui/joy/Typography'; +import React from 'react'; +import { MdOutlineChevronLeft, MdOutlineChevronRight } from 'react-icons/md'; + +import InternalLink from '../link/internal'; + +type Query = () => Readonly>; + +const PaginationIconButton = ( + props: Readonly<{ + direction: 'left' | 'right'; + isDisabled?: true; + }> +) => { + const Direction = + props.direction === 'left' + ? MdOutlineChevronLeft + : MdOutlineChevronRight; + + return ( + + + + ); +}; + +const DirectionPaginationButton = ( + props: Readonly<{ + direction: Argument['direction']; + path: string; + isLimit: boolean; + query: Query; + }> +) => { + if (props.isLimit) { + return ( + + ); + } else { + return ( + + + + ); + } +}; + +const PaginationButton = ( + props: Readonly<{ + value: string | number; + path: string; + isCurrent: boolean; + query: Query; + }> +) => { + if (typeof props.value === 'string') { + return {props.value}; + } + + return ( + + + + ); +}; + +export type { Query }; + +export { DirectionPaginationButton, PaginationButton }; diff --git a/apps/web/src/web/components/compounds/row-select.tsx b/apps/web/src/web/components/compounds/row-select.tsx new file mode 100644 index 0000000..cd3a5ee --- /dev/null +++ b/apps/web/src/web/components/compounds/row-select.tsx @@ -0,0 +1,67 @@ +import type { Query } from './pagination-button'; +import type { Return } from '@poolofdeath20/util'; + +import FormControl from '@mui/joy/FormControl'; +import FormLabel from '@mui/joy/FormLabel'; +import Option from '@mui/joy/Option'; +import Select from '@mui/joy/Select'; +import { Defined } from '@poolofdeath20/util'; +import { useRouter } from 'next/router'; +import React from 'react'; + +type QueryValue = ( + props: Readonly<{ + old: number; + new: number; + }> +) => Return; + +const RowsSelect = ( + props: Readonly<{ + path: string; + query: QueryValue; + rows: number; + }> +) => { + const router = useRouter(); + + return ( + + Rows per page: + + + ); +}; + +export default RowsSelect; diff --git a/apps/web/src/web/components/elements/properties-list.tsx b/apps/web/src/web/components/elements/properties-list.tsx new file mode 100644 index 0000000..709e2a9 --- /dev/null +++ b/apps/web/src/web/components/elements/properties-list.tsx @@ -0,0 +1,670 @@ +import type Properties from './properties'; +import type { GetStaticPropsType } from '../../../../pages/elements/[name]'; +import type { Argument } from '@poolofdeath20/util'; + +import Box from '@mui/joy/Box'; +import Stack from '@mui/joy/Stack'; +import Tooltip from '@mui/joy/Tooltip'; +import Typography from '@mui/joy/Typography'; +import { capitalize } from '@poolofdeath20/util'; +import Image from 'next/image'; +import { parse, string, union, number } from 'valibot'; + +import constants from '../../constant'; +import { obtainNameFromUrl } from '../../util/asset'; +import BohrThreeDimensional from '../bohr/three-dimensional'; +import BohrTwoDimensional from '../bohr/two-dimensional'; +import ListOfCompounds from '../compounds'; +import ExternalLink from '../link/external'; + +const generatePostfix = () => { + const valueWithSpace = ( + prop: Readonly<{ + value: string | number; + postfix: string; + }> + ) => { + return valueWithoutSpace({ + value: prop.value, + postfix: ` ${prop.postfix}`, + }); + }; + + const valueWithoutSpace = ( + prop: Readonly<{ + value: string | number; + postfix: string; + }> + ) => { + if (!prop.value) { + return ''; + } + + return `${prop.value}${prop.postfix}`; + }; + + const temperature = (value: string | number) => { + return valueWithSpace({ + value, + postfix: '°K', + }); + }; + + const density = (value: string | number) => { + return valueWithSpace({ + value, + postfix: 'kg/cm³', + }); + }; + + const heat = (value: string | number) => { + return valueWithSpace({ + value, + postfix: 'kJ/mol', + }); + }; + + const heatjoule = (value: string | number) => { + return valueWithSpace({ + value, + postfix: 'J/gK', + }); + }; + + const elastic = (value: string | number) => { + return valueWithSpace({ + value, + postfix: 'GPa', + }); + }; + + const speed = (value: string | number) => { + return valueWithSpace({ + value, + postfix: 'm/s', + }); + }; + + const electrical = (value: string | number) => { + return valueWithSpace({ + value, + postfix: 'nΩm', + }); + }; + + return { + valueWithSpace, + valueWithoutSpace, + temperature, + density, + heat, + heatjoule, + elastic, + speed, + electrical, + }; +}; + +const listOfPropertiesTitle = () => { + return [ + 'Basic Information', + 'Bohr Model', + 'Descriptive Numbers', + 'Mass', + 'Periodic Position', + 'Classification', + 'Abundance', + 'Color', + 'Atomic Radius', + 'Temperature', + 'Density', + 'Heat', + 'Speed of Sound', + 'Electrical Resistance', + 'Magnetic Properties', + 'Elasticity', + 'Hardness', + 'Etymology', + 'Discovery & Isolation', + 'Production & Use', + 'Radioactivity', + 'Electron Affinity', + 'Dipole Polarity', + 'Lattice', + 'Electron & Quantum', + 'List of Compounds', + ] as const; +}; + +const Color = (color: string | number) => { + if (!color) { + return null; + } + + return ( + + + {`#${typeof color === 'number' ? color : color.toUpperCase()}`} + + ); +}; + +const Compounds = ({ element }: GetStaticPropsType) => { + if (!element.compounds.length) { + return There are no known compound; + } + + return ( + + ); +}; + +const listOfProperties = (props: GetStaticPropsType) => { + const { element } = props; + + const postfix = generatePostfix(); + + const titles = listOfPropertiesTitle(); + + const stringOrNumber = (value: unknown) => { + return parse(union([string(), number()]), value); + }; + + return [ + { + title: titles[0], + properties: { + Name: element.name_en, + Alternative_Name: element.alternate_name, + Atomic_Number: element.number, + Gas_Phase: element.gas_phase, + Allotropes: element.allotrope_names, + Appearance: capitalize(element.appearance), + Refractive_Index: element.refractive_index, + Phase_At_STP: element.phase_at_stp, + Spectrum_Image: !element.spectrum ? null : ( + {`Spectrum + ), + Source: ( + + + Wikipedia + + + ), + }, + }, + { + title: titles[1], + properties: { + Static: ( + + {!element.bohrModel.static ? null : ( + + )} + + ), + Interactive: ( + + {!element.bohrModel.interactive ? null : ( + + )} + + ), + }, + }, + { + title: titles[2], + properties: { + CAS_Number: element.cas_number, + CID_Number: element.cid_number, + DOT_Number: element.dot_number, + RTECS_Number: element.rtecs_number, + Mendeleev_Number: element.mendeleev_number, + Pettifor_Number: element.pettifor_number, + Eu_Number: element.eu_number, + Space_Group_Number: element.space_group_number, + Glawe_Number: element.glawe_number, + }, + }, + { + title: titles[3], + properties: { + Atomic_Mass: `${element.atomic_mass} Da`, + Uncertainty: element.atomic_mass_uncertainty, + }, + }, + { + title: titles[4], + properties: { + X_Position: element.xpos, + Y_Position: element.ypos, + Period: element.period, + Group: element.group, + }, + }, + { + title: titles[5], + properties: { + Block: element.block, + Category: element.category_code + .split('_') + .map(capitalize) + .join(' '), + Geochemical: element.geochemical_class, + Goldschmidt: element.goldschmidt_class, + Electrical_Type: element.electrical_type, + }, + }, + { + title: titles[6], + properties: { + Urban_Soil: postfix.valueWithSpace({ + value: element.abundance_urban_soil, + postfix: 'mg/kg', + }), + Seawater: postfix.valueWithSpace({ + value: element.abundance_seawater_w1, + postfix: 'kg/L', + }), + Sun: postfix.valueWithSpace({ + value: element.abundance_sun_s1, + postfix: 'mole ratio to silicon', + }), + Earth_Crust: postfix.valueWithSpace({ + value: element.abundance_in_earth_crust_c1, + postfix: 'g', + }), + Human_Body: postfix.valueWithSpace({ + value: element.abundance_humans, + postfix: '%', + }), + Solar_System: postfix.valueWithSpace({ + value: element.abundance_solar_system_y1, + postfix: 'mole ratio to silicon', + }), + Meteorites: postfix.valueWithSpace({ + value: element.abundance_meteorite, + postfix: '%', + }), + }, + }, + { + title: titles[7], + properties: { + Jmol: Color(element.jmol_color), + Molcas_Gv: Color(element.molcas_gv_color), + CPK: Color(element.cpk_color), + }, + }, + { + title: titles[8], + properties: { + Empirical: element.atomic_radius_empirical, + Calculated: element.atomic_radius_calculated, + Van_Der_Waals: element.atomic_radius_vanderwaals, + Bonding: element.vdw_radius_bondi, + Room_Temperature: element.vdw_radius_rt, + Batsanov: element.vdw_radius_batsanov, + Rahm: element.atomic_radius_rahm, + Dreiding: element.vdw_radius_dreiding, + Uff: element.vdw_radius_uff, + Mm3: element.vdw_radius_mm3, + Alvarez: element.vdw_radius_alvarez, + Bragg: element.covalent_radius_bragg, + Truhlar: element.vdw_radius_truhlar, + 'Covalent (Single Bound)': + element.atomic_radius_covalent_single_bond, + 'Covalent (Triple Bound)': + element.atomic_radius_covalent_triple_bond, + 'Covalent (Cordero)': element.covalent_radius_cordero, + 'Covalent (Pyykko)': element.covalent_radius_pyykko, + 'Covalent (Pyykko Double)': + element.covalent_radius_pyykko_double, + 'Covalent (Pyykko Triple)': + element.covalent_radius_pyykko_triple, + Mendeleev: element.metallic_radius_mendeleev, + C12: element.metallic_radius_c12, + Metallic: element.atomic_radius_metallic, + }, + }, + { + title: titles[9], + properties: { + 'Melting/Freeze (USE)': postfix.temperature(element.melt_use), + 'Melting/Freeze (WEL)': postfix.temperature(element.melt_WEL), + 'Melting/Freeze (CRC)': postfix.temperature(element.melt_CRC), + 'Melting/Freeze (LNG)': postfix.temperature(element.melt_LNG), + 'Boiling/Density (USE)': postfix.temperature(element.boil_use), + 'Boiling/Density (WEL)': postfix.temperature(element.boil_WEL), + 'Boiling/Density (CRC)': postfix.temperature(element.boil_CRC), + 'Boiling/Density (LNG)': postfix.temperature(element.boil_LNG), + 'Boiling/Density (Zhang)': postfix.temperature( + element.boil_Zhang + ), + Curie_Point: postfix.valueWithSpace({ + value: element.curie_point, + postfix: 'Tc', + }), + Superconducting_Point: postfix.temperature( + element.superconducting_point + ), + 'Critical Temperature': postfix.temperature( + element.critical_temperature + ), + Flashpoint: postfix.temperature(element.flashpoint), + Autoignition_Point: postfix.temperature( + element.autoignition_point + ), + Critical_Pressure: postfix.valueWithSpace({ + value: element.critical_pressure, + postfix: 'MPa', + }), + }, + }, + { + title: titles[10], + properties: { + STP: postfix.density(element.density_rt), + 'Solid (WEL)': postfix.density(element.density_solid_WEL), + 'Solid (CRC)': postfix.density(element.density_solid_CRC), + 'Solid (LNG)': postfix.density(element.density_solid_LNG), + 'Liquid (CR2)': postfix.density(element.density_liquid_cr2), + Gas: postfix.density(element.density_gas), + }, + }, + { + title: titles[11], + properties: { + Molar_Volume: postfix.valueWithSpace({ + value: element.molar_volume, + postfix: 'cm³/mol', + }), + Atomic_Volume: postfix.valueWithSpace({ + value: element.atomic_volume, + postfix: 'cm³', + }), + Heat_Of_Fusion_USE: postfix.heat(element.enthalpy_of_fusion), + Heat_Of_Fusion_CRC: postfix.heat(element.heat_of_fusion_crc), + Heat_Of_Fusion_LNG: postfix.heat(element.heat_of_fusion_lng), + Heat_Of_Fusion_WEL: postfix.heat(element.heat_of_fusion_wel), + Evaporation_USE: postfix.heat(element.evaporation_heat), + Evaporation_CRC: postfix.heat(element.heat_of_vaporization_crc), + Evaporation_LNG: postfix.heat(element.heat_of_vaporization_lng), + Evaporation_WEL: postfix.heat(element.heat_of_vaporization_wel), + Evaporation_Zhang: postfix.heat( + element.heat_of_vaporization_zhang + ), + Combustion: postfix.valueWithSpace({ + value: element.heat_of_combustion, + postfix: 'kJ/mol', + }), + Molar_Heat: postfix.valueWithSpace({ + value: element.molar_heat, + postfix: 'J/molK', + }), + Heat_Capacity_USE: postfix.heatjoule( + element.specific_heat_capacity + ), + Heat_Capacity_CRC: postfix.heatjoule(element.specific_heat_crc), + Heat_Capacity_LNG: postfix.heatjoule(element.specific_heat_lng), + Heat_Capacity_WEL: postfix.heatjoule(element.specific_heat_wel), + Thermal_Conductivity: postfix.valueWithSpace({ + value: element.thermal_conductivity, + postfix: 'W/m*K', + }), + Thermal_Expansion: postfix.valueWithSpace({ + value: element.thermal_expansion, + postfix: '1/K', + }), + Adiabatic_Index: element.adiabatic_index, + }, + }, + { + title: titles[12], + properties: { + Longitudinal: postfix.speed( + element.speed_of_sound_longitudinal + ), + Transversal: postfix.speed(element.speed_of_sound_transversal), + Extensional: postfix.speed(element.speed_of_sound_extensional), + Fluid: postfix.speed(element.speed_of_sound_fluid), + }, + }, + { + title: titles[13], + properties: { + '80k': postfix.electrical(element.electrical_resistivity_80K), + '273k': postfix.electrical(element.electrical_resistivity_273K), + '293k': postfix.electrical(element.electrical_resistivity_293K), + '298k': postfix.electrical(element.electrical_resistivity_298K), + '300k': postfix.electrical(element.electrical_resistivity_300K), + '500k': postfix.electrical(element.electrical_resistivity_500K), + }, + }, + { + title: titles[14], + properties: { + Order: element.magnetic_ordering, + Neel_Point: postfix.valueWithSpace({ + value: element.neel_point, + postfix: 'Tn', + }), + Susceptibility: postfix.valueWithSpace({ + value: element.magnetic_susceptibility, + postfix: 'm3/kg', + }), + }, + }, + { + title: titles[15], + properties: { + Shear_Modulus: postfix.elastic(element.shear_modulus), + Bulk_Modulus: postfix.elastic(element.bulk_modulus), + Poisson_Ratio: postfix.valueWithSpace({ + value: element.poisson_ratio, + postfix: 'ν', + }), + Youngs_Modulus: postfix.elastic(element.youngs_modulus), + }, + }, + { + title: titles[16], + properties: { + Mohs: element.mohs_hardness, + Brinell: element.brinell_hardness, + Vickers: element.vickers_hardness, + }, + }, + { + title: titles[17], + noGrid: true, + properties: { + Description: element.description, + Language_Of_Origin: element.language_of_origin, + Origin_Of_Word: element.origin_of_word, + Meaning: element.meaning, + Symbol_Origin: element.symbol_origin, + Etymological_Description: element.etymological_description, + }, + }, + { + title: titles[18], + properties: { + 'Observed/Predicted By': element.observed_predicted_by, + 'Observed/Discovery Year': + element.observation_or_discovery_year, + Discovery_Location: !element.countries.length ? null : ( + + {element.countries.map((country) => { + return ( + + {country.name} + + ); + })} + + ), + Isolated_Sample_By: element.isolated_sampled_by, + Isolated_Sample_Year: element.isolation_sample_year, + Named_By: element.named_by, + }, + }, + { + title: titles[19], + noGrid: true, + properties: { + Sources: element.sources, + Uses: element.uses, + }, + }, + { + title: titles[20], + properties: { + Half_Life: element.half_life, + Lifetime: element.lifetime, + Decay_Mode: element.decay_mode, + Neutron_Mass_Absorption: element.neutron_mass_absorption, + Neutron_Cross_Section: element.neutron_cross_section, + }, + }, + { + title: titles[21], + properties: { + Proton_Affinity: element.proton_affinity, + 'Electron_Affinity (eV)': element.electron_affinity_eV, + 'Electron_Affinity (kJ/mol)': element.electron_affinity_kJmol, + 'Electron_Affinity (pauling)': + element.electronegativity_pauling, + 'Electron_Affinity (allen)': element.electronegativity_allen, + 'Electron_Affinity (ghosh)': element.electronegativity_ghosh, + }, + }, + { + title: titles[22], + properties: { + Accepted: element.dipole_polarizability, + Uncertainty: element.dipole_polarizability_unc, + 'C6 GB': element.c6_gb, + 'C6 Coefficient': element.c6_coeff, + }, + }, + { + title: titles[23], + properties: { + Constant_Internal_Default_Radius: + element.lattice_constant_internal_default_radii, + Constant: element.lattice_constant, + Strucutre: element.lattice_structure, + Angles: element.lattice_angles, + }, + }, + { + title: titles[24], + properties: { + Oxidation_States: element.oxidation_states, + Electron_Configuration: element.electron_configuration, + Quantum_Number: element.quantum_number, + Electron_Configuration_Semantic: + element.electron_configuration_semantic, + ...Array.from({ length: 8 }, (_, index) => { + return { + [`Shells-${index}`]: stringOrNumber( + // @ts-expect-error: length of 8 will generate 0 to 7, which is within the range of `shells-number` + element[`shells-${index}`] + ), + }; + }).reduce((accumulator, value) => { + return { + ...accumulator, + ...value, + }; + }, {}), + ...Array.from({ length: 30 }, (_, index) => { + return { + [`Ionization Energies-${index}`]: stringOrNumber( + // @ts-expect-error: length of 30 will be within 30, which is within the range of `ionization_energies-number` + element[`ionization_energies-${index}`] + ), + }; + }).reduce((accumulator, value) => { + return { + ...accumulator, + ...value, + }; + }, {}), + }, + }, + { + title: titles[25], + noGrid: true, + properties: { + None: , + }, + }, + ] as const satisfies ReadonlyArray< + Omit, 'top'> + >; +}; + +export { listOfPropertiesTitle }; +export default listOfProperties; diff --git a/apps/web/src/web/components/elements/properties.tsx b/apps/web/src/web/components/elements/properties.tsx new file mode 100644 index 0000000..e1cf3d3 --- /dev/null +++ b/apps/web/src/web/components/elements/properties.tsx @@ -0,0 +1,135 @@ +import type { Argument, DeepReadonly } from '@poolofdeath20/util'; + +import Box from '@mui/joy/Box'; +import Grid from '@mui/joy/Grid'; +import Stack from '@mui/joy/Stack'; +import Typography from '@mui/joy/Typography'; + +import { spaceToDash } from '../../../common/string'; + +type Properties = Record; + +const titleToId = (name: string) => { + return spaceToDash(name).toLowerCase(); +}; + +const filterProperties = (properties: Properties) => { + return Object.entries(properties).filter(([_, value]) => { + return value && value !== 'NULL'; + }); +}; +const Value = ( + props: Readonly<{ + value: React.ReactNode; + }> +) => { + switch (typeof props.value) { + case 'object': { + return props.value; + } + case 'string': + case 'number': { + return ( + + {props.value === '' ? 'N/A' : props.value} + + ); + } + default: { + return null; + } + } +}; + +const Property = ( + props: Argument & + Readonly<{ + name: string; + }> +) => { + return ( + + {props.name === 'None' ? null : ( + + {props.name.replace(/_/g, ' ')} + + )} + + + ); +}; + +const Properties = ( + props: DeepReadonly<{ + title: string; + properties: Properties; + top: number; + noGrid?: true; + }> +) => { + const properties = filterProperties(props.properties); + + switch (properties.length) { + case 0: { + return null; + } + default: { + const isBohrModel = props.title === 'Bohr Model'; + + return ( + + {props.title} + {props.noGrid ? ( + + {properties.map(([key, value]) => { + return ( + + ); + })} + + ) : ( + + {properties.map(([key, value]) => { + return ( + + + + ); + })} + + )} + + ); + } + } +}; + +export { titleToId, filterProperties }; +export default Properties; diff --git a/apps/web/src/web/components/error/boundary.tsx b/apps/web/src/web/components/error/boundary.tsx index 92bed70..9ae72a0 100644 --- a/apps/web/src/web/components/error/boundary.tsx +++ b/apps/web/src/web/components/error/boundary.tsx @@ -1,26 +1,22 @@ -import React from 'react'; +import type { NextRouter } from 'next/router'; + import Head from 'next/head'; -import Layout from '../layout'; import { withRouter } from 'next/router'; -import type { NextRouter } from 'next/router'; +import React from 'react'; + +import Layout from '../layout'; + +type Props = Readonly<{ + router: NextRouter; + children: React.ReactNode; +}>; type State = Readonly<{ closedAlert: boolean; error: Error | undefined; }>; -class ErrorBoundary extends React.Component< - Readonly<{ - router: NextRouter; - children: React.ReactNode; - }>, - State -> { - state: State = { - error: undefined, - closedAlert: false, - }; - +class ErrorBoundary extends React.Component { static getDerivedStateFromError = (error: Error): State => { return { error, @@ -28,12 +24,25 @@ class ErrorBoundary extends React.Component< }; }; - componentDidCatch = (error: Error, errorInfo: React.ErrorInfo) => { + constructor(props: Props) { + super(props); + this.state = { + error: undefined, + closedAlert: false, + }; + } + + override shouldComponentUpdate(_: Props, nextState: State) { + return nextState.error?.message !== this.state.error?.message; + } + + override componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { console.error({ error, errorInfo }); + // eslint-disable-next-line react/no-set-state this.setState({ error }); - }; + } - render = () => { + override render() { return !this.state.error ? ( this.props.children ) : ( @@ -47,7 +56,7 @@ class ErrorBoundary extends React.Component< )} ); - }; + } } export default withRouter(ErrorBoundary); diff --git a/apps/web/src/web/components/layout/index.tsx b/apps/web/src/web/components/layout/index.tsx index 5e5902e..f24e410 100644 --- a/apps/web/src/web/components/layout/index.tsx +++ b/apps/web/src/web/components/layout/index.tsx @@ -1,10 +1,11 @@ -import React, { PropsWithChildren } from 'react'; +import type { PropsWithChildren } from 'react'; -import Stack from '@mui/joy/Stack'; import GlobalStyles from '@mui/joy/GlobalStyles'; +import Stack from '@mui/joy/Stack'; +import React from 'react'; -import Header from '../common/header'; import Footer from '../common/footer'; +import Header from '../common/header'; const Layout = (props: Readonly) => { return ( diff --git a/apps/web/src/web/components/link/external.tsx b/apps/web/src/web/components/link/external.tsx index 63f22f4..92e5286 100644 --- a/apps/web/src/web/components/link/external.tsx +++ b/apps/web/src/web/components/link/external.tsx @@ -1,13 +1,14 @@ -import React from 'react'; +import type { LinkProps } from '@mui/joy/Link'; -import Link, { type LinkProps } from '@mui/joy/Link'; +import Link from '@mui/joy/Link'; +import React from 'react'; const ExternalLink = (props: LinkProps) => { return ( ); }; diff --git a/apps/web/src/web/components/link/internal.tsx b/apps/web/src/web/components/link/internal.tsx index 7e50056..9b0e1f5 100644 --- a/apps/web/src/web/components/link/internal.tsx +++ b/apps/web/src/web/components/link/internal.tsx @@ -1,8 +1,7 @@ -import React from 'react'; +import type { Argument } from '@poolofdeath20/util'; import Link from 'next/link'; - -import type { Argument } from '@poolofdeath20/util'; +import React from 'react'; const InternalLink = (props: Argument) => { return ( diff --git a/apps/web/src/web/components/pages/error/common.tsx b/apps/web/src/web/components/pages/error/common.tsx index 9824072..1c24058 100644 --- a/apps/web/src/web/components/pages/error/common.tsx +++ b/apps/web/src/web/components/pages/error/common.tsx @@ -1,17 +1,16 @@ -import React from 'react'; - -import { useRouter } from 'next/router'; +import type { Argument, Return } from '@poolofdeath20/util'; import Box from '@mui/joy/Box'; +import Button from '@mui/joy/Button'; import Stack from '@mui/joy/Stack'; import Typography from '@mui/joy/Typography'; -import Button from '@mui/joy/Button'; - -import { Argument, Optional, Return, isTruthy } from '@poolofdeath20/util'; +import { Optional } from '@poolofdeath20/util'; +import { useRouter } from 'next/router'; +import React from 'react'; -import { ErrorTile } from '../../table/element'; -import Seo from '../../seo'; import useBreakpoint from '../../../hooks/break-point'; +import Seo from '../../seo'; +import { ErrorTile } from '../../table/element'; const scrambleAndShowBase = (listOfCharacters: string) => { return ( @@ -98,7 +97,7 @@ const useWordScramble = ( } as Result, } as const); - const ended = isTruthy( + const ended = Boolean( result.current.status === 'started' && words.at(result.current.index)?.isSame ); @@ -159,15 +158,17 @@ const useWordScramble = ( if (words.at(current.index)?.isSame) { setCurrentResult(previous); } else { - return changeWord(); + changeWord(); } } } } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [result.previous.status]); React.useEffect(() => { changeWord(); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [result.current.status === 'started' && result.current.index]); return { @@ -214,45 +215,45 @@ const Error = ( React.useEffect(() => { name.start(); symbol.start(); - }, []); + }, [name, symbol]); return ( Element Not Found @@ -261,6 +262,10 @@ const Error = ( not be here diff --git a/apps/web/src/web/components/pages/error/web.tsx b/apps/web/src/web/components/pages/error/web.tsx index 59320b1..d490419 100644 --- a/apps/web/src/web/components/pages/error/web.tsx +++ b/apps/web/src/web/components/pages/error/web.tsx @@ -1,15 +1,15 @@ -import React from 'react'; - import type { GetServerSideProps, InferGetServerSidePropsType } from 'next'; +import React from 'react'; + import Error from './common'; -const getServerSideProps = (async (context) => { - return { +const getServerSideProps = ((context) => { + return Promise.resolve({ props: { statusCode: context.res.statusCode, }, - }; + }); }) satisfies GetServerSideProps; const WebError = ( diff --git a/apps/web/src/web/components/pages/index/common.tsx b/apps/web/src/web/components/pages/index/common.tsx index b4c37da..0d858a6 100644 --- a/apps/web/src/web/components/pages/index/common.tsx +++ b/apps/web/src/web/components/pages/index/common.tsx @@ -1,597 +1,25 @@ -import React from 'react'; - -import { useRouter } from 'next/router'; +import type { ClassificationProps } from '../../../../../pages/classifications/[classification]'; +import type { SubshellProps } from '../../../../../pages/subshells/[subshell]'; import Box from '@mui/joy/Box'; import Stack from '@mui/joy/Stack'; -import Typography from '@mui/joy/Typography'; -import Sheet from '@mui/joy/Sheet'; -import Grid from '@mui/joy/Grid'; -import Select from '@mui/joy/Select'; -import Chip from '@mui/joy/Chip'; -import Option from '@mui/joy/Option'; -import IconButton from '@mui/joy/IconButton'; -import FormControl from '@mui/joy/FormControl'; -import FormLabel from '@mui/joy/FormLabel'; - -import { - Optional, - Defined, - capitalize, - type DeepReadonly, - type Return, -} from '@poolofdeath20/util'; - -import { CgClose } from 'react-icons/cg'; - -import data from '@periotable/data'; - -import Seo from '../../../components/seo'; -import { DemoTile, EmptyTile, Tile } from '../../../components/table/element'; -import SearchBar from '../../../components/common/input'; -import InternalLink from '../../../components/link/internal'; -import useSearchQuery from '../../../hooks/search'; -import useBreakpoint from '../../../hooks/break-point'; +import { Optional } from '@poolofdeath20/util'; +import React from 'react'; import classifications, { transformCategory, } from '../../../../common/classfication'; -import subshells from '../../../../common/subshell'; -import { spaceToUnderscore } from '../../../../common/string'; - -import { type ClassificationProps } from '../../../../../pages/classifications/[classification]'; -import { type SubshellProps } from '../../../../../pages/subshells/[subshell]'; +import SearchBar from '../../../components/common/input'; +import Seo from '../../../components/seo'; +import { DemoTile } from '../../../components/table/element'; -type NullableString = string | undefined; +import Position, { useSearch } from './position'; +import { GenerateClassificationSelect, GenerateSpdfSelect } from './select'; type DeviceType = Readonly<{ type: 'desktop' | 'tablet' | 'mobile'; }>; -const useSearch = () => { - const numberOnly = (single: (typeof data)[number]) => { - return single.number; - }; - - const [matchingNumbers, setMatchingNumbers] = React.useState( - data.map(numberOnly) - ); - - const [value, setValue] = useSearchQuery(); - - React.useEffect(() => { - setMatchingNumbers( - value - .map((value) => { - return value.toLowerCase(); - }) - .map((value) => { - return data - .filter((element) => { - const nameMatch = element.name_en - .toLowerCase() - .includes(value); - - switch (nameMatch) { - case true: { - return true; - } - case false: { - const symbolMatch = element.symbol - .toLowerCase() - .includes(value); - - switch (symbolMatch) { - case true: { - return true; - } - case false: { - const massMatch = - element.atomic_mass - .toString() - .includes(value); - - return massMatch; - } - } - } - } - }) - .map(numberOnly); - }) - .unwrapOrGet([]) - ); - }, [value.unwrapOrGet('')]); - - return { matchingNumbers, value, setValue }; -}; - -const Position = ( - props: DeviceType & - Readonly<{ - search: Return; - classification: Return< - typeof GenerateClassificationSelect - >['state']; - subshell: Return['state']; - }> -) => { - const oldBreakpoint = useBreakpoint(); - const breakpoint = oldBreakpoint ?? props.type; - - const constant = { - grid: { - max: 12, - }, - table: { - column: 18, - row: 9, - }, - } as const; - - const transformCategory = (category: string) => { - return spaceToUnderscore(category.toLowerCase().replace(/-/gm, '_')); - }; - - switch (breakpoint) { - case 'desktop': - case 'md': - case 'lg': - case 'xl': { - return ( - - {Array.from( - { - length: constant.table.column * constant.table.row, - }, - (_, index) => { - const position = index + 1; - - const element = data.find((element) => { - return ( - (element.ypos - 1) * constant.table.column + - element.xpos === - position - ); - }); - - const size = - constant.grid.max / constant.table.column; - - if (!element) { - return ( - - - - ); - } - - const color = - classifications.find((classification) => { - return element.category_code.startsWith( - transformCategory( - classification.category - ) - ); - }) ?? classifications[9]; - - const highlight = props.classification.match({ - some: (classification) => { - switch ( - element.category_code.startsWith( - transformCategory(classification) - ) - ) { - case false: { - return undefined; - } - case true: { - return color; - } - } - }, - none: () => { - return props.subshell.match({ - none: () => { - return undefined; - }, - some: (subshell) => { - switch ( - element.block === subshell - ) { - case false: { - return undefined; - } - case true: { - return Defined.parse( - subshells.find( - (shell) => { - return ( - shell.subshell === - subshell - ); - } - ) - ).orThrow( - `Subshell of "${subshell}" is not found` - ); - } - } - }, - }); - }, - }); - - return ( - - - { - return props.search.matchingNumbers.includes( - element.number - ); - }) - .unwrapOrGet(undefined)} - /> - - - ); - } - )} - - ); - } - case 'tablet': - case 'mobile': - case 'xs': - case 'sm': { - return ( - - {Array.from( - { - // 18 groups + `No Group` - length: 19, - }, - (_, index) => { - const group = index + 1; - - const newGroup = group === 19 ? 'N/A' : group; - - return { - group: newGroup, - elements: data.filter((element) => { - return element.group === newGroup; - }), - }; - } - ).map((values) => { - return ( - - - Group {values.group} - - - {values.elements.map((element) => { - const color = - classifications.find( - (classification) => { - return element.category_code.startsWith( - transformCategory( - classification.category - ) - ); - } - ) ?? classifications[9]; - - const highlight = - props.classification.match({ - some: (classification) => { - switch ( - element.category_code.startsWith( - transformCategory( - classification - ) - ) - ) { - case false: { - return undefined; - } - case true: { - return color; - } - } - }, - none: () => { - return props.subshell.match( - { - none: () => { - return undefined; - }, - some: ( - subshell - ) => { - switch ( - element.block === - subshell - ) { - case false: { - return undefined; - } - case true: { - return Defined.parse( - subshells.find( - ( - shell - ) => { - return ( - shell.subshell === - subshell - ); - } - ) - ).orThrow( - `Subshell of "${subshell}" is not found` - ); - } - } - }, - } - ); - }, - }); - - return ( - - - - - - ); - })} - - - ); - })} - - ); - } - } -}; - -const GenerateMultiSelect = ( - props: DeepReadonly<{ - key: string; - value: NullableString; - kind: string; - placeholder: string; - options: { - id: string; - label: string; - color?: string; - }[]; - onChange: ( - props: Readonly<{ - router: Return; - value: string | undefined; - }> - ) => void; - }> -) => { - const router = useRouter(); - - const query = router.query[props.key]; - - if (Array.isArray(query)) { - throw new Error(`Value for "${props.kind}" of "${query}" is an array`); - } - - const ids = { - label: `select-field-${props.kind}-label`, - button: `select-field-${props.kind}-button`, - }; - - const value = props.options - .map(({ id }) => { - return id; - }) - .find((id) => { - return id === query; - }); - - const Component = ( - - - {capitalize(props.kind)} - - - - ); - - return { - Component, - state: Optional.from(value), - }; -}; - -const GenerateClassificationSelect = (value: NullableString) => { - return GenerateMultiSelect({ - value, - key: 'classification', - kind: 'category', - placeholder: 'Select a category', - options: classifications.map((classification) => { - return { - id: transformCategory(classification), - label: classification.category, - color: classification.color, - }; - }), - onChange: (props) => { - const href = !props.value ? '/' : `/classifications/${props.value}`; - - props.router.push(href, undefined, { - shallow: true, - scroll: false, - }); - }, - }); -}; - -const GenerateSpdfSelect = (value: NullableString) => { - return GenerateMultiSelect({ - value, - key: 'subshell', - kind: 'subshell', - placeholder: 'Select a subshell', - options: subshells.map(({ subshell, color }) => { - return { - id: subshell, - label: subshell.toUpperCase(), - color, - }; - }), - onChange: (props) => { - const href = !props.value ? '/' : `/subshells/${props.value}`; - - props.router.push(href, undefined, { - shallow: true, - scroll: false, - }); - }, - }); -}; - const Index = (props: ClassificationProps & SubshellProps & DeviceType) => { const ClassificationSelect = GenerateClassificationSelect( Optional.from(props.classification) @@ -604,24 +32,24 @@ const Index = (props: ClassificationProps & SubshellProps & DeviceType) => { const search = useSearch(); return ( - + { + return classification.category; + })} + title={Optional.from(props.classification).map( + (classification) => { + return classification.category; + } + )} url={ props.classification ? `/classifications/${transformCategory(props.classification)}` : props.subshell - ? `/subshells/${props.subshell}` + ? `/subshells/${props.subshell.subshell}` : undefined } - title={Optional.from(props.classification).map( - (classification) => { - return classification.category; - } - )} - description="The home page of Pt, A modern take on Periodic Table of the Elements with interactive features" - keywords={classifications.map((classification) => { - return classification.category; - })} /> @@ -630,21 +58,21 @@ const Index = (props: ClassificationProps & SubshellProps & DeviceType) => { search={search} /> {ClassificationSelect.Component} {SpdfSelect.Component} - + diff --git a/apps/web/src/web/components/pages/index/native.tsx b/apps/web/src/web/components/pages/index/native.tsx index cfbdf4c..d614b90 100644 --- a/apps/web/src/web/components/pages/index/native.tsx +++ b/apps/web/src/web/components/pages/index/native.tsx @@ -1,12 +1,12 @@ +import type { ClassificationProps } from '../../../../../pages/classifications/[classification]'; +import type { SubshellProps } from '../../../../../pages/subshells/[subshell]'; + import React from 'react'; import Index from './common'; -import type { ClassificationProps } from '../../../../../pages/classifications/[classification]'; -import type { SubshellProps } from '../../../../../pages/subshells/[subshell]'; - const NativeIndex = (props: ClassificationProps & SubshellProps) => { - const device = process.env.DEVICE; + const device = process.env['DEVICE']; if (device === 'mobile') { return ; diff --git a/apps/web/src/web/components/pages/index/position.tsx b/apps/web/src/web/components/pages/index/position.tsx new file mode 100644 index 0000000..d6d22e6 --- /dev/null +++ b/apps/web/src/web/components/pages/index/position.tsx @@ -0,0 +1,379 @@ +import type { + GenerateClassificationSelect, + GenerateSpdfSelect, +} from './select'; +import type { Return } from '@poolofdeath20/util'; + +import Grid from '@mui/joy/Grid'; +import Stack from '@mui/joy/Stack'; +import Typography from '@mui/joy/Typography'; +import data from '@periotable/data'; +import { Defined } from '@poolofdeath20/util'; +import React from 'react'; + +import classifications from '../../../../common/classfication'; +import { spaceToUnderscore } from '../../../../common/string'; +import subshells from '../../../../common/subshell'; +import InternalLink from '../../../components/link/internal'; +import { EmptyTile, Tile } from '../../../components/table/element'; +import useBreakpoint from '../../../hooks/break-point'; +import useSearchQuery from '../../../hooks/search'; + +type DeviceType = Readonly<{ + type: 'desktop' | 'tablet' | 'mobile'; +}>; + +const useSearch = () => { + const numberOnly = (single: (typeof data)[number]) => { + return single.number; + }; + + const [matchingNumbers, setMatchingNumbers] = React.useState( + data.map(numberOnly) + ); + + const [value, setValue] = useSearchQuery(); + + React.useEffect(() => { + setMatchingNumbers( + value + .map((value) => { + return value.toLowerCase(); + }) + .map((value) => { + return data + .filter((element) => { + const nameMatch = element.name_en + .toLowerCase() + .includes(value); + + switch (nameMatch) { + case true: { + return true; + } + case false: { + const symbolMatch = element.symbol + .toLowerCase() + .includes(value); + + switch (symbolMatch) { + case true: { + return true; + } + case false: { + const massMatch = + element.atomic_mass + .toString() + .includes(value); + + return massMatch; + } + } + } + } + }) + .map(numberOnly); + }) + .unwrapOrGet([]) + ); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [value.unwrapOrGet('')]); + + return { matchingNumbers, value, setValue }; +}; + +const Position = ( + props: DeviceType & + Readonly<{ + search: Return; + classification: Return< + typeof GenerateClassificationSelect + >['state']; + subshell: Return['state']; + }> +) => { + const oldBreakpoint = useBreakpoint(); + const breakpoint = oldBreakpoint ?? props.type; + + const constant = { + grid: { + max: 12, + }, + table: { + column: 18, + row: 9, + }, + } as const; + + const transformCategory = (category: string) => { + return spaceToUnderscore(category.toLowerCase().replace(/-/gm, '_')); + }; + + switch (breakpoint) { + case 'desktop': + case 'md': + case 'lg': + case 'xl': { + return ( + + {Array.from( + { + length: constant.table.column * constant.table.row, + }, + (_, index) => { + const position = index + 1; + + const element = data.find((element) => { + return ( + (element.ypos - 1) * constant.table.column + + element.xpos === + position + ); + }); + + const size = + constant.grid.max / constant.table.column; + + if (!element) { + return ( + + + + ); + } + + const color = + classifications.find((classification) => { + return element.category_code.startsWith( + transformCategory( + classification.category + ) + ); + }) ?? classifications[9]; + + const highlight = props.classification.match({ + some: (classification) => { + switch ( + element.category_code.startsWith( + transformCategory(classification) + ) + ) { + case false: { + return undefined; + } + case true: { + return color; + } + } + }, + none: () => { + return props.subshell.match({ + none: () => { + return undefined; + }, + some: (subshell) => { + switch ( + element.block === subshell + ) { + case false: { + return undefined; + } + case true: { + return Defined.parse( + subshells.find( + (shell) => { + return ( + shell.subshell === + subshell + ); + } + ) + ).orThrow( + `Subshell of "${subshell}" is not found` + ); + } + } + }, + }); + }, + }); + + return ( + + + { + return props.search.matchingNumbers.includes( + element.number + ); + }) + .unwrapOrGet(undefined)} + mass={element.atomic_mass} + name={element.name_en} + symbol={element.symbol} + /> + + + ); + } + )} + + ); + } + case 'tablet': + case 'mobile': + case 'xs': + case 'sm': { + return ( + + {Array.from( + { + // 18 groups + `No Group` + length: 19, + }, + (_, index) => { + const group = index + 1; + + const newGroup = group === 19 ? 'N/A' : group; + + return { + group: newGroup, + elements: data.filter((element) => { + return element.group === newGroup; + }), + }; + } + ).map((values) => { + return ( + + + Group {values.group} + + + {values.elements.map((element) => { + const color = + classifications.find( + (classification) => { + return element.category_code.startsWith( + transformCategory( + classification.category + ) + ); + } + ) ?? classifications[9]; + + const highlight = + props.classification.match({ + some: (classification) => { + switch ( + element.category_code.startsWith( + transformCategory( + classification + ) + ) + ) { + case false: { + return undefined; + } + case true: { + return color; + } + } + }, + none: () => { + return props.subshell.match( + { + none: () => { + return undefined; + }, + some: ( + subshell + ) => { + switch ( + element.block === + subshell + ) { + case false: { + return undefined; + } + case true: { + return Defined.parse( + subshells.find( + ( + shell + ) => { + return ( + shell.subshell === + subshell + ); + } + ) + ).orThrow( + `Subshell of "${subshell}" is not found` + ); + } + } + }, + } + ); + }, + }); + + return ( + + + + + + ); + })} + + + ); + })} + + ); + } + } +}; + +export { useSearch }; +export default Position; diff --git a/apps/web/src/web/components/pages/index/select.tsx b/apps/web/src/web/components/pages/index/select.tsx new file mode 100644 index 0000000..1ead7d0 --- /dev/null +++ b/apps/web/src/web/components/pages/index/select.tsx @@ -0,0 +1,218 @@ +import type { DeepReadonly, Return } from '@poolofdeath20/util'; + +import Chip from '@mui/joy/Chip'; +import FormControl from '@mui/joy/FormControl'; +import FormLabel from '@mui/joy/FormLabel'; +import IconButton from '@mui/joy/IconButton'; +import Option from '@mui/joy/Option'; +import Select from '@mui/joy/Select'; +import Sheet from '@mui/joy/Sheet'; +import Typography from '@mui/joy/Typography'; +import { Optional, capitalize } from '@poolofdeath20/util'; +import { useRouter } from 'next/router'; +import React from 'react'; +import { CgClose } from 'react-icons/cg'; + +import classifications, { + transformCategory, +} from '../../../../common/classfication'; +import subshells from '../../../../common/subshell'; + +type NullableString = string | undefined; + +const GenerateMultiSelect = ( + props: DeepReadonly<{ + key: string; + value: NullableString; + kind: string; + placeholder: string; + options: Array<{ + id: string; + label: string; + color?: string; + }>; + onChange: ( + props: Readonly<{ + router: Return; + value: string | undefined; + }> + ) => void; + }> +) => { + const router = useRouter(); + + const query = router.query[props.key]; + + if (Array.isArray(query)) { + throw new Error( + `Value for "${props.kind}" of "${query.join()}" is an array` + ); + } + + const ids = { + label: `select-field-${props.kind}-label`, + button: `select-field-${props.kind}-button`, + }; + + const value = props.options + .map(({ id }) => { + return id; + }) + .find((id) => { + return id === query; + }); + + const Component = ( + + + {capitalize(props.kind)} + + + + ); + + return { + Component, + state: Optional.from(value), + }; +}; + +const GenerateClassificationSelect = (value: NullableString) => { + return GenerateMultiSelect({ + value, + key: 'classification', + kind: 'category', + placeholder: 'Select a category', + options: classifications.map((classification) => { + return { + id: transformCategory(classification), + label: classification.category, + color: classification.color, + }; + }), + onChange: (props) => { + const href = !props.value ? '/' : `/classifications/${props.value}`; + + void props.router.push(href, undefined, { + shallow: true, + scroll: false, + }); + }, + }); +}; + +const GenerateSpdfSelect = (value: NullableString) => { + return GenerateMultiSelect({ + value, + key: 'subshell', + kind: 'subshell', + placeholder: 'Select a subshell', + options: subshells.map(({ subshell, color }) => { + return { + id: subshell, + label: subshell.toUpperCase(), + color, + }; + }), + onChange: (props) => { + const href = !props.value ? '/' : `/subshells/${props.value}`; + + void props.router.push(href, undefined, { + shallow: true, + scroll: false, + }); + }, + }); +}; + +export { GenerateClassificationSelect, GenerateSpdfSelect }; diff --git a/apps/web/src/web/components/pages/index/web.tsx b/apps/web/src/web/components/pages/index/web.tsx index d281a5b..d3e77c5 100644 --- a/apps/web/src/web/components/pages/index/web.tsx +++ b/apps/web/src/web/components/pages/index/web.tsx @@ -1,17 +1,14 @@ -import React from 'react'; - +import type { ClassificationProps } from '../../../../../pages/classifications/[classification]'; +import type { SubshellProps } from '../../../../../pages/subshells/[subshell]'; import type { GetServerSideProps, InferGetServerSidePropsType } from 'next'; -import bowser from 'bowser'; - import { Defined } from '@poolofdeath20/util'; +import bowser from 'bowser'; +import React from 'react'; import Index from './common'; -import type { ClassificationProps } from '../../../../../pages/classifications/[classification]'; -import type { SubshellProps } from '../../../../../pages/subshells/[subshell]'; - -const getServerSideProps = (async (context) => { +const getServerSideProps = ((context) => { const { platform } = Defined.parse(context.req.headers['user-agent']) .map(bowser.parse) .orThrow('User agent is not defined'); @@ -20,11 +17,11 @@ const getServerSideProps = (async (context) => { case 'mobile': case 'tablet': case 'desktop': { - return { + return Promise.resolve({ props: { type: platform.type, }, - }; + }); } default: { throw new Error(`Platform of "${platform.type}" is not supported`); diff --git a/apps/web/src/web/components/seo/index.tsx b/apps/web/src/web/components/seo/index.tsx index b19df44..4e8dcc8 100644 --- a/apps/web/src/web/components/seo/index.tsx +++ b/apps/web/src/web/components/seo/index.tsx @@ -1,13 +1,13 @@ -import React from 'react'; +import type { DeepReadonly, Optional } from '@poolofdeath20/util'; +import { Defined } from '@poolofdeath20/util'; import { DefaultSeo } from 'next-seo'; +import React from 'react'; -import { type DeepReadonly, type Optional, Defined } from '@poolofdeath20/util'; +import icons from '../../images/icons'; import Schema from './schema'; -import icons from '../../images/icons'; - const Seo = ( props: DeepReadonly<{ title: Optional; @@ -38,33 +38,32 @@ const Seo = ( { - const size = Defined.parse(icon.sizes.split('x').at(0)) - .map(parseInt) - .orThrow('icon size not found'); - - return { - alt: `website icon as dimension of ${icon.sizes}`, - width: size, - height: size, - url: `${iconPath}/icon-${icon.sizes}.png`, - }; + additionalLinkTags={[ + { + rel: 'icon', + type: 'image/x-icon', + href: `${iconPath}/favicon.ico`, + }, + { + rel: 'apple-touch-icon', + type: 'image/x-icon', + href: `${iconPath}/favicon.ico`, + }, + ...icons().flatMap(({ sizes, src: href }) => { + return [ + { + href, + sizes, + rel: 'icon', + }, + { + href, + sizes, + rel: 'apple-touch-icon', + }, + ]; }), - }} + ]} additionalMetaTags={[ { name: 'keyword', @@ -113,32 +112,33 @@ const Seo = ( content: 'index.html', }, ]} - additionalLinkTags={[ - { - rel: 'icon', - type: 'image/x-icon', - href: `${iconPath}/favicon.ico`, - }, - { - rel: 'apple-touch-icon', - type: 'image/x-icon', - href: `${iconPath}/favicon.ico`, - }, - ...icons().flatMap(({ sizes, src: href }) => { - return [ - { - href, - sizes, - rel: 'icon', - }, - { - href, - sizes, - rel: 'apple-touch-icon', - }, - ]; + canonical={url} + defaultTitle={title} + description={description} + openGraph={{ + url, + title, + description, + images: icons().map((icon) => { + const size = Defined.parse(icon.sizes.split('x').at(0)) + .map(parseInt) + .orThrow('icon size not found'); + + return { + alt: `website icon as dimension of ${icon.sizes}`, + width: size, + height: size, + url: `${iconPath}/icon-${icon.sizes}.png`, + }; }), - ]} + }} + title={title} + titleTemplate={title} + twitter={{ + handle: `@${name}`, + site: `@${name}`, + cardType: 'summary_large_image', + }} /> ); diff --git a/apps/web/src/web/components/seo/schema.tsx b/apps/web/src/web/components/seo/schema.tsx index 3cce693..1718d69 100644 --- a/apps/web/src/web/components/seo/schema.tsx +++ b/apps/web/src/web/components/seo/schema.tsx @@ -1,6 +1,5 @@ -import React from 'react'; - import Script from 'next/script'; +import React from 'react'; import names from '../../generated/schema'; @@ -26,10 +25,10 @@ const Schema = () => { return (