chore: initialize project
All checks were successful
Deploy monie-landing (kaniko) / build-and-deploy (push) Successful in 10m34s

This commit is contained in:
2026-04-05 17:16:55 +03:00
commit 9c6f4e5621
39 changed files with 11385 additions and 0 deletions

21
.cta.json Normal file
View File

@@ -0,0 +1,21 @@
{
"projectName": "monie-landing",
"mode": "file-router",
"typescript": true,
"packageManager": "npm",
"includeExamples": false,
"tailwind": true,
"addOnOptions": {},
"envVarValues": {},
"git": false,
"routerOnly": false,
"version": 1,
"framework": "react",
"chosenAddOns": [
"biome",
"paraglide",
"posthog",
"sentry",
"t3env"
]
}

22
.cursorrules Normal file
View File

@@ -0,0 +1,22 @@
We use Sentry for watching for errors in our deployed application, as well as for instrumentation of our application.
## Error collection
Error collection is automatic and configured in `src/router.tsx`.
## Instrumentation
We want our server functions instrumented. So if you see a function name like `createServerFn`, you can instrument it with Sentry. You'll need to import `Sentry`:
```tsx
import * as Sentry from '@sentry/tanstackstart-react'
```
And then wrap the implementation of the server function with `Sentry.startSpan`, like so:
```tsx
Sentry.startSpan({ name: 'Requesting all the pokemon' }, async () => {
// Some lengthy operation here
await fetch('https://api.pokemon.com/data/')
})
```

13
.dockerignore Normal file
View File

@@ -0,0 +1,13 @@
.git
.gitea
node_modules
.output
dist
coverage
.npm
*.log
.env
.env.*
README.md
.codex
.vscode

View File

@@ -0,0 +1,185 @@
name: Deploy monie-landing to dev (kaniko)
on:
push:
branches: [ develop ]
jobs:
build-and-deploy:
runs-on: [self-hosted, linux, k8s]
env:
CI_NS: ci
APP_NS: dev
# Kaniko job runs inside cluster pods and can reach registry via node IP.
PUSH_REGISTRY: 192.168.1.250:32000
# Runtime pull should use the endpoint configured in MicroK8s containerd.
DEPLOY_REGISTRY: localhost:32000
IMAGE: monie-landing
DEPLOYMENT: monie-landing
CONTAINER: monie-landing
# repo без кредов (креды берём из secret внутри Kaniko Job)
REPO_HOST: git.denjs.ru
REPO_PATH: monie/monie-landing.git
steps:
- name: Debug
run: |
set -eu
echo "sha=${{ github.sha }}"
echo "ref=${{ github.ref_name }}"
echo "repo=git://${REPO_HOST}/${REPO_PATH}"
microk8s kubectl version --client=true
- name: Build & push with Kaniko (K8s Job)
env:
SHA: ${{ github.sha }}
REF: ${{ github.ref_name }}
run: |
set -euo pipefail
JOB="kaniko-${SHA}"
DEST="${PUSH_REGISTRY}/${IMAGE}:${SHA}"
echo "JOB=${JOB}"
echo "DEST=${DEST}"
echo "REF=${REF}"
echo "REPO=git://${REPO_HOST}/${REPO_PATH}"
microk8s kubectl -n "${CI_NS}" delete job "${JOB}" --ignore-not-found=true
cat <<EOF_JOB | microk8s kubectl -n "${CI_NS}" apply -f -
apiVersion: batch/v1
kind: Job
metadata:
name: ${JOB}
labels:
app: kaniko
spec:
backoffLimit: 0
template:
spec:
restartPolicy: Never
containers:
- name: kaniko
image: gcr.io/kaniko-project/executor:latest
env:
- name: GIT_USERNAME
value: denis
- name: GIT_PASSWORD
valueFrom:
secretKeyRef:
name: gitea-git-token
key: token
args:
- --context=git://${REPO_HOST}/${REPO_PATH}#refs/heads/${REF}
- --dockerfile=Dockerfile
- --destination=${DEST}
- --verbosity=debug
- --cache=true
- --cache-repo=${PUSH_REGISTRY}/${IMAGE}-cache
- --insecure-registry=${PUSH_REGISTRY}
- --skip-tls-verify-registry=${PUSH_REGISTRY}
ttlSecondsAfterFinished: 3600
EOF_JOB
DEADLINE_SECONDS=1800
START_TS="$(date +%s)"
OK=1
while true; do
SUCCEEDED="$(microk8s kubectl -n "${CI_NS}" get job "${JOB}" -o jsonpath='{.status.succeeded}' 2>/dev/null || true)"
FAILED="$(microk8s kubectl -n "${CI_NS}" get job "${JOB}" -o jsonpath='{.status.failed}' 2>/dev/null || true)"
SUCCEEDED="${SUCCEEDED:-0}"
FAILED="${FAILED:-0}"
if [ "${SUCCEEDED}" -ge 1 ]; then
OK=0
break
fi
if [ "${FAILED}" -ge 1 ]; then
OK=1
break
fi
NOW_TS="$(date +%s)"
if [ $((NOW_TS - START_TS)) -ge "${DEADLINE_SECONDS}" ]; then
OK=2
break
fi
sleep 5
done
echo "[ci] job status:"
microk8s kubectl -n "${CI_NS}" get job "${JOB}" -o wide || true
echo "[ci] job logs (tail):"
microk8s kubectl -n "${CI_NS}" logs "job/${JOB}" --tail=300 || true
if [ "${OK}" -ne 0 ]; then
echo "[ci] job did not reach Complete; describing job/pods for debug"
microk8s kubectl -n "${CI_NS}" describe job "${JOB}" || true
microk8s kubectl -n "${CI_NS}" get pods -l job-name="${JOB}" -o wide || true
microk8s kubectl -n "${CI_NS}" describe pod -l job-name="${JOB}" || true
exit 1
fi
- name: Deploy to dev
env:
SHA: ${{ github.sha }}
run: |
set -euo pipefail
TARGET_IMAGE="${DEPLOY_REGISTRY}/${IMAGE}:${SHA}"
microk8s kubectl -n "${APP_NS}" set image "deployment/${DEPLOYMENT}" \
"${CONTAINER}=${TARGET_IMAGE}"
set +e
microk8s kubectl -n "${APP_NS}" rollout status "deployment/${DEPLOYMENT}" --timeout=15m
ROLLOUT_RC=$?
set -e
if [ "${ROLLOUT_RC}" -ne 0 ]; then
echo "[deploy] rollout did not complete in time; collecting diagnostics"
SELECTOR="$(microk8s kubectl -n "${APP_NS}" get deployment "${DEPLOYMENT}" \
-o jsonpath='{range $k,$v := .spec.selector.matchLabels}{$k}={$v},{end}' 2>/dev/null || true)"
SELECTOR="${SELECTOR%,}"
microk8s kubectl -n "${APP_NS}" get deployment "${DEPLOYMENT}" -o wide || true
microk8s kubectl -n "${APP_NS}" describe deployment "${DEPLOYMENT}" || true
if [ -n "${SELECTOR}" ]; then
microk8s kubectl -n "${APP_NS}" get rs -l "${SELECTOR}" -o wide || true
microk8s kubectl -n "${APP_NS}" get pods -l "${SELECTOR}" -o wide || true
microk8s kubectl -n "${APP_NS}" describe pods -l "${SELECTOR}" || true
fi
microk8s kubectl -n "${APP_NS}" get events --sort-by=.lastTimestamp | tail -n 100 || true
DESIRED="$(microk8s kubectl -n "${APP_NS}" get deployment "${DEPLOYMENT}" \
-o jsonpath='{.spec.replicas}' 2>/dev/null || true)"
UPDATED="$(microk8s kubectl -n "${APP_NS}" get deployment "${DEPLOYMENT}" \
-o jsonpath='{.status.updatedReplicas}' 2>/dev/null || true)"
AVAILABLE="$(microk8s kubectl -n "${APP_NS}" get deployment "${DEPLOYMENT}" \
-o jsonpath='{.status.availableReplicas}' 2>/dev/null || true)"
DESIRED="${DESIRED:-0}"
UPDATED="${UPDATED:-0}"
AVAILABLE="${AVAILABLE:-0}"
echo "[deploy] desired=${DESIRED} updated=${UPDATED} available=${AVAILABLE}"
if [ "${UPDATED}" -ge "${DESIRED}" ] && [ "${AVAILABLE}" -ge "${DESIRED}" ]; then
echo "[deploy] New replica is healthy; old replica termination is delayed. Continuing."
exit 0
fi
exit "${ROLLOUT_RC}"
fi

View File

@@ -0,0 +1,174 @@
# .gitea/workflows/deploy-prod.yml
name: Deploy monie-landing (kaniko)
on:
push:
branches: [ main ]
jobs:
build-and-deploy:
runs-on: [self-hosted, linux, k8s]
env:
CI_NS: ci
APP_NS: prod
# Kaniko job runs inside cluster pods and can reach registry via node IP.
PUSH_REGISTRY: 192.168.1.250:32000
# Runtime pull should use the endpoint configured in MicroK8s containerd.
DEPLOY_REGISTRY: localhost:32000
IMAGE: monie-landing
DEPLOYMENT: monie-landing
CONTAINER: monie-landing
# repo без кредов (креды берём из secret внутри Kaniko Job)
REPO_HOST: git.denjs.ru
REPO_PATH: monie/monie-landing.git
steps:
- name: Build & push image with Kaniko (K8s Job)
env:
SHA: ${{ github.sha }}
REF: ${{ github.ref_name }}
run: |
set -euo pipefail
JOB="kaniko-${SHA}"
DEST="${PUSH_REGISTRY}/${IMAGE}:${SHA}"
kubectl -n "${CI_NS}" delete job "${JOB}" --ignore-not-found=true
cat <<EOF_JOB | kubectl -n "${CI_NS}" apply -f -
apiVersion: batch/v1
kind: Job
metadata:
name: ${JOB}
spec:
backoffLimit: 0
activeDeadlineSeconds: 1800
ttlSecondsAfterFinished: 3600
template:
spec:
restartPolicy: Never
containers:
- name: kaniko
image: gcr.io/kaniko-project/executor:latest
imagePullPolicy: IfNotPresent
env:
- name: GIT_USERNAME
value: denis
- name: GIT_PASSWORD
valueFrom:
secretKeyRef:
name: gitea-git-token
key: token
args:
- --dockerfile=Dockerfile
- --context=git://${REPO_HOST}/${REPO_PATH}#refs/heads/${REF}
- --destination=${DEST}
- --verbosity=debug
- --cache=true
- --cache-repo=${PUSH_REGISTRY}/${IMAGE}-cache
- --insecure-registry=${PUSH_REGISTRY}
- --skip-tls-verify-registry=${PUSH_REGISTRY}
EOF_JOB
# ждём terminal state job и не висим 30 минут при явном Failed
DEADLINE_SECONDS=1800
START_TS="$(date +%s)"
OK=1
while true; do
SUCCEEDED="$(kubectl -n "${CI_NS}" get job "${JOB}" -o jsonpath='{.status.succeeded}' 2>/dev/null || true)"
FAILED="$(kubectl -n "${CI_NS}" get job "${JOB}" -o jsonpath='{.status.failed}' 2>/dev/null || true)"
SUCCEEDED="${SUCCEEDED:-0}"
FAILED="${FAILED:-0}"
if [ "${SUCCEEDED}" -ge 1 ]; then
OK=0
break
fi
if [ "${FAILED}" -ge 1 ]; then
OK=1
break
fi
NOW_TS="$(date +%s)"
if [ $((NOW_TS - START_TS)) -ge "${DEADLINE_SECONDS}" ]; then
OK=2
break
fi
sleep 5
done
echo "[ci] job status:"
kubectl -n "${CI_NS}" get job "${JOB}" -o wide || true
echo "[ci] job logs (tail):"
kubectl -n "${CI_NS}" logs "job/${JOB}" --tail=300 || true
if [ "${OK}" -ne 0 ]; then
echo "[ci] job did not reach Complete; describing job/pods for debug"
kubectl -n "${CI_NS}" describe job "${JOB}" || true
kubectl -n "${CI_NS}" get pods -l job-name="${JOB}" -o wide || true
kubectl -n "${CI_NS}" describe pod -l job-name="${JOB}" || true
exit 1
fi
- name: Deploy to prod
env:
SHA: ${{ github.sha }}
run: |
set -euo pipefail
TARGET_IMAGE="${DEPLOY_REGISTRY}/${IMAGE}:${SHA}"
kubectl -n "${APP_NS}" set image "deployment/${DEPLOYMENT}" \
"${CONTAINER}=${TARGET_IMAGE}"
set +e
kubectl -n "${APP_NS}" rollout status "deployment/${DEPLOYMENT}" --timeout=15m
ROLLOUT_RC=$?
set -e
if [ "${ROLLOUT_RC}" -ne 0 ]; then
echo "[deploy] rollout did not complete in time; collecting diagnostics"
SELECTOR="$(kubectl -n "${APP_NS}" get deployment "${DEPLOYMENT}" \
-o jsonpath='{range $k,$v := .spec.selector.matchLabels}{$k}={$v},{end}' 2>/dev/null || true)"
SELECTOR="${SELECTOR%,}"
kubectl -n "${APP_NS}" get deployment "${DEPLOYMENT}" -o wide || true
kubectl -n "${APP_NS}" describe deployment "${DEPLOYMENT}" || true
if [ -n "${SELECTOR}" ]; then
kubectl -n "${APP_NS}" get rs -l "${SELECTOR}" -o wide || true
kubectl -n "${APP_NS}" get pods -l "${SELECTOR}" -o wide || true
kubectl -n "${APP_NS}" describe pods -l "${SELECTOR}" || true
fi
kubectl -n "${APP_NS}" get events --sort-by=.lastTimestamp | tail -n 100 || true
DESIRED="$(kubectl -n "${APP_NS}" get deployment "${DEPLOYMENT}" \
-o jsonpath='{.spec.replicas}' 2>/dev/null || true)"
UPDATED="$(kubectl -n "${APP_NS}" get deployment "${DEPLOYMENT}" \
-o jsonpath='{.status.updatedReplicas}' 2>/dev/null || true)"
AVAILABLE="$(kubectl -n "${APP_NS}" get deployment "${DEPLOYMENT}" \
-o jsonpath='{.status.availableReplicas}' 2>/dev/null || true)"
DESIRED="${DESIRED:-0}"
UPDATED="${UPDATED:-0}"
AVAILABLE="${AVAILABLE:-0}"
echo "[deploy] desired=${DESIRED} updated=${UPDATED} available=${AVAILABLE}"
if [ "${UPDATED}" -ge "${DESIRED}" ] && [ "${AVAILABLE}" -ge "${DESIRED}" ]; then
echo "[deploy] New replica is healthy; old replica termination is delayed. Continuing."
exit 0
fi
exit "${ROLLOUT_RC}"
fi

15
.gitignore vendored Normal file
View File

@@ -0,0 +1,15 @@
node_modules
.DS_Store
dist
dist-ssr
*.local
.env
.nitro
.tanstack
.wrangler
.output
.vinxi
__unconfig*
todos.json
.codex
.vscode

1
.husky/commit-msg Executable file
View File

@@ -0,0 +1 @@
npx --no -- commitlint --edit "$1"

1
.husky/pre-commit Executable file
View File

@@ -0,0 +1 @@
npm test

19
.husky/pre-push Executable file
View File

@@ -0,0 +1,19 @@
#!/usr/bin/env sh
# branch="$(git rev-parse --abbrev-ref HEAD)"
# case "$branch" in
# main|develop)
# echo "Direct pushes to $branch are not allowed."
# echo "Please create a feature/... or bugfix/... branch and open a PR/MR."
# exit 1
# ;;
# feature/*|bugfix/*|hotfix/*|chore/*)
# exit 0
# ;;
# *)
# echo "Invalid branch name: $branch"
# echo "Allowed branch prefixes: feature/*, bugfix/*, hotfix/*, chore/*"
# exit 1
# ;;
# esac

19
Dockerfile Normal file
View File

@@ -0,0 +1,19 @@
FROM node:24-bookworm-slim AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci
FROM node:24-bookworm-slim AS build
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build && npm prune --omit=dev
FROM node:24-bookworm-slim AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY package*.json ./
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/.output ./.output
EXPOSE 3000
CMD ["npm", "run", "start"]

242
README.md Normal file
View File

@@ -0,0 +1,242 @@
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
```
# Paraglide i18n
This add-on wires up ParaglideJS for localized routing and message formatting.
- Messages live in `project.inlang/messages`.
- URLs are localized through the Paraglide Vite plugin and router `rewrite` hooks.
- Run the dev server or build to regenerate the `src/paraglide` outputs.
## Setting up PostHog
1. Create a PostHog account at [posthog.com](https://posthog.com)
2. Get your Project API Key from [Project Settings](https://app.posthog.com/project/settings)
3. Set `VITE_POSTHOG_KEY` in your `.env.local`
### Optional Configuration
- `VITE_POSTHOG_HOST` - Set this if you're using PostHog Cloud EU (`https://eu.i.posthog.com`) or self-hosting
## T3Env
- You can use T3Env to add type safety to your environment variables.
- Add Environment variables to the `src/env.mjs` file.
- Use the environment variables in your code.
### Usage
```ts
import { env } from "#/env";
console.log(env.VITE_APP_TITLE);
```
## 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).

36
biome.json Normal file
View File

@@ -0,0 +1,36 @@
{
"$schema": "https://biomejs.dev/schemas/2.2.4/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"
}
}
}

3
commitlint.config.mjs Normal file
View File

@@ -0,0 +1,3 @@
export default {
extends: ['@commitlint/config-conventional'],
};

17
instrument.server.mjs Normal file
View File

@@ -0,0 +1,17 @@
import * as Sentry from '@sentry/tanstackstart-react'
const sentryDsn = import.meta.env?.VITE_SENTRY_DSN ?? process.env.VITE_SENTRY_DSN
if (!sentryDsn) {
console.warn('VITE_SENTRY_DSN is not defined. Sentry is not running.')
} else {
Sentry.init({
dsn: sentryDsn,
// Adds request headers and IP for users, for more info visit:
// https://docs.sentry.io/platforms/javascript/guides/tanstackstart-react/configuration/options/#sendDefaultPii
sendDefaultPii: true,
tracesSampleRate: 1.0,
replaysSessionSampleRate: 1.0,
replaysOnErrorSampleRate: 1.0,
})
}

9
messages/de.json Normal file
View File

@@ -0,0 +1,9 @@
{
"$schema": "https://inlang.com/schema/inlang-message-format",
"home_page": "Startseite",
"about_page": "Über uns",
"example_message": "Willkommen in deiner i18n-App.",
"language_label": "Sprache",
"current_locale": "Aktuelle Sprache: {locale}",
"learn_router": "Paraglide JS lernen"
}

9
messages/en.json Normal file
View File

@@ -0,0 +1,9 @@
{
"$schema": "https://inlang.com/schema/inlang-message-format",
"home_page": "Home page",
"about_page": "About page",
"example_message": "Welcome to your i18n app.",
"language_label": "Language",
"current_locale": "Current locale: {locale}",
"learn_router": "Learn Paraglide JS"
}

9558
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

65
package.json Normal file
View File

@@ -0,0 +1,65 @@
{
"name": "monie-landing",
"private": true,
"type": "module",
"imports": {
"#/*": "./src/*"
},
"scripts": {
"dev": "dotenv -e .env.local -- sh -c \"NODE_OPTIONS='--import ./instrument.server.mjs' vite dev --port 3000\"",
"build": "vite build && cp instrument.server.mjs .output/server",
"preview": "vite preview",
"test": "vitest run",
"format": "biome format",
"lint": "biome lint",
"check": "biome check",
"start": "node --import ./.output/server/instrument.server.mjs .output/server/index.mjs",
"prepare": "husky"
},
"dependencies": {
"@posthog/react": "^1.7.0",
"@sentry/tanstackstart-react": "^10.42.0",
"@t3-oss/env-core": "^0.13.10",
"@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",
"dotenv-cli": "^11.0.0",
"lucide-react": "^0.545.0",
"nitro": "^3.0.260311-beta",
"posthog-js": "^1.358.1",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"tailwindcss": "^4.1.18",
"zod": "^4.3.6"
},
"devDependencies": {
"@biomejs/biome": "2.4.5",
"@commitlint/cli": "^20.5.0",
"@commitlint/config-conventional": "^20.5.0",
"@inlang/paraglide-js": "^2.13.1",
"@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": "^5.1.4",
"husky": "^9.1.7",
"jsdom": "^28.1.0",
"typescript": "^5.7.2",
"vite": "^7.3.1",
"vite-tsconfig-paths": "^5.1.4",
"vitest": "^3.0.5"
},
"pnpm": {
"onlyBuiltDependencies": [
"esbuild",
"lightningcss"
]
}
}

View File

@@ -0,0 +1,12 @@
{
"$schema": "https://inlang.com/schema/project-settings",
"baseLocale": "en",
"locales": ["en", "de"],
"modules": [
"https://cdn.jsdelivr.net/npm/@inlang/plugin-message-format@4/dist/index.js",
"https://cdn.jsdelivr.net/npm/@inlang/plugin-m-function-matcher@2/dist/index.js"
],
"plugin.inlang.messageFormat": {
"pathPattern": "./messages/{locale}.json"
}
}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

BIN
public/logo192.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

BIN
public/logo512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

25
public/manifest.json Normal file
View File

@@ -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"
}

3
public/robots.txt Normal file
View File

@@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

23
public/theme-init.js Normal file
View File

@@ -0,0 +1,23 @@
(function () {
try {
var stored = window.localStorage.getItem("theme");
var mode =
stored === "light" || stored === "dark" || stored === "auto"
? stored
: "auto";
var prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
var resolved = mode === "auto" ? (prefersDark ? "dark" : "light") : mode;
var root = document.documentElement;
root.classList.remove("light", "dark");
root.classList.add(resolved);
if (mode === "auto") {
root.removeAttribute("data-theme");
} else {
root.setAttribute("data-theme", mode);
}
root.style.colorScheme = resolved;
} catch {}
})();

44
src/components/Footer.tsx Normal file
View File

@@ -0,0 +1,44 @@
export default function Footer() {
const year = new Date().getFullYear()
return (
<footer className="mt-20 border-t border-[var(--line)] px-4 pb-14 pt-10 text-[var(--sea-ink-soft)]">
<div className="page-wrap flex flex-col items-center justify-between gap-4 text-center sm:flex-row sm:text-left">
<p className="m-0 text-sm">
&copy; {year} Your name here. All rights reserved.
</p>
<p className="island-kicker m-0">Built with TanStack Start</p>
</div>
<div className="mt-4 flex justify-center gap-4">
<a
href="https://x.com/tan_stack"
target="_blank"
rel="noreferrer"
className="rounded-xl p-2 text-[var(--sea-ink-soft)] transition hover:bg-[var(--link-bg-hover)] hover:text-[var(--sea-ink)]"
>
<span className="sr-only">Follow TanStack on X</span>
<svg viewBox="0 0 16 16" aria-hidden="true" width="32" height="32">
<path
fill="currentColor"
d="M12.6 1h2.2L10 6.48 15.64 15h-4.41L7.78 9.82 3.23 15H1l5.14-5.84L.72 1h4.52l3.12 4.73L12.6 1zm-.77 12.67h1.22L4.57 2.26H3.26l8.57 11.41z"
/>
</svg>
</a>
<a
href="https://github.com/TanStack"
target="_blank"
rel="noreferrer"
className="rounded-xl p-2 text-[var(--sea-ink-soft)] transition hover:bg-[var(--link-bg-hover)] hover:text-[var(--sea-ink)]"
>
<span className="sr-only">Go to TanStack GitHub</span>
<svg viewBox="0 0 16 16" aria-hidden="true" width="32" height="32">
<path
fill="currentColor"
d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8z"
/>
</svg>
</a>
</div>
</footer>
)
}

80
src/components/Header.tsx Normal file
View File

@@ -0,0 +1,80 @@
import { Link } from '@tanstack/react-router'
import ParaglideLocaleSwitcher from './LocaleSwitcher.tsx'
import ThemeToggle from './ThemeToggle'
export default function Header() {
return (
<header className="sticky top-0 z-50 border-b border-[var(--line)] bg-[var(--header-bg)] px-4 backdrop-blur-lg">
<nav className="page-wrap flex flex-wrap items-center gap-x-3 gap-y-2 py-3 sm:py-4">
<h2 className="m-0 flex-shrink-0 text-base font-semibold tracking-tight">
<Link
to="/"
className="inline-flex items-center gap-2 rounded-full border border-[var(--chip-line)] bg-[var(--chip-bg)] px-3 py-1.5 text-sm text-[var(--sea-ink)] no-underline shadow-[0_8px_24px_rgba(30,90,72,0.08)] sm:px-4 sm:py-2"
>
<span className="h-2 w-2 rounded-full bg-[linear-gradient(90deg,#56c6be,#7ed3bf)]" />
TanStack Start
</Link>
</h2>
<div className="ml-auto flex items-center gap-1.5 sm:ml-0 sm:gap-2">
<a
href="https://x.com/tan_stack"
target="_blank"
rel="noreferrer"
className="hidden rounded-xl p-2 text-[var(--sea-ink-soft)] transition hover:bg-[var(--link-bg-hover)] hover:text-[var(--sea-ink)] sm:block"
>
<span className="sr-only">Follow TanStack on X</span>
<svg viewBox="0 0 16 16" aria-hidden="true" width="24" height="24">
<path
fill="currentColor"
d="M12.6 1h2.2L10 6.48 15.64 15h-4.41L7.78 9.82 3.23 15H1l5.14-5.84L.72 1h4.52l3.12 4.73L12.6 1zm-.77 12.67h1.22L4.57 2.26H3.26l8.57 11.41z"
/>
</svg>
</a>
<a
href="https://github.com/TanStack"
target="_blank"
rel="noreferrer"
className="hidden rounded-xl p-2 text-[var(--sea-ink-soft)] transition hover:bg-[var(--link-bg-hover)] hover:text-[var(--sea-ink)] sm:block"
>
<span className="sr-only">Go to TanStack GitHub</span>
<svg viewBox="0 0 16 16" aria-hidden="true" width="24" height="24">
<path
fill="currentColor"
d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8z"
/>
</svg>
</a>
<ParaglideLocaleSwitcher />
<ThemeToggle />
</div>
<div className="order-3 flex w-full flex-wrap items-center gap-x-4 gap-y-1 pb-1 text-sm font-semibold sm:order-2 sm:w-auto sm:flex-nowrap sm:pb-0">
<Link
to="/"
className="nav-link"
activeProps={{ className: 'nav-link is-active' }}
>
Home
</Link>
<Link
to="/about"
className="nav-link"
activeProps={{ className: 'nav-link is-active' }}
>
About
</Link>
<a
href="https://tanstack.com/start/latest/docs/framework/react/overview"
className="nav-link"
target="_blank"
rel="noreferrer"
>
Docs
</a>
</div>
</nav>
</header>
)
}

View File

@@ -0,0 +1,46 @@
// Locale switcher refs:
// - Paraglide docs: https://inlang.com/m/gerre34r/library-inlang-paraglideJs
// - Router example: https://github.com/TanStack/router/tree/main/examples/react/i18n-paraglide#switching-locale
import { getLocale, locales, setLocale } from '#/paraglide/runtime'
import { m } from '#/paraglide/messages'
export default function ParaglideLocaleSwitcher() {
const currentLocale = getLocale()
return (
<div
style={{
display: 'flex',
gap: '0.5rem',
alignItems: 'center',
color: 'inherit',
}}
aria-label={m.language_label()}
>
<span style={{ opacity: 0.85 }}>
{m.current_locale({ locale: currentLocale })}
</span>
<div style={{ display: 'flex', gap: '0.25rem' }}>
{locales.map((locale) => (
<button
key={locale}
onClick={() => setLocale(locale)}
aria-pressed={locale === currentLocale}
style={{
cursor: 'pointer',
padding: '0.35rem 0.75rem',
borderRadius: '999px',
border: '1px solid #d1d5db',
background: locale === currentLocale ? '#0f172a' : 'transparent',
color: locale === currentLocale ? '#f8fafc' : 'inherit',
fontWeight: locale === currentLocale ? 700 : 500,
letterSpacing: '0.01em',
}}
>
{locale.toUpperCase()}
</button>
))}
</div>
</div>
)
}

View File

@@ -0,0 +1,81 @@
import { useEffect, useState } from 'react'
type ThemeMode = 'light' | 'dark' | 'auto'
function getInitialMode(): ThemeMode {
if (typeof window === 'undefined') {
return 'auto'
}
const stored = window.localStorage.getItem('theme')
if (stored === 'light' || stored === 'dark' || stored === 'auto') {
return stored
}
return 'auto'
}
function applyThemeMode(mode: ThemeMode) {
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
const resolved = mode === 'auto' ? (prefersDark ? 'dark' : 'light') : mode
document.documentElement.classList.remove('light', 'dark')
document.documentElement.classList.add(resolved)
if (mode === 'auto') {
document.documentElement.removeAttribute('data-theme')
} else {
document.documentElement.setAttribute('data-theme', mode)
}
document.documentElement.style.colorScheme = resolved
}
export default function ThemeToggle() {
const [mode, setMode] = useState<ThemeMode>('auto')
useEffect(() => {
const initialMode = getInitialMode()
setMode(initialMode)
applyThemeMode(initialMode)
}, [])
useEffect(() => {
if (mode !== 'auto') {
return
}
const media = window.matchMedia('(prefers-color-scheme: dark)')
const onChange = () => applyThemeMode('auto')
media.addEventListener('change', onChange)
return () => {
media.removeEventListener('change', onChange)
}
}, [mode])
function toggleMode() {
const nextMode: ThemeMode =
mode === 'light' ? 'dark' : mode === 'dark' ? 'auto' : 'light'
setMode(nextMode)
applyThemeMode(nextMode)
window.localStorage.setItem('theme', nextMode)
}
const label =
mode === 'auto'
? 'Theme mode: auto (system). Click to switch to light mode.'
: `Theme mode: ${mode}. Click to switch mode.`
return (
<button
type="button"
onClick={toggleMode}
aria-label={label}
title={label}
className="rounded-full border border-[var(--chip-line)] bg-[var(--chip-bg)] px-3 py-1.5 text-sm font-semibold text-[var(--sea-ink)] shadow-[0_8px_22px_rgba(30,90,72,0.08)] transition hover:-translate-y-0.5"
>
{mode === 'auto' ? 'Auto' : mode === 'dark' ? 'Dark' : 'Light'}
</button>
)
}

39
src/env.ts Normal file
View File

@@ -0,0 +1,39 @@
import { createEnv } from '@t3-oss/env-core'
import { z } from 'zod'
export const env = createEnv({
server: {
SERVER_URL: z.string().url().optional(),
},
/**
* The prefix that client-side variables must have. This is enforced both at
* a type-level and at runtime.
*/
clientPrefix: 'VITE_',
client: {
VITE_APP_TITLE: z.string().min(1).optional(),
},
/**
* What object holds the environment variables at runtime. This is usually
* `process.env` or `import.meta.env`.
*/
runtimeEnv: import.meta.env,
/**
* By default, this library will feed the environment variables directly to
* the Zod validator.
*
* This means that if you have an empty string for a value that is supposed
* to be a number (e.g. `PORT=` in a ".env" file), Zod will incorrectly flag
* it as a type mismatch violation. Additionally, if you have an empty string
* for a value that is supposed to be a string with a default value (e.g.
* `DOMAIN=` in an ".env" file), the default value will never be applied.
*
* In order to solve these issues, we recommend that all new projects
* explicitly specify this option as true.
*/
emptyStringAsUndefined: true,
})

View File

@@ -0,0 +1,20 @@
import posthog from 'posthog-js'
import { PostHogProvider as BasePostHogProvider } from '@posthog/react'
import type { ReactNode } from 'react'
if (typeof window !== 'undefined' && import.meta.env.VITE_POSTHOG_KEY) {
posthog.init(import.meta.env.VITE_POSTHOG_KEY, {
api_host: import.meta.env.VITE_POSTHOG_HOST || 'https://us.i.posthog.com',
person_profiles: 'identified_only',
capture_pageview: false,
defaults: '2025-11-30',
})
}
interface PostHogProviderProps {
children: ReactNode
}
export default function PostHogProvider({ children }: PostHogProviderProps) {
return <BasePostHogProvider client={posthog}>{children}</BasePostHogProvider>
}

86
src/routeTree.gen.ts Normal file
View File

@@ -0,0 +1,86 @@
/* 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 AboutRouteImport } from './routes/about'
import { Route as IndexRouteImport } from './routes/index'
const AboutRoute = AboutRouteImport.update({
id: '/about',
path: '/about',
getParentRoute: () => rootRouteImport,
} as any)
const IndexRoute = IndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => rootRouteImport,
} as any)
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/about': typeof AboutRoute
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
'/about': typeof AboutRoute
}
export interface FileRoutesById {
__root__: typeof rootRouteImport
'/': typeof IndexRoute
'/about': typeof AboutRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fullPaths: '/' | '/about'
fileRoutesByTo: FileRoutesByTo
to: '/' | '/about'
id: '__root__' | '/' | '/about'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
AboutRoute: typeof AboutRoute
}
declare module '@tanstack/react-router' {
interface FileRoutesByPath {
'/about': {
id: '/about'
path: '/about'
fullPath: '/about'
preLoaderRoute: typeof AboutRouteImport
parentRoute: typeof rootRouteImport
}
'/': {
id: '/'
path: '/'
fullPath: '/'
preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport
}
}
}
const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
AboutRoute: AboutRoute,
}
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>>
}
}

19
src/router.tsx Normal file
View File

@@ -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>
}
}

73
src/routes/__root.tsx Normal file
View File

@@ -0,0 +1,73 @@
import { TanStackDevtools } from "@tanstack/react-devtools";
import { createRootRoute, HeadContent, Scripts } from "@tanstack/react-router";
import { TanStackRouterDevtoolsPanel } from "@tanstack/react-router-devtools";
import { getLocale } from "#/paraglide/runtime";
import Footer from "../components/Footer";
import Header from "../components/Header";
import PostHogProvider from "../integrations/posthog/provider";
import appCss from "../styles.css?url";
const THEME_INIT_SCRIPT = `(function(){try{var stored=window.localStorage.getItem('theme');var mode=(stored==='light'||stored==='dark'||stored==='auto')?stored:'auto';var prefersDark=window.matchMedia('(prefers-color-scheme: dark)').matches;var resolved=mode==='auto'?(prefersDark?'dark':'light'):mode;var root=document.documentElement;root.classList.remove('light','dark');root.classList.add(resolved);if(mode==='auto'){root.removeAttribute('data-theme')}else{root.setAttribute('data-theme',mode)}root.style.colorScheme=resolved;}catch(e){}})();`;
export const Route = createRootRoute({
beforeLoad: async () => {
// Other redirect strategies are possible; see
// https://github.com/TanStack/router/tree/main/examples/react/i18n-paraglide#offline-redirect
if (typeof document !== "undefined") {
document.documentElement.setAttribute("lang", getLocale());
}
},
head: () => ({
meta: [
{
charSet: "utf-8",
},
{
name: "viewport",
content: "width=device-width, initial-scale=1",
},
{
title: "TanStack Start Starter",
},
],
links: [
{
rel: "stylesheet",
href: appCss,
},
],
}),
shellComponent: RootDocument,
});
function RootDocument({ children }: { children: React.ReactNode }) {
return (
<html lang={getLocale()} suppressHydrationWarning>
<head>
<script src="/theme-init.js" />
<HeadContent />
</head>
<body className="font-sans antialiased [overflow-wrap:anywhere] selection:bg-[rgba(79,184,178,0.24)]">
<PostHogProvider>
<Header />
{children}
<Footer />
<TanStackDevtools
config={{
position: "bottom-right",
}}
plugins={[
{
name: "Tanstack Router",
render: <TanStackRouterDevtoolsPanel />,
},
]}
/>
</PostHogProvider>
<Scripts />
</body>
</html>
);
}

23
src/routes/about.tsx Normal file
View File

@@ -0,0 +1,23 @@
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/about')({
component: About,
})
function About() {
return (
<main className="page-wrap px-4 py-12">
<section className="island-shell rounded-2xl p-6 sm:p-8">
<p className="island-kicker mb-2">About</p>
<h1 className="display-title mb-3 text-4xl font-bold text-[var(--sea-ink)] sm:text-5xl">
A small starter with room to grow.
</h1>
<p className="m-0 max-w-3xl text-base leading-8 text-[var(--sea-ink-soft)]">
TanStack Start gives you type-safe routing, server functions, and
modern SSR defaults. Use this as a clean foundation, then layer in
your own routes, styling, and add-ons.
</p>
</section>
</main>
)
}

87
src/routes/index.tsx Normal file
View File

@@ -0,0 +1,87 @@
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/')({ component: App })
function App() {
return (
<main className="page-wrap px-4 pb-8 pt-14">
<section className="island-shell rise-in relative overflow-hidden rounded-[2rem] px-6 py-10 sm:px-10 sm:py-14">
<div className="pointer-events-none absolute -left-20 -top-24 h-56 w-56 rounded-full bg-[radial-gradient(circle,rgba(79,184,178,0.32),transparent_66%)]" />
<div className="pointer-events-none absolute -bottom-20 -right-20 h-56 w-56 rounded-full bg-[radial-gradient(circle,rgba(47,106,74,0.18),transparent_66%)]" />
<p className="island-kicker mb-3">TanStack Start Base Template</p>
<h1 className="display-title mb-5 max-w-3xl text-4xl leading-[1.02] font-bold tracking-tight text-[var(--sea-ink)] sm:text-6xl">
Start simple, ship quickly.
</h1>
<p className="mb-8 max-w-2xl text-base text-[var(--sea-ink-soft)] sm:text-lg">
This base starter intentionally keeps things light: two routes, clean
structure, and the essentials you need to build from scratch.
</p>
<div className="flex flex-wrap gap-3">
<a
href="/about"
className="rounded-full border border-[rgba(50,143,151,0.3)] bg-[rgba(79,184,178,0.14)] px-5 py-2.5 text-sm font-semibold text-[var(--lagoon-deep)] no-underline transition hover:-translate-y-0.5 hover:bg-[rgba(79,184,178,0.24)]"
>
About This Starter
</a>
<a
href="https://tanstack.com/router"
target="_blank"
rel="noopener noreferrer"
className="rounded-full border border-[rgba(23,58,64,0.2)] bg-white/50 px-5 py-2.5 text-sm font-semibold text-[var(--sea-ink)] no-underline transition hover:-translate-y-0.5 hover:border-[rgba(23,58,64,0.35)]"
>
Router Guide
</a>
</div>
</section>
<section className="mt-8 grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
{[
[
'Type-Safe Routing',
'Routes and links stay in sync across every page.',
],
[
'Server Functions',
'Call server code from your UI without creating API boilerplate.',
],
[
'Streaming by Default',
'Ship progressively rendered responses for faster experiences.',
],
[
'Tailwind Native',
'Design quickly with utility-first styling and reusable tokens.',
],
].map(([title, desc], index) => (
<article
key={title}
className="island-shell feature-card rise-in rounded-2xl p-5"
style={{ animationDelay: `${index * 90 + 80}ms` }}
>
<h2 className="mb-2 text-base font-semibold text-[var(--sea-ink)]">
{title}
</h2>
<p className="m-0 text-sm text-[var(--sea-ink-soft)]">{desc}</p>
</article>
))}
</section>
<section className="island-shell mt-8 rounded-2xl p-6">
<p className="island-kicker mb-2">Quick Start</p>
<ul className="m-0 list-disc space-y-2 pl-5 text-sm text-[var(--sea-ink-soft)]">
<li>
Edit <code>src/routes/index.tsx</code> to customize the home page.
</li>
<li>
Update <code>src/components/Header.tsx</code> and{' '}
<code>src/components/Footer.tsx</code> for brand links.
</li>
<li>
Add routes in <code>src/routes</code> and tweak visual tokens in{' '}
<code>src/styles.css</code>.
</li>
</ul>
</section>
</main>
)
}

259
src/styles.css Normal file
View File

@@ -0,0 +1,259 @@
@import url("https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,500;9..144,700&family=Manrope:wght@400;500;600;700;800&display=swap");
@import "tailwindcss";
@plugin "@tailwindcss/typography";
@theme {
--font-sans: "Manrope", ui-sans-serif, system-ui, sans-serif;
}
:root {
--sea-ink: #173a40;
--sea-ink-soft: #416166;
--lagoon: #4fb8b2;
--lagoon-deep: #328f97;
--palm: #2f6a4a;
--sand: #e7f0e8;
--foam: #f3faf5;
--surface: rgba(255, 255, 255, 0.74);
--surface-strong: rgba(255, 255, 255, 0.9);
--line: rgba(23, 58, 64, 0.14);
--inset-glint: rgba(255, 255, 255, 0.82);
--kicker: rgba(47, 106, 74, 0.9);
--bg-base: #e7f3ec;
--header-bg: rgba(251, 255, 248, 0.84);
--chip-bg: rgba(255, 255, 255, 0.8);
--chip-line: rgba(47, 106, 74, 0.18);
--link-bg-hover: rgba(255, 255, 255, 0.9);
--hero-a: rgba(79, 184, 178, 0.36);
--hero-b: rgba(47, 106, 74, 0.2);
}
:root[data-theme="dark"] {
--sea-ink: #d7ece8;
--sea-ink-soft: #afcdc8;
--lagoon: #60d7cf;
--lagoon-deep: #8de5db;
--palm: #6ec89a;
--sand: #0f1a1e;
--foam: #101d22;
--surface: rgba(16, 30, 34, 0.8);
--surface-strong: rgba(15, 27, 31, 0.92);
--line: rgba(141, 229, 219, 0.18);
--inset-glint: rgba(194, 247, 238, 0.14);
--kicker: #b8efe5;
--bg-base: #0a1418;
--header-bg: rgba(10, 20, 24, 0.8);
--chip-bg: rgba(13, 28, 32, 0.9);
--chip-line: rgba(141, 229, 219, 0.24);
--link-bg-hover: rgba(24, 44, 49, 0.8);
--hero-a: rgba(96, 215, 207, 0.18);
--hero-b: rgba(110, 200, 154, 0.12);
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--sea-ink: #d7ece8;
--sea-ink-soft: #afcdc8;
--lagoon: #60d7cf;
--lagoon-deep: #8de5db;
--palm: #6ec89a;
--sand: #0f1a1e;
--foam: #101d22;
--surface: rgba(16, 30, 34, 0.8);
--surface-strong: rgba(15, 27, 31, 0.92);
--line: rgba(141, 229, 219, 0.18);
--inset-glint: rgba(194, 247, 238, 0.14);
--kicker: #b8efe5;
--bg-base: #0a1418;
--header-bg: rgba(10, 20, 24, 0.8);
--chip-bg: rgba(13, 28, 32, 0.9);
--chip-line: rgba(141, 229, 219, 0.24);
--link-bg-hover: rgba(24, 44, 49, 0.8);
--hero-a: rgba(96, 215, 207, 0.18);
--hero-b: rgba(110, 200, 154, 0.12);
}
}
* {
box-sizing: border-box;
}
html,
body,
#app {
min-height: 100%;
}
body {
margin: 0;
color: var(--sea-ink);
font-family: var(--font-sans);
background-color: var(--bg-base);
background:
radial-gradient(1100px 620px at -8% -10%, var(--hero-a), transparent 58%),
radial-gradient(1050px 620px at 112% -12%, var(--hero-b), transparent 62%),
radial-gradient(720px 380px at 50% 115%, rgba(79, 184, 178, 0.1), transparent 68%),
linear-gradient(180deg, color-mix(in oklab, var(--sand) 68%, white) 0%, var(--foam) 44%, var(--bg-base) 100%);
overflow-x: hidden;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body::before {
content: "";
position: fixed;
inset: 0;
pointer-events: none;
z-index: -1;
opacity: 0.28;
background:
radial-gradient(circle at 20% 15%, rgba(255, 255, 255, 0.8), transparent 34%),
radial-gradient(circle at 78% 26%, rgba(79, 184, 178, 0.2), transparent 42%),
radial-gradient(circle at 42% 82%, rgba(47, 106, 74, 0.14), transparent 36%);
}
body::after {
content: "";
position: fixed;
inset: 0;
pointer-events: none;
z-index: -1;
opacity: 0.14;
background-image:
linear-gradient(rgba(255, 255, 255, 0.07) 1px, transparent 1px),
linear-gradient(90deg, rgba(255, 255, 255, 0.06) 1px, transparent 1px);
background-size: 28px 28px;
mask-image: radial-gradient(circle at 50% 30%, black, transparent 78%);
}
a {
color: var(--lagoon-deep);
text-decoration-color: rgba(50, 143, 151, 0.4);
text-decoration-thickness: 1px;
text-underline-offset: 2px;
}
a:hover {
color: #246f76;
}
code {
font-size: 0.9em;
border: 1px solid var(--line);
background: color-mix(in oklab, var(--surface-strong) 82%, white 18%);
border-radius: 7px;
padding: 2px 7px;
}
pre code {
border: 0;
background: transparent;
padding: 0;
border-radius: 0;
font-size: inherit;
color: inherit;
}
.page-wrap {
width: min(1080px, calc(100% - 2rem));
margin-inline: auto;
}
.display-title {
font-family: "Fraunces", Georgia, serif;
}
.island-shell {
border: 1px solid var(--line);
background: linear-gradient(165deg, var(--surface-strong), var(--surface));
box-shadow:
0 1px 0 var(--inset-glint) inset,
0 22px 44px rgba(30, 90, 72, 0.1),
0 6px 18px rgba(23, 58, 64, 0.08);
backdrop-filter: blur(4px);
}
.feature-card {
background: linear-gradient(165deg, color-mix(in oklab, var(--surface-strong) 93%, white 7%), var(--surface));
box-shadow:
0 1px 0 var(--inset-glint) inset,
0 18px 34px rgba(30, 90, 72, 0.1),
0 4px 14px rgba(23, 58, 64, 0.06);
}
.feature-card:hover {
transform: translateY(-2px);
border-color: color-mix(in oklab, var(--lagoon-deep) 35%, var(--line));
}
button,
.island-shell,
a {
transition: background-color 180ms ease, color 180ms ease, border-color 180ms ease,
transform 180ms ease;
}
.island-kicker {
letter-spacing: 0.16em;
text-transform: uppercase;
font-weight: 700;
font-size: 0.69rem;
color: var(--kicker);
}
.nav-link {
position: relative;
display: inline-flex;
align-items: center;
text-decoration: none;
color: var(--sea-ink-soft);
}
.nav-link::after {
content: "";
position: absolute;
left: 0;
bottom: -6px;
width: 100%;
height: 2px;
transform: scaleX(0);
transform-origin: left;
background: linear-gradient(90deg, var(--lagoon), #7ed3bf);
transition: transform 170ms ease;
}
.nav-link:hover,
.nav-link.is-active {
color: var(--sea-ink);
}
.nav-link:hover::after,
.nav-link.is-active::after {
transform: scaleX(1);
}
@media (max-width: 640px) {
.nav-link::after {
bottom: -4px;
}
}
.site-footer {
border-top: 1px solid var(--line);
background: color-mix(in oklab, var(--header-bg) 84%, transparent 16%);
}
.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);
}
}

30
tsconfig.json Normal file
View File

@@ -0,0 +1,30 @@
{
"include": ["**/*.ts", "**/*.tsx"],
"compilerOptions": {
"target": "ES2022",
"jsx": "react-jsx",
"module": "ESNext",
"baseUrl": ".",
"paths": {
"#/*": ["./src/*"],
"@/*": ["./src/*"]
},
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"types": ["vite/client"],
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"allowJs": true,
"verbatimModuleSyntax": true,
"noEmit": true,
/* Linting */
"skipLibCheck": true,
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
}
}

26
vite.config.ts Normal file
View File

@@ -0,0 +1,26 @@
import { paraglideVitePlugin } from "@inlang/paraglide-js";
import tailwindcss from "@tailwindcss/vite";
import { devtools } from "@tanstack/devtools-vite";
import { tanstackStart } from "@tanstack/react-start/plugin/vite";
import viteReact from "@vitejs/plugin-react";
import { nitro } from "nitro/vite";
import { defineConfig } from "vite";
import tsconfigPaths from "vite-tsconfig-paths";
const config = defineConfig({
plugins: [
devtools(),
paraglideVitePlugin({
project: "./project.inlang",
outdir: "./src/paraglide",
strategy: ["url", "baseLocale"],
}),
tsconfigPaths({ projects: ["./tsconfig.json"] }),
tailwindcss(),
tanstackStart(),
viteReact(),
nitro(),
],
});
export default config;