chore: initial commit
This commit is contained in:
commit
3ae6d2fac3
38
.dockerignore
Normal file
38
.dockerignore
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
**/.git
|
||||||
|
**/.turbo
|
||||||
|
**/.next
|
||||||
|
**/out
|
||||||
|
**/dist
|
||||||
|
**/build
|
||||||
|
**/storybook-static
|
||||||
|
**/coverage
|
||||||
|
**/node_modules
|
||||||
|
|
||||||
|
# envs
|
||||||
|
.env
|
||||||
|
.env.production
|
||||||
|
.env.development
|
||||||
|
secrets
|
||||||
|
|
||||||
|
# IDEs and editors
|
||||||
|
.idea
|
||||||
|
.project
|
||||||
|
.classpath
|
||||||
|
.c9/
|
||||||
|
*.launch
|
||||||
|
.settings/
|
||||||
|
*.sublime-workspace
|
||||||
|
.vscode
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
*.pem
|
||||||
|
Dockerfile
|
||||||
|
README.md
|
||||||
|
|
||||||
|
# vercel
|
||||||
|
.vercel
|
||||||
|
|
||||||
|
# typescript
|
||||||
|
*.tsbuildinfo
|
||||||
|
next-env.d.ts
|
11
.editorconfig
Normal file
11
.editorconfig
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
# https://editorconfig.org/
|
||||||
|
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
end_of_line = lf
|
||||||
|
charset = utf-8
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
1
.env.example
Normal file
1
.env.example
Normal file
@ -0,0 +1 @@
|
|||||||
|
WEBSITE_PORT=5000
|
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
* text=auto eol=lf
|
33
.github/workflows/chromatic.yml
vendored
Normal file
33
.github/workflows/chromatic.yml
vendored
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
name: "Chromatic"
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [develop]
|
||||||
|
pull_request:
|
||||||
|
branches: [develop, staging, main]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
chromatic:
|
||||||
|
timeout-minutes: 30
|
||||||
|
runs-on: "ubuntu-latest"
|
||||||
|
steps:
|
||||||
|
- uses: "actions/checkout@v4.1.7"
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- uses: "pnpm/action-setup@v4.0.0"
|
||||||
|
|
||||||
|
- name: "Setup Node.js"
|
||||||
|
uses: "actions/setup-node@v4.0.3"
|
||||||
|
with:
|
||||||
|
node-version: "22.x"
|
||||||
|
cache: "pnpm"
|
||||||
|
|
||||||
|
- name: "Install dependencies"
|
||||||
|
run: "pnpm install --frozen-lockfile"
|
||||||
|
|
||||||
|
- name: "Run Chromatic"
|
||||||
|
uses: "chromaui/action@latest"
|
||||||
|
with:
|
||||||
|
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
|
||||||
|
workingDir: "apps/storybook"
|
42
.github/workflows/ci.yml
vendored
Normal file
42
.github/workflows/ci.yml
vendored
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
name: "CI"
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [develop]
|
||||||
|
pull_request:
|
||||||
|
branches: [develop, staging, main]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
ci:
|
||||||
|
timeout-minutes: 30
|
||||||
|
runs-on: "ubuntu-latest"
|
||||||
|
steps:
|
||||||
|
- uses: "actions/checkout@v4.1.7"
|
||||||
|
|
||||||
|
- uses: "pnpm/action-setup@v4.0.0"
|
||||||
|
|
||||||
|
- name: "Setup Node.js"
|
||||||
|
uses: "actions/setup-node@v4.0.3"
|
||||||
|
with:
|
||||||
|
node-version: "22.x"
|
||||||
|
cache: "pnpm"
|
||||||
|
|
||||||
|
- name: "Install dependencies"
|
||||||
|
run: "pnpm install --frozen-lockfile"
|
||||||
|
|
||||||
|
# - name: "Install Playwright"
|
||||||
|
# run: "pnpm exec playwright install --with-deps"
|
||||||
|
|
||||||
|
- run: "node --run lint:editorconfig"
|
||||||
|
- run: "node --run lint:prettier"
|
||||||
|
- run: "node --run lint:eslint"
|
||||||
|
- run: "node --run lint:typescript"
|
||||||
|
- run: "node --run test"
|
||||||
|
- run: "node --run build"
|
||||||
|
|
||||||
|
commitlint:
|
||||||
|
runs-on: "ubuntu-latest"
|
||||||
|
steps:
|
||||||
|
- uses: "actions/checkout@v4.1.7"
|
||||||
|
|
||||||
|
- uses: "wagoid/commitlint-github-action@v6.0.1"
|
48
.github/workflows/release.yml
vendored
Normal file
48
.github/workflows/release.yml
vendored
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
name: "Release"
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [staging]
|
||||||
|
# branches: [main, staging]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
timeout-minutes: 30
|
||||||
|
runs-on: "ubuntu-latest"
|
||||||
|
permissions:
|
||||||
|
contents: "write"
|
||||||
|
issues: "write"
|
||||||
|
pull-requests: "write"
|
||||||
|
id-token: "write"
|
||||||
|
steps:
|
||||||
|
- uses: "actions/checkout@v4.1.7"
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: "Import GPG key"
|
||||||
|
uses: "crazy-max/ghaction-import-gpg@v6.0.0"
|
||||||
|
with:
|
||||||
|
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||||
|
git_user_signingkey: true
|
||||||
|
git_commit_gpgsign: true
|
||||||
|
|
||||||
|
- uses: "pnpm/action-setup@v4.0.0"
|
||||||
|
|
||||||
|
- name: "Setup Node.js"
|
||||||
|
uses: "actions/setup-node@v4.0.3"
|
||||||
|
with:
|
||||||
|
node-version: "22.x"
|
||||||
|
cache: "pnpm"
|
||||||
|
|
||||||
|
- name: "Install dependencies"
|
||||||
|
run: "pnpm install --frozen-lockfile"
|
||||||
|
|
||||||
|
- name: "Release"
|
||||||
|
run: "node --run release"
|
||||||
|
env:
|
||||||
|
GH_URL: ${{ secrets.GH_URL }}
|
||||||
|
GH_PREFIX: ${{ secrets.GH_PREFIX }}
|
||||||
|
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||||
|
GIT_COMMITTER_NAME: ${{ secrets.GIT_NAME }}
|
||||||
|
GIT_COMMITTER_EMAIL: ${{ secrets.GIT_EMAIL }}
|
53
.gitignore
vendored
Normal file
53
.gitignore
vendored
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
# dependencies
|
||||||
|
node_modules
|
||||||
|
.npm
|
||||||
|
package-lock.json
|
||||||
|
.pnpm-store
|
||||||
|
.pnp
|
||||||
|
.pnp.js
|
||||||
|
.yarn/install-state.gz
|
||||||
|
|
||||||
|
# testing
|
||||||
|
coverage
|
||||||
|
|
||||||
|
# production
|
||||||
|
.next/
|
||||||
|
out/
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
*.pem
|
||||||
|
.turbo
|
||||||
|
bin/
|
||||||
|
|
||||||
|
# debug
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# Storybook
|
||||||
|
*storybook.log
|
||||||
|
storybook-static
|
||||||
|
|
||||||
|
# IDEs and editors
|
||||||
|
.idea
|
||||||
|
.project
|
||||||
|
.classpath
|
||||||
|
.c9/
|
||||||
|
*.launch
|
||||||
|
.settings/
|
||||||
|
*.sublime-workspace
|
||||||
|
|
||||||
|
# local env files
|
||||||
|
.env
|
||||||
|
.env.production
|
||||||
|
.env*.local
|
||||||
|
|
||||||
|
# vercel
|
||||||
|
.vercel
|
||||||
|
|
||||||
|
# typescript
|
||||||
|
*.tsbuildinfo
|
||||||
|
# next-env.d.ts
|
13
.prettierrc.json
Executable file
13
.prettierrc.json
Executable file
@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"semi": false,
|
||||||
|
"plugins": ["prettier-plugin-tailwindcss"],
|
||||||
|
"tailwindFunctions": ["classNames", "cva"],
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": "pnpm-lock.yaml",
|
||||||
|
"options": {
|
||||||
|
"rangeEnd": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
40
.releaserc.json
Normal file
40
.releaserc.json
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"branches": ["main", { "name": "staging", "prerelease": true }],
|
||||||
|
"plugins": [
|
||||||
|
"@semantic-release/commit-analyzer",
|
||||||
|
"@semantic-release/release-notes-generator",
|
||||||
|
[
|
||||||
|
"@semantic-release/exec",
|
||||||
|
{
|
||||||
|
"prepareCmd": "replace-in-files --regex='version\": *\"[^\"]*' --replacement='\"version\": \"${nextRelease.version}\"' '**/package.json' '!**/node_modules/**'"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"@semantic-release/git",
|
||||||
|
{
|
||||||
|
"assets": [
|
||||||
|
"package.json",
|
||||||
|
"apps/*/package.json",
|
||||||
|
"packages/*/package.json"
|
||||||
|
],
|
||||||
|
"message": "chore(release): ${nextRelease.version} [skip ci]"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"@semantic-release/github",
|
||||||
|
{
|
||||||
|
"successComment": false,
|
||||||
|
"failComment": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"@saithodev/semantic-release-backmerge",
|
||||||
|
{
|
||||||
|
"branches": [
|
||||||
|
{ "from": "main", "to": "develop" },
|
||||||
|
{ "from": "staging", "to": "develop" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
11
.vscode/extensions.json
vendored
Normal file
11
.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"Vercel.turbo-vsc",
|
||||||
|
"editorconfig.editorconfig",
|
||||||
|
"esbenp.prettier-vscode",
|
||||||
|
"dbaeumer.vscode-eslint",
|
||||||
|
"bradlc.vscode-tailwindcss",
|
||||||
|
"mikestead.dotenv",
|
||||||
|
"ms-azuretools.vscode-docker"
|
||||||
|
]
|
||||||
|
}
|
43
.vscode/react.code-snippets
vendored
Normal file
43
.vscode/react.code-snippets
vendored
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"React Component": {
|
||||||
|
"scope": "typescriptreact",
|
||||||
|
"prefix": "rfc",
|
||||||
|
"body": [
|
||||||
|
"interface ${1:ComponentName}Props {}",
|
||||||
|
"",
|
||||||
|
"export const ${1:ComponentName}: React.FC<${1:ComponentName}Props> = () => {",
|
||||||
|
" return (",
|
||||||
|
" <div>",
|
||||||
|
" <h1>${1:ComponentName}</h1>",
|
||||||
|
" </div>",
|
||||||
|
" )",
|
||||||
|
"}",
|
||||||
|
"",
|
||||||
|
],
|
||||||
|
"description": "React Component",
|
||||||
|
},
|
||||||
|
"React Component Story": {
|
||||||
|
"scope": "typescriptreact",
|
||||||
|
"prefix": "rfcs",
|
||||||
|
"body": [
|
||||||
|
"import type { Meta, StoryObj } from \"@storybook/react\"",
|
||||||
|
"",
|
||||||
|
"import { ${1:ComponentName} as ${1:ComponentName}Component } from \"./${1:ComponentName}\"",
|
||||||
|
"",
|
||||||
|
"const meta = {",
|
||||||
|
" title: \"${1:ComponentName}\",",
|
||||||
|
" component: ${1:ComponentName}Component",
|
||||||
|
"} satisfies Meta<typeof ${1:ComponentName}Component>",
|
||||||
|
"",
|
||||||
|
"export default meta",
|
||||||
|
"",
|
||||||
|
"type Story = StoryObj<typeof meta>",
|
||||||
|
"",
|
||||||
|
"export const ${1:ComponentName}: Story = {",
|
||||||
|
" args: {}",
|
||||||
|
"}",
|
||||||
|
"",
|
||||||
|
],
|
||||||
|
"description": "React Component Story",
|
||||||
|
},
|
||||||
|
}
|
20
.vscode/settings.json
vendored
Normal file
20
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"typescript.tsdk": "node_modules/typescript/lib",
|
||||||
|
"editor.bracketPairColorization.enabled": true,
|
||||||
|
"editor.wordWrap": "on",
|
||||||
|
"prettier.configPath": ".prettierrc.json",
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.fixAll": "explicit",
|
||||||
|
"source.organizeImports": "explicit"
|
||||||
|
},
|
||||||
|
"eslint.options": {
|
||||||
|
"ignorePath": ".gitignore"
|
||||||
|
},
|
||||||
|
"prettier.ignorePath": ".gitignore",
|
||||||
|
"tailwindCSS.experimental.classRegex": [
|
||||||
|
["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"],
|
||||||
|
["cx\\(([^)]*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"]
|
||||||
|
]
|
||||||
|
}
|
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# MIT License
|
||||||
|
|
||||||
|
Copyright (c) Théo LUDWIG <contact@theoludwig.fr>
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
75
README.md
Normal file
75
README.md
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
# Wikipedia Game Solver
|
||||||
|
|
||||||
|
> \[!IMPORTANT\]
|
||||||
|
> This project is a work in progress, at an early stage of development.
|
||||||
|
|
||||||
|
## About
|
||||||
|
|
||||||
|
The Wikipedia Game involves players competing to navigate from one [Wikipedia](https://en.wikipedia.org/) page to another using only internal links.
|
||||||
|
|
||||||
|
[**Wikipedia Game Solver**](https://wikipedia-game-solver.theoludwig.fr) is a tool that helps you find the shortest path between two Wikipedia pages, using only internal links, basically solving the Wikipedia Game for you.
|
||||||
|
|
||||||
|
Available online: <https://wikipedia-game-solver.theoludwig.fr>
|
||||||
|
|
||||||
|
> \[!NOTE\]
|
||||||
|
> The project is also a way to learn and experiment with a monorepo architecture, with [Turborepo](https://turbo.build/repo), and [TypeScript](https://www.typescriptlang.org/) as the main language.
|
||||||
|
>
|
||||||
|
> The project setup **can be used as a template/boilerplate for new projects**.
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- [Node.js](https://nodejs.org/) >= 22.0.0
|
||||||
|
- [pnpm](https://pnpm.io/) >= 9.5.0
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Go to the project root
|
||||||
|
cd wikipedia-game-solver
|
||||||
|
|
||||||
|
# Configure environment variables
|
||||||
|
cp .env.example .env
|
||||||
|
cp apps/website/.env.example apps/website/.env
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
# Install Playwright browser binaries and their dependencies (tests)
|
||||||
|
pnpm exec playwright install --with-deps
|
||||||
|
```
|
||||||
|
|
||||||
|
### Development
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Start the development server
|
||||||
|
node --run dev
|
||||||
|
|
||||||
|
# Lint
|
||||||
|
node --run lint:editorconfig
|
||||||
|
node --run lint:prettier
|
||||||
|
node --run lint:eslint
|
||||||
|
node --run lint:typescript
|
||||||
|
|
||||||
|
# Tests
|
||||||
|
node --run test
|
||||||
|
|
||||||
|
# Build
|
||||||
|
node --run build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Production environment with [Docker](https://www.docker.com/)
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Setup and run all the services for you
|
||||||
|
docker compose up --build
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Services started
|
||||||
|
|
||||||
|
`wikipedia-game-solver`: <http://127.0.0.1:5000>
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
[MIT](./LICENSE)
|
20
TODO.md
Normal file
20
TODO.md
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
# TODO
|
||||||
|
|
||||||
|
- [x] chore: initial commit (+ mirror on GitHub)
|
||||||
|
- [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 articles to go from one to another, or none if it is not possible
|
||||||
|
- [ ] v1.0.0-staging.2
|
||||||
|
- [ ] Implement CLI (`cli`)
|
||||||
|
- [ ] v1.0.0-staging.3
|
||||||
|
- [ ] Implement REST API (`api`) with JSON responses ([AdonisJS](https://adonisjs.com/))
|
||||||
|
- [ ] v1.0.0-staging.4
|
||||||
|
- [ ] v1.0.0
|
||||||
|
|
||||||
|
## Links
|
||||||
|
|
||||||
|
- <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>
|
||||||
|
- <https://en.wikipedia.org/w/api.php?action=query&titles=Title&prop=links&pllimit=max&format=json>
|
||||||
|
- [YouTube (Amixem) - WIKIPEDIA CHALLENGE ! (ce jeu est génial)](https://www.youtube.com/watch?v=wgKlFNGU174)
|
||||||
|
- [YouTube (adumb) - I Made a Graph of Wikipedia... This Is What I Found](https://www.youtube.com/watch?v=JheGL6uSF-4)
|
14
apps/cli/.eslintrc.json
Normal file
14
apps/cli/.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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
31
apps/cli/package.json
Normal file
31
apps/cli/package.json
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"name": "@repo/cli",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"imports": {
|
||||||
|
"#*": "./src/*"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"wikipedia-game-solver": "./src/index.ts"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"start": "node --import=tsx ./src/index.ts",
|
||||||
|
"dev": "node --import=tsx --watch --watch-preserve-output ./src/index.ts",
|
||||||
|
"lint:eslint": "eslint src --max-warnings 0 --report-unused-disable-directives",
|
||||||
|
"lint:typescript": "tsc --noEmit"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@repo/wikipedia-game-solver": "workspace:*",
|
||||||
|
"@repo/constants": "workspace:*",
|
||||||
|
"tsx": "catalog:"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@repo/eslint-config": "workspace:*",
|
||||||
|
"@repo/config-typescript": "workspace:*",
|
||||||
|
"@types/node": "catalog:",
|
||||||
|
"@total-typescript/ts-reset": "catalog:",
|
||||||
|
"eslint": "catalog:",
|
||||||
|
"typescript": "catalog:"
|
||||||
|
}
|
||||||
|
}
|
3
apps/cli/src/abc/def/add.ts
Normal file
3
apps/cli/src/abc/def/add.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export const add = (a: number, b: number): number => {
|
||||||
|
return a + b
|
||||||
|
}
|
11
apps/cli/src/index.ts
Executable file
11
apps/cli/src/index.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/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}`)
|
13
apps/cli/tsconfig.json
Normal file
13
apps/cli/tsconfig.json
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"extends": "@repo/config-typescript/tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"module": "NodeNext",
|
||||||
|
"moduleResolution": "NodeNext",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"lib": ["ESNext"],
|
||||||
|
"types": ["@total-typescript/ts-reset", "@types/node"],
|
||||||
|
|
||||||
|
"noEmit": true
|
||||||
|
}
|
||||||
|
}
|
4
apps/storybook/.eslintrc.json
Normal file
4
apps/storybook/.eslintrc.json
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"root": true,
|
||||||
|
"extends": ["@repo/eslint-config"]
|
||||||
|
}
|
31
apps/storybook/.storybook/main.ts
Normal file
31
apps/storybook/.storybook/main.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import type { StorybookConfig } from "@storybook/nextjs"
|
||||||
|
|
||||||
|
const config: StorybookConfig = {
|
||||||
|
core: {
|
||||||
|
disableTelemetry: true,
|
||||||
|
},
|
||||||
|
docs: {
|
||||||
|
defaultName: "Documentation",
|
||||||
|
},
|
||||||
|
stories: ["../../../packages/**/*.stories.tsx", "../stories/*.mdx"],
|
||||||
|
addons: [
|
||||||
|
"@storybook/addon-essentials",
|
||||||
|
"@storybook/addon-storysource",
|
||||||
|
"@storybook/addon-a11y",
|
||||||
|
"@storybook/addon-links",
|
||||||
|
"@chromatic-com/storybook",
|
||||||
|
"@storybook/addon-interactions",
|
||||||
|
"storybook-dark-mode",
|
||||||
|
],
|
||||||
|
framework: {
|
||||||
|
name: "@storybook/nextjs",
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
|
typescript: {
|
||||||
|
check: false,
|
||||||
|
reactDocgen: "react-docgen-typescript",
|
||||||
|
},
|
||||||
|
staticDirs: ["../../website/public"],
|
||||||
|
}
|
||||||
|
|
||||||
|
export default config
|
50
apps/storybook/.storybook/preview.tsx
Normal file
50
apps/storybook/.storybook/preview.tsx
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import "@repo/config-tailwind/styles.css"
|
||||||
|
import { defaultTranslationValues } from "@repo/i18n/config"
|
||||||
|
import i18nMessagesEnglish from "@repo/i18n/translations/en-US.json"
|
||||||
|
import { ThemeProvider } from "@repo/ui/Header/SwitchTheme"
|
||||||
|
import type { Preview } from "@storybook/react"
|
||||||
|
import { NextIntlClientProvider } from "next-intl"
|
||||||
|
import React from "react"
|
||||||
|
|
||||||
|
const preview: Preview = {
|
||||||
|
parameters: {
|
||||||
|
nextjs: {
|
||||||
|
appDirectory: true,
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
storySort: {
|
||||||
|
order: ["Design System", "User Interface", "Feature"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
backgrounds: { disable: true },
|
||||||
|
darkMode: {
|
||||||
|
darkClass: "dark",
|
||||||
|
lightClass: "light",
|
||||||
|
classTarget: "html",
|
||||||
|
stylePreview: true,
|
||||||
|
},
|
||||||
|
controls: {
|
||||||
|
matchers: {
|
||||||
|
color: /(background|color)$/i,
|
||||||
|
date: /Date$/i,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
decorators: [
|
||||||
|
(Story) => {
|
||||||
|
return (
|
||||||
|
<ThemeProvider>
|
||||||
|
<NextIntlClientProvider
|
||||||
|
messages={i18nMessagesEnglish}
|
||||||
|
locale="en"
|
||||||
|
defaultTranslationValues={defaultTranslationValues}
|
||||||
|
>
|
||||||
|
<Story />
|
||||||
|
</NextIntlClientProvider>
|
||||||
|
</ThemeProvider>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
export default preview
|
35
apps/storybook/.storybook/test-runner.ts
Normal file
35
apps/storybook/.storybook/test-runner.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import type { TestRunnerConfig } from "@storybook/test-runner"
|
||||||
|
import { getStoryContext } from "@storybook/test-runner"
|
||||||
|
|
||||||
|
import { checkA11y, configureAxe, injectAxe } from "axe-playwright"
|
||||||
|
|
||||||
|
/*
|
||||||
|
* See https://storybook.js.org/docs/writing-tests/test-runner#test-hook-api
|
||||||
|
* to learn more about the test-runner hooks API.
|
||||||
|
*/
|
||||||
|
const config: TestRunnerConfig = {
|
||||||
|
async preVisit(page) {
|
||||||
|
await injectAxe(page)
|
||||||
|
},
|
||||||
|
async postVisit(page, context) {
|
||||||
|
const storyContext = await getStoryContext(page, context)
|
||||||
|
|
||||||
|
if (storyContext.parameters?.a11y?.disable) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await configureAxe(page, {
|
||||||
|
rules: storyContext.parameters?.a11y?.config?.rules,
|
||||||
|
})
|
||||||
|
|
||||||
|
await checkA11y(page, "#storybook-root", {
|
||||||
|
verbose: false,
|
||||||
|
detailedReport: true,
|
||||||
|
detailedReportOptions: {
|
||||||
|
html: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default config
|
7
apps/storybook/chromatic.config.json
Normal file
7
apps/storybook/chromatic.config.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"projectId": "Project:668708614e9ac6d0b97ea5e5",
|
||||||
|
"buildScriptName": "build",
|
||||||
|
"storybookBaseDir": "apps/storybook",
|
||||||
|
"onlyChanged": true,
|
||||||
|
"zip": true
|
||||||
|
}
|
54
apps/storybook/package.json
Normal file
54
apps/storybook/package.json
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
{
|
||||||
|
"name": "@repo/storybook",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"build": "storybook build",
|
||||||
|
"dev": "storybook dev --port 6006 --no-open",
|
||||||
|
"start": "http-server \"storybook-static\" --port 6006 --silent",
|
||||||
|
"test": "start-server-and-test \"dev\" http://127.0.0.1:6006 \"test:storybook\"",
|
||||||
|
"test:storybook": "test-storybook",
|
||||||
|
"test:storybook-coverage": "test-storybook --coverage",
|
||||||
|
"chromatic": "chromatic"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@repo/config-tailwind": "workspace:*",
|
||||||
|
"@repo/i18n": "workspace:*",
|
||||||
|
"@repo/ui": "workspace:*",
|
||||||
|
"@repo/wikipedia-game-solver": "workspace:*",
|
||||||
|
"next": "catalog:",
|
||||||
|
"next-intl": "catalog:",
|
||||||
|
"react": "catalog:",
|
||||||
|
"react-dom": "catalog:"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@repo/eslint-config": "workspace:*",
|
||||||
|
"@chromatic-com/storybook": "catalog:",
|
||||||
|
"@playwright/test": "catalog:",
|
||||||
|
"@storybook/addon-a11y": "catalog:",
|
||||||
|
"@storybook/addon-essentials": "catalog:",
|
||||||
|
"@storybook/addon-interactions": "catalog:",
|
||||||
|
"@storybook/addon-links": "catalog:",
|
||||||
|
"@storybook/addon-storysource": "catalog:",
|
||||||
|
"@storybook/addon-themes": "catalog:",
|
||||||
|
"@storybook/blocks": "catalog:",
|
||||||
|
"@storybook/nextjs": "catalog:",
|
||||||
|
"@storybook/react": "catalog:",
|
||||||
|
"@storybook/test": "catalog:",
|
||||||
|
"@storybook/test-runner": "catalog:",
|
||||||
|
"@types/node": "catalog:",
|
||||||
|
"@types/react": "catalog:",
|
||||||
|
"@types/react-dom": "catalog:",
|
||||||
|
"axe-playwright": "catalog:",
|
||||||
|
"chromatic": "catalog:",
|
||||||
|
"eslint": "catalog:",
|
||||||
|
"http-server": "catalog:",
|
||||||
|
"start-server-and-test": "catalog:",
|
||||||
|
"storybook": "catalog:",
|
||||||
|
"storybook-dark-mode": "catalog:",
|
||||||
|
"postcss": "catalog:",
|
||||||
|
"tailwindcss": "catalog:",
|
||||||
|
"typescript": "catalog:"
|
||||||
|
}
|
||||||
|
}
|
7
apps/storybook/postcss.config.js
Normal file
7
apps/storybook/postcss.config.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
const config = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default config
|
31
apps/storybook/stories/Colors.mdx
Normal file
31
apps/storybook/stories/Colors.mdx
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { Meta, Title, ColorPalette, ColorItem } from "@storybook/blocks"
|
||||||
|
import tailwindConfig from "@repo/config-tailwind"
|
||||||
|
|
||||||
|
<Meta title="Design System/Colors" />
|
||||||
|
|
||||||
|
<Title>Colors</Title>
|
||||||
|
|
||||||
|
<ColorPalette>
|
||||||
|
{Object.entries(tailwindConfig.theme.colors).map(
|
||||||
|
([colorName, colorValue]) => {
|
||||||
|
const colors = {}
|
||||||
|
|
||||||
|
if (typeof colorValue === "string") {
|
||||||
|
colors[colorName] = colorValue
|
||||||
|
} else {
|
||||||
|
colors.light = colorValue.DEFAULT
|
||||||
|
colors.dark = colorValue.dark
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ColorItem
|
||||||
|
key={colorName}
|
||||||
|
title={colorName}
|
||||||
|
colors={colors}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
)}
|
||||||
|
|
||||||
|
</ColorPalette>
|
9
apps/storybook/tailwind.config.js
Normal file
9
apps/storybook/tailwind.config.js
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import sharedConfig from "@repo/config-tailwind"
|
||||||
|
|
||||||
|
/** @type {Pick<import('tailwindcss').Config, "presets" | "content">} */
|
||||||
|
const config = {
|
||||||
|
content: [".storybook/preview.tsx", "../../packages/**/*.tsx"],
|
||||||
|
presets: [sharedConfig],
|
||||||
|
}
|
||||||
|
|
||||||
|
export default config
|
3
apps/website/.env.example
Normal file
3
apps/website/.env.example
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
HOSTNAME=0.0.0.0
|
||||||
|
PORT=5000
|
||||||
|
NEXT_TELEMETRY_DISABLED=1
|
14
apps/website/.eslintrc.json
Normal file
14
apps/website/.eslintrc.json
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"root": true,
|
||||||
|
"extends": ["@repo/eslint-config/nextjs/.eslintrc.json"],
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": ["*.ts", "*.tsx"],
|
||||||
|
"plugins": ["@typescript-eslint"],
|
||||||
|
"parser": "@typescript-eslint/parser",
|
||||||
|
"parserOptions": {
|
||||||
|
"project": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
36
apps/website/Dockerfile
Normal file
36
apps/website/Dockerfile
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
FROM node:22.4.1-slim AS node-pnpm
|
||||||
|
ENV PNPM_HOME="/pnpm"
|
||||||
|
ENV PATH="$PNPM_HOME:$PATH"
|
||||||
|
RUN corepack enable
|
||||||
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
|
FROM node-pnpm AS builder
|
||||||
|
RUN pnpm install --global turbo@2.0.9
|
||||||
|
COPY ./ ./
|
||||||
|
RUN turbo prune @repo/website --docker
|
||||||
|
|
||||||
|
FROM node-pnpm AS installer
|
||||||
|
ENV IS_STANDALONE=true
|
||||||
|
|
||||||
|
COPY .gitignore .gitignore
|
||||||
|
COPY --from=builder /usr/src/app/out/json/ ./
|
||||||
|
COPY --from=builder /usr/src/app/out/pnpm-lock.yaml ./pnpm-lock.yaml
|
||||||
|
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
|
||||||
|
COPY --from=builder /usr/src/app/out/full/ ./
|
||||||
|
COPY turbo.json turbo.json
|
||||||
|
RUN pnpm --filter=@repo/website... exec turbo run build
|
||||||
|
|
||||||
|
FROM node-pnpm AS runner
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
ENV HOSTNAME=0.0.0.0
|
||||||
|
ENV NEXT_TELEMETRY_DISABLED=1
|
||||||
|
ENV IS_STANDALONE=true
|
||||||
|
|
||||||
|
RUN addgroup --system --gid 1001 nodejs && adduser --system --uid 1001 applicationrunner
|
||||||
|
USER applicationrunner
|
||||||
|
COPY --from=installer /usr/src/app/apps/website/next.config.js ./
|
||||||
|
COPY --from=installer /usr/src/app/apps/website/package.json ./
|
||||||
|
COPY --from=installer --chown=applicationrunner:nodejs /usr/src/app/apps/website/.next/standalone ./
|
||||||
|
COPY --from=installer --chown=applicationrunner:nodejs /usr/src/app/apps/website/.next/static ./apps/website/.next/static
|
||||||
|
COPY --from=installer --chown=applicationrunner:nodejs /usr/src/app/apps/website/public ./apps/website/public
|
||||||
|
CMD ["node", "apps/website/server.js"]
|
7
apps/website/app/[locale]/[...rest]/page.tsx
Normal file
7
apps/website/app/[locale]/[...rest]/page.tsx
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { notFound } from "next/navigation"
|
||||||
|
|
||||||
|
const CatchAllPage: React.FC = () => {
|
||||||
|
return notFound()
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CatchAllPage
|
26
apps/website/app/[locale]/about/page.tsx
Normal file
26
apps/website/app/[locale]/about/page.tsx
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import type { LocaleProps } from "@repo/i18n/config"
|
||||||
|
import { MainLayout } from "@repo/ui/MainLayout"
|
||||||
|
import { Button } from "@repo/ui/design/Button"
|
||||||
|
import { unstable_setRequestLocale } from "next-intl/server"
|
||||||
|
import { FaRocket } from "react-icons/fa6"
|
||||||
|
|
||||||
|
interface HomePageProps extends LocaleProps {}
|
||||||
|
|
||||||
|
const AboutPage: React.FC<HomePageProps> = (props) => {
|
||||||
|
const { params } = props
|
||||||
|
|
||||||
|
// Enable static rendering
|
||||||
|
unstable_setRequestLocale(params.locale)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MainLayout>
|
||||||
|
<section className="my-6 flex items-center space-x-6">
|
||||||
|
<Button leftIcon={<FaRocket size={18} />} href="/">
|
||||||
|
Home
|
||||||
|
</Button>
|
||||||
|
</section>
|
||||||
|
</MainLayout>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AboutPage
|
38
apps/website/app/[locale]/error.tsx
Normal file
38
apps/website/app/[locale]/error.tsx
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { MainLayout } from "@repo/ui/MainLayout"
|
||||||
|
import { useTranslations } from "next-intl"
|
||||||
|
import { useEffect } from "react"
|
||||||
|
|
||||||
|
interface ErrorBoundaryPageProps {
|
||||||
|
error: Error & { digest?: string }
|
||||||
|
reset: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const ErrorBoundaryPage: React.FC<ErrorBoundaryPageProps> = (props) => {
|
||||||
|
const { error, reset } = props
|
||||||
|
|
||||||
|
const t = useTranslations()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.error(error)
|
||||||
|
}, [error])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MainLayout>
|
||||||
|
<h1 className="text-3xl font-semibold text-red-600">
|
||||||
|
{t("errors.error")} 500 - {t("errors.server-error")}
|
||||||
|
</h1>
|
||||||
|
<p className="mb-4 mt-2">
|
||||||
|
<button
|
||||||
|
className="rounded-md bg-blue-600 px-3 py-2 text-white hover:bg-blue-800"
|
||||||
|
onClick={reset}
|
||||||
|
>
|
||||||
|
{t("errors.try-again")}
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
</MainLayout>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ErrorBoundaryPage
|
63
apps/website/app/[locale]/layout.tsx
Normal file
63
apps/website/app/[locale]/layout.tsx
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import "@repo/config-tailwind/styles.css"
|
||||||
|
import { VERSION } from "@repo/constants"
|
||||||
|
import type { Locale, LocaleProps } from "@repo/i18n/config"
|
||||||
|
import { LOCALES } from "@repo/i18n/config"
|
||||||
|
import { Footer } from "@repo/ui/Footer"
|
||||||
|
import { Header } from "@repo/ui/Header"
|
||||||
|
import { ThemeProvider } from "@repo/ui/Header/SwitchTheme"
|
||||||
|
import type { Metadata } from "next"
|
||||||
|
import { NextIntlClientProvider } from "next-intl"
|
||||||
|
import {
|
||||||
|
getMessages,
|
||||||
|
getTranslations,
|
||||||
|
unstable_setRequestLocale,
|
||||||
|
} from "next-intl/server"
|
||||||
|
|
||||||
|
export const generateMetadata = async ({
|
||||||
|
params,
|
||||||
|
}: LocaleProps): Promise<Metadata> => {
|
||||||
|
const t = await getTranslations({ locale: params.locale })
|
||||||
|
return {
|
||||||
|
title: t("meta.title"),
|
||||||
|
description: t("meta.description"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const generateStaticParams = (): Array<{ locale: Locale }> => {
|
||||||
|
return LOCALES.map((locale) => {
|
||||||
|
return {
|
||||||
|
locale,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LocaleLayoutProps extends React.PropsWithChildren {
|
||||||
|
params: {
|
||||||
|
locale: Locale
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const LocaleLayout: React.FC<LocaleLayoutProps> = async (props) => {
|
||||||
|
const { children, params } = props
|
||||||
|
|
||||||
|
// Enable static rendering
|
||||||
|
unstable_setRequestLocale(params.locale)
|
||||||
|
|
||||||
|
const messages = await getMessages()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<html lang={params.locale} suppressHydrationWarning>
|
||||||
|
<body>
|
||||||
|
<ThemeProvider>
|
||||||
|
<NextIntlClientProvider messages={messages}>
|
||||||
|
<Header />
|
||||||
|
{children}
|
||||||
|
<Footer version={VERSION} />
|
||||||
|
</NextIntlClientProvider>
|
||||||
|
</ThemeProvider>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LocaleLayout
|
12
apps/website/app/[locale]/loading.tsx
Normal file
12
apps/website/app/[locale]/loading.tsx
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { MainLayout } from "@repo/ui/MainLayout"
|
||||||
|
import { Spinner } from "@repo/ui/design/Spinner"
|
||||||
|
|
||||||
|
const Loading: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<MainLayout className="items-center justify-center">
|
||||||
|
<Spinner size={50} />
|
||||||
|
</MainLayout>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Loading
|
26
apps/website/app/[locale]/not-found.tsx
Normal file
26
apps/website/app/[locale]/not-found.tsx
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { Link } from "@repo/i18n/navigation"
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
const NotFound: React.FC = () => {
|
||||||
|
const t = useTranslations()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MainLayout>
|
||||||
|
<h1 className="text-3xl font-semibold text-red-600">
|
||||||
|
{t("errors.error")} 404 - {t("errors.not-found")}
|
||||||
|
</h1>
|
||||||
|
<p className="mb-4 mt-2">
|
||||||
|
{t("errors.page-doesnt-exist")}{" "}
|
||||||
|
<Link href="/" className="text-blue-800 hover:underline">
|
||||||
|
{t("errors.return-to-home-page")}
|
||||||
|
</Link>
|
||||||
|
</p>
|
||||||
|
</MainLayout>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default NotFound
|
68
apps/website/app/[locale]/page.tsx
Normal file
68
apps/website/app/[locale]/page.tsx
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import type { LocaleProps } from "@repo/i18n/config"
|
||||||
|
import { Button } from "@repo/ui/design/Button"
|
||||||
|
import { Link } from "@repo/ui/design/Link"
|
||||||
|
import { Typography } from "@repo/ui/design/Typography"
|
||||||
|
import { MainLayout } from "@repo/ui/MainLayout"
|
||||||
|
import { WikipediaClient } from "@repo/wikipedia-game-solver/WikipediaClient"
|
||||||
|
import { useTranslations } from "next-intl"
|
||||||
|
import { unstable_setRequestLocale } from "next-intl/server"
|
||||||
|
import Image from "next/image"
|
||||||
|
import { FaRocket } from "react-icons/fa6"
|
||||||
|
|
||||||
|
import WikipediaLogo from "#public/images/Wikipedia-Logo.png"
|
||||||
|
|
||||||
|
interface HomePageProps extends LocaleProps {}
|
||||||
|
|
||||||
|
const HomePage: React.FC<HomePageProps> = (props) => {
|
||||||
|
const { params } = props
|
||||||
|
|
||||||
|
// Enable static rendering
|
||||||
|
unstable_setRequestLocale(params.locale)
|
||||||
|
|
||||||
|
const t = useTranslations()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MainLayout>
|
||||||
|
<section className="text-center">
|
||||||
|
<Typography as="h1" variant="h1">
|
||||||
|
{t("home.title")}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Typography as="p" variant="text1" className="mt-3">
|
||||||
|
{t.rich("home.description", {
|
||||||
|
wikipedia: (children) => {
|
||||||
|
return (
|
||||||
|
<Link href="https://en.wikipedia.org/" target="_blank">
|
||||||
|
{children}
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
</Typography>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="my-6 flex items-center justify-center">
|
||||||
|
<Image src={WikipediaLogo} alt="Wikipedia" className="w-72" />
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="my-6 flex items-center space-x-6">
|
||||||
|
<Button leftIcon={<FaRocket size={18} />} href="/about">
|
||||||
|
About
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
leftIcon={<FaRocket size={18} />}
|
||||||
|
variant="outline"
|
||||||
|
href="/"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
Get Started
|
||||||
|
</Button>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<WikipediaClient />
|
||||||
|
</MainLayout>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default HomePage
|
BIN
apps/website/app/favicon.ico
Normal file
BIN
apps/website/app/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
7
apps/website/app/layout.tsx
Normal file
7
apps/website/app/layout.tsx
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
interface RootLayoutProps extends React.PropsWithChildren {}
|
||||||
|
|
||||||
|
const RootLayout = ({ children }: RootLayoutProps): React.ReactNode => {
|
||||||
|
return children
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RootLayout
|
20
apps/website/app/not-found.tsx
Normal file
20
apps/website/app/not-found.tsx
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import Error from "next/error"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render the default Next.js 404 page when a route
|
||||||
|
* is requested that doesn't match the middleware and
|
||||||
|
* therefore doesn't have a locale associated with it.
|
||||||
|
*/
|
||||||
|
const NotFound: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<html lang="en">
|
||||||
|
<body>
|
||||||
|
<Error statusCode={404} />
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default NotFound
|
3
apps/website/i18n.ts
Normal file
3
apps/website/i18n.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import i18nConfig from "@repo/i18n/i18n"
|
||||||
|
|
||||||
|
export default i18nConfig
|
23
apps/website/middleware.ts
Normal file
23
apps/website/middleware.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { LOCALES, LOCALE_DEFAULT, LOCALE_PREFIX } from "@repo/i18n/config"
|
||||||
|
import createMiddleware from "next-intl/middleware"
|
||||||
|
|
||||||
|
export default createMiddleware({
|
||||||
|
locales: LOCALES,
|
||||||
|
defaultLocale: LOCALE_DEFAULT,
|
||||||
|
localePrefix: LOCALE_PREFIX,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const config = {
|
||||||
|
matcher: [
|
||||||
|
// Enable a redirect to a matching locale at the root
|
||||||
|
"/",
|
||||||
|
|
||||||
|
// Set a cookie to remember the previous locale for
|
||||||
|
// all requests that have a locale prefix
|
||||||
|
"/(en-US|fr-FR)/:path*",
|
||||||
|
|
||||||
|
// Enable redirects that add missing locales
|
||||||
|
// (e.g. `/pathnames` -> `/en/pathnames`)
|
||||||
|
"/((?!_next|_vercel|.*\\..*).*)",
|
||||||
|
],
|
||||||
|
}
|
5
apps/website/next-env.d.ts
vendored
Normal file
5
apps/website/next-env.d.ts
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
/// <reference types="next" />
|
||||||
|
/// <reference types="next/image-types/global" />
|
||||||
|
|
||||||
|
// NOTE: This file should not be edited
|
||||||
|
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
18
apps/website/next.config.js
Normal file
18
apps/website/next.config.js
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import createNextIntlPlugin from "next-intl/plugin"
|
||||||
|
|
||||||
|
const IS_STANDALONE = process.env.IS_STANDALONE === "true"
|
||||||
|
|
||||||
|
/** @type {import('next').NextConfig} */
|
||||||
|
const nextConfig = {
|
||||||
|
output: IS_STANDALONE ? "standalone" : undefined,
|
||||||
|
eslint: {
|
||||||
|
ignoreDuringBuilds: true,
|
||||||
|
},
|
||||||
|
typescript: {
|
||||||
|
ignoreBuildErrors: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const withNextIntl = createNextIntlPlugin()
|
||||||
|
|
||||||
|
export default withNextIntl(nextConfig)
|
40
apps/website/package.json
Normal file
40
apps/website/package.json
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"name": "@repo/website",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"imports": {
|
||||||
|
"#*": "./*"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev --port 5000 --turbo",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start --port 5000",
|
||||||
|
"lint:eslint": "eslint . --max-warnings 0 --report-unused-disable-directives",
|
||||||
|
"lint:typescript": "tsc --noEmit"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@repo/config-tailwind": "workspace:*",
|
||||||
|
"@repo/constants": "workspace:*",
|
||||||
|
"@repo/i18n": "workspace:*",
|
||||||
|
"@repo/ui": "workspace:*",
|
||||||
|
"@repo/wikipedia-game-solver": "workspace:*",
|
||||||
|
"next": "catalog:",
|
||||||
|
"next-intl": "catalog:",
|
||||||
|
"react": "catalog:",
|
||||||
|
"react-dom": "catalog:",
|
||||||
|
"react-icons": "catalog:"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@repo/eslint-config": "workspace:*",
|
||||||
|
"@repo/config-typescript": "workspace:*",
|
||||||
|
"@types/node": "catalog:",
|
||||||
|
"@types/react": "catalog:",
|
||||||
|
"@types/react-dom": "catalog:",
|
||||||
|
"@total-typescript/ts-reset": "catalog:",
|
||||||
|
"eslint": "catalog:",
|
||||||
|
"postcss": "catalog:",
|
||||||
|
"tailwindcss": "catalog:",
|
||||||
|
"typescript": "catalog:"
|
||||||
|
}
|
||||||
|
}
|
7
apps/website/postcss.config.js
Normal file
7
apps/website/postcss.config.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
const config = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default config
|
0
apps/website/public/.gitkeep
Normal file
0
apps/website/public/.gitkeep
Normal file
BIN
apps/website/public/images/Wikipedia-Logo.png
Normal file
BIN
apps/website/public/images/Wikipedia-Logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 272 KiB |
13
apps/website/tailwind.config.js
Normal file
13
apps/website/tailwind.config.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import sharedConfig from "@repo/config-tailwind"
|
||||||
|
|
||||||
|
/** @type {Pick<import('tailwindcss').Config, "presets" | "content">} */
|
||||||
|
const config = {
|
||||||
|
content: [
|
||||||
|
"./**/*.tsx",
|
||||||
|
"../../packages/ui/**/*.tsx",
|
||||||
|
"../../packages/wikipedia-game-solver/**/*.tsx",
|
||||||
|
],
|
||||||
|
presets: [sharedConfig],
|
||||||
|
}
|
||||||
|
|
||||||
|
export default config
|
25
apps/website/tsconfig.json
Normal file
25
apps/website/tsconfig.json
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"extends": "@repo/config-typescript/tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
||||||
|
"types": ["@total-typescript/ts-reset", "@repo/i18n/messages.d.ts"],
|
||||||
|
"incremental": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"allowJs": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"paths": {
|
||||||
|
"#*": ["./*"]
|
||||||
|
},
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "next"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||||
|
"exclude": ["node_modules", ".next"]
|
||||||
|
}
|
13
compose.yaml
Normal file
13
compose.yaml
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
services:
|
||||||
|
wikipedia-game-solver:
|
||||||
|
container_name: "wikipedia-game-solver"
|
||||||
|
image: "wikipedia-game-solver"
|
||||||
|
restart: "unless-stopped"
|
||||||
|
build:
|
||||||
|
context: "./"
|
||||||
|
dockerfile: "./apps/website/Dockerfile"
|
||||||
|
ports:
|
||||||
|
- "${WEBSITE_PORT-5000}:${WEBSITE_PORT-5000}"
|
||||||
|
environment:
|
||||||
|
PORT: ${WEBSITE_PORT-5000}
|
||||||
|
env_file: "./apps/website/.env"
|
34
package.json
Normal file
34
package.json
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"name": "repo",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"packageManager": "pnpm@9.5.0+sha512.140036830124618d624a2187b50d04289d5a087f326c9edfc0ccd733d76c4f52c3a313d4fc148794a2a9d81553016004e6742e8cf850670268a7387fc220c903",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=22.0.0",
|
||||||
|
"pnpm": ">=9.5.0"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "turbo run build",
|
||||||
|
"dev": "turbo run dev --parallel",
|
||||||
|
"start": "turbo run start --parallel",
|
||||||
|
"test": "turbo run test",
|
||||||
|
"lint:editorconfig": "editorconfig-checker",
|
||||||
|
"lint:prettier": "prettier . --check",
|
||||||
|
"lint:eslint": "turbo run lint:eslint",
|
||||||
|
"lint:typescript": "turbo run lint:typescript",
|
||||||
|
"release": "semantic-release"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@saithodev/semantic-release-backmerge": "4.0.1",
|
||||||
|
"@semantic-release/exec": "6.0.3",
|
||||||
|
"@semantic-release/git": "10.0.1",
|
||||||
|
"editorconfig-checker": "5.1.8",
|
||||||
|
"prettier": "3.3.3",
|
||||||
|
"prettier-plugin-tailwindcss": "0.6.5",
|
||||||
|
"replace-in-files-cli": "3.0.0",
|
||||||
|
"semantic-release": "23.1.1",
|
||||||
|
"turbo": "2.0.9",
|
||||||
|
"typescript": "catalog:"
|
||||||
|
}
|
||||||
|
}
|
3
packages/config-eslint/.eslintrc.json
Normal file
3
packages/config-eslint/.eslintrc.json
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"extends": ["conventions"]
|
||||||
|
}
|
57
packages/config-eslint/nextjs/.eslintrc.json
Normal file
57
packages/config-eslint/nextjs/.eslintrc.json
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
{
|
||||||
|
"extends": [
|
||||||
|
"conventions",
|
||||||
|
"next/core-web-vitals",
|
||||||
|
"plugin:tailwindcss/recommended",
|
||||||
|
"plugin:storybook/recommended"
|
||||||
|
],
|
||||||
|
"ignorePatterns": [
|
||||||
|
"next.config.js",
|
||||||
|
"tailwind.config.js",
|
||||||
|
"postcss.config.js",
|
||||||
|
"vitest.config.ts"
|
||||||
|
],
|
||||||
|
"settings": {
|
||||||
|
"tailwindcss": {
|
||||||
|
"callees": ["classNames", "cva"]
|
||||||
|
},
|
||||||
|
"react": {
|
||||||
|
"version": "detect"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"tailwindcss/classnames-order": "off",
|
||||||
|
"tailwindcss/no-custom-classname": "off",
|
||||||
|
"@next/next/no-html-link-for-pages": "off",
|
||||||
|
"react/self-closing-comp": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"component": true,
|
||||||
|
"html": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"react/void-dom-elements-no-children": "error",
|
||||||
|
"react/jsx-boolean-value": "error",
|
||||||
|
"no-restricted-imports": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"paths": [
|
||||||
|
{
|
||||||
|
"name": "next/link",
|
||||||
|
"message": "Please import from `@repo/i18n/navigation` instead."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "next/navigation",
|
||||||
|
"importNames": [
|
||||||
|
"redirect",
|
||||||
|
"permanentRedirect",
|
||||||
|
"useRouter",
|
||||||
|
"usePathname"
|
||||||
|
],
|
||||||
|
"message": "Please import from `@repo/i18n/navigation` instead."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
22
packages/config-eslint/package.json
Normal file
22
packages/config-eslint/package.json
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"name": "@repo/eslint-config",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"private": true,
|
||||||
|
"main": ".eslintrc.json",
|
||||||
|
"files": [
|
||||||
|
".eslintrc.json",
|
||||||
|
"nextjs/.eslintrc.json"
|
||||||
|
],
|
||||||
|
"devDependencies": {
|
||||||
|
"@typescript-eslint/eslint-plugin": "catalog:",
|
||||||
|
"@typescript-eslint/parser": "catalog:",
|
||||||
|
"eslint": "catalog:",
|
||||||
|
"eslint-config-conventions": "catalog:",
|
||||||
|
"eslint-plugin-promise": "catalog:",
|
||||||
|
"eslint-plugin-unicorn": "catalog:",
|
||||||
|
"eslint-config-next": "catalog:",
|
||||||
|
"eslint-plugin-storybook": "catalog:",
|
||||||
|
"eslint-plugin-tailwindcss": "catalog:",
|
||||||
|
"typescript": "catalog:"
|
||||||
|
}
|
||||||
|
}
|
14
packages/config-tailwind/.eslintrc.json
Normal file
14
packages/config-tailwind/.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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
6
packages/config-tailwind/classNames.ts
Normal file
6
packages/config-tailwind/classNames.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { type ClassValue, clsx } from "clsx"
|
||||||
|
import { twMerge } from "tailwind-merge"
|
||||||
|
|
||||||
|
export const classNames = (...inputs: ClassValue[]): string => {
|
||||||
|
return twMerge(clsx(inputs))
|
||||||
|
}
|
3
packages/config-tailwind/index.d.ts
vendored
Normal file
3
packages/config-tailwind/index.d.ts
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import type { Config } from "tailwindcss"
|
||||||
|
|
||||||
|
export default Config
|
30
packages/config-tailwind/package.json
Normal file
30
packages/config-tailwind/package.json
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"name": "@repo/config-tailwind",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"main": "./tailwind.config.js",
|
||||||
|
"types": "./index.d.ts",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./index.d.ts",
|
||||||
|
"import": "./tailwind.config.js",
|
||||||
|
"require": "./tailwind.config.js",
|
||||||
|
"default": "./tailwind.config.js"
|
||||||
|
},
|
||||||
|
"./classNames": "./classNames.ts",
|
||||||
|
"./styles.css": "./styles.css"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@fontsource/montserrat": "catalog:",
|
||||||
|
"clsx": "catalog:",
|
||||||
|
"tailwind-merge": "catalog:"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@repo/eslint-config": "workspace:*",
|
||||||
|
"@repo/config-typescript": "workspace:*",
|
||||||
|
"eslint": "catalog:",
|
||||||
|
"postcss": "catalog:",
|
||||||
|
"tailwindcss": "catalog:"
|
||||||
|
}
|
||||||
|
}
|
7
packages/config-tailwind/postcss.config.js
Normal file
7
packages/config-tailwind/postcss.config.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
const config = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default config
|
73
packages/config-tailwind/styles.css
Normal file
73
packages/config-tailwind/styles.css
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
@import "@fontsource/montserrat/400.css";
|
||||||
|
@import "@fontsource/montserrat/500.css";
|
||||||
|
@import "@fontsource/montserrat/600.css";
|
||||||
|
@import "@fontsource/montserrat/700.css";
|
||||||
|
@import "@fontsource/montserrat/800.css";
|
||||||
|
|
||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
b,
|
||||||
|
strong {
|
||||||
|
@apply font-semibold;
|
||||||
|
}
|
||||||
|
|
||||||
|
i,
|
||||||
|
em {
|
||||||
|
@apply italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
u {
|
||||||
|
@apply underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
s {
|
||||||
|
@apply line-through;
|
||||||
|
}
|
||||||
|
|
||||||
|
abbr[title] {
|
||||||
|
@apply underline decoration-dotted underline-offset-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
q,
|
||||||
|
blockquote {
|
||||||
|
@apply italic tracking-wider;
|
||||||
|
}
|
||||||
|
blockquote {
|
||||||
|
@apply border-gray border-l-4 pl-3 italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
kbd {
|
||||||
|
@apply bg-gray rounded-md px-2 dark:text-black;
|
||||||
|
}
|
||||||
|
|
||||||
|
mark {
|
||||||
|
@apply bg-yellow rounded-md px-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
ol {
|
||||||
|
@apply list-inside list-decimal;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
@apply list-inside list-disc;
|
||||||
|
}
|
||||||
|
|
||||||
|
dfn {
|
||||||
|
@apply font-semibold italic;
|
||||||
|
cursor: help;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
@apply bg-background dark:bg-background-dark font-sans text-black dark:text-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes ripple {
|
||||||
|
to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(2);
|
||||||
|
}
|
||||||
|
}
|
34
packages/config-tailwind/tailwind.config.js
Normal file
34
packages/config-tailwind/tailwind.config.js
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import { fontFamily } from "tailwindcss/defaultTheme"
|
||||||
|
|
||||||
|
/** @type {Omit<import('tailwindcss').Config, "content">} */
|
||||||
|
const config = {
|
||||||
|
darkMode: "class",
|
||||||
|
theme: {
|
||||||
|
colors: {
|
||||||
|
primary: {
|
||||||
|
DEFAULT: "#0056b3",
|
||||||
|
dark: "#00aeff",
|
||||||
|
},
|
||||||
|
background: {
|
||||||
|
DEFAULT: "#fff",
|
||||||
|
dark: "#181818",
|
||||||
|
},
|
||||||
|
white: "#fff",
|
||||||
|
black: "#000",
|
||||||
|
gray: "#d1d5db",
|
||||||
|
"gray-darker": {
|
||||||
|
DEFAULT: "#4b5563",
|
||||||
|
dark: "#9ca3af",
|
||||||
|
},
|
||||||
|
yellow: "#fef08a",
|
||||||
|
transparent: "transparent",
|
||||||
|
inherit: "inherit",
|
||||||
|
},
|
||||||
|
fontFamily: {
|
||||||
|
sans: ["'Montserrat'", ...fontFamily.sans],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
export default config
|
10
packages/config-tailwind/tsconfig.json
Normal file
10
packages/config-tailwind/tsconfig.json
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"extends": "@repo/config-typescript/tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
|
||||||
|
"noEmit": true
|
||||||
|
}
|
||||||
|
}
|
8
packages/config-typescript/package.json
Normal file
8
packages/config-typescript/package.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"name": "@repo/config-typescript",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"private": true,
|
||||||
|
"files": [
|
||||||
|
"tsconfig.json"
|
||||||
|
]
|
||||||
|
}
|
18
packages/config-typescript/tsconfig.json
Normal file
18
packages/config-typescript/tsconfig.json
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"strict": true,
|
||||||
|
"allowUnusedLabels": false,
|
||||||
|
"allowUnreachableCode": false,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noImplicitOverride": true,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"noPropertyAccessFromIndexSignature": true,
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
|
||||||
|
"isolatedModules": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true
|
||||||
|
}
|
||||||
|
}
|
17
packages/constants/package.json
Normal file
17
packages/constants/package.json
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"name": "@repo/constants",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"exports": {
|
||||||
|
".": "./version.ts"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"lint:typescript": "tsc --noEmit"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@repo/config-typescript": "workspace:*",
|
||||||
|
"@types/node": "catalog:",
|
||||||
|
"typescript": "catalog:"
|
||||||
|
}
|
||||||
|
}
|
10
packages/constants/tsconfig.json
Normal file
10
packages/constants/tsconfig.json
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"extends": "@repo/config-typescript/tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"noEmit": true
|
||||||
|
}
|
||||||
|
}
|
6
packages/constants/version.ts
Normal file
6
packages/constants/version.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import packageJSON from "./package.json"
|
||||||
|
|
||||||
|
export const VERSION =
|
||||||
|
process.env["NODE_ENV"] === "development"
|
||||||
|
? "0.0.0-development"
|
||||||
|
: packageJSON.version
|
14
packages/i18n/.eslintrc.json
Normal file
14
packages/i18n/.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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
33
packages/i18n/package.json
Normal file
33
packages/i18n/package.json
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"name": "@repo/i18n",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"exports": {
|
||||||
|
"./translations/*.json": "./src/translations/*.json",
|
||||||
|
"./config": "./src/config.tsx",
|
||||||
|
"./i18n": "./src/i18n.ts",
|
||||||
|
"./messages.d.ts": "./src/messages.d.ts",
|
||||||
|
"./navigation": "./src/navigation.ts"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"lint:eslint": "eslint src --max-warnings 0 --report-unused-disable-directives",
|
||||||
|
"lint:typescript": "tsc --noEmit"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"deepmerge": "catalog:",
|
||||||
|
"next": "catalog:",
|
||||||
|
"next-intl": "catalog:",
|
||||||
|
"react": "catalog:",
|
||||||
|
"react-dom": "catalog:"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@repo/eslint-config": "workspace:*",
|
||||||
|
"@repo/config-typescript": "workspace:*",
|
||||||
|
"@types/react": "catalog:",
|
||||||
|
"@types/react-dom": "catalog:",
|
||||||
|
"@total-typescript/ts-reset": "catalog:",
|
||||||
|
"eslint": "catalog:",
|
||||||
|
"typescript": "catalog:"
|
||||||
|
}
|
||||||
|
}
|
30
packages/i18n/src/config.tsx
Normal file
30
packages/i18n/src/config.tsx
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import type { RichTranslationValues } from "next-intl"
|
||||||
|
|
||||||
|
export const LOCALES = ["en-US", "fr-FR"] as const
|
||||||
|
export type Locale = (typeof LOCALES)[number]
|
||||||
|
export const LOCALE_DEFAULT = "en-US" satisfies Locale
|
||||||
|
export const LOCALE_PREFIX = "never"
|
||||||
|
|
||||||
|
export interface LocaleProps {
|
||||||
|
params: {
|
||||||
|
locale: Locale
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const defaultTranslationValues: RichTranslationValues = {
|
||||||
|
br: () => {
|
||||||
|
return <br />
|
||||||
|
},
|
||||||
|
strong: (children) => {
|
||||||
|
return <strong>{children}</strong>
|
||||||
|
},
|
||||||
|
em: (children) => {
|
||||||
|
return <em>{children}</em>
|
||||||
|
},
|
||||||
|
s: (children) => {
|
||||||
|
return <s>{children}</s>
|
||||||
|
},
|
||||||
|
u: (children) => {
|
||||||
|
return <u>{children}</u>
|
||||||
|
},
|
||||||
|
}
|
27
packages/i18n/src/i18n.ts
Normal file
27
packages/i18n/src/i18n.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import deepmerge from "deepmerge"
|
||||||
|
import type { AbstractIntlMessages } from "next-intl"
|
||||||
|
import { getRequestConfig } from "next-intl/server"
|
||||||
|
import { notFound } from "next/navigation"
|
||||||
|
|
||||||
|
import type { Locale } from "./config"
|
||||||
|
import { defaultTranslationValues, LOCALE_DEFAULT, LOCALES } from "./config"
|
||||||
|
|
||||||
|
export default getRequestConfig(async ({ locale }) => {
|
||||||
|
if (!LOCALES.includes(locale as Locale)) {
|
||||||
|
return notFound()
|
||||||
|
}
|
||||||
|
|
||||||
|
const userMessages = (await import(`./translations/${locale}.json`)).default
|
||||||
|
const defaultMessages = (
|
||||||
|
await import(`./translations/${LOCALE_DEFAULT}.json`)
|
||||||
|
).default
|
||||||
|
const messages = deepmerge<AbstractIntlMessages>(
|
||||||
|
defaultMessages,
|
||||||
|
userMessages,
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
messages,
|
||||||
|
defaultTranslationValues,
|
||||||
|
}
|
||||||
|
})
|
10
packages/i18n/src/messages.d.ts
vendored
Normal file
10
packages/i18n/src/messages.d.ts
vendored
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import type en from "./translations/en-US.json"
|
||||||
|
|
||||||
|
type Messages = typeof en
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
/**
|
||||||
|
* Use type safe message keys with `next-intl`.
|
||||||
|
*/
|
||||||
|
interface IntlMessages extends Messages {}
|
||||||
|
}
|
9
packages/i18n/src/navigation.ts
Normal file
9
packages/i18n/src/navigation.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { createSharedPathnamesNavigation } from "next-intl/navigation"
|
||||||
|
|
||||||
|
import { LOCALES, LOCALE_PREFIX } from "./config"
|
||||||
|
|
||||||
|
export const { Link, redirect, usePathname, useRouter, permanentRedirect } =
|
||||||
|
createSharedPathnamesNavigation({
|
||||||
|
locales: LOCALES,
|
||||||
|
localePrefix: LOCALE_PREFIX,
|
||||||
|
})
|
23
packages/i18n/src/translations/en-US.json
Normal file
23
packages/i18n/src/translations/en-US.json
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"meta": {
|
||||||
|
"title": "Wikipedia Game Solver",
|
||||||
|
"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": {
|
||||||
|
"en-US": "🇺🇸 English",
|
||||||
|
"fr-FR": "🇫🇷 French"
|
||||||
|
},
|
||||||
|
"errors": {
|
||||||
|
"error": "Error",
|
||||||
|
"page-doesnt-exist": "This page doesn't exist!",
|
||||||
|
"not-found": "Not Found",
|
||||||
|
"server-error": "Internal Server Error!",
|
||||||
|
"return-to-home-page": "Return to the home page?",
|
||||||
|
"try-again": "Try again ?"
|
||||||
|
},
|
||||||
|
"home": {
|
||||||
|
"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."
|
||||||
|
}
|
||||||
|
}
|
23
packages/i18n/src/translations/fr-FR.json
Normal file
23
packages/i18n/src/translations/fr-FR.json
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"meta": {
|
||||||
|
"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.",
|
||||||
|
"all-rights-reserved": "Tous droits réservés"
|
||||||
|
},
|
||||||
|
"locales": {
|
||||||
|
"en-US": "🇺🇸 Anglais",
|
||||||
|
"fr-FR": "🇫🇷 Français"
|
||||||
|
},
|
||||||
|
"errors": {
|
||||||
|
"error": "Erreur",
|
||||||
|
"page-doesnt-exist": "Cette page n'existe pas!",
|
||||||
|
"not-found": "Introuvable",
|
||||||
|
"server-error": "Erreur interne du serveur !",
|
||||||
|
"return-to-home-page": "Retour à la page d'accueil ?",
|
||||||
|
"try-again": "Réessayer ?"
|
||||||
|
},
|
||||||
|
"home": {
|
||||||
|
"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."
|
||||||
|
}
|
||||||
|
}
|
14
packages/i18n/tsconfig.json
Normal file
14
packages/i18n/tsconfig.json
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"extends": "@repo/config-typescript/tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
||||||
|
"types": ["@total-typescript/ts-reset"],
|
||||||
|
"jsx": "preserve",
|
||||||
|
|
||||||
|
"noEmit": true
|
||||||
|
}
|
||||||
|
}
|
14
packages/react-hooks/.eslintrc.json
Normal file
14
packages/react-hooks/.eslintrc.json
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"root": true,
|
||||||
|
"extends": ["@repo/eslint-config/nextjs/.eslintrc.json"],
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": ["*.ts", "*.tsx"],
|
||||||
|
"plugins": ["@typescript-eslint"],
|
||||||
|
"parser": "@typescript-eslint/parser",
|
||||||
|
"parserOptions": {
|
||||||
|
"project": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
26
packages/react-hooks/package.json
Normal file
26
packages/react-hooks/package.json
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"name": "@repo/react-hooks",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"exports": {
|
||||||
|
"./useIsMounted": "./src/useIsMounted.ts"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"lint:eslint": "eslint src --max-warnings 0 --report-unused-disable-directives",
|
||||||
|
"lint:typescript": "tsc --noEmit"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"react": "catalog:",
|
||||||
|
"react-dom": "catalog:"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@repo/eslint-config": "workspace:*",
|
||||||
|
"@repo/config-typescript": "workspace:*",
|
||||||
|
"@types/react": "catalog:",
|
||||||
|
"@types/react-dom": "catalog:",
|
||||||
|
"@total-typescript/ts-reset": "catalog:",
|
||||||
|
"eslint": "catalog:",
|
||||||
|
"typescript": "catalog:"
|
||||||
|
}
|
||||||
|
}
|
15
packages/react-hooks/src/useIsMounted.ts
Normal file
15
packages/react-hooks/src/useIsMounted.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { useEffect, useState } from "react"
|
||||||
|
|
||||||
|
export interface UseIsMountedOutput {
|
||||||
|
isMounted: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useIsMounted = (): UseIsMountedOutput => {
|
||||||
|
const [isMounted, setIsMounted] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsMounted(true)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return { isMounted }
|
||||||
|
}
|
14
packages/react-hooks/tsconfig.json
Normal file
14
packages/react-hooks/tsconfig.json
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"extends": "@repo/config-typescript/tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
||||||
|
"types": ["@total-typescript/ts-reset"],
|
||||||
|
"jsx": "preserve",
|
||||||
|
|
||||||
|
"noEmit": true
|
||||||
|
}
|
||||||
|
}
|
14
packages/ui/.eslintrc.json
Normal file
14
packages/ui/.eslintrc.json
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"root": true,
|
||||||
|
"extends": ["@repo/eslint-config/nextjs/.eslintrc.json"],
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": ["*.ts", "*.tsx"],
|
||||||
|
"plugins": ["@typescript-eslint"],
|
||||||
|
"parser": "@typescript-eslint/parser",
|
||||||
|
"parserOptions": {
|
||||||
|
"project": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
46
packages/ui/package.json
Normal file
46
packages/ui/package.json
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
{
|
||||||
|
"name": "@repo/ui",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"exports": {
|
||||||
|
"./design/Button": "./src/design/Button/Button.tsx",
|
||||||
|
"./design/Link": "./src/design/Link/Link.tsx",
|
||||||
|
"./design/Spinner": "./src/design/Spinner/Spinner.tsx",
|
||||||
|
"./design/Typography": "./src/design/Typography/Typography.tsx",
|
||||||
|
"./Footer": "./src/Footer/Footer.tsx",
|
||||||
|
"./Header": "./src/Header/Header.tsx",
|
||||||
|
"./Header/SwitchTheme": "./src/Header/SwitchTheme.tsx",
|
||||||
|
"./MainLayout": "./src/MainLayout/MainLayout.tsx"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"lint:eslint": "eslint src --max-warnings 0 --report-unused-disable-directives",
|
||||||
|
"lint:typescript": "tsc --noEmit"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@repo/config-tailwind": "workspace:*",
|
||||||
|
"@repo/i18n": "workspace:*",
|
||||||
|
"@repo/react-hooks": "workspace:*",
|
||||||
|
"cva": "catalog:",
|
||||||
|
"next": "catalog:",
|
||||||
|
"next-intl": "catalog:",
|
||||||
|
"next-themes": "catalog:",
|
||||||
|
"react": "catalog:",
|
||||||
|
"react-dom": "catalog:",
|
||||||
|
"react-icons": "catalog:"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@repo/eslint-config": "workspace:*",
|
||||||
|
"@repo/config-typescript": "workspace:*",
|
||||||
|
"@types/react": "catalog:",
|
||||||
|
"@types/react-dom": "catalog:",
|
||||||
|
"@total-typescript/ts-reset": "catalog:",
|
||||||
|
"@storybook/blocks": "catalog:",
|
||||||
|
"@storybook/react": "catalog:",
|
||||||
|
"@storybook/test": "catalog:",
|
||||||
|
"eslint": "catalog:",
|
||||||
|
"postcss": "catalog:",
|
||||||
|
"tailwindcss": "catalog:",
|
||||||
|
"typescript": "catalog:"
|
||||||
|
}
|
||||||
|
}
|
7
packages/ui/postcss.config.js
Normal file
7
packages/ui/postcss.config.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
const config = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default config
|
18
packages/ui/src/Footer/Footer.stories.tsx
Normal file
18
packages/ui/src/Footer/Footer.stories.tsx
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import type { Meta, StoryObj } from "@storybook/react"
|
||||||
|
|
||||||
|
import { Footer as FooterComponent } from "./Footer"
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: "User Interface/Footer",
|
||||||
|
component: FooterComponent,
|
||||||
|
} satisfies Meta<typeof FooterComponent>
|
||||||
|
|
||||||
|
export default meta
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof meta>
|
||||||
|
|
||||||
|
export const Footer: Story = {
|
||||||
|
args: {
|
||||||
|
version: "1.0.0",
|
||||||
|
},
|
||||||
|
}
|
31
packages/ui/src/Footer/Footer.tsx
Normal file
31
packages/ui/src/Footer/Footer.tsx
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { useTranslations } from "next-intl"
|
||||||
|
|
||||||
|
import { Link } from "../design/Link/Link"
|
||||||
|
|
||||||
|
export interface FooterProps {
|
||||||
|
version: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Footer: React.FC<FooterProps> = (props) => {
|
||||||
|
const { version } = props
|
||||||
|
|
||||||
|
const t = useTranslations()
|
||||||
|
|
||||||
|
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">
|
||||||
|
<p>
|
||||||
|
<Link href="https://theoludwig.fr" target="_blank" isExternal={false}>
|
||||||
|
Théo LUDWIG
|
||||||
|
</Link>{" "}
|
||||||
|
| {t("meta.all-rights-reserved")}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Version{" "}
|
||||||
|
<strong className="text-primary dark:text-primary-dark hover:underline">
|
||||||
|
{version}
|
||||||
|
</strong>
|
||||||
|
</p>
|
||||||
|
</footer>
|
||||||
|
)
|
||||||
|
}
|
16
packages/ui/src/Header/Header.stories.tsx
Normal file
16
packages/ui/src/Header/Header.stories.tsx
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import type { Meta, StoryObj } from "@storybook/react"
|
||||||
|
|
||||||
|
import { Header as HeaderComponent } from "./Header"
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: "User Interface/Header",
|
||||||
|
component: HeaderComponent,
|
||||||
|
} satisfies Meta<typeof HeaderComponent>
|
||||||
|
|
||||||
|
export default meta
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof meta>
|
||||||
|
|
||||||
|
export const Header: Story = {
|
||||||
|
args: {},
|
||||||
|
}
|
15
packages/ui/src/Header/Header.tsx
Normal file
15
packages/ui/src/Header/Header.tsx
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { Locales } from "./Locales"
|
||||||
|
import { SwitchTheme } from "./SwitchTheme"
|
||||||
|
|
||||||
|
export const Header: React.FC = () => {
|
||||||
|
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">
|
||||||
|
<div className="flex w-full items-center justify-between">
|
||||||
|
<Locales />
|
||||||
|
<SwitchTheme />
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
)
|
||||||
|
}
|
42
packages/ui/src/Header/Locales.tsx
Normal file
42
packages/ui/src/Header/Locales.tsx
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { classNames } from "@repo/config-tailwind/classNames"
|
||||||
|
import { LOCALES } from "@repo/i18n/config"
|
||||||
|
import { usePathname, useRouter } from "@repo/i18n/navigation"
|
||||||
|
import { useLocale, useTranslations } from "next-intl"
|
||||||
|
|
||||||
|
export const Locales: React.FC = () => {
|
||||||
|
const router = useRouter()
|
||||||
|
const pathname = usePathname()
|
||||||
|
const currentLocale = useLocale()
|
||||||
|
|
||||||
|
const t = useTranslations()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section>
|
||||||
|
<ul className="flex list-none gap-6">
|
||||||
|
{LOCALES.map((locale) => {
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
key={locale}
|
||||||
|
className={classNames("rounded-md p-2", {
|
||||||
|
"border-primary dark:border-primary-dark border":
|
||||||
|
locale === currentLocale,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
className="text-primary dark:text-primary-dark font-semibold hover:underline focus:rounded-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
|
onClick={() => {
|
||||||
|
router.replace(pathname, { locale, scroll: false })
|
||||||
|
router.refresh()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t(`locales.${locale}`)}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
86
packages/ui/src/Header/SwitchTheme.tsx
Normal file
86
packages/ui/src/Header/SwitchTheme.tsx
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { classNames } from "@repo/config-tailwind/classNames"
|
||||||
|
import { useIsMounted } from "@repo/react-hooks/useIsMounted"
|
||||||
|
import { ThemeProvider as NextThemeProvider, useTheme } from "next-themes"
|
||||||
|
|
||||||
|
export const THEMES = ["light", "dark"] as const
|
||||||
|
export type Theme = (typeof THEMES)[number]
|
||||||
|
export const THEME_DEFAULT = "light" as Theme
|
||||||
|
|
||||||
|
export const ThemeProvider: React.FC<React.PropsWithChildren> = (props) => {
|
||||||
|
const { children } = props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NextThemeProvider
|
||||||
|
attribute="class"
|
||||||
|
defaultTheme={THEME_DEFAULT}
|
||||||
|
enableSystem={false}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</NextThemeProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SwitchTheme: React.FC = () => {
|
||||||
|
const { setTheme, theme: themeData } = useTheme()
|
||||||
|
const { isMounted } = useIsMounted()
|
||||||
|
|
||||||
|
const theme = isMounted ? (themeData as Theme) : THEME_DEFAULT
|
||||||
|
|
||||||
|
const handleClick = (): void => {
|
||||||
|
const newTheme = theme === "dark" ? "light" : "dark"
|
||||||
|
setTheme(newTheme)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center" onClick={handleClick}>
|
||||||
|
<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={classNames(
|
||||||
|
"absolute inset-y-0 left-[8px] my-auto h-[10px] w-[14px] leading-[0] transition-opacity duration-[250ms] ease-in-out",
|
||||||
|
{
|
||||||
|
"opacity-100": theme === "dark",
|
||||||
|
"opacity-0": theme === "light",
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span className="relative flex size-[10px] items-center justify-center">
|
||||||
|
🌜
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
"absolute inset-y-0 right-[10px] my-auto size-[10px] leading-[0]",
|
||||||
|
{
|
||||||
|
"opacity-100": theme === "light",
|
||||||
|
"opacity-0": theme === "dark",
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span className="relative flex size-[10px] items-center justify-center">
|
||||||
|
🌞
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
"absolute top-px box-border size-[22px] rounded-[50%] bg-[#fafafa] text-white transition-all duration-[250ms] ease-in-out",
|
||||||
|
{
|
||||||
|
"left-[27px]": theme === "dark",
|
||||||
|
"left-0": theme === "light",
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
style={{ border: "1px solid #4d4d4d" }}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
aria-label="Dark mode toggle"
|
||||||
|
className="absolute -m-px hidden size-px overflow-hidden border-0 p-0"
|
||||||
|
defaultChecked
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
20
packages/ui/src/MainLayout/MainLayout.tsx
Normal file
20
packages/ui/src/MainLayout/MainLayout.tsx
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
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>
|
||||||
|
)
|
||||||
|
}
|
148
packages/ui/src/design/Button/Button.stories.tsx
Normal file
148
packages/ui/src/design/Button/Button.stories.tsx
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
import type { Meta, StoryObj } from "@storybook/react"
|
||||||
|
import { expect, fn, userEvent, within } from "@storybook/test"
|
||||||
|
import { FaCheck } from "react-icons/fa6"
|
||||||
|
|
||||||
|
import type { ButtonLinkProps } from "./Button"
|
||||||
|
import { Button } from "./Button"
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: "Design System/Button",
|
||||||
|
component: Button,
|
||||||
|
tags: ["autodocs"],
|
||||||
|
args: { onClick: fn() },
|
||||||
|
} satisfies Meta<typeof Button>
|
||||||
|
|
||||||
|
export default meta
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof meta>
|
||||||
|
|
||||||
|
const ButtonContainer: React.FC<React.PropsWithChildren> = (props) => {
|
||||||
|
const { children } = props
|
||||||
|
|
||||||
|
return <div className="flex gap-4">{children}</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Component: Story = {
|
||||||
|
args: {
|
||||||
|
children: "Button",
|
||||||
|
},
|
||||||
|
play: async ({ canvasElement, args }) => {
|
||||||
|
const canvas = within(canvasElement)
|
||||||
|
await userEvent.click(canvas.getByText("Button"))
|
||||||
|
await expect(args.onClick).toHaveBeenCalled()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Variants: Story = {
|
||||||
|
render: (args) => {
|
||||||
|
return (
|
||||||
|
<ButtonContainer>
|
||||||
|
<Button variant="solid" {...args}>
|
||||||
|
Solid
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline" {...args}>
|
||||||
|
Outline
|
||||||
|
</Button>
|
||||||
|
</ButtonContainer>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Sizes: Story = {
|
||||||
|
render: (args) => {
|
||||||
|
return (
|
||||||
|
<ButtonContainer>
|
||||||
|
<Button size="small" {...args}>
|
||||||
|
Small
|
||||||
|
</Button>
|
||||||
|
<Button size="medium" {...args}>
|
||||||
|
Medium
|
||||||
|
</Button>
|
||||||
|
<Button size="large" {...args}>
|
||||||
|
Large
|
||||||
|
</Button>
|
||||||
|
</ButtonContainer>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Disabled: Story = {
|
||||||
|
render: (args) => {
|
||||||
|
return (
|
||||||
|
<ButtonContainer>
|
||||||
|
<Button variant="solid" disabled {...args}>
|
||||||
|
Solid
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline" disabled {...args}>
|
||||||
|
Outline
|
||||||
|
</Button>
|
||||||
|
</ButtonContainer>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Loading: Story = {
|
||||||
|
render: (args) => {
|
||||||
|
return (
|
||||||
|
<ButtonContainer>
|
||||||
|
<Button variant="solid" isLoading {...args}>
|
||||||
|
Solid
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline" isLoading {...args}>
|
||||||
|
Outline
|
||||||
|
</Button>
|
||||||
|
</ButtonContainer>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Icons: Story = {
|
||||||
|
render: (args) => {
|
||||||
|
return (
|
||||||
|
<ButtonContainer>
|
||||||
|
<Button leftIcon={<FaCheck size={18} />} {...args}>
|
||||||
|
Left Icon
|
||||||
|
</Button>
|
||||||
|
<Button rightIcon={<FaCheck size={18} />} {...args}>
|
||||||
|
Right Icon
|
||||||
|
</Button>
|
||||||
|
</ButtonContainer>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Link: Story = {
|
||||||
|
args: {
|
||||||
|
children: "Link",
|
||||||
|
href: "/",
|
||||||
|
},
|
||||||
|
play: async ({ canvasElement, args }) => {
|
||||||
|
const canvas = within(canvasElement)
|
||||||
|
await expect(
|
||||||
|
canvas.getByRole("link", {
|
||||||
|
name: "Link",
|
||||||
|
}),
|
||||||
|
).toHaveAttribute("href", args.href)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LinkWithIcons: Story = {
|
||||||
|
args: {
|
||||||
|
href: "/",
|
||||||
|
},
|
||||||
|
render: (args) => {
|
||||||
|
return (
|
||||||
|
<ButtonContainer>
|
||||||
|
<Button leftIcon={<FaCheck size={18} />} {...(args as ButtonLinkProps)}>
|
||||||
|
Link Left Icon
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
rightIcon={<FaCheck size={18} />}
|
||||||
|
{...(args as ButtonLinkProps)}
|
||||||
|
>
|
||||||
|
Link Right Icon
|
||||||
|
</Button>
|
||||||
|
</ButtonContainer>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}
|
111
packages/ui/src/design/Button/Button.tsx
Normal file
111
packages/ui/src/design/Button/Button.tsx
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
import { classNames } from "@repo/config-tailwind/classNames"
|
||||||
|
import { Link as NextLink } from "@repo/i18n/navigation"
|
||||||
|
import type { VariantProps } from "cva"
|
||||||
|
import { cva } from "cva"
|
||||||
|
|
||||||
|
import { Spinner } from "../Spinner/Spinner"
|
||||||
|
import { Ripple } from "./Ripple"
|
||||||
|
|
||||||
|
const buttonVariants = cva({
|
||||||
|
base: "relative inline-flex items-center justify-center overflow-hidden rounded-md text-base font-semibold transition duration-150 ease-in-out focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
solid: "bg-primary hover:bg-primary/80 text-white",
|
||||||
|
outline:
|
||||||
|
"dark:border-primary-dark/60 dark:text-primary-dark dark:hover:border-primary-dark border-primary/60 text-primary hover:border-primary hover:bg-gray border bg-transparent dark:hover:bg-transparent",
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
small: "h-9 rounded-md px-3",
|
||||||
|
medium: "h-10 px-4 py-2",
|
||||||
|
large: "h-11 rounded-md px-8",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "solid",
|
||||||
|
size: "medium",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
interface ButtonBaseProps extends VariantProps<typeof buttonVariants> {
|
||||||
|
leftIcon?: React.ReactNode
|
||||||
|
rightIcon?: React.ReactNode
|
||||||
|
disabled?: boolean
|
||||||
|
isLoading?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ButtonElementProps extends React.ComponentPropsWithoutRef<"button"> {}
|
||||||
|
interface LinkElementProps
|
||||||
|
extends React.ComponentPropsWithoutRef<typeof NextLink> {}
|
||||||
|
|
||||||
|
export type ButtonLinkProps = ButtonBaseProps &
|
||||||
|
LinkElementProps & { href: string }
|
||||||
|
export type ButtonButtonProps = ButtonBaseProps &
|
||||||
|
ButtonElementProps & { href?: never }
|
||||||
|
|
||||||
|
export type ButtonProps = ButtonButtonProps | ButtonLinkProps
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Buttons allow users to take actions, and make choices, with a single click.
|
||||||
|
* @param props
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const Button: React.FC<ButtonProps> = (props) => {
|
||||||
|
const rippleColor =
|
||||||
|
props.variant === "outline" ? "rgb(30, 64, 175)" : "rgb(229, 231, 235)"
|
||||||
|
|
||||||
|
if (typeof props.href === "string") {
|
||||||
|
const { variant, size, leftIcon, rightIcon, className, children, ...rest } =
|
||||||
|
props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NextLink
|
||||||
|
className={classNames(buttonVariants({ variant, size }), className)}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
{leftIcon != null ? <span className="mr-2">{leftIcon}</span> : null}
|
||||||
|
<span>{children}</span>
|
||||||
|
{rightIcon != null ? <span className="ml-2">{rightIcon}</span> : null}
|
||||||
|
|
||||||
|
<Ripple color={rippleColor} />
|
||||||
|
</NextLink>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
variant,
|
||||||
|
size,
|
||||||
|
leftIcon,
|
||||||
|
rightIcon,
|
||||||
|
className,
|
||||||
|
isLoading = false,
|
||||||
|
disabled = false,
|
||||||
|
children,
|
||||||
|
...rest
|
||||||
|
} = props
|
||||||
|
|
||||||
|
const isDisabled = disabled || isLoading
|
||||||
|
|
||||||
|
const leftIconElement = isLoading ? (
|
||||||
|
<Spinner size={18} className="text-inherit dark:text-inherit" />
|
||||||
|
) : (
|
||||||
|
leftIcon
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className={classNames(buttonVariants({ variant, size }), className)}
|
||||||
|
disabled={isDisabled}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
{leftIconElement != null ? (
|
||||||
|
<span className="mr-2">{leftIconElement}</span>
|
||||||
|
) : null}
|
||||||
|
<span>{children}</span>
|
||||||
|
{rightIcon != null && !isLoading ? (
|
||||||
|
<span className="ml-2">{rightIcon}</span>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<Ripple color={rippleColor} />
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
91
packages/ui/src/design/Button/Ripple.tsx
Normal file
91
packages/ui/src/design/Button/Ripple.tsx
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useLayoutEffect, useState } from "react"
|
||||||
|
|
||||||
|
const useDebouncedRippleCleanUp = (
|
||||||
|
rippleCount: number,
|
||||||
|
duration: number,
|
||||||
|
cleanUpFunction: () => void,
|
||||||
|
): void => {
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
let bounce: ReturnType<typeof setTimeout> | undefined
|
||||||
|
if (rippleCount > 0) {
|
||||||
|
clearTimeout(bounce)
|
||||||
|
|
||||||
|
bounce = setTimeout(() => {
|
||||||
|
cleanUpFunction()
|
||||||
|
clearTimeout(bounce)
|
||||||
|
}, duration * 4)
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
return clearTimeout(bounce)
|
||||||
|
}
|
||||||
|
}, [rippleCount, duration, cleanUpFunction])
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RippleProps {
|
||||||
|
/**
|
||||||
|
* The color of the ripple effect.
|
||||||
|
*/
|
||||||
|
color?: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The duration of the ripple animation in milliseconds.
|
||||||
|
*/
|
||||||
|
duration?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RippleItem {
|
||||||
|
x: number
|
||||||
|
y: number
|
||||||
|
size: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Ripple: React.FC<RippleProps> = (props) => {
|
||||||
|
const { duration = 1_200, color = "rgb(229, 231, 235)" } = props
|
||||||
|
const [rippleArray, setRippleArray] = useState<RippleItem[]>([])
|
||||||
|
|
||||||
|
useDebouncedRippleCleanUp(rippleArray.length, duration, () => {
|
||||||
|
setRippleArray([])
|
||||||
|
})
|
||||||
|
|
||||||
|
const addRipple: React.MouseEventHandler<HTMLDivElement> = (event) => {
|
||||||
|
const rippleContainer = event.currentTarget.getBoundingClientRect()
|
||||||
|
const size =
|
||||||
|
rippleContainer.width > rippleContainer.height
|
||||||
|
? rippleContainer.width
|
||||||
|
: rippleContainer.height
|
||||||
|
const x = event.pageX - rippleContainer.x - size / 2
|
||||||
|
const y = event.pageY - rippleContainer.y - size / 2
|
||||||
|
const newRipple: RippleItem = {
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
size,
|
||||||
|
}
|
||||||
|
setRippleArray([...rippleArray, newRipple])
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="absolute inset-0" onMouseDown={addRipple}>
|
||||||
|
{rippleArray.map((ripple, index) => {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
key={"span" + index}
|
||||||
|
className="absolute rounded-full opacity-75"
|
||||||
|
style={{
|
||||||
|
transform: "scale(0)",
|
||||||
|
backgroundColor: color,
|
||||||
|
animationName: "ripple",
|
||||||
|
animationDuration: `${duration}ms`,
|
||||||
|
top: ripple.y,
|
||||||
|
left: ripple.x,
|
||||||
|
width: ripple.size,
|
||||||
|
height: ripple.size,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
29
packages/ui/src/design/Link/Link.stories.tsx
Normal file
29
packages/ui/src/design/Link/Link.stories.tsx
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import type { Meta, StoryObj } from "@storybook/react"
|
||||||
|
|
||||||
|
import { Link } from "./Link"
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: "Design System/Link",
|
||||||
|
component: Link,
|
||||||
|
tags: ["autodocs"],
|
||||||
|
} satisfies Meta<typeof Link>
|
||||||
|
|
||||||
|
export default meta
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof meta>
|
||||||
|
|
||||||
|
export const Component: Story = {
|
||||||
|
args: {
|
||||||
|
children: "Link",
|
||||||
|
href: "/",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const External: Story = {
|
||||||
|
args: {
|
||||||
|
children: "Link",
|
||||||
|
href: "/",
|
||||||
|
target: "_blank",
|
||||||
|
isExternal: true,
|
||||||
|
},
|
||||||
|
}
|
35
packages/ui/src/design/Link/Link.tsx
Normal file
35
packages/ui/src/design/Link/Link.tsx
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { classNames } from "@repo/config-tailwind/classNames"
|
||||||
|
import { Link as NextLink } from "@repo/i18n/navigation"
|
||||||
|
import { FiExternalLink } from "react-icons/fi"
|
||||||
|
|
||||||
|
export interface LinkProps extends React.ComponentProps<typeof NextLink> {
|
||||||
|
isExternal?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Link is an actionable text component with connection to another web pages.
|
||||||
|
* @param props
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const Link: React.FC<LinkProps> = (props) => {
|
||||||
|
const { className, children, target, isExternal = true, ...rest } = props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NextLink
|
||||||
|
className={classNames(
|
||||||
|
"text-primary dark:text-primary-dark inline-flex items-center gap-1 font-semibold hover:underline focus:rounded-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
target={target}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
|
||||||
|
{target === "_blank" && isExternal ? (
|
||||||
|
<FiExternalLink size={16} strokeWidth={2.5} />
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
)}
|
||||||
|
</NextLink>
|
||||||
|
)
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user