initial commit
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"projectName": "chess-scribe-website",
|
||||
"mode": "file-router",
|
||||
"typescript": true,
|
||||
"packageManager": "npm",
|
||||
"includeExamples": true,
|
||||
"tailwind": true,
|
||||
"addOnOptions": {},
|
||||
"envVarValues": {},
|
||||
"git": true,
|
||||
"install": true,
|
||||
"routerOnly": false,
|
||||
"version": 1,
|
||||
"framework": "react",
|
||||
"chosenAddOns": [
|
||||
"biome",
|
||||
"nitro"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
.env
|
||||
.nitro
|
||||
.tanstack
|
||||
.wrangler
|
||||
.output
|
||||
.vinxi
|
||||
__unconfig*
|
||||
todos.json
|
||||
@@ -0,0 +1,10 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
# Ignored default folder with query files
|
||||
/queries/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="BiomeSettings">
|
||||
<option name="applySafeFixesOnSave" value="true" />
|
||||
<option name="enableLspFormat" value="true" />
|
||||
<option name="formatOnSave" value="true" />
|
||||
<option name="sortImportOnSave" value="true" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="WEB_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="GitToolBoxProjectSettings">
|
||||
<option name="commitMessageIssueKeyValidationOverride">
|
||||
<BoolValueOverride>
|
||||
<option name="enabled" value="true" />
|
||||
</BoolValueOverride>
|
||||
</option>
|
||||
<option name="commitMessageValidationEnabledOverride">
|
||||
<BoolValueOverride>
|
||||
<option name="enabled" value="true" />
|
||||
</BoolValueOverride>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
@@ -0,0 +1,51 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="PyPackageRequirementsInspection" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="ignoredPackages">
|
||||
<value>
|
||||
<list size="38">
|
||||
<item index="0" class="java.lang.String" itemvalue="fastapi" />
|
||||
<item index="1" class="java.lang.String" itemvalue="pydantic" />
|
||||
<item index="2" class="java.lang.String" itemvalue="dotenv" />
|
||||
<item index="3" class="java.lang.String" itemvalue="geopy" />
|
||||
<item index="4" class="java.lang.String" itemvalue="async-timeout" />
|
||||
<item index="5" class="java.lang.String" itemvalue="requests-cache" />
|
||||
<item index="6" class="java.lang.String" itemvalue="python-dateutil" />
|
||||
<item index="7" class="java.lang.String" itemvalue="python-dotenv" />
|
||||
<item index="8" class="java.lang.String" itemvalue="wcwidth" />
|
||||
<item index="9" class="java.lang.String" itemvalue="requests" />
|
||||
<item index="10" class="java.lang.String" itemvalue="importlib-metadata" />
|
||||
<item index="11" class="java.lang.String" itemvalue="frozenlist" />
|
||||
<item index="12" class="java.lang.String" itemvalue="pyrate-limiter" />
|
||||
<item index="13" class="java.lang.String" itemvalue="appdirs" />
|
||||
<item index="14" class="java.lang.String" itemvalue="url-normalize" />
|
||||
<item index="15" class="java.lang.String" itemvalue="Pygments" />
|
||||
<item index="16" class="java.lang.String" itemvalue="zipp" />
|
||||
<item index="17" class="java.lang.String" itemvalue="certifi" />
|
||||
<item index="18" class="java.lang.String" itemvalue="soupsieve" />
|
||||
<item index="19" class="java.lang.String" itemvalue="urllib3" />
|
||||
<item index="20" class="java.lang.String" itemvalue="commonmark" />
|
||||
<item index="21" class="java.lang.String" itemvalue="beautifulsoup4" />
|
||||
<item index="22" class="java.lang.String" itemvalue="cattrs" />
|
||||
<item index="23" class="java.lang.String" itemvalue="python-forge" />
|
||||
<item index="24" class="java.lang.String" itemvalue="orjson" />
|
||||
<item index="25" class="java.lang.String" itemvalue="rich" />
|
||||
<item index="26" class="java.lang.String" itemvalue="attrs" />
|
||||
<item index="27" class="java.lang.String" itemvalue="typing_extensions" />
|
||||
<item index="28" class="java.lang.String" itemvalue="charset-normalizer" />
|
||||
<item index="29" class="java.lang.String" itemvalue="requests-ratelimiter" />
|
||||
<item index="30" class="java.lang.String" itemvalue="aiohttp" />
|
||||
<item index="31" class="java.lang.String" itemvalue="multidict" />
|
||||
<item index="32" class="java.lang.String" itemvalue="yarl" />
|
||||
<item index="33" class="java.lang.String" itemvalue="aiosignal" />
|
||||
<item index="34" class="java.lang.String" itemvalue="py-cord" />
|
||||
<item index="35" class="java.lang.String" itemvalue="pyyaml" />
|
||||
<item index="36" class="java.lang.String" itemvalue="httptools" />
|
||||
<item index="37" class="java.lang.String" itemvalue="websockets" />
|
||||
</list>
|
||||
</value>
|
||||
</option>
|
||||
</inspection_tool>
|
||||
</profile>
|
||||
</component>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/chess-scribe-website.iml" filepath="$PROJECT_DIR$/.idea/chess-scribe-website.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"files.watcherExclude": {
|
||||
"**/routeTree.gen.ts": true
|
||||
},
|
||||
"search.exclude": {
|
||||
"**/routeTree.gen.ts": true
|
||||
},
|
||||
"files.readonlyInclude": {
|
||||
"**/routeTree.gen.ts": true
|
||||
},
|
||||
"[javascript]": {
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
},
|
||||
"[javascriptreact]": {
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
},
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
},
|
||||
"[typescriptreact]": {
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
},
|
||||
"[json]": {
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
},
|
||||
"[jsonc]": {
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
},
|
||||
"[css]": {
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
},
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.organizeImports.biome": "explicit"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
Welcome to your new TanStack Start app!
|
||||
|
||||
# Getting Started
|
||||
|
||||
To run this application:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
# Building For Production
|
||||
|
||||
To build this application for production:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
This project uses [Vitest](https://vitest.dev/) for testing. You can run the tests with:
|
||||
|
||||
```bash
|
||||
npm run test
|
||||
```
|
||||
|
||||
## Styling
|
||||
|
||||
This project uses [Tailwind CSS](https://tailwindcss.com/) for styling.
|
||||
|
||||
### Removing Tailwind CSS
|
||||
|
||||
If you prefer not to use Tailwind CSS:
|
||||
|
||||
1. Remove the demo pages in `src/routes/demo/`
|
||||
2. Replace the Tailwind import in `src/styles.css` with your own styles
|
||||
3. Remove `tailwindcss()` from the plugins array in `vite.config.ts`
|
||||
4. Uninstall the packages: `npm install @tailwindcss/vite tailwindcss -D`
|
||||
|
||||
## Linting & Formatting
|
||||
|
||||
This project uses [Biome](https://biomejs.dev/) for linting and formatting. The following scripts are available:
|
||||
|
||||
|
||||
```bash
|
||||
npm run lint
|
||||
npm run format
|
||||
npm run check
|
||||
```
|
||||
|
||||
|
||||
## Deploy with Nitro
|
||||
|
||||
This project uses Nitro as a generic server adapter, so it can run on any Node-compatible host.
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
node dist/server/index.mjs
|
||||
```
|
||||
|
||||
The build output is a self-contained Node server. To deploy, push the `dist/` directory to your host (Render, Fly.io, your own VPS, etc.) and run the server command above.
|
||||
|
||||
For host-specific presets (Vercel, Netlify, Cloudflare, AWS Lambda, etc.) and tuning, see https://v3.nitro.build/deploy.
|
||||
|
||||
|
||||
|
||||
## Routing
|
||||
|
||||
This project uses [TanStack Router](https://tanstack.com/router) with file-based routing. Routes are managed as files in `src/routes`.
|
||||
|
||||
### Adding A Route
|
||||
|
||||
To add a new route to your application just add a new file in the `./src/routes` directory.
|
||||
|
||||
TanStack will automatically generate the content of the route file for you.
|
||||
|
||||
Now that you have two routes you can use a `Link` component to navigate between them.
|
||||
|
||||
### Adding Links
|
||||
|
||||
To use SPA (Single Page Application) navigation you will need to import the `Link` component from `@tanstack/react-router`.
|
||||
|
||||
```tsx
|
||||
import { Link } from "@tanstack/react-router";
|
||||
```
|
||||
|
||||
Then anywhere in your JSX you can use it like so:
|
||||
|
||||
```tsx
|
||||
<Link to="/about">About</Link>
|
||||
```
|
||||
|
||||
This will create a link that will navigate to the `/about` route.
|
||||
|
||||
More information on the `Link` component can be found in the [Link documentation](https://tanstack.com/router/v1/docs/framework/react/api/router/linkComponent).
|
||||
|
||||
### Using A Layout
|
||||
|
||||
In the File Based Routing setup the layout is located in `src/routes/__root.tsx`. Anything you add to the root route will appear in all the routes. The route content will appear in the JSX where you render `{children}` in the `shellComponent`.
|
||||
|
||||
Here is an example layout that includes a header:
|
||||
|
||||
```tsx
|
||||
import { HeadContent, Scripts, createRootRoute } from '@tanstack/react-router'
|
||||
|
||||
export const Route = createRootRoute({
|
||||
head: () => ({
|
||||
meta: [
|
||||
{ charSet: 'utf-8' },
|
||||
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
|
||||
{ title: 'My App' },
|
||||
],
|
||||
}),
|
||||
shellComponent: ({ children }) => (
|
||||
<html lang="en">
|
||||
<head>
|
||||
<HeadContent />
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<nav>
|
||||
<Link to="/">Home</Link>
|
||||
<Link to="/about">About</Link>
|
||||
</nav>
|
||||
</header>
|
||||
{children}
|
||||
<Scripts />
|
||||
</body>
|
||||
</html>
|
||||
),
|
||||
})
|
||||
```
|
||||
|
||||
More information on layouts can be found in the [Layouts documentation](https://tanstack.com/router/latest/docs/framework/react/guide/routing-concepts#layouts).
|
||||
|
||||
## Server Functions
|
||||
|
||||
TanStack Start provides server functions that allow you to write server-side code that seamlessly integrates with your client components.
|
||||
|
||||
```tsx
|
||||
import { createServerFn } from '@tanstack/react-start'
|
||||
|
||||
const getServerTime = createServerFn({
|
||||
method: 'GET',
|
||||
}).handler(async () => {
|
||||
return new Date().toISOString()
|
||||
})
|
||||
|
||||
// Use in a component
|
||||
function MyComponent() {
|
||||
const [time, setTime] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
getServerTime().then(setTime)
|
||||
}, [])
|
||||
|
||||
return <div>Server time: {time}</div>
|
||||
}
|
||||
```
|
||||
|
||||
## API Routes
|
||||
|
||||
You can create API routes by using the `server` property in your route definitions:
|
||||
|
||||
```tsx
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { json } from '@tanstack/react-start'
|
||||
|
||||
export const Route = createFileRoute('/api/hello')({
|
||||
server: {
|
||||
handlers: {
|
||||
GET: () => json({ message: 'Hello, World!' }),
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## Data Fetching
|
||||
|
||||
There are multiple ways to fetch data in your application. You can use TanStack Query to fetch data from a server. But you can also use the `loader` functionality built into TanStack Router to load the data for a route before it's rendered.
|
||||
|
||||
For example:
|
||||
|
||||
```tsx
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
export const Route = createFileRoute('/people')({
|
||||
loader: async () => {
|
||||
const response = await fetch('https://swapi.dev/api/people')
|
||||
return response.json()
|
||||
},
|
||||
component: PeopleComponent,
|
||||
})
|
||||
|
||||
function PeopleComponent() {
|
||||
const data = Route.useLoaderData()
|
||||
return (
|
||||
<ul>
|
||||
{data.results.map((person) => (
|
||||
<li key={person.name}>{person.name}</li>
|
||||
))}
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
Loaders simplify your data fetching logic dramatically. Check out more information in the [Loader documentation](https://tanstack.com/router/latest/docs/framework/react/guide/data-loading#loader-parameters).
|
||||
|
||||
# Demo files
|
||||
|
||||
Files prefixed with `demo` can be safely deleted. They are there to provide a starting point for you to play around with the features you've installed.
|
||||
|
||||
# Learn More
|
||||
|
||||
You can learn more about all of the offerings from TanStack in the [TanStack documentation](https://tanstack.com).
|
||||
|
||||
For TanStack Start specific documentation, visit [TanStack Start](https://tanstack.com/start).
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"$schema": "https://biomejs.dev/schemas/2.4.5/schema.json",
|
||||
"vcs": {
|
||||
"enabled": false,
|
||||
"clientKind": "git",
|
||||
"useIgnoreFile": false
|
||||
},
|
||||
"files": {
|
||||
"ignoreUnknown": false,
|
||||
"includes": [
|
||||
"**/src/**/*",
|
||||
"**/.vscode/**/*",
|
||||
"**/index.html",
|
||||
"**/vite.config.ts",
|
||||
"!**/src/routeTree.gen.ts",
|
||||
"!**/src/styles.css"
|
||||
]
|
||||
},
|
||||
"formatter": {
|
||||
"enabled": true,
|
||||
"indentStyle": "tab"
|
||||
},
|
||||
"assist": { "actions": { "source": { "organizeImports": "on" } } },
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
"recommended": true
|
||||
}
|
||||
},
|
||||
"javascript": {
|
||||
"formatter": {
|
||||
"quoteStyle": "double"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"name": "chess-scribe-website",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"imports": {
|
||||
"#/*": "./src/*"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vite dev --port 3000",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"test": "vitest run",
|
||||
"format": "biome format",
|
||||
"lint": "biome lint",
|
||||
"check": "biome check"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource-variable/inter": "^5.2.8",
|
||||
"@socialgouv/matomo-next": "^1.13.1",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"@tanstack/react-devtools": "latest",
|
||||
"@tanstack/react-router": "latest",
|
||||
"@tanstack/react-router-devtools": "latest",
|
||||
"@tanstack/react-router-ssr-query": "latest",
|
||||
"@tanstack/react-start": "latest",
|
||||
"@tanstack/router-plugin": "^1.132.0",
|
||||
"@unpic/react": "^1.0.2",
|
||||
"lichess-pgn-viewer": "^2.4.5",
|
||||
"nitro": "npm:nitro-nightly@latest",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-icons": "^5.6.0",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"zod": "^4.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "2.4.5",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"@tanstack/devtools-vite": "latest",
|
||||
"@testing-library/dom": "^10.4.1",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@types/node": "^22.10.2",
|
||||
"@types/react": "^19.2.0",
|
||||
"@types/react-dom": "^19.2.0",
|
||||
"@vitejs/plugin-react": "^6.0.1",
|
||||
"jsdom": "^28.1.0",
|
||||
"typescript": "^6.0.2",
|
||||
"vite": "^8.0.0",
|
||||
"vitest": "^4.1.5"
|
||||
},
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": [
|
||||
"esbuild",
|
||||
"lightningcss"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 9.4 KiB |
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"short_name": "TanStack App",
|
||||
"name": "Create TanStack App Sample",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
},
|
||||
{
|
||||
"src": "logo192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "logo512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
||||
|
After Width: | Height: | Size: 8.3 KiB |
|
After Width: | Height: | Size: 57 KiB |
|
After Width: | Height: | Size: 40 KiB |
@@ -0,0 +1,44 @@
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||
width="64.000000pt" height="64.000000pt" viewBox="0 0 64.000000 64.000000"
|
||||
preserveAspectRatio="xMidYMid meet">
|
||||
|
||||
<g transform="translate(0.000000,64.000000) scale(0.100000,-0.100000)"
|
||||
fill="#ffffff" stroke="white">
|
||||
<path d="M460 605 c0 -8 -7 -15 -15 -15 -10 0 -15 -10 -15 -30 0 -20 5 -30 15
|
||||
-30 20 0 19 -16 -1 -24 -12 -4 -15 -14 -11 -34 3 -15 1 -34 -4 -44 -16 -29 0
|
||||
-48 41 -48 42 0 44 -2 55 -80 4 -25 9 -53 11 -62 5 -16 -2 -18 -46 -18 -40 0
|
||||
-51 3 -47 13 12 32 19 130 9 123 -6 -3 -14 -32 -18 -62 -4 -36 -13 -62 -25
|
||||
-72 -11 -9 -19 -22 -19 -28 0 -7 -7 -14 -15 -18 -8 -3 -15 -17 -15 -31 0 -25
|
||||
0 -25 90 -25 53 0 90 4 90 10 0 6 -33 10 -80 10 -47 0 -80 4 -80 10 0 6 43 10
|
||||
110 10 67 0 110 -4 110 -10 0 -5 -9 -10 -20 -10 -11 0 -20 -4 -20 -10 0 -5 14
|
||||
-10 30 -10 25 0 30 4 30 25 0 14 -7 28 -15 31 -8 4 -15 12 -15 20 0 7 -7 17
|
||||
-16 22 -11 6 -21 35 -29 83 -11 64 -11 76 2 91 9 10 12 22 6 30 -4 7 -8 29 -8
|
||||
48 0 32 -3 35 -32 38 -25 2 -33 8 -33 23 0 10 -7 19 -15 19 -8 0 -15 5 -15 10
|
||||
0 6 7 10 15 10 8 0 15 7 15 15 0 8 5 15 10 15 6 0 10 -7 10 -15 0 -8 7 -15 15
|
||||
-15 8 0 15 -4 15 -10 0 -5 -7 -10 -15 -10 -8 0 -15 -4 -15 -10 0 -5 11 -10 25
|
||||
-10 21 0 25 5 25 30 0 20 -5 30 -15 30 -8 0 -15 7 -15 15 0 10 -10 15 -30 15
|
||||
-20 0 -30 -5 -30 -15z m66 -131 c-3 -9 -6 -20 -6 -25 0 -5 -13 -9 -30 -9 -16
|
||||
0 -30 4 -30 9 0 5 -3 16 -6 25 -5 13 2 16 36 16 34 0 41 -3 36 -16z m14 -64
|
||||
c0 -5 -22 -10 -50 -10 -27 0 -50 5 -50 10 0 6 23 10 50 10 28 0 50 -4 50 -10z
|
||||
m30 -220 c0 -6 -33 -10 -80 -10 -47 0 -80 4 -80 10 0 6 33 10 80 10 47 0 80
|
||||
-4 80 -10z"/>
|
||||
<path d="M118 519 c-56 -29 -78 -80 -78 -179 0 -47 4 -80 10 -80 6 0 10 34 10
|
||||
83 0 64 4 89 20 115 11 17 24 32 30 32 14 0 13 -5 -10 -42 -17 -28 -20 -51
|
||||
-20 -160 0 -79 4 -128 10 -128 6 0 10 49 10 126 0 135 9 169 41 159 22 -7 23
|
||||
-2 8 27 -16 29 -2 34 20 7 22 -25 28 -11 11 22 -9 16 -7 19 14 19 29 0 76 -19
|
||||
76 -32 0 -10 -3 -10 -34 2 -16 6 -27 6 -31 0 -4 -6 -2 -10 3 -10 5 0 30 -16
|
||||
56 -35 48 -37 64 -78 26 -68 -22 6 -28 -12 -7 -20 24 -10 -21 -7 -47 3 -34 13
|
||||
-55 4 -62 -27 -6 -21 2 -33 50 -77 45 -42 56 -58 56 -84 0 -27 -4 -32 -24 -32
|
||||
-17 0 -27 9 -36 30 -13 32 -37 56 -46 47 -4 -3 3 -18 15 -33 11 -15 21 -31 21
|
||||
-35 0 -5 -31 -9 -68 -9 -70 0 -92 -10 -92 -41 0 -9 -7 -19 -15 -23 -17 -6 -21
|
||||
-56 -5 -56 6 0 10 9 10 20 0 19 7 20 150 20 143 0 150 -1 150 -20 0 -11 5 -20
|
||||
10 -20 16 0 12 50 -5 56 -8 4 -15 14 -15 24 0 10 -7 23 -15 30 -8 7 -15 28
|
||||
-15 48 0 30 -8 43 -57 87 -41 38 -54 57 -49 70 4 11 12 15 24 10 46 -18 85
|
||||
-18 98 0 22 30 17 71 -11 94 -20 15 -24 25 -20 44 5 20 1 27 -28 41 -46 21
|
||||
-93 20 -139 -5z m190 -421 c3 -16 -8 -18 -117 -18 -94 0 -121 3 -121 13 0 25
|
||||
14 28 124 25 96 -3 111 -5 114 -20z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.7 KiB |
@@ -0,0 +1,7 @@
|
||||
<svg
|
||||
viewBox="0 0 50 50"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="M38.956.5c-3.53.418-6.452.902-9.286 2.984C5.534 1.786-.692 18.533.68 29.364 3.493 50.214 31.918 55.785 41.329 41.7c-7.444 7.696-19.276 8.752-28.323 3.084C3.959 39.116-.506 27.392 4.683 17.567 9.873 7.742 18.996 4.535 29.03 6.405c2.43-1.418 5.225-3.22 7.655-3.187l-1.694 4.86 12.752 21.37c-.439 5.654-5.459 6.112-5.459 6.112-.574-1.47-1.634-2.942-4.842-6.036-3.207-3.094-17.465-10.177-15.788-16.207-2.001 6.967 10.311 14.152 14.04 17.663 3.73 3.51 5.426 6.04 5.795 6.756 0 0 9.392-2.504 7.838-8.927L37.4 7.171z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 631 B |
@@ -0,0 +1,44 @@
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||
width="64.000000pt" height="64.000000pt" viewBox="0 0 64.000000 64.000000"
|
||||
preserveAspectRatio="xMidYMid meet">
|
||||
|
||||
<g transform="translate(0.000000,64.000000) scale(0.100000,-0.100000)"
|
||||
fill="#6f884d" stroke="black">
|
||||
<path d="M460 605 c0 -8 -7 -15 -15 -15 -10 0 -15 -10 -15 -30 0 -20 5 -30 15
|
||||
-30 20 0 19 -16 -1 -24 -12 -4 -15 -14 -11 -34 3 -15 1 -34 -4 -44 -16 -29 0
|
||||
-48 41 -48 42 0 44 -2 55 -80 4 -25 9 -53 11 -62 5 -16 -2 -18 -46 -18 -40 0
|
||||
-51 3 -47 13 12 32 19 130 9 123 -6 -3 -14 -32 -18 -62 -4 -36 -13 -62 -25
|
||||
-72 -11 -9 -19 -22 -19 -28 0 -7 -7 -14 -15 -18 -8 -3 -15 -17 -15 -31 0 -25
|
||||
0 -25 90 -25 53 0 90 4 90 10 0 6 -33 10 -80 10 -47 0 -80 4 -80 10 0 6 43 10
|
||||
110 10 67 0 110 -4 110 -10 0 -5 -9 -10 -20 -10 -11 0 -20 -4 -20 -10 0 -5 14
|
||||
-10 30 -10 25 0 30 4 30 25 0 14 -7 28 -15 31 -8 4 -15 12 -15 20 0 7 -7 17
|
||||
-16 22 -11 6 -21 35 -29 83 -11 64 -11 76 2 91 9 10 12 22 6 30 -4 7 -8 29 -8
|
||||
48 0 32 -3 35 -32 38 -25 2 -33 8 -33 23 0 10 -7 19 -15 19 -8 0 -15 5 -15 10
|
||||
0 6 7 10 15 10 8 0 15 7 15 15 0 8 5 15 10 15 6 0 10 -7 10 -15 0 -8 7 -15 15
|
||||
-15 8 0 15 -4 15 -10 0 -5 -7 -10 -15 -10 -8 0 -15 -4 -15 -10 0 -5 11 -10 25
|
||||
-10 21 0 25 5 25 30 0 20 -5 30 -15 30 -8 0 -15 7 -15 15 0 10 -10 15 -30 15
|
||||
-20 0 -30 -5 -30 -15z m66 -131 c-3 -9 -6 -20 -6 -25 0 -5 -13 -9 -30 -9 -16
|
||||
0 -30 4 -30 9 0 5 -3 16 -6 25 -5 13 2 16 36 16 34 0 41 -3 36 -16z m14 -64
|
||||
c0 -5 -22 -10 -50 -10 -27 0 -50 5 -50 10 0 6 23 10 50 10 28 0 50 -4 50 -10z
|
||||
m30 -220 c0 -6 -33 -10 -80 -10 -47 0 -80 4 -80 10 0 6 33 10 80 10 47 0 80
|
||||
-4 80 -10z"/>
|
||||
<path d="M118 519 c-56 -29 -78 -80 -78 -179 0 -47 4 -80 10 -80 6 0 10 34 10
|
||||
83 0 64 4 89 20 115 11 17 24 32 30 32 14 0 13 -5 -10 -42 -17 -28 -20 -51
|
||||
-20 -160 0 -79 4 -128 10 -128 6 0 10 49 10 126 0 135 9 169 41 159 22 -7 23
|
||||
-2 8 27 -16 29 -2 34 20 7 22 -25 28 -11 11 22 -9 16 -7 19 14 19 29 0 76 -19
|
||||
76 -32 0 -10 -3 -10 -34 2 -16 6 -27 6 -31 0 -4 -6 -2 -10 3 -10 5 0 30 -16
|
||||
56 -35 48 -37 64 -78 26 -68 -22 6 -28 -12 -7 -20 24 -10 -21 -7 -47 3 -34 13
|
||||
-55 4 -62 -27 -6 -21 2 -33 50 -77 45 -42 56 -58 56 -84 0 -27 -4 -32 -24 -32
|
||||
-17 0 -27 9 -36 30 -13 32 -37 56 -46 47 -4 -3 3 -18 15 -33 11 -15 21 -31 21
|
||||
-35 0 -5 -31 -9 -68 -9 -70 0 -92 -10 -92 -41 0 -9 -7 -19 -15 -23 -17 -6 -21
|
||||
-56 -5 -56 6 0 10 9 10 20 0 19 7 20 150 20 143 0 150 -1 150 -20 0 -11 5 -20
|
||||
10 -20 16 0 12 50 -5 56 -8 4 -15 14 -15 24 0 10 -7 23 -15 30 -8 7 -15 28
|
||||
-15 48 0 30 -8 43 -57 87 -41 38 -54 57 -49 70 4 11 12 15 24 10 46 -18 85
|
||||
-18 98 0 22 30 17 71 -11 94 -20 15 -24 25 -20 44 5 20 1 27 -28 41 -46 21
|
||||
-93 20 -139 -5z m190 -421 c3 -16 -8 -18 -117 -18 -94 0 -121 3 -121 13 0 25
|
||||
14 28 124 25 96 -3 111 -5 114 -20z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.7 KiB |
@@ -0,0 +1,34 @@
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
interface IProps {
|
||||
children: ReactNode;
|
||||
title: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
const AccordionItem = ({ title, id, children }: IProps) => (
|
||||
<div className="hero__accordion-item">
|
||||
<details className="hero__accordion-details" name="linked">
|
||||
<summary
|
||||
className="hero__accordion-item__summary"
|
||||
aria-describedby={title}
|
||||
>
|
||||
<span className="hero__accordion-item__title-container">
|
||||
<span className="hero__accordion-item__title">{title}</span>
|
||||
</span>
|
||||
</summary>
|
||||
</details>
|
||||
|
||||
<div className="hero__accordion-item__content">
|
||||
<p
|
||||
className="hero__accordion-item__content__inner"
|
||||
role="definition"
|
||||
id={id}
|
||||
>
|
||||
{children}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default AccordionItem;
|
||||
@@ -0,0 +1,28 @@
|
||||
import { Image } from "@unpic/react";
|
||||
import buyMeACoffeeLogo from "@/assets/images/buymeacoffeelogo.svg?url";
|
||||
|
||||
interface IProps {
|
||||
height: number;
|
||||
width: number;
|
||||
}
|
||||
|
||||
const CoffeeWidget = ({ height, width }: IProps) => {
|
||||
return (
|
||||
<a
|
||||
href="https://buymeacoffee.com/owenreesdev"
|
||||
target="_blank"
|
||||
className="ml-auto flex items-center gap-2 text-sm"
|
||||
rel="noopener"
|
||||
>
|
||||
<Image
|
||||
src={buyMeACoffeeLogo}
|
||||
alt="Buy Me A Coffee Logo"
|
||||
width={width}
|
||||
height={height}
|
||||
/>
|
||||
<p>Support This Project</p>
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
||||
export default CoffeeWidget;
|
||||
@@ -0,0 +1,17 @@
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
interface IProps {
|
||||
title: string;
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
const Feature = ({ title, children }: IProps) => (
|
||||
<div className="mb-auto">
|
||||
<h6 className="text-(--base-content) text-2xl font-bold">{title}</h6>
|
||||
<p className="text-(--neutral-content) font-medium" data-testid="text">
|
||||
{children}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default Feature;
|
||||
@@ -0,0 +1,49 @@
|
||||
import { Link } from "@tanstack/react-router";
|
||||
import { Image } from "@unpic/react";
|
||||
|
||||
import { LuGithub, LuMail } from "react-icons/lu";
|
||||
|
||||
import CoffeeWidget from "#/components/CoffeeWidget.tsx";
|
||||
import footerLogo from "@/assets/images/footerLogo.svg?url";
|
||||
|
||||
export default function Footer() {
|
||||
return (
|
||||
<footer className="text-white w-full grid-cols-2 items-center bg-(--accent) px-10 py-5">
|
||||
<div className="flex w-full items-center">
|
||||
<Link to="/">
|
||||
<Image
|
||||
src={footerLogo}
|
||||
alt="ChessScribe Logo"
|
||||
width={10}
|
||||
height={10}
|
||||
className="hidden h-10 w-10 sm:block"
|
||||
/>
|
||||
</Link>
|
||||
<div>
|
||||
<p data-testid="copyright">
|
||||
Copyright © 2023 - {new Date().getFullYear()}
|
||||
</p>
|
||||
</div>
|
||||
<div className="ml-auto flex gap-2">
|
||||
<a
|
||||
href={import.meta.env.VITE_GITHUB_URL} // TODO ZOD
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-2xl hover:text-(--neutral-content) transition-colors duration-300 ease-in-out cursor-pointer"
|
||||
>
|
||||
<LuGithub />
|
||||
</a>
|
||||
|
||||
<Link
|
||||
to="/contact"
|
||||
rel="noopener noreferrer"
|
||||
className="text-2xl hover:text-(--neutral-content) transition-colors duration-300 ease-in-out"
|
||||
>
|
||||
<LuMail />
|
||||
</Link>
|
||||
</div>
|
||||
<CoffeeWidget width={20} height={50} />
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { Link } from "@tanstack/react-router";
|
||||
import { Image } from "@unpic/react";
|
||||
|
||||
import logo from "@/assets/images/logo.svg?url";
|
||||
|
||||
export default function Header() {
|
||||
return (
|
||||
<header>
|
||||
<nav className="w-full max-w-screen-2xl place-self-center p-8">
|
||||
<div className="container mx-auto flex items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
<Link to="/" className="flex flex-row items-center justify-center">
|
||||
<Image
|
||||
src={logo}
|
||||
alt="ChessScribe Logo"
|
||||
width={50}
|
||||
height={50}
|
||||
className="mr-4"
|
||||
/>
|
||||
<span className="nav-title hidden text-2xl font-extrabold sm:block">
|
||||
ChessScribe
|
||||
</span>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<ul className="flex flex-1 items-center gap-14 font-semibold">
|
||||
<li>
|
||||
<Link to="/chessboard" className="nav-item">
|
||||
Chessboard
|
||||
</Link>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<Link to="/contact" className="nav-item">
|
||||
Contact
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
interface IProps {
|
||||
onClickHandler: () => void;
|
||||
}
|
||||
|
||||
const LichessButton = ({ onClickHandler }: IProps) => {
|
||||
return (
|
||||
<button
|
||||
className="text-(--accent) border rounded-xl px-6 py-3 cursor-pointer flex items-center gap-2 p-2 hover:bg-(--accent) hover:text-(--header-bg) transition-colors duration-300 ease-in-out"
|
||||
onClick={onClickHandler}
|
||||
type="button"
|
||||
>
|
||||
<div className="w-6 h-6">
|
||||
<svg
|
||||
viewBox="0 0 50 50"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="currentColor"
|
||||
>
|
||||
<title>Lichess Logo</title>
|
||||
<path d="M38.956.5c-3.53.418-6.452.902-9.286 2.984C5.534 1.786-.692 18.533.68 29.364 3.493 50.214 31.918 55.785 41.329 41.7c-7.444 7.696-19.276 8.752-28.323 3.084C3.959 39.116-.506 27.392 4.683 17.567 9.873 7.742 18.996 4.535 29.03 6.405c2.43-1.418 5.225-3.22 7.655-3.187l-1.694 4.86 12.752 21.37c-.439 5.654-5.459 6.112-5.459 6.112-.574-1.47-1.634-2.942-4.842-6.036-3.207-3.094-17.465-10.177-15.788-16.207-2.001 6.967 10.311 14.152 14.04 17.663 3.73 3.51 5.426 6.04 5.795 6.756 0 0 9.392-2.504 7.838-8.927L37.4 7.171z" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<p>Log into Lichess.org</p>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default LichessButton;
|
||||
@@ -0,0 +1,33 @@
|
||||
import { trackAppRouter } from "@socialgouv/matomo-next";
|
||||
import { useLocation, useSearch } from "@tanstack/react-router";
|
||||
import { useEffect } from "react";
|
||||
|
||||
const MATOMO_URL = import.meta.env.NEXT_PUBLIC_MATOMO_URL;
|
||||
const MATOMO_SITE_ID = import.meta.env.NEXT_PUBLIC_MATOMO_SITE_ID;
|
||||
|
||||
export function MatomoAnalytics() {
|
||||
const location = useLocation();
|
||||
const searchParams = useSearch({ strict: false });
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === "undefined") return;
|
||||
|
||||
const serializedParams = new URLSearchParams(
|
||||
searchParams as Record<string, string>,
|
||||
);
|
||||
|
||||
trackAppRouter({
|
||||
url: MATOMO_URL || "",
|
||||
siteId: MATOMO_SITE_ID || "",
|
||||
pathname: location.pathname,
|
||||
searchParams: serializedParams,
|
||||
enableHeatmapSessionRecording: false,
|
||||
enableHeartBeatTimer: true,
|
||||
cleanUrl: true,
|
||||
disableCookies: true,
|
||||
debug: import.meta.env.DEV,
|
||||
});
|
||||
}, [location.pathname, searchParams]);
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import LichessPgnViewer from "lichess-pgn-viewer";
|
||||
import type PgnViewerType from "lichess-pgn-viewer/pgnViewer";
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
interface IProps {
|
||||
gamePgn: string;
|
||||
}
|
||||
|
||||
const PgnViewer = ({ gamePgn }: IProps) => {
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
const viewerRef = useRef<PgnViewerType | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const element: HTMLElement | null = document.querySelector(".lpv-board");
|
||||
if (!element) return;
|
||||
|
||||
viewerRef.current = LichessPgnViewer(element, {
|
||||
pgn: gamePgn,
|
||||
scrollToMove: false,
|
||||
});
|
||||
|
||||
return () => {
|
||||
if (containerRef.current) {
|
||||
containerRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [gamePgn]);
|
||||
|
||||
return <div ref={containerRef} className="lpv-board" />;
|
||||
};
|
||||
|
||||
export default PgnViewer;
|
||||
@@ -0,0 +1,31 @@
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
interface IProps {
|
||||
title: string;
|
||||
smallTitle?: string;
|
||||
description?: string;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
const Section = ({ title, smallTitle, description, children }: IProps) => {
|
||||
const words = title.trim().split(" ");
|
||||
const lastWord = words.pop();
|
||||
const mainTitle = words.join(" ");
|
||||
|
||||
return (
|
||||
<section className="mx-auto mt-8 grid max-w-5xl place-items-center gap-4 p-8 text-center md:grid-cols-3 md:text-left text-(--accent)">
|
||||
<h3 className="md:col-span-3 mb-4 text-center text-lg font-bold tracking-wider text-primary">
|
||||
{smallTitle}
|
||||
</h3>
|
||||
<h4 className="md:col-span-3 mb-4 text-center text-4xl font-extrabold text-(--neutral-content) md:text-5xl">
|
||||
{mainTitle} <span className="text-(--accent)">{lastWord}</span>
|
||||
</h4>
|
||||
<p className="md:col-span-3 mb-8 w-1/2 text-center font-semibold text-(--neutral-content) ">
|
||||
{description}
|
||||
</p>
|
||||
{children}
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default Section;
|
||||
@@ -0,0 +1,84 @@
|
||||
import { type ChangeEvent, useReducer, useRef } from "react";
|
||||
import { gameReducer, initialGameState } from "#/reducers/gameReducer.ts";
|
||||
import { buildPgnString, getHeaders } from "#/utils/pgnUtils.ts";
|
||||
import { downloadString } from "#/utils/stringUtils.ts";
|
||||
|
||||
export const useChessGame = () => {
|
||||
const [gameState, gameDispatch] = useReducer(gameReducer, initialGameState);
|
||||
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
||||
|
||||
const handleSavePgn = () => {
|
||||
const pgnString = buildPgnString(gameState);
|
||||
downloadString(pgnString, "game.pgn");
|
||||
};
|
||||
|
||||
const handleSavePdf = () => {
|
||||
console.log("Save PDF");
|
||||
};
|
||||
|
||||
const handleClearGame = () => {
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = "";
|
||||
}
|
||||
|
||||
gameDispatch({
|
||||
type: "CLEAR_GAME",
|
||||
});
|
||||
};
|
||||
|
||||
const handleLoadPgn = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
e.preventDefault();
|
||||
|
||||
const selectedFile = e.target.files?.[0];
|
||||
|
||||
if (selectedFile?.name.endsWith(".pgn")) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (event) => {
|
||||
const pgnData = event.target?.result;
|
||||
let pgnString = "";
|
||||
|
||||
if (pgnData) {
|
||||
if (typeof pgnData === "string") {
|
||||
pgnString = pgnData;
|
||||
} else {
|
||||
pgnString = new TextDecoder().decode(pgnData);
|
||||
}
|
||||
}
|
||||
|
||||
if (pgnString) {
|
||||
const headers = {
|
||||
...getHeaders(pgnString),
|
||||
title: "",
|
||||
subtitle: "",
|
||||
};
|
||||
gameDispatch({
|
||||
type: "SET_GAME",
|
||||
payload: { pgn: pgnString, headers: headers },
|
||||
});
|
||||
} else {
|
||||
e.target.files = null;
|
||||
handleClearGame();
|
||||
}
|
||||
};
|
||||
reader.readAsText(selectedFile);
|
||||
} else {
|
||||
handleClearGame();
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggleClock = () => {
|
||||
gameDispatch({
|
||||
type: "TOGGLE_DIAGRAM_CLOCK",
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
gameState,
|
||||
fileInputRef,
|
||||
handleSavePgn,
|
||||
handleSavePdf,
|
||||
handleClearGame,
|
||||
handleLoadPgn,
|
||||
handleToggleClock,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
import { useChessGame } from "#/hooks/useChessGame.ts";
|
||||
import Chessboard from "#/pages/Chessboard.tsx";
|
||||
|
||||
const ChessboardContainer = () => {
|
||||
const chessGameProps = useChessGame();
|
||||
|
||||
return (
|
||||
<Chessboard
|
||||
gameState={chessGameProps.gameState}
|
||||
handleLoadPgn={chessGameProps.handleLoadPgn}
|
||||
handleClearGame={chessGameProps.handleClearGame}
|
||||
handleSavePgn={chessGameProps.handleSavePgn}
|
||||
handleSavePdf={chessGameProps.handleSavePdf}
|
||||
handleToggleClock={chessGameProps.handleToggleClock}
|
||||
fileInputRef={chessGameProps.fileInputRef}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChessboardContainer;
|
||||
@@ -0,0 +1,80 @@
|
||||
import type { ChangeEvent, RefObject } from "react";
|
||||
import LichessButton from "#/components/LichessButton.tsx";
|
||||
import PgnViewer from "#/components/PgnViewer.tsx";
|
||||
import Section from "#/components/Section.tsx";
|
||||
import type { IGameState } from "#/reducers/gameReducer.ts";
|
||||
|
||||
interface IProps {
|
||||
gameState: IGameState;
|
||||
handleLoadPgn: (e: ChangeEvent<HTMLInputElement>) => void;
|
||||
handleClearGame: () => void;
|
||||
handleSavePgn: () => void;
|
||||
handleSavePdf: () => void;
|
||||
handleToggleClock: () => void;
|
||||
fileInputRef: RefObject<HTMLInputElement | null>;
|
||||
}
|
||||
|
||||
const Chessboard = ({
|
||||
gameState,
|
||||
handleLoadPgn,
|
||||
handleClearGame,
|
||||
handleSavePgn,
|
||||
handleSavePdf,
|
||||
handleToggleClock,
|
||||
fileInputRef,
|
||||
}: IProps) => {
|
||||
return (
|
||||
<main className="px-4 pb-8 place-self-center min-h-[calc(100vh-226px)]">
|
||||
<Section title="Convert PGN to PDF">
|
||||
<LichessButton /> or{" "}
|
||||
<input
|
||||
type="file"
|
||||
id="file-input"
|
||||
ref={fileInputRef}
|
||||
className="file-input max-w-xs"
|
||||
onChange={handleLoadPgn}
|
||||
/>
|
||||
</Section>
|
||||
|
||||
<div className="flex gap-4">
|
||||
<button
|
||||
className="cursor-pointer"
|
||||
type="button"
|
||||
onClick={handleClearGame}
|
||||
>
|
||||
Clear Game
|
||||
</button>
|
||||
|
||||
<button
|
||||
className="cursor-pointer"
|
||||
type="button"
|
||||
onClick={handleSavePgn}
|
||||
disabled={!gameState.pgn}
|
||||
>
|
||||
Save PGN
|
||||
</button>
|
||||
|
||||
<button
|
||||
className="cursor-pointer"
|
||||
type="button"
|
||||
onClick={handleSavePdf}
|
||||
disabled={!gameState.pgn}
|
||||
>
|
||||
Save PDF
|
||||
</button>
|
||||
</div>
|
||||
<PgnViewer gamePgn={gameState.pgn || ""} />
|
||||
<label htmlFor="diagram-clock">
|
||||
Render Clock Times
|
||||
<input
|
||||
id="diagram-clock"
|
||||
name="diagram-clock"
|
||||
type="checkbox"
|
||||
onChange={handleToggleClock}
|
||||
/>
|
||||
</label>
|
||||
</main>
|
||||
);
|
||||
};
|
||||
|
||||
export default Chessboard;
|
||||
@@ -0,0 +1,3 @@
|
||||
const Contact = () => <div>Hello "/contact"!</div>;
|
||||
|
||||
export default Contact;
|
||||
@@ -0,0 +1,96 @@
|
||||
import { Link } from "@tanstack/react-router";
|
||||
import { Image } from "@unpic/react";
|
||||
import AccordionItem from "#/components/AccordionItem.tsx";
|
||||
import Feature from "#/components/Feature.tsx";
|
||||
import Section from "#/components/Section.tsx";
|
||||
import examplePdf1 from "@/assets/images/examplepdf1.webp?url";
|
||||
import examplePdf2 from "@/assets/images/examplepdf2.webp?url";
|
||||
|
||||
const Home = () => (
|
||||
<main className="px-4 pb-8 pt-14">
|
||||
<section
|
||||
id="hero"
|
||||
className="grid max-w-screen-2xl items-center gap-8 p-8 text-center md:grid-cols-2 md:text-left mx-auto"
|
||||
>
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-base-content md:text-4xl">
|
||||
Convert your Chess PGN
|
||||
</h1>
|
||||
<h1 className="text-3xl font-bold text-base-content md:text-4xl">
|
||||
into a <span className="text-(--accent)">Publishable PDF.</span>
|
||||
</h1>
|
||||
<h2 className="mb-8 mt-4 text-(--base-content)">
|
||||
Upload and convert your games into a book format, along with variation
|
||||
and annotations.
|
||||
</h2>
|
||||
<Link
|
||||
to="/chessboard"
|
||||
className="rounded-3xl text-xl font-bold btn btn-primary"
|
||||
>
|
||||
Get Started
|
||||
</Link>
|
||||
</div>
|
||||
<div className="example-pdfs">
|
||||
<Image src={examplePdf2} alt="Example PDF 2" width={500} height={500} />
|
||||
<Image
|
||||
src={examplePdf1}
|
||||
alt="Example PDF 1"
|
||||
width={500}
|
||||
height={500}
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
<Section
|
||||
title="A Unique Chess Publication Service"
|
||||
smallTitle="FEATURES"
|
||||
description="Convert your chess PGN files into a publishable PDF, complete with diagrams of chosen positions."
|
||||
>
|
||||
<Feature title="Fast">
|
||||
Quick PDF generation using a custom TeX Live server.
|
||||
</Feature>
|
||||
|
||||
<Feature title="Easy">
|
||||
Upload a PGN of your game, and choose the diagrams you want in your PDF.
|
||||
Annotations are added automatically.
|
||||
</Feature>
|
||||
|
||||
<Feature title="Reliable">
|
||||
Dedicated servers ensure that downtime is kept to a minimum.
|
||||
</Feature>
|
||||
</Section>
|
||||
|
||||
<Section
|
||||
title="Your Questions Answered"
|
||||
smallTitle="FAQs"
|
||||
description="Below we answer some of the most common questions we get regarding this service."
|
||||
>
|
||||
<div className="hero__accordion-container">
|
||||
<AccordionItem title="Is this service free?" id="accordion-1">
|
||||
Yes. There are no current plans to charge for this service. If in the
|
||||
future there is a payment plan, it will come with a generous free
|
||||
tier.
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem
|
||||
title="Will you be adding more features?"
|
||||
id="accordion-2"
|
||||
>
|
||||
We have a lot more features to implement in the near future. If you
|
||||
have a feature that you would like to see implemented, please feel
|
||||
free to send us a message via our contact form.
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem
|
||||
title="Do you hold any copyright on the produced materials?"
|
||||
id="accordion-3"
|
||||
>
|
||||
No. We will never implement any terms whereby we have any interest in
|
||||
the materials created with this service.
|
||||
</AccordionItem>
|
||||
</div>
|
||||
</Section>
|
||||
</main>
|
||||
);
|
||||
|
||||
export default Home;
|
||||
@@ -0,0 +1,100 @@
|
||||
interface IDiagrams {
|
||||
ply: number;
|
||||
fen: string;
|
||||
}
|
||||
|
||||
export interface IHeader {
|
||||
event: string;
|
||||
site: string;
|
||||
date: string;
|
||||
round: string;
|
||||
white: string;
|
||||
black: string;
|
||||
result: string;
|
||||
eco: string;
|
||||
whiteElo: string;
|
||||
blackElo: string;
|
||||
plyCount: string;
|
||||
eventDate: string;
|
||||
source: string;
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
export interface IGameState {
|
||||
pgn: string;
|
||||
headers: IHeader;
|
||||
diagrams: IDiagrams[];
|
||||
diagramClock: boolean;
|
||||
}
|
||||
|
||||
type GameAction =
|
||||
| { type: "SET_GAME"; payload: { pgn: string; headers: IHeader } }
|
||||
| { type: "CLEAR_GAME" }
|
||||
| { type: "SET_HEADERS"; payload: IHeader }
|
||||
| { type: "ADD_DIAGRAM"; payload: IDiagrams }
|
||||
| { type: "DELETE_DIAGRAM"; payload: { ply: number } }
|
||||
| { type: "TOGGLE_DIAGRAM_CLOCK" };
|
||||
|
||||
const defaultHeaderFields = {
|
||||
event: "",
|
||||
site: "",
|
||||
date: "",
|
||||
round: "",
|
||||
white: "",
|
||||
black: "",
|
||||
result: "",
|
||||
eco: "",
|
||||
whiteElo: "",
|
||||
blackElo: "",
|
||||
plyCount: "",
|
||||
eventDate: "",
|
||||
source: "",
|
||||
title: "",
|
||||
subtitle: "",
|
||||
author: "",
|
||||
} satisfies IHeader;
|
||||
|
||||
export const initialGameState: IGameState = {
|
||||
pgn: "",
|
||||
headers: defaultHeaderFields,
|
||||
diagrams: [],
|
||||
diagramClock: false,
|
||||
};
|
||||
|
||||
export const gameReducer = (state: IGameState, action: GameAction) => {
|
||||
switch (action.type) {
|
||||
case "SET_GAME":
|
||||
return {
|
||||
pgn: action.payload.pgn,
|
||||
headers: action.payload.headers,
|
||||
diagrams: [],
|
||||
diagramClock: false,
|
||||
};
|
||||
case "CLEAR_GAME":
|
||||
return initialGameState;
|
||||
case "SET_HEADERS":
|
||||
return {
|
||||
...state,
|
||||
headers: action.payload,
|
||||
};
|
||||
case "ADD_DIAGRAM":
|
||||
return {
|
||||
...state,
|
||||
diagrams: [...state.diagrams, action.payload],
|
||||
};
|
||||
case "DELETE_DIAGRAM":
|
||||
return {
|
||||
...state,
|
||||
diagrams: state.diagrams.filter(
|
||||
(diagram: IDiagrams) => diagram.ply !== action.payload.ply,
|
||||
),
|
||||
};
|
||||
case "TOGGLE_DIAGRAM_CLOCK":
|
||||
return {
|
||||
...state,
|
||||
diagramClock: !state.diagramClock,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,104 @@
|
||||
/* eslint-disable */
|
||||
|
||||
// @ts-nocheck
|
||||
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
|
||||
// This file was automatically generated by TanStack Router.
|
||||
// You should NOT make any changes in this file as it will be overwritten.
|
||||
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
|
||||
|
||||
import { Route as rootRouteImport } from './routes/__root'
|
||||
import { Route as ContactRouteImport } from './routes/contact'
|
||||
import { Route as ChessboardRouteImport } from './routes/chessboard'
|
||||
import { Route as IndexRouteImport } from './routes/index'
|
||||
|
||||
const ContactRoute = ContactRouteImport.update({
|
||||
id: '/contact',
|
||||
path: '/contact',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const ChessboardRoute = ChessboardRouteImport.update({
|
||||
id: '/chessboard',
|
||||
path: '/chessboard',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const IndexRoute = IndexRouteImport.update({
|
||||
id: '/',
|
||||
path: '/',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
|
||||
export interface FileRoutesByFullPath {
|
||||
'/': typeof IndexRoute
|
||||
'/chessboard': typeof ChessboardRoute
|
||||
'/contact': typeof ContactRoute
|
||||
}
|
||||
export interface FileRoutesByTo {
|
||||
'/': typeof IndexRoute
|
||||
'/chessboard': typeof ChessboardRoute
|
||||
'/contact': typeof ContactRoute
|
||||
}
|
||||
export interface FileRoutesById {
|
||||
__root__: typeof rootRouteImport
|
||||
'/': typeof IndexRoute
|
||||
'/chessboard': typeof ChessboardRoute
|
||||
'/contact': typeof ContactRoute
|
||||
}
|
||||
export interface FileRouteTypes {
|
||||
fileRoutesByFullPath: FileRoutesByFullPath
|
||||
fullPaths: '/' | '/chessboard' | '/contact'
|
||||
fileRoutesByTo: FileRoutesByTo
|
||||
to: '/' | '/chessboard' | '/contact'
|
||||
id: '__root__' | '/' | '/chessboard' | '/contact'
|
||||
fileRoutesById: FileRoutesById
|
||||
}
|
||||
export interface RootRouteChildren {
|
||||
IndexRoute: typeof IndexRoute
|
||||
ChessboardRoute: typeof ChessboardRoute
|
||||
ContactRoute: typeof ContactRoute
|
||||
}
|
||||
|
||||
declare module '@tanstack/react-router' {
|
||||
interface FileRoutesByPath {
|
||||
'/contact': {
|
||||
id: '/contact'
|
||||
path: '/contact'
|
||||
fullPath: '/contact'
|
||||
preLoaderRoute: typeof ContactRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/chessboard': {
|
||||
id: '/chessboard'
|
||||
path: '/chessboard'
|
||||
fullPath: '/chessboard'
|
||||
preLoaderRoute: typeof ChessboardRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/': {
|
||||
id: '/'
|
||||
path: '/'
|
||||
fullPath: '/'
|
||||
preLoaderRoute: typeof IndexRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const rootRouteChildren: RootRouteChildren = {
|
||||
IndexRoute: IndexRoute,
|
||||
ChessboardRoute: ChessboardRoute,
|
||||
ContactRoute: ContactRoute,
|
||||
}
|
||||
export const routeTree = rootRouteImport
|
||||
._addFileChildren(rootRouteChildren)
|
||||
._addFileTypes<FileRouteTypes>()
|
||||
|
||||
import type { getRouter } from './router.tsx'
|
||||
import type { createStart } from '@tanstack/react-start'
|
||||
declare module '@tanstack/react-start' {
|
||||
interface Register {
|
||||
ssr: true
|
||||
router: Awaited<ReturnType<typeof getRouter>>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { createRouter as createTanStackRouter } from '@tanstack/react-router'
|
||||
import { routeTree } from './routeTree.gen'
|
||||
|
||||
export function getRouter() {
|
||||
const router = createTanStackRouter({
|
||||
routeTree,
|
||||
scrollRestoration: true,
|
||||
defaultPreload: 'intent',
|
||||
defaultPreloadStaleTime: 0,
|
||||
})
|
||||
|
||||
return router
|
||||
}
|
||||
|
||||
declare module '@tanstack/react-router' {
|
||||
interface Register {
|
||||
router: ReturnType<typeof getRouter>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import { TanStackDevtools } from "@tanstack/react-devtools";
|
||||
import { createRootRoute, HeadContent, Scripts } from "@tanstack/react-router";
|
||||
import { TanStackRouterDevtoolsPanel } from "@tanstack/react-router-devtools";
|
||||
import { type ReactNode, Suspense } from "react";
|
||||
import { MatomoAnalytics } from "#/components/MatomoAnalytics.tsx";
|
||||
import Footer from "../components/Footer";
|
||||
import Header from "../components/Header";
|
||||
import appCss from "../styles.css?url";
|
||||
|
||||
export const Route = createRootRoute({
|
||||
head: () => ({
|
||||
meta: [
|
||||
{
|
||||
charSet: "utf-8",
|
||||
},
|
||||
{
|
||||
name: "viewport",
|
||||
content: "width=device-width, initial-scale=1",
|
||||
},
|
||||
{
|
||||
title: "ChessScribe",
|
||||
},
|
||||
],
|
||||
links: [
|
||||
{
|
||||
rel: "stylesheet",
|
||||
href: appCss,
|
||||
},
|
||||
],
|
||||
}),
|
||||
shellComponent: RootDocument,
|
||||
});
|
||||
|
||||
function RootDocument({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<head>
|
||||
<HeadContent />
|
||||
</head>
|
||||
<body className="font-sans antialiased">
|
||||
<Header />
|
||||
{children}
|
||||
<Footer />
|
||||
<TanStackDevtools
|
||||
config={{
|
||||
position: "bottom-right",
|
||||
}}
|
||||
plugins={[
|
||||
{
|
||||
name: "Tanstack Router",
|
||||
render: <TanStackRouterDevtoolsPanel />,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Scripts />
|
||||
<Suspense fallback={null}>
|
||||
<MatomoAnalytics />
|
||||
</Suspense>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import ChessboardContainer from "#/pages/Chessboard.container.tsx";
|
||||
|
||||
export const Route = createFileRoute("/chessboard")({
|
||||
component: RouteComponent,
|
||||
});
|
||||
|
||||
function RouteComponent() {
|
||||
return <ChessboardContainer />;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import Contact from "#/pages/Contact.tsx";
|
||||
|
||||
export const Route = createFileRoute("/contact")({
|
||||
component: RouteComponent,
|
||||
});
|
||||
|
||||
function RouteComponent() {
|
||||
return <Contact />;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import Home from "#/pages";
|
||||
|
||||
export const Route = createFileRoute("/")({ component: App });
|
||||
|
||||
function App() {
|
||||
return <Home />;
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
@import "tailwindcss";
|
||||
@import "styles/_accordion.css";
|
||||
@import "./styles/_file-input.css";
|
||||
@import './styles/_pgn-viewer.css';
|
||||
|
||||
@plugin "@tailwindcss/typography";
|
||||
|
||||
/* inter-latin-wght-normal */
|
||||
@font-face {
|
||||
font-family: 'Inter Variable';
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
font-weight: 100 900;
|
||||
src: url('@fontsource-variable/inter/files/inter-latin-wght-normal.woff2') format('woff2-variations');
|
||||
unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
|
||||
}
|
||||
|
||||
@theme {
|
||||
--font-sans: "Inter Variable", sans-serif;
|
||||
}
|
||||
|
||||
:root {
|
||||
--base-content: #1f2937;
|
||||
--neutral-content: #4b5563;
|
||||
--accent: #6f884d;
|
||||
--bg-base: #e7f3ec;
|
||||
--header-bg: rgba(251, 255, 248, 0.84);
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#app {
|
||||
min-height: 100%;
|
||||
background-color: var(--bg-base);
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: var(--font-sans), sans-serif;
|
||||
background-color: var(--bg-base);
|
||||
overflow-x: hidden;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.nav-container {
|
||||
background-color: var(--header-bg);
|
||||
}
|
||||
|
||||
.nav-title {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
color: var(--neutral-content);
|
||||
|
||||
&:hover {
|
||||
color: var(--accent);
|
||||
text-decoration: underline;
|
||||
text-decoration-thickness: 2px;
|
||||
text-underline-offset: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 1rem 4rem;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: var(--accent);
|
||||
color: var(--bg-base);
|
||||
transition: filter 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
filter: brightness(0.85);
|
||||
}
|
||||
}
|
||||
|
||||
.example-pdfs {
|
||||
max-width: 450px;
|
||||
position: relative;
|
||||
justify-self: center;
|
||||
transform: translate(-10%, 10%);
|
||||
img {
|
||||
width: 300px;
|
||||
box-shadow: -1px 1px 62px -18px black;
|
||||
&:nth-child(2) {
|
||||
position: absolute;
|
||||
top: -10%;
|
||||
right: -20%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.rise-in {
|
||||
animation: rise-in 700ms cubic-bezier(0.16, 1, 0.3, 1) both;
|
||||
}
|
||||
|
||||
@keyframes rise-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(12px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
.hero__accordion-container {
|
||||
grid-column: 1 / -1;
|
||||
max-width: 600px;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.hero__accordion-item {
|
||||
overflow: clip;
|
||||
transition-property: all;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition-duration: 0.15s;
|
||||
cursor: pointer;
|
||||
padding: 1rem 0;
|
||||
}
|
||||
|
||||
.hero__accordion-item:not(:last-child) {
|
||||
border-bottom: 1px solid var(--neutral-content);
|
||||
}
|
||||
|
||||
.hero__accordion-details[open] + .hero__accordion-item__content {
|
||||
grid-template-rows: 1fr;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.hero__accordion-details[open] .hero__accordion-item__title-container:after {
|
||||
transform: rotate(270deg);
|
||||
}
|
||||
|
||||
.hero__accordion-item__summary {
|
||||
display: block;
|
||||
|
||||
&::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.hero__accordion-item__title-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
&:after {
|
||||
content: "\276F";
|
||||
color: var(--base-content);
|
||||
margin-right: 1rem;
|
||||
transform: rotate(90deg);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
}
|
||||
|
||||
.hero__accordion-item__title {
|
||||
color: var(--base-content);
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.hero__accordion-item__content {
|
||||
padding: 0 10px;
|
||||
display: grid;
|
||||
grid-template-rows: 0fr;
|
||||
transition:
|
||||
grid-template-rows 0.3s ease,
|
||||
padding 0.3s ease;
|
||||
|
||||
@media (prefers-reduced-motion) {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
.hero__accordion-item__content__inner {
|
||||
color: var(--neutral-content);
|
||||
overflow: hidden;
|
||||
margin: 0;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
.file-input {
|
||||
height: 3rem;
|
||||
padding-inline-end: 1rem;
|
||||
font-size: 0.875rem;
|
||||
line-height: 2;
|
||||
border-width: 1px;
|
||||
border-color: var(--accent);
|
||||
border-radius: 0.5rem;
|
||||
background-color: var(--header-bg);
|
||||
flex-shrink: 1;
|
||||
}
|
||||
|
||||
.file-input::file-selector-button {
|
||||
color: var(--header-bg);
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
text-align: center;
|
||||
background-color: var(--accent);
|
||||
cursor: pointer;
|
||||
height: 100%;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
@import "./vendor/lichess-pgn-viewer.css";
|
||||
|
||||
.lpv-board {
|
||||
max-width: 600px;
|
||||
--board-color: #b58863;
|
||||
font-family: "Noto Sans", sans-serif;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export const downloadPDF = (pdf: Blob) => {
|
||||
const fileName = "game.pdf";
|
||||
const fileURL = URL.createObjectURL(pdf);
|
||||
const a = document.createElement("a");
|
||||
a.href = fileURL;
|
||||
a.download = fileName;
|
||||
a.click();
|
||||
};
|
||||
@@ -0,0 +1,61 @@
|
||||
import type { IGameState } from "#/reducers/gameReducer.ts";
|
||||
|
||||
export const getHeaders = (pgn: string) => {
|
||||
const pgnHeader = pgn.split(/\n\n/g)[0];
|
||||
|
||||
const getHeaderField = (field: string) => {
|
||||
const regex = new RegExp(`(?<=${field}.").*(?=")`);
|
||||
const match = pgnHeader.match(regex);
|
||||
return match && match[0] !== "undefined" ? match[0] : "";
|
||||
};
|
||||
|
||||
return {
|
||||
event: getHeaderField("Event"),
|
||||
site: getHeaderField("Site"),
|
||||
date: getHeaderField("Date"),
|
||||
round: getHeaderField("Round"),
|
||||
white: getHeaderField("White"),
|
||||
black: getHeaderField("Black"),
|
||||
result: getHeaderField("Result"),
|
||||
eco: getHeaderField("ECO"),
|
||||
whiteElo: getHeaderField("WhiteElo"),
|
||||
blackElo: getHeaderField("BlackElo"),
|
||||
plyCount: getHeaderField("PlyCount"),
|
||||
eventDate: getHeaderField("EventDate"),
|
||||
source: getHeaderField("Source"),
|
||||
};
|
||||
};
|
||||
|
||||
// Create a string based on new header values
|
||||
export const buildPgnString = (game: IGameState) => {
|
||||
const moves = game.pgn?.split(/\n\n/g)[1];
|
||||
|
||||
// if customer headers are present, use them
|
||||
if (
|
||||
game.headers.title?.length > 0 ||
|
||||
game.headers.subtitle?.length > 0 ||
|
||||
game.headers.author?.length > 0
|
||||
) {
|
||||
return `[Result "${game.headers.result}"]
|
||||
[Title "${game.headers.title}"]
|
||||
[Subtitle "${game.headers.subtitle}"]
|
||||
[Date "${game.headers.date}"]
|
||||
[Author "${game.headers.author}"]\n
|
||||
${moves}`;
|
||||
}
|
||||
|
||||
return `[Event "${game.headers.event}"]
|
||||
[Site "${game.headers.site}"]
|
||||
[Date "${game.headers.date}"]
|
||||
[Round "${game.headers.round}"]
|
||||
[White "${game.headers.white}"]
|
||||
[Black "${game.headers.black}"]
|
||||
[Result "${game.headers.result}"]
|
||||
[ECO "${game.headers.eco}"]
|
||||
[WhiteElo "${game.headers.whiteElo}"]
|
||||
[BlackElo "${game.headers.blackElo}"]
|
||||
[PlyCount "${game.headers.plyCount}"]
|
||||
[EventDate "${game.headers.eventDate}"]
|
||||
[Source "${game.headers.source}"]\n
|
||||
${moves}`;
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
export const downloadString = (string: string, filename: string) => {
|
||||
try {
|
||||
const element = document.createElement("a");
|
||||
const file = new Blob([string], { type: "text/plain" });
|
||||
|
||||
element.href = URL.createObjectURL(file);
|
||||
element.download = filename;
|
||||
document.body.appendChild(element);
|
||||
element.click();
|
||||
} catch (error) {
|
||||
throw new Error("Failed to download file");
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"include": ["**/*.ts", "**/*.tsx"],
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"jsx": "react-jsx",
|
||||
"module": "ESNext",
|
||||
"paths": {
|
||||
"#/*": ["./src/*"],
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"types": ["vite/client"],
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import { devtools } from '@tanstack/devtools-vite'
|
||||
|
||||
import { tanstackStart } from '@tanstack/react-start/plugin/vite'
|
||||
|
||||
import viteReact from '@vitejs/plugin-react'
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
import { nitro } from 'nitro/vite'
|
||||
|
||||
const config = defineConfig({
|
||||
resolve: { tsconfigPaths: true },
|
||||
plugins: [
|
||||
devtools(),
|
||||
nitro({ rollupConfig: { external: [/^@sentry\//] } }),
|
||||
tailwindcss(),
|
||||
tanstackStart(),
|
||||
viteReact(),
|
||||
],
|
||||
})
|
||||
|
||||
export default config
|
||||