Compare commits

...

10 Commits

Author SHA1 Message Date
33b57bf173
refactor: components struture
All checks were successful
Chromatic / chromatic (push) Successful in 4m47s
CI / ci (push) Successful in 3m50s
CI / commitlint (push) Successful in 18s
Release / release (push) Successful in 1m0s
2024-07-31 15:46:01 +02:00
90a8a50ad0
chore: enable turbo tui
All checks were successful
Chromatic / chromatic (push) Successful in 2m37s
CI / ci (push) Successful in 3m23s
CI / commitlint (push) Successful in 15s
2024-07-30 09:10:12 +02:00
74a3148e92
test: increase coverage 2024-07-30 09:10:02 +02:00
89ec7443a0
build(deps): update latest
All checks were successful
Chromatic / chromatic (push) Successful in 2m11s
CI / ci (push) Successful in 3m20s
CI / commitlint (push) Successful in 17s
2024-07-29 19:38:21 +02:00
ccd44c10fa
feat: cache.json for wikipedia links 2024-07-28 17:57:41 +02:00
867fc131b1
chore: enable TypeScript incremental option for faster lint:typescript 2024-07-27 11:39:15 +02:00
624d235a0e
chore: fix types
All checks were successful
Chromatic / chromatic (push) Successful in 2m4s
CI / ci (push) Successful in 3m39s
CI / commitlint (push) Successful in 18s
2024-07-27 11:20:23 +02:00
90abfb6de8
feat: try deep internal links of wikipedia pages
Some checks failed
Chromatic / chromatic (push) Successful in 4m43s
CI / ci (push) Failing after 1m30s
CI / commitlint (push) Successful in 14s
2024-07-26 19:05:59 +02:00
0ee7b35530
feat: get all internal links from a Wikipedia article 2024-07-26 14:05:05 +02:00
6c717e5768
feat(ui): link version on footer 2024-07-24 22:04:33 +02:00
72 changed files with 2963 additions and 1835 deletions

1
.gitignore vendored
View File

@ -21,6 +21,7 @@ build/
*.pem *.pem
.turbo .turbo
bin/ bin/
cache.json
# debug # debug
npm-debug.log* npm-debug.log*

View File

@ -3,7 +3,7 @@
"scope": "typescriptreact", "scope": "typescriptreact",
"prefix": "rfc", "prefix": "rfc",
"body": [ "body": [
"interface ${1:ComponentName}Props {}", "export interface ${1:ComponentName}Props {}",
"", "",
"export const ${1:ComponentName}: React.FC<${1:ComponentName}Props> = () => {", "export const ${1:ComponentName}: React.FC<${1:ComponentName}Props> = () => {",
" return (", " return (",

13
TODO.md
View File

@ -2,17 +2,18 @@
- [x] chore: initial commit (+ mirror on GitHub) - [x] chore: initial commit (+ mirror on GitHub)
- [x] Deploy first staging version (v1.0.0-staging.1) - [x] Deploy first staging version (v1.0.0-staging.1)
- [ ] Add docs to add locale/edit translations, create component, install a dependency in a package, create a new package, technology used, architecture, links where it's deployed, how to use/install for end users, how to update dependencies with `npx taze -l` etc. - [ ] Implement Wikipedia Game Solver (`website`) with inputs, button to submit, and list all pages to go from one to another, or none if it is not possible
- [ ] Implement Wikipedia Game Solver (`website`) with inputs, button to submit, and list all articles to go from one to another, or none if it is not possible - [ ] Check, cache and store (in `.json` file) all Wikipedia Pages and its internal links, maybe use Wikipedia Dump (<https://en.wikipedia.org/wiki/Wikipedia:Database_download>)?
- [ ] v1.0.0-staging.2 - [ ] Implement toast notifications for errors, warnings, and success messages
- [ ] Implement CLI (`cli`) - [ ] Implement CLI (`cli`)
- [ ] v1.0.0-staging.3
- [ ] Implement REST API (`api`) with JSON responses ([AdonisJS](https://adonisjs.com/)) - [ ] Implement REST API (`api`) with JSON responses ([AdonisJS](https://adonisjs.com/))
- [ ] v1.0.0-staging.4 - [ ] Add docs to add locale/edit translations, create component, install a dependency in a package, create a new package, technology used, architecture, links where it's deployed, how to use/install for end users, how to update dependencies with `npx taze -l` etc.
- [ ] v1.0.0
## Links ## Links
- <https://www.sixdegreesofwikipedia.com/> and <https://github.com/jwngr/sdow>
- <https://github.com/shyamupa/wikidump_preprocessing>
- <https://www.mediawiki.org/wiki/API:Allpages>
- <https://www.thewikigame.com/> - <https://www.thewikigame.com/>
- How to get all URLs in a Wikipedia page: <https://stackoverflow.com/questions/14882571/how-to-get-all-urls-in-a-wikipedia-page> - How to get all URLs in a Wikipedia page: <https://stackoverflow.com/questions/14882571/how-to-get-all-urls-in-a-wikipedia-page>
- <https://en.wikipedia.org/w/api.php?action=query&titles=Title&prop=links&pllimit=max&format=json> - <https://en.wikipedia.org/w/api.php?action=query&titles=Title&prop=links&pllimit=max&format=json>

View File

@ -11,13 +11,13 @@
}, },
"scripts": { "scripts": {
"start": "node --import=tsx ./src/index.ts", "start": "node --import=tsx ./src/index.ts",
"dev": "node --import=tsx --watch --watch-preserve-output ./src/index.ts", "dev-test": "node --import=tsx --watch --watch-preserve-output ./src/index.ts",
"lint:eslint": "eslint src --max-warnings 0 --report-unused-disable-directives", "lint:eslint": "eslint src --max-warnings 0 --report-unused-disable-directives",
"lint:typescript": "tsc --noEmit" "lint:typescript": "tsc --noEmit"
}, },
"dependencies": { "dependencies": {
"@repo/wikipedia-game-solver": "workspace:*", "@repo/wikipedia-game-solver": "workspace:*",
"@repo/constants": "workspace:*", "@repo/utils": "workspace:*",
"tsx": "catalog:" "tsx": "catalog:"
}, },
"devDependencies": { "devDependencies": {

View File

@ -1,11 +1,62 @@
#!/usr/bin/env -S node --import=tsx #!/usr/bin/env -S node --import=tsx
import { add } from "#abc/def/add.js" import type { WikipediaPagesInternalLinks } from "@repo/wikipedia-game-solver/wikipedia-api"
import { getWikipediaPageInternalLinks } from "@repo/wikipedia-game-solver/wikipedia-api"
import fs from "node:fs"
import path from "node:path"
import { VERSION } from "@repo/constants" const localeWikipedia = "en"
import { sum } from "@repo/wikipedia-game-solver/wikipedia-api" const cachePath = path.join(process.cwd(), "cache.json")
console.log("Hello, world!") const fromPageInput = "New York City"
console.log(sum(1, 2)) // const fromPageInput = "Linux"
console.log(add(2, 3)) // const toPageInput = "Node.js"
console.log(`v${VERSION}`) // console.log({
// fromPageInput,
// toPageInput,
// })
// const [fromPageWikipediaLinks, toPageWikipediaLinks] = await Promise.all([
// getWikipediaPageInternalLinks({
// title: fromPageInput,
// locale: localeWikipedia,
// }),
// getWikipediaPageInternalLinks({
// title: toPageInput,
// locale: localeWikipedia,
// }),
// ])
// console.log({
// fromPageWikipediaLinks,
// toPageWikipediaLinks,
// })
// const data = {
// [fromPageWikipediaLinks.title]: fromPageWikipediaLinks,
// [toPageWikipediaLinks.title]: toPageWikipediaLinks,
// }
const data = JSON.parse(
await fs.promises.readFile(cachePath, { encoding: "utf-8" }),
) as WikipediaPagesInternalLinks
// let maxLinks = { max: 0, title: "" }
// for (const [title, page] of Object.entries(data)) {
// if (page.links.length > maxLinks.max) {
// maxLinks = { max: page.links.length, title }
// }
// }
// console.log(maxLinks)
const pageLinks = (data[fromPageInput]?.links ?? []).slice(0, 1100)
for (const pageLink of pageLinks) {
if (pageLink in data) {
continue
}
console.log("Fetching", pageLink)
data[pageLink] = await getWikipediaPageInternalLinks({
title: pageLink,
locale: localeWikipedia,
})
}
await fs.promises.writeFile(cachePath, JSON.stringify(data, null, 2), {
encoding: "utf-8",
})

11
apps/cli/src/main.ts Executable file
View File

@ -0,0 +1,11 @@
#!/usr/bin/env -S node --import=tsx
import { add } from "#abc/def/add.js"
import { VERSION } from "@repo/utils/constants"
import { sum } from "@repo/wikipedia-game-solver/wikipedia-api"
console.log("Hello, world!")
console.log(sum(1, 2))
console.log(add(2, 3))
console.log(`v${VERSION}`)

View File

@ -1,9 +1,9 @@
import "@repo/config-tailwind/styles.css" import "@repo/config-tailwind/styles.css"
import { defaultTranslationValues } from "@repo/i18n/config" import { defaultTranslationValues, Locale } from "@repo/i18n/config"
import i18nMessagesEnglish from "@repo/i18n/translations/en-US.json" import i18nMessagesEnglish from "@repo/i18n/translations/en-US.json"
import { ThemeProvider } from "@repo/ui/Header/SwitchTheme"
import type { Preview } from "@storybook/react" import type { Preview } from "@storybook/react"
import { NextIntlClientProvider } from "next-intl" import { NextIntlClientProvider } from "next-intl"
import { ThemeProvider as NextThemeProvider } from "next-themes"
import React from "react" import React from "react"
const preview: Preview = { const preview: Preview = {
@ -13,7 +13,7 @@ const preview: Preview = {
}, },
options: { options: {
storySort: { storySort: {
order: ["Design System", "User Interface", "Feature"], order: ["Design System", "Layout", "Errors"],
}, },
}, },
backgrounds: { disable: true }, backgrounds: { disable: true },
@ -32,16 +32,18 @@ const preview: Preview = {
}, },
decorators: [ decorators: [
(Story) => { (Story) => {
const locale = "en-US" satisfies Locale
return ( return (
<ThemeProvider> <NextThemeProvider enableColorScheme={false}>
<NextIntlClientProvider <NextIntlClientProvider
messages={i18nMessagesEnglish} messages={i18nMessagesEnglish}
locale="en" locale={locale}
defaultTranslationValues={defaultTranslationValues} defaultTranslationValues={defaultTranslationValues}
> >
<Story /> <Story />
</NextIntlClientProvider> </NextIntlClientProvider>
</ThemeProvider> </NextThemeProvider>
) )
}, },
], ],

View File

@ -19,6 +19,7 @@
"@repo/wikipedia-game-solver": "workspace:*", "@repo/wikipedia-game-solver": "workspace:*",
"next": "catalog:", "next": "catalog:",
"next-intl": "catalog:", "next-intl": "catalog:",
"next-themes": "catalog:",
"react": "catalog:", "react": "catalog:",
"react-dom": "catalog:" "react-dom": "catalog:"
}, },

View File

@ -5,7 +5,7 @@ RUN corepack enable
WORKDIR /usr/src/app WORKDIR /usr/src/app
FROM node-pnpm AS builder FROM node-pnpm AS builder
RUN pnpm install --global turbo@2.0.9 RUN pnpm install --global turbo@2.0.10
COPY ./ ./ COPY ./ ./
RUN turbo prune @repo/website --docker RUN turbo prune @repo/website --docker

View File

@ -1,34 +1,10 @@
"use client" "use client"
import { Button } from "@repo/ui/design/Button" import type { ErrorServerProps } from "@repo/ui/Errors/ErrorServer"
import { MainLayout } from "@repo/ui/MainLayout" import { ErrorServer } from "@repo/ui/Errors/ErrorServer"
import { useTranslations } from "next-intl"
import { useEffect } from "react"
interface ErrorBoundaryPageProps { const ErrorBoundaryPage: React.FC<ErrorServerProps> = (props) => {
error: Error & { digest?: string } return <ErrorServer {...props} />
reset: () => void
}
const ErrorBoundaryPage: React.FC<ErrorBoundaryPageProps> = (props) => {
const { error, reset } = props
const t = useTranslations()
useEffect(() => {
console.error(error)
}, [error])
return (
<MainLayout className="items-center justify-center text-center">
<h1 className="text-3xl font-semibold">
{t("errors.error")} 500 - {t("errors.server-error")}
</h1>
<p className="mb-4 mt-2">
<Button onClick={reset}>{t("errors.try-again")}</Button>
</p>
</MainLayout>
)
} }
export default ErrorBoundaryPage export default ErrorBoundaryPage

View File

@ -1,10 +1,10 @@
import "@repo/config-tailwind/styles.css" import "@repo/config-tailwind/styles.css"
import { VERSION } from "@repo/constants"
import type { Locale, LocaleProps } from "@repo/i18n/config" import type { Locale, LocaleProps } from "@repo/i18n/config"
import { LOCALES } from "@repo/i18n/config" import { LOCALES } from "@repo/i18n/config"
import { Footer } from "@repo/ui/Footer" import { Footer } from "@repo/ui/Layout/Footer"
import { Header } from "@repo/ui/Header" import { Header } from "@repo/ui/Layout/Header"
import { ThemeProvider } from "@repo/ui/Header/SwitchTheme" import { ThemeProvider } from "@repo/ui/Layout/Header/SwitchTheme"
import { VERSION } from "@repo/utils/constants"
import type { Metadata } from "next" import type { Metadata } from "next"
import { NextIntlClientProvider } from "next-intl" import { NextIntlClientProvider } from "next-intl"
import { import {
@ -17,9 +17,37 @@ export const generateMetadata = async ({
params, params,
}: LocaleProps): Promise<Metadata> => { }: LocaleProps): Promise<Metadata> => {
const t = await getTranslations({ locale: params.locale }) const t = await getTranslations({ locale: params.locale })
const title = t("meta.title")
const description = t("meta.description")
const image = "/images/Wikipedia-Logo.webp"
const url = new URL("https://wikipedia-game-solver.theoludwig.fr")
const locale = LOCALES.join(", ")
return { return {
title: t("meta.title"), title,
description: t("meta.description"), description,
metadataBase: url,
openGraph: {
title,
description,
url,
siteName: title,
images: [
{
url: image,
width: 96,
height: 96,
},
],
locale,
type: "website",
},
twitter: {
card: "summary",
title,
description,
images: [image],
},
} }
} }

View File

@ -1,9 +1,9 @@
import { MainLayout } from "@repo/ui/MainLayout" import { Spinner } from "@repo/ui/Design/Spinner"
import { Spinner } from "@repo/ui/design/Spinner" import { MainLayout } from "@repo/ui/Layout/MainLayout"
const Loading: React.FC = () => { const Loading: React.FC = () => {
return ( return (
<MainLayout className="items-center justify-center"> <MainLayout center>
<Spinner size={50} /> <Spinner size={50} />
</MainLayout> </MainLayout>
) )

View File

@ -1,24 +1,10 @@
import { Link } from "@repo/ui/design/Link" import { ErrorNotFound } from "@repo/ui/Errors/ErrorNotFound"
import { MainLayout } from "@repo/ui/MainLayout"
import { useTranslations } from "next-intl"
/** /**
* Note that `app/[locale]/[...rest]/page.tsx` is necessary for this page to render. * Note that `app/[locale]/[...rest]/page.tsx` is necessary for this page to render.
*/ */
const NotFound: React.FC = () => { const NotFound: React.FC = () => {
const t = useTranslations() return <ErrorNotFound />
return (
<MainLayout className="items-center justify-center text-center">
<h1 className="text-3xl font-semibold">
{t("errors.error")} 404 - {t("errors.not-found")}
</h1>
<p className="mb-4 mt-2">
{t("errors.page-doesnt-exist")}{" "}
<Link href="/">{t("errors.return-to-home-page")}</Link>
</p>
</MainLayout>
)
} }
export default NotFound export default NotFound

View File

@ -1,13 +1,17 @@
import type { LocaleProps } from "@repo/i18n/config" import type { LocaleProps } from "@repo/i18n/config"
import { Link } from "@repo/ui/design/Link" import { Link } from "@repo/ui/Design/Link"
import { Typography } from "@repo/ui/design/Typography" import { Typography } from "@repo/ui/Design/Typography"
import { MainLayout } from "@repo/ui/MainLayout" import { MainLayout } from "@repo/ui/Layout/MainLayout"
import {
fromLocaleToWikipediaLocale,
getWikipediaLink,
} from "@repo/wikipedia-game-solver/wikipedia-api"
import { WikipediaClient } from "@repo/wikipedia-game-solver/WikipediaClient" import { WikipediaClient } from "@repo/wikipedia-game-solver/WikipediaClient"
import { useTranslations } from "next-intl" import { useTranslations } from "next-intl"
import { unstable_setRequestLocale } from "next-intl/server" import { unstable_setRequestLocale } from "next-intl/server"
import Image from "next/image" import Image from "next/image"
import WikipediaLogo from "#public/images/Wikipedia-Logo.png" import WikipediaLogo from "#public/images/Wikipedia-Logo.webp"
interface HomePageProps extends LocaleProps {} interface HomePageProps extends LocaleProps {}
@ -19,6 +23,8 @@ const HomePage: React.FC<HomePageProps> = (props) => {
const t = useTranslations() const t = useTranslations()
const localeWikipedia = fromLocaleToWikipediaLocale(params.locale)
return ( return (
<MainLayout> <MainLayout>
<section className="text-center"> <section className="text-center">
@ -28,9 +34,9 @@ const HomePage: React.FC<HomePageProps> = (props) => {
<Typography as="p" variant="text1" className="mt-3"> <Typography as="p" variant="text1" className="mt-3">
{t.rich("home.description", { {t.rich("home.description", {
wikipedia: (children) => { "wikipedia-link": (children) => {
return ( return (
<Link href="https://en.wikipedia.org/" target="_blank"> <Link href={getWikipediaLink(localeWikipedia)} target="_blank">
{children} {children}
</Link> </Link>
) )

View File

@ -14,6 +14,8 @@ export const config = {
// Set a cookie to remember the previous locale for // Set a cookie to remember the previous locale for
// all requests that have a locale prefix // all requests that have a locale prefix
// Next.js issue, middleware matcher should support template literals:
// https://github.com/vercel/next.js/issues/56398
"/(en-US|fr-FR)/:path*", "/(en-US|fr-FR)/:path*",
// Enable redirects that add missing locales // Enable redirects that add missing locales

View File

@ -15,7 +15,7 @@
}, },
"dependencies": { "dependencies": {
"@repo/config-tailwind": "workspace:*", "@repo/config-tailwind": "workspace:*",
"@repo/constants": "workspace:*", "@repo/utils": "workspace:*",
"@repo/i18n": "workspace:*", "@repo/i18n": "workspace:*",
"@repo/ui": "workspace:*", "@repo/ui": "workspace:*",
"@repo/wikipedia-game-solver": "workspace:*", "@repo/wikipedia-game-solver": "workspace:*",
@ -23,7 +23,7 @@
"next-intl": "catalog:", "next-intl": "catalog:",
"react": "catalog:", "react": "catalog:",
"react-dom": "catalog:", "react-dom": "catalog:",
"react-icons": "catalog:" "sharp": "catalog:"
}, },
"devDependencies": { "devDependencies": {
"@repo/eslint-config": "workspace:*", "@repo/eslint-config": "workspace:*",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 272 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

View File

@ -2,11 +2,7 @@ import sharedConfig from "@repo/config-tailwind"
/** @type {Pick<import('tailwindcss').Config, "presets" | "content">} */ /** @type {Pick<import('tailwindcss').Config, "presets" | "content">} */
const config = { const config = {
content: [ content: ["./**/*.tsx", "../../packages/**/*.tsx"],
"./**/*.tsx",
"../../packages/ui/**/*.tsx",
"../../packages/wikipedia-game-solver/**/*.tsx",
],
presets: [sharedConfig], presets: [sharedConfig],
} }

17
data/README.md Normal file
View File

@ -0,0 +1,17 @@
# Wikipedia data
Database layout: <https://www.mediawiki.org/wiki/Manual:Database_layout>
<https://stackoverflow.com/questions/43954631/issues-with-wikipedia-dump-table-pagelinks>
<https://stackoverflow.com/questions/40384864/importing-wikipedia-dump-to-mysql>
## Dumps Links
- <https://dumps.wikimedia.org/enwiki/>
- <https://dumps.wikimedia.org/enwiki/latest/enwiki-latest-pagelinks.sql.gz>
- <https://dumps.wikimedia.org/enwiki/latest/enwiki-latest-page.sql.gz>
- <https://dumps.wikimedia.org/enwiki/latest/enwiki-latest-all-titles-in-ns0.gz>
- <https://dumps.wikimedia.org/enwiki/latest/enwiki-latest-iwlinks.sql.gz>
- <https://dumps.wikimedia.org/enwiki/latest/enwiki-latest-all-titles.gz>

View File

@ -24,11 +24,12 @@
"@semantic-release/exec": "6.0.3", "@semantic-release/exec": "6.0.3",
"@semantic-release/git": "10.0.1", "@semantic-release/git": "10.0.1",
"editorconfig-checker": "5.1.8", "editorconfig-checker": "5.1.8",
"playwright": "catalog:",
"prettier": "3.3.3", "prettier": "3.3.3",
"prettier-plugin-tailwindcss": "0.6.5", "prettier-plugin-tailwindcss": "0.6.5",
"replace-in-files-cli": "3.0.0", "replace-in-files-cli": "3.0.0",
"semantic-release": "23.1.1", "semantic-release": "23.1.1",
"turbo": "2.0.9", "turbo": "2.0.10",
"typescript": "catalog:" "typescript": "catalog:"
} }
} }

View File

@ -23,6 +23,7 @@ const config = {
yellow: "#fef08a", yellow: "#fef08a",
transparent: "transparent", transparent: "transparent",
inherit: "inherit", inherit: "inherit",
current: "currentColor",
}, },
fontFamily: { fontFamily: {
sans: ["'Montserrat'", ...fontFamily.sans], sans: ["'Montserrat'", ...fontFamily.sans],

View File

@ -13,6 +13,8 @@
"isolatedModules": true, "isolatedModules": true,
"esModuleInterop": true, "esModuleInterop": true,
"skipLibCheck": true "skipLibCheck": true,
"jsx": "preserve",
"incremental": true
} }
} }

View File

@ -1,17 +0,0 @@
{
"name": "@repo/constants",
"version": "1.0.0-staging.1",
"private": true,
"type": "module",
"exports": {
".": "./version.ts"
},
"scripts": {
"lint:typescript": "tsc --noEmit"
},
"devDependencies": {
"@repo/config-typescript": "workspace:*",
"@types/node": "catalog:",
"typescript": "catalog:"
}
}

View File

@ -1,6 +0,0 @@
import packageJSON from "./package.json"
export const VERSION =
process.env["NODE_ENV"] === "development"
? "0.0.0-development"
: packageJSON.version

View File

@ -1,13 +1,15 @@
{ {
"meta": { "meta": {
"title": "Wikipedia Game Solver", "title": "Wikipedia Game Solver",
"description": "The Wikipedia Game involves players competing to navigate from one Wikipedia page to another using only internal links.", "description": "The Wikipedia Game involves players competing to navigate from one Wikipedia page to another using only internal links."
"all-rights-reserved": "All rights reserved"
}, },
"locales": { "locales": {
"en-US": "🇺🇸 English", "en-US": "🇺🇸 English",
"fr-FR": "🇫🇷 French" "fr-FR": "🇫🇷 French"
}, },
"footer": {
"all-rights-reserved": "All rights reserved"
},
"errors": { "errors": {
"error": "Error", "error": "Error",
"page-doesnt-exist": "This page doesn't exist!", "page-doesnt-exist": "This page doesn't exist!",
@ -18,6 +20,6 @@
}, },
"home": { "home": {
"title": "Wikipedia Game Solver", "title": "Wikipedia Game Solver",
"description": "The Wikipedia Game involves players competing to navigate from one <wikipedia>Wikipedia</wikipedia> page to another using only internal links." "description": "The Wikipedia Game involves players competing to navigate from one <wikipedia-link>Wikipedia</wikipedia-link> page to another using only internal links."
} }
} }

View File

@ -1,13 +1,15 @@
{ {
"meta": { "meta": {
"title": "Wikipedia Game Solver", "title": "Wikipedia Game Solver",
"description": "Le jeu Wikipédia implique des joueurs en compétition pour naviguer d'une page Wikipédia à une autre en utilisant uniquement des liens internes.", "description": "Le jeu Wikipédia implique des joueurs en compétition pour naviguer d'une page Wikipédia à une autre en utilisant uniquement des liens internes."
"all-rights-reserved": "Tous droits réservés"
}, },
"locales": { "locales": {
"en-US": "🇺🇸 Anglais", "en-US": "🇺🇸 Anglais",
"fr-FR": "🇫🇷 Français" "fr-FR": "🇫🇷 Français"
}, },
"footer": {
"all-rights-reserved": "Tous droits réservés"
},
"errors": { "errors": {
"error": "Erreur", "error": "Erreur",
"page-doesnt-exist": "Cette page n'existe pas !", "page-doesnt-exist": "Cette page n'existe pas !",
@ -18,6 +20,6 @@
}, },
"home": { "home": {
"title": "Wikipedia Game Solver", "title": "Wikipedia Game Solver",
"description": "Le jeu Wikipédia implique des joueurs en compétition pour naviguer d'une page Wikipédia à une autre en utilisant uniquement des liens internes." "description": "Le jeu Wikipédia implique des joueurs en compétition pour naviguer d'une page <wikipedia-link>Wikipedia</wikipedia-link> à une autre en utilisant uniquement des liens internes."
} }
} }

View File

@ -4,11 +4,14 @@
"private": true, "private": true,
"type": "module", "type": "module",
"exports": { "exports": {
"./useBoolean": "./src/useBoolean.ts",
"./useIsMounted": "./src/useIsMounted.ts" "./useIsMounted": "./src/useIsMounted.ts"
}, },
"scripts": { "scripts": {
"lint:eslint": "eslint src --max-warnings 0 --report-unused-disable-directives", "lint:eslint": "eslint src --max-warnings 0 --report-unused-disable-directives",
"lint:typescript": "tsc --noEmit" "lint:typescript": "tsc --noEmit",
"test": "vitest run --browser.headless",
"test:ui": "vitest --ui --no-open"
}, },
"dependencies": { "dependencies": {
"react": "catalog:", "react": "catalog:",
@ -17,10 +20,16 @@
"devDependencies": { "devDependencies": {
"@repo/eslint-config": "workspace:*", "@repo/eslint-config": "workspace:*",
"@repo/config-typescript": "workspace:*", "@repo/config-typescript": "workspace:*",
"@testing-library/react": "catalog:",
"@types/react": "catalog:", "@types/react": "catalog:",
"@types/react-dom": "catalog:", "@types/react-dom": "catalog:",
"@total-typescript/ts-reset": "catalog:", "@total-typescript/ts-reset": "catalog:",
"@vitest/browser": "catalog:",
"@vitest/coverage-istanbul": "catalog:",
"@vitest/ui": "catalog:",
"eslint": "catalog:", "eslint": "catalog:",
"typescript": "catalog:" "playwright": "catalog:",
"typescript": "catalog:",
"vitest": "catalog:"
} }
} }

View File

@ -0,0 +1,83 @@
import { act, renderHook } from "@testing-library/react"
import { describe, expect, it } from "vitest"
import { useBoolean } from "../useBoolean"
describe("useBoolean", () => {
const initialValues = [true, false]
for (const initialValue of initialValues) {
it(`should set the initial value to ${initialValue}`, () => {
// Arrange - Given
const { result } = renderHook(() => {
return useBoolean({ initialValue })
})
// Assert - Then
expect(result.current.value).toBe(initialValue)
})
}
it("should by default set the initial value to false", () => {
// Arrange - Given
const { result } = renderHook(() => {
return useBoolean()
})
// Assert - Then
expect(result.current.value).toBe(false)
})
it("should toggle the value", () => {
// Arrange - Given
const { result } = renderHook(() => {
return useBoolean({ initialValue: false })
})
// Act - When
act(() => {
return result.current.toggle()
})
// Assert - Then
expect(result.current.value).toBe(true)
// Act - When
act(() => {
return result.current.toggle()
})
// Assert - Then
expect(result.current.value).toBe(false)
})
it("should set the value to true", () => {
// Arrange - Given
const { result } = renderHook(() => {
return useBoolean({ initialValue: false })
})
// Act - When
act(() => {
return result.current.setTrue()
})
// Assert - Then
expect(result.current.value).toBe(true)
})
it("should set the value to false", () => {
// Arrange - Given
const { result } = renderHook(() => {
return useBoolean({ initialValue: true })
})
// Act - When
act(() => {
return result.current.setFalse()
})
// Assert - Then
expect(result.current.value).toBe(false)
})
})

View File

@ -0,0 +1,16 @@
import { renderHook } from "@testing-library/react"
import { describe, expect, it } from "vitest"
import { useIsMounted } from "../useIsMounted"
describe("useIsMounted", () => {
it("should return true", () => {
// Arrange - Given
const { result } = renderHook(() => {
return useIsMounted()
})
// Assert - Then
expect(result.current.isMounted).toBe(true)
})
})

View File

@ -0,0 +1,50 @@
import { useState } from "react"
export interface UseBooleanOutput {
value: boolean
setValue: React.Dispatch<React.SetStateAction<boolean>>
setTrue: () => void
setFalse: () => void
toggle: () => void
}
export interface UseBooleanInput {
/**
* The initial value of the boolean.
* @default false
*/
initialValue?: boolean
}
/**
* Hook to manage a boolean state.
* @param options
* @returns
*/
export const useBoolean = (options: UseBooleanInput = {}): UseBooleanOutput => {
const { initialValue = false } = options
const [value, setValue] = useState(initialValue)
const toggle = (): void => {
setValue((old) => {
return !old
})
}
const setTrue = (): void => {
setValue(true)
}
const setFalse = (): void => {
setValue(false)
}
return {
value,
setValue,
toggle,
setTrue,
setFalse,
}
}

View File

@ -0,0 +1,15 @@
import { defineConfig } from "vitest/config"
export default defineConfig({
test: {
browser: {
provider: "playwright",
enabled: true,
name: "chromium",
},
coverage: {
enabled: true,
provider: "istanbul",
},
},
})

View File

@ -4,14 +4,17 @@
"private": true, "private": true,
"type": "module", "type": "module",
"exports": { "exports": {
"./design/Button": "./src/design/Button/Button.tsx", "./Design/Button": "./src/Design/Button/Button.tsx",
"./design/Link": "./src/design/Link/Link.tsx", "./Design/Link": "./src/Design/Link/Link.tsx",
"./design/Spinner": "./src/design/Spinner/Spinner.tsx", "./Design/Spinner": "./src/Design/Spinner/Spinner.tsx",
"./design/Typography": "./src/design/Typography/Typography.tsx", "./Design/Typography": "./src/Design/Typography/Typography.tsx",
"./Footer": "./src/Footer/Footer.tsx", "./Errors/ErrorNotFound": "./src/Errors/ErrorNotFound/ErrorNotFound.tsx",
"./Header": "./src/Header/Header.tsx", "./Errors/ErrorServer": "./src/Errors/ErrorServer/ErrorServer.tsx",
"./Header/SwitchTheme": "./src/Header/SwitchTheme.tsx", "./Layout/Footer": "./src/Layout/Footer/Footer.tsx",
"./MainLayout": "./src/MainLayout/MainLayout.tsx" "./Layout/Header": "./src/Layout/Header/Header.tsx",
"./Layout/Header/SwitchTheme": "./src/Layout/Header/SwitchTheme.tsx",
"./Layout/MainLayout": "./src/Layout/MainLayout/MainLayout.tsx",
"./Layout/Section": "./src/Layout/Section/Section.tsx"
}, },
"scripts": { "scripts": {
"lint:eslint": "eslint src --max-warnings 0 --report-unused-disable-directives", "lint:eslint": "eslint src --max-warnings 0 --report-unused-disable-directives",
@ -19,6 +22,7 @@
}, },
"dependencies": { "dependencies": {
"@repo/config-tailwind": "workspace:*", "@repo/config-tailwind": "workspace:*",
"@repo/utils": "workspace:*",
"@repo/i18n": "workspace:*", "@repo/i18n": "workspace:*",
"@repo/react-hooks": "workspace:*", "@repo/react-hooks": "workspace:*",
"cva": "catalog:", "cva": "catalog:",

View File

@ -11,8 +11,8 @@ const typographyVariants = cva({
h4: "text-xl font-semibold", h4: "text-xl font-semibold",
h5: "text-xl font-medium", h5: "text-xl font-medium",
h6: "text-lg font-medium", h6: "text-lg font-medium",
text1: "text-base", text1: "break-words text-base",
text2: "text-sm", text2: "break-words text-sm",
}, },
}, },
}) })

View File

@ -0,0 +1,16 @@
import type { Meta, StoryObj } from "@storybook/react"
import { ErrorNotFound as ErrorNotFoundComponent } from "./ErrorNotFound"
const meta = {
title: "Errors/ErrorNotFound",
component: ErrorNotFoundComponent,
} satisfies Meta<typeof ErrorNotFoundComponent>
export default meta
type Story = StoryObj<typeof meta>
export const ErrorNotFound: Story = {
args: {},
}

View File

@ -0,0 +1,26 @@
import { useTranslations } from "next-intl"
import { Link } from "../../Design/Link/Link"
import { Typography } from "../../Design/Typography/Typography"
import { MainLayout } from "../../Layout/MainLayout/MainLayout"
import { Section } from "../../Layout/Section/Section"
export interface ErrorNotFoundProps {}
export const ErrorNotFound: React.FC<ErrorNotFoundProps> = () => {
const t = useTranslations()
return (
<MainLayout center>
<Section horizontalSpacing>
<Typography variant="h1" as="h1">
{t("errors.error")} 404 - {t("errors.not-found")}
</Typography>
<Typography variant="text1" as="p" className="mt-4">
{t("errors.page-doesnt-exist")}{" "}
<Link href="/">{t("errors.return-to-home-page")}</Link>
</Typography>
</Section>
</MainLayout>
)
}

View File

@ -0,0 +1,22 @@
import type { Meta, StoryObj } from "@storybook/react"
import { expect, fn, userEvent, within } from "@storybook/test"
import { ErrorServer as ErrorServerComponent } from "./ErrorServer"
const meta = {
title: "Errors/ErrorServer",
component: ErrorServerComponent,
} satisfies Meta<typeof ErrorServerComponent>
export default meta
type Story = StoryObj<typeof meta>
export const ErrorServer: Story = {
args: { reset: fn(), error: new Error("Server error") },
play: async ({ canvasElement, args }) => {
const canvas = within(canvasElement)
await userEvent.click(canvas.getByText("Try again?"))
await expect(args.reset).toHaveBeenCalled()
},
}

View File

@ -0,0 +1,37 @@
"use client"
import { useTranslations } from "next-intl"
import { useEffect } from "react"
import { Button } from "../../Design/Button/Button"
import { Typography } from "../../Design/Typography/Typography"
import { MainLayout } from "../../Layout/MainLayout/MainLayout"
import { Section } from "../../Layout/Section/Section"
export interface ErrorServerProps {
error: Error & { digest?: string }
reset: () => void
}
export const ErrorServer: React.FC<ErrorServerProps> = (props) => {
const { error, reset } = props
const t = useTranslations()
useEffect(() => {
console.error(error)
}, [error])
return (
<MainLayout center>
<Section horizontalSpacing>
<Typography variant="h1" as="h1">
{t("errors.error")} 500 - {t("errors.server-error")}
</Typography>
<Typography variant="text1" as="p" className="mt-4">
<Button onClick={reset}>{t("errors.try-again")}</Button>
</Typography>
</Section>
</MainLayout>
)
}

View File

@ -3,7 +3,7 @@ import type { Meta, StoryObj } from "@storybook/react"
import { Footer as FooterComponent } from "./Footer" import { Footer as FooterComponent } from "./Footer"
const meta = { const meta = {
title: "User Interface/Footer", title: "Layout/Footer",
component: FooterComponent, component: FooterComponent,
} satisfies Meta<typeof FooterComponent> } satisfies Meta<typeof FooterComponent>

View File

@ -1,6 +1,7 @@
import { useTranslations } from "next-intl" import { useTranslations } from "next-intl"
import { Link } from "../design/Link/Link" import { GIT_REPO_LINK } from "@repo/utils/constants"
import { Link } from "../../Design/Link/Link"
export interface FooterProps { export interface FooterProps {
version: string version: string
@ -12,19 +13,23 @@ export const Footer: React.FC<FooterProps> = (props) => {
const t = useTranslations() const t = useTranslations()
return ( return (
<footer className="bg-background dark:bg-background-dark border-gray-darker dark:border-gray-darker-dark flex h-20 flex-col items-center justify-center border-t-2 px-6 py-2 text-lg"> <footer className="bg-background dark:bg-background-dark border-gray-darker dark:border-gray-darker-dark flex flex-col items-center justify-center border-t-2 px-6 py-2 text-lg">
<p> <p>
<Link href="https://theoludwig.fr" target="_blank" isExternal={false}> <Link href="https://theoludwig.fr" target="_blank" isExternal={false}>
Théo LUDWIG Théo LUDWIG
</Link>{" "} </Link>{" "}
| {t("meta.all-rights-reserved")} | {t("footer.all-rights-reserved")}
</p> </p>
<p> <p>
Version{" "} Version{" "}
<strong className="text-primary dark:text-primary-dark hover:underline"> <Link
href={`${GIT_REPO_LINK}/releases/tag/v${version}`}
target="_blank"
isExternal={false}
>
{version} {version}
</strong> </Link>
</p> </p>
</footer> </footer>
) )

View File

@ -3,7 +3,7 @@ import type { Meta, StoryObj } from "@storybook/react"
import { Header as HeaderComponent } from "./Header" import { Header as HeaderComponent } from "./Header"
const meta = { const meta = {
title: "User Interface/Header", title: "Layout/Header",
component: HeaderComponent, component: HeaderComponent,
} satisfies Meta<typeof HeaderComponent> } satisfies Meta<typeof HeaderComponent>

View File

@ -5,7 +5,7 @@ import { SwitchTheme } from "./SwitchTheme"
export const Header: React.FC = () => { export const Header: React.FC = () => {
return ( return (
<header className="bg-background dark:bg-background-dark border-gray-darker dark:border-gray-darker-dark sticky top-0 z-50 flex h-16 flex-col items-center justify-center gap-4 border-b-2 px-4 py-2"> <header className="bg-background dark:bg-background-dark border-gray-darker dark:border-gray-darker-dark sticky top-0 z-50 flex flex-col items-center justify-center gap-4 border-b-2 px-4 py-2">
<div className="flex w-full items-center justify-between"> <div className="flex w-full items-center justify-between">
<Locales /> <Locales />
<SwitchTheme /> <SwitchTheme />

View File

@ -1,6 +1,7 @@
"use client" "use client"
import { classNames } from "@repo/config-tailwind/classNames" import { classNames } from "@repo/config-tailwind/classNames"
import type { Locale } from "@repo/i18n/config"
import { LOCALES } from "@repo/i18n/config" import { LOCALES } from "@repo/i18n/config"
import { usePathname, useRouter } from "@repo/i18n/navigation" import { usePathname, useRouter } from "@repo/i18n/navigation"
import { useLocale, useTranslations } from "next-intl" import { useLocale, useTranslations } from "next-intl"
@ -8,7 +9,7 @@ import { useLocale, useTranslations } from "next-intl"
export const Locales: React.FC = () => { export const Locales: React.FC = () => {
const router = useRouter() const router = useRouter()
const pathname = usePathname() const pathname = usePathname()
const currentLocale = useLocale() const localeCurrent = useLocale() as Locale
const t = useTranslations() const t = useTranslations()
@ -21,7 +22,7 @@ export const Locales: React.FC = () => {
key={locale} key={locale}
className={classNames("rounded-md p-2", { className={classNames("rounded-md p-2", {
"border-primary dark:border-primary-dark border": "border-primary dark:border-primary-dark border":
locale === currentLocale, locale === localeCurrent,
})} })}
> >
<button <button

View File

@ -2,13 +2,18 @@
import { classNames } from "@repo/config-tailwind/classNames" import { classNames } from "@repo/config-tailwind/classNames"
import { useIsMounted } from "@repo/react-hooks/useIsMounted" import { useIsMounted } from "@repo/react-hooks/useIsMounted"
import { ThemeProvider as NextThemeProvider, useTheme } from "next-themes" import {
ThemeProvider as NextThemeProvider,
useTheme as useNextTheme,
} from "next-themes"
export const THEMES = ["light", "dark"] as const export const THEMES = ["light", "dark"] as const
export type Theme = (typeof THEMES)[number] export type Theme = (typeof THEMES)[number]
export const THEME_DEFAULT = "light" as Theme export const THEME_DEFAULT = "dark" as Theme
export const ThemeProvider: React.FC<React.PropsWithChildren> = (props) => { export interface ThemeProviderProps extends React.PropsWithChildren {}
export const ThemeProvider: React.FC<ThemeProviderProps> = (props) => {
const { children } = props const { children } = props
return ( return (
@ -22,19 +27,35 @@ export const ThemeProvider: React.FC<React.PropsWithChildren> = (props) => {
) )
} }
export const SwitchTheme: React.FC = () => { export interface UseThemeOutput {
const { setTheme, theme: themeData } = useTheme() theme: Theme
toggleTheme: () => void
}
export const useTheme = (): UseThemeOutput => {
const { setTheme, theme: themeData } = useNextTheme()
const { isMounted } = useIsMounted() const { isMounted } = useIsMounted()
const theme = isMounted ? (themeData as Theme) : THEME_DEFAULT const theme = isMounted ? (themeData as Theme) : THEME_DEFAULT
const handleClick = (): void => { const toggleTheme: UseThemeOutput["toggleTheme"] = () => {
const newTheme = theme === "dark" ? "light" : "dark" const newTheme = theme === "dark" ? "light" : "dark"
setTheme(newTheme) setTheme(newTheme)
} }
return {
theme,
toggleTheme,
}
}
export interface SwitchThemeProps {}
export const SwitchTheme: React.FC<SwitchThemeProps> = () => {
const { theme, toggleTheme } = useTheme()
return ( return (
<div className="flex items-center justify-center" onClick={handleClick}> <div className="flex items-center justify-center" onClick={toggleTheme}>
<div className="relative inline-block cursor-pointer touch-pan-x select-none border-0 bg-transparent p-0"> <div className="relative inline-block cursor-pointer touch-pan-x select-none border-0 bg-transparent p-0">
<div className="h-[24px] w-[50px] rounded-[30px] bg-[#4d4d4d] p-0 text-white transition-all duration-200 ease-in-out"> <div className="h-[24px] w-[50px] rounded-[30px] bg-[#4d4d4d] p-0 text-white transition-all duration-200 ease-in-out">
<div <div

View File

@ -0,0 +1,24 @@
import { classNames } from "@repo/config-tailwind/classNames"
export interface MainLayoutProps
extends React.ComponentPropsWithoutRef<"main"> {
className?: string
center?: boolean
}
export const MainLayout: React.FC<MainLayoutProps> = (props) => {
const { className, center = false, ...rest } = props
return (
<main
className={classNames(
"min-h-[calc(100vh-188px)] md:mx-auto md:max-w-4xl lg:max-w-7xl",
{
"flex flex-col items-center justify-center text-center": center,
},
className,
)}
{...rest}
/>
)
}

View File

@ -0,0 +1,29 @@
import { classNames } from "@repo/config-tailwind/classNames"
export interface SectionProps
extends React.ComponentPropsWithoutRef<"section"> {
verticalSpacing?: boolean
horizontalSpacing?: boolean
}
export const Section: React.FC<SectionProps> = (props) => {
const {
className,
verticalSpacing = false,
horizontalSpacing = false,
...rest
} = props
return (
<section
className={classNames(
{
"my-12": verticalSpacing,
"mx-6": horizontalSpacing,
},
className,
)}
{...rest}
/>
)
}

View File

@ -1,20 +0,0 @@
import { classNames } from "@repo/config-tailwind/classNames"
export interface MainLayoutProps extends React.PropsWithChildren {
className?: string
}
export const MainLayout: React.FC<MainLayoutProps> = (props) => {
const { children, className } = props
return (
<main
className={classNames(
"flex min-h-[calc(100vh-144px)] flex-col p-6",
className,
)}
>
{children}
</main>
)
}

View File

@ -0,0 +1,14 @@
{
"root": true,
"extends": ["@repo/eslint-config"],
"overrides": [
{
"files": ["*.ts", "*.tsx"],
"plugins": ["@typescript-eslint"],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"project": true
}
}
]
}

View File

@ -0,0 +1,28 @@
{
"name": "@repo/utils",
"version": "3.3.2",
"private": true,
"type": "module",
"exports": {
"./constants": "./src/constants.ts",
"./dates": "./src/dates.ts",
"./strings": "./src/strings.ts"
},
"scripts": {
"lint:eslint": "eslint src --max-warnings 0 --report-unused-disable-directives",
"lint:typescript": "tsc --noEmit",
"test": "vitest run",
"test:ui": "vitest --ui --no-open"
},
"devDependencies": {
"@repo/eslint-config": "workspace:*",
"@repo/config-typescript": "workspace:*",
"@types/node": "catalog:",
"@total-typescript/ts-reset": "catalog:",
"@vitest/coverage-istanbul": "catalog:",
"@vitest/ui": "catalog:",
"eslint": "catalog:",
"typescript": "catalog:",
"vitest": "catalog:"
}
}

View File

@ -0,0 +1,9 @@
import packageJSON from "../package.json"
export const VERSION =
process.env["NODE_ENV"] === "development"
? "0.0.0-development"
: packageJSON.version
export const GIT_REPO_LINK =
"https://git.theoludwig.fr/theoludwig/wikipedia-game-solver"

View File

@ -0,0 +1,10 @@
/**
* Returns a date as a string value in ISO 8601 format (without time information).
*
* @param date
* @returns
* @example getISODate(new Date("2012-05-23")) // "2012-05-23"
*/
export const getISODate = (date: Date): string => {
return date.toISOString().slice(0, 10)
}

View File

@ -0,0 +1,9 @@
/**
* Capitalize the first letter of a string.
* @param string
* @returns
* @example capitalize("hello, world!") // "Hello, world!"
*/
export const capitalize = (string: string): string => {
return string.charAt(0).toUpperCase() + string.slice(1)
}

View File

@ -0,0 +1,36 @@
import { afterEach, describe, expect, it, vi } from "vitest"
describe("VERSION", () => {
afterEach(() => {
vi.unstubAllEnvs()
vi.resetModules()
vi.restoreAllMocks()
})
it('should return "0.0.0-development" when NODE_ENV is development', async () => {
// Arrange - Given
vi.stubEnv("NODE_ENV", "development")
// Act - When
const { VERSION } = await import("../constants.js")
// Assert - Then
const expected = "0.0.0-development"
expect(VERSION).toEqual(expected)
})
it("should return the version from package.json when NODE_ENV is not development", async () => {
// Arrange - Given
vi.stubEnv("NODE_ENV", "production")
vi.mock("../../package.json", () => {
return { default: { version: "1.0.0" } }
})
// Act - When
const { VERSION } = await import("../constants.js")
// Assert - Then
const expected = "1.0.0"
expect(VERSION).toEqual(expected)
})
})

View File

@ -0,0 +1,17 @@
import { describe, expect, it } from "vitest"
import { getISODate } from "../dates.js"
describe("getISODate", () => {
it("should return the correct date in ISO format (e.g: 2012-05-23)", () => {
// Arrange - Given
const input = new Date("2012-05-23")
// Act - When
const output = getISODate(input)
// Assert - Then
const expected = "2012-05-23"
expect(output).toEqual(expected)
})
})

View File

@ -0,0 +1,41 @@
import { describe, expect, it } from "vitest"
import { capitalize } from "../strings.js"
describe("capitalize", () => {
it("should capitalize the first letter of a string", () => {
// Arrange - Given
const input = "hello, world!"
// Act - When
const output = capitalize(input)
// Assert - Then
const expected = "Hello, world!"
expect(output).toEqual(expected)
})
it("should return an empty string when the input is an empty string", () => {
// Arrange - Given
const input = ""
// Act - When
const output = capitalize(input)
// Assert - Then
const expected = ""
expect(output).toEqual(expected)
})
it("should return the same string when the first letter is already capitalized", () => {
// Arrange - Given
const input = "Hello, world!"
// Act - When
const output = capitalize(input)
// Assert - Then
const expected = "Hello, world!"
expect(output).toEqual(expected)
})
})

View File

@ -2,9 +2,12 @@
"extends": "@repo/config-typescript/tsconfig.json", "extends": "@repo/config-typescript/tsconfig.json",
"compilerOptions": { "compilerOptions": {
"target": "ESNext", "target": "ESNext",
"module": "ESNext", "module": "NodeNext",
"moduleResolution": "Bundler", "moduleResolution": "NodeNext",
"resolveJsonModule": true, "resolveJsonModule": true,
"lib": ["ESNext"],
"types": ["@types/node", "@total-typescript/ts-reset"],
"noEmit": true "noEmit": true
} }
} }

View File

@ -0,0 +1,10 @@
import { defineConfig } from "vitest/config"
export default defineConfig({
test: {
coverage: {
enabled: true,
provider: "istanbul",
},
},
})

View File

@ -17,6 +17,7 @@
"@repo/config-tailwind": "workspace:*", "@repo/config-tailwind": "workspace:*",
"@repo/i18n": "workspace:*", "@repo/i18n": "workspace:*",
"@repo/ui": "workspace:*", "@repo/ui": "workspace:*",
"ky": "catalog:",
"next": "catalog:", "next": "catalog:",
"next-intl": "catalog:", "next-intl": "catalog:",
"react": "catalog:", "react": "catalog:",

View File

@ -1,21 +1,51 @@
"use client" "use client"
import { Button } from "@repo/ui/design/Button" import { Button } from "@repo/ui/Design/Button"
import { Link } from "@repo/ui/Design/Link"
import { Typography } from "@repo/ui/Design/Typography"
import { useState } from "react" import { useState } from "react"
import {
const wait = async (ms: number): Promise<void> => { fromLocaleToWikipediaLocale,
return await new Promise((resolve) => { getWikipediaLink,
setTimeout(resolve, ms) getWikipediaPageInternalLinks,
}) } from "./wikipedia-api"
}
export const WikipediaClient: React.FC = () => { export const WikipediaClient: React.FC = () => {
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
const localeWikipedia = fromLocaleToWikipediaLocale("en-US")
const wikipediaLink = getWikipediaLink(localeWikipedia)
const handleClick: React.MouseEventHandler<HTMLButtonElement> = async () => { const handleClick: React.MouseEventHandler<HTMLButtonElement> = async () => {
console.log("clicked")
setIsLoading(true) setIsLoading(true)
await wait(2_000) const fromPageInput = "Linux"
const toPageInput = "Node.js"
console.log({
fromPageInput,
toPageInput,
})
const [fromPageWikipediaLinks, toPageWikipediaLinks] = await Promise.all([
getWikipediaPageInternalLinks({
title: fromPageInput,
locale: localeWikipedia,
}),
getWikipediaPageInternalLinks({
title: toPageInput,
locale: localeWikipedia,
}),
])
console.log({
fromPageWikipediaLinks,
toPageWikipediaLinks,
})
// const deepInternalLinks = await getDeepWikipediaPageInternalLinks({
// locale: localeWikipedia,
// data: {
// [fromPageWikipediaLinks.title]: fromPageWikipediaLinks,
// [toPageWikipediaLinks.title]: toPageWikipediaLinks,
// },
// })
// console.log(deepInternalLinks)
setIsLoading(false) setIsLoading(false)
} }
@ -24,15 +54,11 @@ export const WikipediaClient: React.FC = () => {
<Button onClick={handleClick} isLoading={isLoading} className="w-36"> <Button onClick={handleClick} isLoading={isLoading} className="w-36">
Wikipedia Wikipedia
</Button> </Button>
<Typography variant="text1" as="p">
<Button <Link href={wikipediaLink} target="_blank">
onClick={handleClick} {wikipediaLink.replace("https://", "")}
variant="outline" </Link>
isLoading={isLoading} </Typography>
className="w-36"
>
Wikipedia
</Button>
</section> </section>
) )
} }

View File

@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest"
import { sum } from "../wikipedia-api" import { sum } from "../wikipedia-api"
describe("wikipedia-game-solver", () => { describe("sum", () => {
it("adds 1 + 2 to equal 3", () => { it("adds 1 + 2 to equal 3", () => {
expect(sum(1, 2)).toBe(3) expect(sum(1, 2)).toBe(3)
}) })

View File

@ -1,28 +1,194 @@
import type { Locale } from "@repo/i18n/config"
import ky from "ky"
export const sum = (a: number, b: number): number => { export const sum = (a: number, b: number): number => {
return a + b return a + b
} }
export const wikipediaAPIBaseURL = new URL("https://en.wikipedia.org/w/api.php") /**
* @see https://www.mediawiki.org/wiki/Wikimedia_REST_API#Terms_and_conditions
* To avoid impacting other API users, limit your clients to no more than 200 requests/sec to this API overall. Many entry points additionally specify and enforce more restrictive rate limits (HTTP 429 error).
*/
// const getWikipediaPageLinks = async (title: string): Promise<string[]> => { export const WIKIPEDIA_LOCALES = ["en", "fr"] as const
// const url = new URL(wikipediaAPIBaseURL) export type WikipediaLocale = (typeof WIKIPEDIA_LOCALES)[number]
// url.searchParams.append("action", "query")
// url.searchParams.append("titles", title)
// url.searchParams.append("prop", "links")
// url.searchParams.append("pllimit", "max")
// url.searchParams.append("format", "json")
// url.searchParams.append("origin", "*")
// const response = await fetch(url, {
// method: "GET",
// })
// if (!response.ok) {
// throw new Error(response.statusText)
// }
// const json = await response.json()
// return json
// }
// const links = await getWikipediaPageLinks("France") const WIKIPEDIA_LOCALES_MAP: Record<Locale, WikipediaLocale> = {
// const links2 = await getWikipediaPageLinks("Frddgdgdgance") "en-US": "en",
// console.log("links.length", links) "fr-FR": "fr",
// console.log("links.length", links2) }
export const fromLocaleToWikipediaLocale = (
locale: Locale,
): WikipediaLocale => {
return WIKIPEDIA_LOCALES_MAP[locale]
}
export const getWikipediaLink = (locale: WikipediaLocale): string => {
return `https://${locale}.wikipedia.org`
}
interface WikipediaQueryLinksResponse {
continue?: {
plcontinue: string
continue: string
}
query: {
pages: {
[key: string]: {
pageid: number
ns: number
title: string
links: [
{
ns: number
title: string
},
]
}
}
}
limits: {
links: number
}
}
interface GetWikipediaPageInternalLinksInput {
title: string
locale: WikipediaLocale
}
interface GetWikipediaPageInternalLinksOutput {
/**
* Title of the Wikipedia page.
*/
title: string
/**
* Page id is unique for each page on Wikipedia, can be used to link to the page.
* @example `https://${locale}.wikipedia.org/?curid=${pageId}`
*/
pageId: number
/**
* List of internal links on the Wikipedia page.
*/
links: string[]
}
/**
* Get internal links from a Wikipedia page.
* @param input
* @returns
*/
export const getWikipediaPageInternalLinks = async (
input: GetWikipediaPageInternalLinksInput,
): Promise<GetWikipediaPageInternalLinksOutput> => {
const links: string[] = []
let title = input.title
let pageId = 0
let plcontinue: string | null = null
const fetchLinks = async (): Promise<WikipediaQueryLinksResponse> => {
const url = new URL("/w/api.php", getWikipediaLink(input.locale))
url.searchParams.append("action", "query")
url.searchParams.append("titles", title)
url.searchParams.append("prop", "links")
url.searchParams.append("pllimit", "max")
url.searchParams.append("format", "json")
url.searchParams.append("origin", "*")
if (plcontinue != null) {
url.searchParams.set("plcontinue", plcontinue)
}
return await ky
.get(url, {
method: "GET",
})
.json()
}
do {
try {
const response = await fetchLinks()
plcontinue = response?.continue?.plcontinue ?? null
const pages = Object.keys(response.query.pages)
const page = pages[0] ?? ""
if (page === "-1" || page === "") {
break
}
const pageData = response.query.pages[page]
if (pageData == null) {
break
}
title = pageData.title
pageId = pageData.pageid
links.push(
...pageData.links.map((link) => {
return link.title
}),
)
} catch (error) {
console.error("Error", error)
console.error("title", title)
throw error
}
} while (plcontinue != null)
return {
title,
pageId,
links,
}
}
export interface WikipediaPagesInternalLinks {
[key: string]: GetWikipediaPageInternalLinksOutput
}
export interface GetDeepWikipediaPageInternalLinksInput {
locale: WikipediaLocale
data: WikipediaPagesInternalLinks
}
export const getDeepWikipediaPageInternalLinks = async (
input: GetDeepWikipediaPageInternalLinksInput,
): Promise<void> => {
const pagesTitles = Object.keys(input.data)
for (const pageTitle of pagesTitles) {
const links = input.data[pageTitle]?.links ?? []
for (const pageTitleLink of links) {
if (pageTitleLink in input.data) {
continue
}
input.data[pageTitleLink] = await getWikipediaPageInternalLinks({
locale: input.locale,
title: pageTitleLink,
})
// await getDeepWikipediaPageInternalLinks({
// locale: input.locale,
// data: input.data,
// })
}
}
// await Promise.all(
// pagesTitles.map(async (pageTitle) => {
// const links = input.data[pageTitle]?.links ?? []
// await Promise.all(
// links.map(async (pageTitleLink) => {
// if (pageTitleLink in input.data) {
// return
// }
// input.data[pageTitleLink] = await getWikipediaPageInternalLinks({
// locale: input.locale,
// title: pageTitleLink,
// })
// await getDeepWikipediaPageInternalLinks({
// locale: input.locale,
// data: input.data,
// })
// }),
// )
// }),
// )
}

3547
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -5,6 +5,7 @@ packages:
catalog: catalog:
# Utils # Utils
"deepmerge": "4.3.1" "deepmerge": "4.3.1"
"ky": "1.5.0"
# React.js/Next.js # React.js/Next.js
"next": "14.2.5" "next": "14.2.5"
@ -15,53 +16,57 @@ catalog:
"react-icons": "5.2.1" "react-icons": "5.2.1"
"@types/react": "18.3.3" "@types/react": "18.3.3"
"@types/react-dom": "18.3.0" "@types/react-dom": "18.3.0"
"sharp": "0.33.4"
# TypeScript # TypeScript
"typescript": "5.5.4" "typescript": "5.5.4"
"@total-typescript/ts-reset": "0.5.1" "@total-typescript/ts-reset": "0.5.1"
"@types/node": "20.14.12" "@types/node": "22.0.0"
"tsx": "4.16.2" "tsx": "4.16.3"
# ESLint # ESLint
"@typescript-eslint/eslint-plugin": "7.17.0" "@typescript-eslint/eslint-plugin": "7.18.0"
"@typescript-eslint/parser": "7.17.0" "@typescript-eslint/parser": "7.18.0"
"eslint": "8.57.0" "eslint": "8.57.0"
"eslint-config-conventions": "14.3.0" "eslint-config-conventions": "14.4.0"
"eslint-plugin-promise": "6.5.1" "eslint-plugin-promise": "7.0.0"
"eslint-plugin-unicorn": "54.0.0" "eslint-plugin-unicorn": "55.0.0"
"eslint-config-next": "14.2.5" "eslint-config-next": "14.2.5"
"eslint-plugin-storybook": "0.8.0" "eslint-plugin-storybook": "0.8.0"
"eslint-plugin-tailwindcss": "3.17.4" "eslint-plugin-tailwindcss": "3.17.4"
# Storybook # Storybook
"@chromatic-com/storybook": "1.6.1" "@chromatic-com/storybook": "1.6.1"
"@storybook/addon-a11y": "8.2.5" "@storybook/addon-a11y": "8.2.6"
"@storybook/addon-essentials": "8.2.5" "@storybook/addon-essentials": "8.2.6"
"@storybook/addon-interactions": "8.2.5" "@storybook/addon-interactions": "8.2.6"
"@storybook/addon-links": "8.2.5" "@storybook/addon-links": "8.2.6"
"@storybook/addon-storysource": "8.2.5" "@storybook/addon-storysource": "8.2.6"
"@storybook/addon-themes": "8.2.5" "@storybook/addon-themes": "8.2.6"
"@storybook/blocks": "8.2.5" "@storybook/blocks": "8.2.6"
"@storybook/nextjs": "8.2.5" "@storybook/nextjs": "8.2.6"
"@storybook/react": "8.2.5" "@storybook/react": "8.2.6"
"@storybook/test": "8.2.5" "@storybook/test": "8.2.6"
"@storybook/test-runner": "0.19.1" "@storybook/test-runner": "0.19.1"
"chromatic": "11.5.6" "chromatic": "11.6.0"
"http-server": "14.1.1" "http-server": "14.1.1"
"storybook": "8.2.5" "storybook": "8.2.6"
"storybook-dark-mode": "4.0.2" "storybook-dark-mode": "4.0.2"
# Testing # Testing
"playwright": "1.45.3"
"@playwright/test": "1.45.3" "@playwright/test": "1.45.3"
"axe-playwright": "2.0.1" "axe-playwright": "2.0.1"
"start-server-and-test": "2.0.4" "start-server-and-test": "2.0.5"
"@vitest/coverage-istanbul": "2.0.4" "@vitest/browser": "2.0.5"
"@vitest/ui": "2.0.4" "@vitest/coverage-istanbul": "2.0.5"
"vitest": "2.0.4" "@vitest/ui": "2.0.5"
"vitest": "2.0.5"
"@testing-library/react": "16.0.0"
# CSS # CSS
"postcss": "8.4.39" "postcss": "8.4.40"
"tailwindcss": "3.4.6" "tailwindcss": "3.4.7"
"@fontsource/montserrat": "5.0.18" "@fontsource/montserrat": "5.0.18"
"clsx": "2.1.0" "clsx": "2.1.0"
"cva": "1.0.0-beta.1" "cva": "1.0.0-beta.1"

View File

@ -1,5 +1,6 @@
{ {
"$schema": "https://turbo.build/schema.json", "$schema": "https://turbo.build/schema.json",
"ui": "tui",
"tasks": { "tasks": {
"build": { "build": {
"inputs": ["$TURBO_DEFAULT$", ".env*"], "inputs": ["$TURBO_DEFAULT$", ".env*"],