Compare commits
10 Commits
dfd9055318
...
33b57bf173
Author | SHA1 | Date | |
---|---|---|---|
33b57bf173 | |||
90a8a50ad0 | |||
74a3148e92 | |||
89ec7443a0 | |||
ccd44c10fa | |||
867fc131b1 | |||
624d235a0e | |||
90abfb6de8 | |||
0ee7b35530 | |||
6c717e5768 |
1
.gitignore
vendored
1
.gitignore
vendored
@ -21,6 +21,7 @@ build/
|
|||||||
*.pem
|
*.pem
|
||||||
.turbo
|
.turbo
|
||||||
bin/
|
bin/
|
||||||
|
cache.json
|
||||||
|
|
||||||
# debug
|
# debug
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
|
2
.vscode/react.code-snippets
vendored
2
.vscode/react.code-snippets
vendored
@ -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
13
TODO.md
@ -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>
|
||||||
|
@ -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": {
|
||||||
|
@ -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
11
apps/cli/src/main.ts
Executable 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}`)
|
@ -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>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -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:"
|
||||||
},
|
},
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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],
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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>
|
||||||
)
|
)
|
||||||
|
@ -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
|
||||||
|
@ -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>
|
||||||
)
|
)
|
||||||
|
@ -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
|
||||||
|
@ -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 |
BIN
apps/website/public/images/Wikipedia-Logo.webp
Normal file
BIN
apps/website/public/images/Wikipedia-Logo.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 91 KiB |
@ -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
17
data/README.md
Normal 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>
|
@ -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:"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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],
|
||||||
|
@ -13,6 +13,8 @@
|
|||||||
|
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"skipLibCheck": true
|
"skipLibCheck": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"incremental": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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:"
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,6 +0,0 @@
|
|||||||
import packageJSON from "./package.json"
|
|
||||||
|
|
||||||
export const VERSION =
|
|
||||||
process.env["NODE_ENV"] === "development"
|
|
||||||
? "0.0.0-development"
|
|
||||||
: packageJSON.version
|
|
@ -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."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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:"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
83
packages/react-hooks/src/tests/useBoolean.test.ts
Normal file
83
packages/react-hooks/src/tests/useBoolean.test.ts
Normal 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)
|
||||||
|
})
|
||||||
|
})
|
16
packages/react-hooks/src/tests/useIsMounted.test.ts
Normal file
16
packages/react-hooks/src/tests/useIsMounted.test.ts
Normal 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)
|
||||||
|
})
|
||||||
|
})
|
50
packages/react-hooks/src/useBoolean.ts
Normal file
50
packages/react-hooks/src/useBoolean.ts
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
15
packages/react-hooks/vitest.config.ts
Normal file
15
packages/react-hooks/vitest.config.ts
Normal 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",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
@ -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:",
|
||||||
|
@ -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",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
@ -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: {},
|
||||||
|
}
|
26
packages/ui/src/Errors/ErrorNotFound/ErrorNotFound.tsx
Normal file
26
packages/ui/src/Errors/ErrorNotFound/ErrorNotFound.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
22
packages/ui/src/Errors/ErrorServer/ErrorServer.stories.tsx
Normal file
22
packages/ui/src/Errors/ErrorServer/ErrorServer.stories.tsx
Normal 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()
|
||||||
|
},
|
||||||
|
}
|
37
packages/ui/src/Errors/ErrorServer/ErrorServer.tsx
Normal file
37
packages/ui/src/Errors/ErrorServer/ErrorServer.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
)
|
)
|
@ -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>
|
||||||
|
|
@ -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 />
|
@ -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
|
@ -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
|
24
packages/ui/src/Layout/MainLayout/MainLayout.tsx
Normal file
24
packages/ui/src/Layout/MainLayout/MainLayout.tsx
Normal 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}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
29
packages/ui/src/Layout/Section/Section.tsx
Normal file
29
packages/ui/src/Layout/Section/Section.tsx
Normal 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}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
14
packages/utils/.eslintrc.json
Normal file
14
packages/utils/.eslintrc.json
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
28
packages/utils/package.json
Normal file
28
packages/utils/package.json
Normal 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:"
|
||||||
|
}
|
||||||
|
}
|
9
packages/utils/src/constants.ts
Normal file
9
packages/utils/src/constants.ts
Normal 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"
|
10
packages/utils/src/dates.ts
Normal file
10
packages/utils/src/dates.ts
Normal 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)
|
||||||
|
}
|
9
packages/utils/src/strings.ts
Normal file
9
packages/utils/src/strings.ts
Normal 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)
|
||||||
|
}
|
36
packages/utils/src/tests/constants.test.ts
Normal file
36
packages/utils/src/tests/constants.test.ts
Normal 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)
|
||||||
|
})
|
||||||
|
})
|
17
packages/utils/src/tests/dates.test.ts
Normal file
17
packages/utils/src/tests/dates.test.ts
Normal 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)
|
||||||
|
})
|
||||||
|
})
|
41
packages/utils/src/tests/strings.test.ts
Normal file
41
packages/utils/src/tests/strings.test.ts
Normal 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)
|
||||||
|
})
|
||||||
|
})
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
10
packages/utils/vitest.config.ts
Normal file
10
packages/utils/vitest.config.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { defineConfig } from "vitest/config"
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
test: {
|
||||||
|
coverage: {
|
||||||
|
enabled: true,
|
||||||
|
provider: "istanbul",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
@ -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:",
|
||||||
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
})
|
})
|
||||||
|
@ -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
3547
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -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"
|
||||||
|
@ -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*"],
|
||||||
|
Reference in New Issue
Block a user