feat(app): prepare Vercel deployment
Add production metadata, social assets, Vercel observability, repository links, and final navigation and drawer polish.
This commit is contained in:
parent
94bc5223e6
commit
3c5f5cb41e
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@ -24,7 +24,7 @@ jobs:
|
|||||||
node-version: '22.12.0'
|
node-version: '22.12.0'
|
||||||
|
|
||||||
- name: Install pnpm
|
- name: Install pnpm
|
||||||
run: npm install --global pnpm@11.21.0
|
run: npm install --global pnpm@10.34.5
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --frozen-lockfile
|
||||||
|
|||||||
@ -4,6 +4,7 @@ legacy/
|
|||||||
LICENSE
|
LICENSE
|
||||||
public/favicon.svg
|
public/favicon.svg
|
||||||
public/robots.txt
|
public/robots.txt
|
||||||
|
public/sitemap.xml
|
||||||
src/features/studio/defaultStudioDocument.json
|
src/features/studio/defaultStudioDocument.json
|
||||||
src/features/export/standaloneEngine.generated.ts
|
src/features/export/standaloneEngine.generated.ts
|
||||||
pnpm-lock.yaml
|
pnpm-lock.yaml
|
||||||
|
|||||||
@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
Bible Strong Avatar Lab is a browser-based authoring studio for procedural 2D avatars. It combines 3D-inspired geometry with SVG rendering so you can construct a character, define its neutral appearance, create expressions, compose reusable animations, and export the result without depending on the Studio UI.
|
Bible Strong Avatar Lab is a browser-based authoring studio for procedural 2D avatars. It combines 3D-inspired geometry with SVG rendering so you can construct a character, define its neutral appearance, create expressions, compose reusable animations, and export the result without depending on the Studio UI.
|
||||||
|
|
||||||
|
Website: [bible-strong.app](https://bible-strong.app) · Source: [GitHub](https://github.com/smontlouis/bible-strong-avatar-lab)
|
||||||
|
|
||||||
The application runs entirely in the browser. Projects are stored locally and can be moved between browsers with JSON export/import; no account or backend is required.
|
The application runs entirely in the browser. Projects are stored locally and can be moved between browsers with JSON export/import; no account or backend is required.
|
||||||
|
|
||||||
## What you can do
|
## What you can do
|
||||||
@ -64,7 +66,7 @@ The JSON project file contains the complete current document: avatars, base and
|
|||||||
### Requirements
|
### Requirements
|
||||||
|
|
||||||
- Node.js 22.12 or newer
|
- Node.js 22.12 or newer
|
||||||
- pnpm 11.21 (declared by the repository's `packageManager` field)
|
- pnpm 10.34 (declared by the repository's `packageManager` field)
|
||||||
|
|
||||||
### Install and run
|
### Install and run
|
||||||
|
|
||||||
@ -116,6 +118,8 @@ pnpm preview
|
|||||||
|
|
||||||
The application is a client-only Vite site with no server runtime or environment variables. The build uses relative asset paths, so the contents of `dist/` can be hosted at a domain root or a subpath, including a GitHub Pages project URL. Any static host that serves `index.html` can deploy it.
|
The application is a client-only Vite site with no server runtime or environment variables. The build uses relative asset paths, so the contents of `dist/` can be hosted at a domain root or a subpath, including a GitHub Pages project URL. Any static host that serves `index.html` can deploy it.
|
||||||
|
|
||||||
|
Vercel Web Analytics and Speed Insights are integrated in the React entry point. Enable both products in the Vercel project dashboard, then redeploy so their collection routes become available.
|
||||||
|
|
||||||
## Technical overview
|
## Technical overview
|
||||||
|
|
||||||
- **React 19** coordinates the editor UI and durable application state.
|
- **React 19** coordinates the editor UI and durable application state.
|
||||||
|
|||||||
18
index.html
18
index.html
@ -10,20 +10,36 @@
|
|||||||
content="Create procedural 2D avatars, expressions and animations in your browser, then export reusable React, JavaScript, SVG and PNG assets."
|
content="Create procedural 2D avatars, expressions and animations in your browser, then export reusable React, JavaScript, SVG and PNG assets."
|
||||||
/>
|
/>
|
||||||
<meta name="application-name" content="Bible Strong Avatar Lab" />
|
<meta name="application-name" content="Bible Strong Avatar Lab" />
|
||||||
|
<meta name="author" content="Stéphane Montlouis-Calixte" />
|
||||||
<meta name="robots" content="index, follow" />
|
<meta name="robots" content="index, follow" />
|
||||||
<meta property="og:type" content="website" />
|
<meta property="og:type" content="website" />
|
||||||
<meta property="og:locale" content="en_US" />
|
<meta property="og:locale" content="en_US" />
|
||||||
|
<meta property="og:url" content="https://bible-strong.app/" />
|
||||||
|
<meta property="og:site_name" content="Bible Strong Avatar Lab" />
|
||||||
<meta property="og:title" content="Bible Strong Avatar Lab — Procedural 2D Avatar Studio" />
|
<meta property="og:title" content="Bible Strong Avatar Lab — Procedural 2D Avatar Studio" />
|
||||||
<meta
|
<meta
|
||||||
property="og:description"
|
property="og:description"
|
||||||
content="Design procedural 2D avatars, expressions and reusable animations directly in your browser."
|
content="Design procedural 2D avatars, expressions and reusable animations directly in your browser."
|
||||||
/>
|
/>
|
||||||
<meta name="twitter:card" content="summary" />
|
<meta property="og:image" content="https://bible-strong.app/og-image.png" />
|
||||||
|
<meta property="og:image:width" content="1200" />
|
||||||
|
<meta property="og:image:height" content="630" />
|
||||||
|
<meta
|
||||||
|
property="og:image:alt"
|
||||||
|
content="Bible Strong Avatar Lab with the blue Strobi procedural avatar"
|
||||||
|
/>
|
||||||
|
<meta name="twitter:card" content="summary_large_image" />
|
||||||
<meta name="twitter:title" content="Bible Strong Avatar Lab" />
|
<meta name="twitter:title" content="Bible Strong Avatar Lab" />
|
||||||
<meta
|
<meta
|
||||||
name="twitter:description"
|
name="twitter:description"
|
||||||
content="Design procedural 2D avatars, expressions and reusable animations directly in your browser."
|
content="Design procedural 2D avatars, expressions and reusable animations directly in your browser."
|
||||||
/>
|
/>
|
||||||
|
<meta name="twitter:image" content="https://bible-strong.app/og-image.png" />
|
||||||
|
<meta
|
||||||
|
name="twitter:image:alt"
|
||||||
|
content="Bible Strong Avatar Lab with the blue Strobi procedural avatar"
|
||||||
|
/>
|
||||||
|
<link rel="canonical" href="https://bible-strong.app/" />
|
||||||
<link rel="icon" href="./favicon.svg" type="image/svg+xml" />
|
<link rel="icon" href="./favicon.svg" type="image/svg+xml" />
|
||||||
<link rel="apple-touch-icon" href="./apple-touch-icon.png" />
|
<link rel="apple-touch-icon" href="./apple-touch-icon.png" />
|
||||||
<link rel="manifest" href="./site.webmanifest" />
|
<link rel="manifest" href="./site.webmanifest" />
|
||||||
|
|||||||
13
package.json
13
package.json
@ -2,10 +2,19 @@
|
|||||||
"name": "bible-strong-avatar-lab",
|
"name": "bible-strong-avatar-lab",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"description": "A browser-based studio for designing procedural 2D avatars, expressions and reusable animations.",
|
"description": "A browser-based studio for designing procedural 2D avatars, expressions and reusable animations.",
|
||||||
|
"author": "Stéphane Montlouis-Calixte",
|
||||||
|
"homepage": "https://bible-strong.app",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/smontlouis/bible-strong-avatar-lab.git"
|
||||||
|
},
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/smontlouis/bible-strong-avatar-lab/issues"
|
||||||
|
},
|
||||||
"license": "AGPL-3.0-only",
|
"license": "AGPL-3.0-only",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"packageManager": "pnpm@11.21.0",
|
"packageManager": "pnpm@10.34.5",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=22.12.0"
|
"node": ">=22.12.0"
|
||||||
},
|
},
|
||||||
@ -24,6 +33,8 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@base-ui/react": "^1.7.0",
|
"@base-ui/react": "^1.7.0",
|
||||||
|
"@vercel/analytics": "^2.0.1",
|
||||||
|
"@vercel/speed-insights": "^2.0.0",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"lucide-react": "^1.31.0",
|
"lucide-react": "^1.31.0",
|
||||||
|
|||||||
69
pnpm-lock.yaml
generated
69
pnpm-lock.yaml
generated
@ -11,6 +11,12 @@ importers:
|
|||||||
'@base-ui/react':
|
'@base-ui/react':
|
||||||
specifier: ^1.7.0
|
specifier: ^1.7.0
|
||||||
version: 1.7.0(@types/react@19.2.18)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
version: 1.7.0(@types/react@19.2.18)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||||
|
'@vercel/analytics':
|
||||||
|
specifier: ^2.0.1
|
||||||
|
version: 2.0.1(react@19.2.3)
|
||||||
|
'@vercel/speed-insights':
|
||||||
|
specifier: ^2.0.0
|
||||||
|
version: 2.0.0(react@19.2.3)
|
||||||
class-variance-authority:
|
class-variance-authority:
|
||||||
specifier: ^0.7.1
|
specifier: ^0.7.1
|
||||||
version: 0.7.1
|
version: 0.7.1
|
||||||
@ -440,6 +446,61 @@ packages:
|
|||||||
'@types/react@19.2.18':
|
'@types/react@19.2.18':
|
||||||
resolution: {integrity: sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==}
|
resolution: {integrity: sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==}
|
||||||
|
|
||||||
|
'@vercel/analytics@2.0.1':
|
||||||
|
resolution: {integrity: sha512-MTQG6V9qQrt1tsDeF+2Uoo5aPjqbVPys1xvnIftXSJYG2SrwXRHnqEvVoYID7BTruDz4lCd2Z7rM1BdkUehk2g==}
|
||||||
|
peerDependencies:
|
||||||
|
'@remix-run/react': ^2
|
||||||
|
'@sveltejs/kit': ^1 || ^2
|
||||||
|
next: '>= 13'
|
||||||
|
nuxt: '>= 3'
|
||||||
|
react: ^18 || ^19 || ^19.0.0-rc
|
||||||
|
svelte: '>= 4'
|
||||||
|
vue: ^3
|
||||||
|
vue-router: ^4
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@remix-run/react':
|
||||||
|
optional: true
|
||||||
|
'@sveltejs/kit':
|
||||||
|
optional: true
|
||||||
|
next:
|
||||||
|
optional: true
|
||||||
|
nuxt:
|
||||||
|
optional: true
|
||||||
|
react:
|
||||||
|
optional: true
|
||||||
|
svelte:
|
||||||
|
optional: true
|
||||||
|
vue:
|
||||||
|
optional: true
|
||||||
|
vue-router:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@vercel/speed-insights@2.0.0':
|
||||||
|
resolution: {integrity: sha512-jwkNcrTeafWxjmWq4AHBaptSqZiJkYU5adLC9QBSqeim0GcqDMgN5Ievh8OG1rJ6W3A4l1oiP7qr9CWxGuzu3w==}
|
||||||
|
peerDependencies:
|
||||||
|
'@sveltejs/kit': ^1 || ^2
|
||||||
|
next: '>= 13'
|
||||||
|
nuxt: '>= 3'
|
||||||
|
react: ^18 || ^19 || ^19.0.0-rc
|
||||||
|
svelte: '>= 4'
|
||||||
|
vue: ^3
|
||||||
|
vue-router: ^4
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@sveltejs/kit':
|
||||||
|
optional: true
|
||||||
|
next:
|
||||||
|
optional: true
|
||||||
|
nuxt:
|
||||||
|
optional: true
|
||||||
|
react:
|
||||||
|
optional: true
|
||||||
|
svelte:
|
||||||
|
optional: true
|
||||||
|
vue:
|
||||||
|
optional: true
|
||||||
|
vue-router:
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@vitejs/plugin-react@6.0.5':
|
'@vitejs/plugin-react@6.0.5':
|
||||||
resolution: {integrity: sha512-BOVzne/NL162sMdResB25mUv+vWMF5NoAjNf09TeGlE7ZpszZWSD3winycicLJw72yeVsoCn/2kOhEuCvEShMA==}
|
resolution: {integrity: sha512-BOVzne/NL162sMdResB25mUv+vWMF5NoAjNf09TeGlE7ZpszZWSD3winycicLJw72yeVsoCn/2kOhEuCvEShMA==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
@ -1298,6 +1359,14 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
csstype: 3.2.3
|
csstype: 3.2.3
|
||||||
|
|
||||||
|
'@vercel/analytics@2.0.1(react@19.2.3)':
|
||||||
|
optionalDependencies:
|
||||||
|
react: 19.2.3
|
||||||
|
|
||||||
|
'@vercel/speed-insights@2.0.0(react@19.2.3)':
|
||||||
|
optionalDependencies:
|
||||||
|
react: 19.2.3
|
||||||
|
|
||||||
'@vitejs/plugin-react@6.0.5(@rolldown/plugin-babel@0.2.3(@babel/core@7.29.7)(@babel/runtime@7.29.7)(rolldown@1.2.4)(vite@8.2.1(@types/node@24.13.3)(jiti@2.7.0)))(babel-plugin-react-compiler@1.0.0)(vite@8.2.1(@types/node@24.13.3)(jiti@2.7.0))':
|
'@vitejs/plugin-react@6.0.5(@rolldown/plugin-babel@0.2.3(@babel/core@7.29.7)(@babel/runtime@7.29.7)(rolldown@1.2.4)(vite@8.2.1(@types/node@24.13.3)(jiti@2.7.0)))(babel-plugin-react-compiler@1.0.0)(vite@8.2.1(@types/node@24.13.3)(jiti@2.7.0))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@rolldown/pluginutils': 1.0.1
|
'@rolldown/pluginutils': 1.0.1
|
||||||
|
|||||||
BIN
public/og-image.png
Normal file
BIN
public/og-image.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 749 KiB |
@ -1,2 +1,4 @@
|
|||||||
User-agent: *
|
User-agent: *
|
||||||
Allow: /
|
Allow: /
|
||||||
|
|
||||||
|
Sitemap: https://bible-strong.app/sitemap.xml
|
||||||
|
|||||||
6
public/sitemap.xml
Normal file
6
public/sitemap.xml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||||
|
<url>
|
||||||
|
<loc>https://bible-strong.app/</loc>
|
||||||
|
</url>
|
||||||
|
</urlset>
|
||||||
@ -222,6 +222,32 @@ input {
|
|||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
.source-link {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
color: #f6f7f8;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 750;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.source-link:hover {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.source-link:focus-visible {
|
||||||
|
border-radius: 5px;
|
||||||
|
outline: 2px solid #7895ee;
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
.source-link svg {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
}
|
||||||
|
.identity-divider {
|
||||||
|
align-self: stretch;
|
||||||
|
width: 1px;
|
||||||
|
background: rgb(255 255 255 / 14%);
|
||||||
|
}
|
||||||
.language-picker [data-slot='select-trigger'] {
|
.language-picker [data-slot='select-trigger'] {
|
||||||
width: auto;
|
width: auto;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
@ -1317,7 +1343,7 @@ p {
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 3px;
|
gap: 3px;
|
||||||
border-radius: 12px;
|
border-radius: 999px;
|
||||||
color: #7b828e;
|
color: #7b828e;
|
||||||
font-size: 9px;
|
font-size: 9px;
|
||||||
font-weight: 750;
|
font-weight: 750;
|
||||||
@ -2386,6 +2412,9 @@ p {
|
|||||||
.stage-identity .language-picker [data-slot='select-trigger'] {
|
.stage-identity .language-picker [data-slot='select-trigger'] {
|
||||||
min-width: 68px;
|
min-width: 68px;
|
||||||
}
|
}
|
||||||
|
.stage-identity .source-link > span {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
.avatar-wrap {
|
.avatar-wrap {
|
||||||
width: min(42dvh, 78vw, 340px);
|
width: min(42dvh, 78vw, 340px);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { Copy, Pencil, Plus, Trash2 } from 'lucide-react'
|
import { Copy, Pencil, Plus, Trash2 } from 'lucide-react'
|
||||||
import { motion } from 'motion/react'
|
import { motion, useDragControls } from 'motion/react'
|
||||||
|
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import {
|
import {
|
||||||
@ -9,7 +9,7 @@ import {
|
|||||||
ContextMenuSeparator,
|
ContextMenuSeparator,
|
||||||
ContextMenuTrigger,
|
ContextMenuTrigger,
|
||||||
} from '@/components/ui/context-menu'
|
} from '@/components/ui/context-menu'
|
||||||
import { DrawerClose, DrawerContent, DrawerDescription, DrawerTitle } from '@/components/ui/drawer'
|
import { DrawerContent, DrawerDescription, DrawerTitle } from '@/components/ui/drawer'
|
||||||
import { defaultAvatarEyes } from '@/features/avatar/avatars'
|
import { defaultAvatarEyes } from '@/features/avatar/avatars'
|
||||||
import { ExpressionPreview } from '@/features/avatar/components/ExpressionWorkspace'
|
import { ExpressionPreview } from '@/features/avatar/components/ExpressionWorkspace'
|
||||||
import { defaultExpression } from '@/features/avatar/presets'
|
import { defaultExpression } from '@/features/avatar/presets'
|
||||||
@ -22,6 +22,7 @@ export function AvatarDrawer({
|
|||||||
controller: StudioController
|
controller: StudioController
|
||||||
onOpenChange: (open: boolean) => void
|
onOpenChange: (open: boolean) => void
|
||||||
}) {
|
}) {
|
||||||
|
const drawerDragControls = useDragControls()
|
||||||
const {
|
const {
|
||||||
activateAvatar,
|
activateAvatar,
|
||||||
activeAvatarId,
|
activeAvatarId,
|
||||||
@ -45,10 +46,30 @@ export function AvatarDrawer({
|
|||||||
} = controller
|
} = controller
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DrawerContent className="avatar-drawer">
|
<DrawerContent
|
||||||
<DrawerClose className="drawer-handle" aria-label="Close avatar picker">
|
className="avatar-drawer"
|
||||||
|
render={
|
||||||
|
<motion.div
|
||||||
|
drag="y"
|
||||||
|
dragControls={drawerDragControls}
|
||||||
|
dragListener={false}
|
||||||
|
dragConstraints={{ top: 0, bottom: 0 }}
|
||||||
|
dragElastic={{ top: 0, bottom: 0.7 }}
|
||||||
|
dragMomentum={false}
|
||||||
|
onDragEnd={(_, info) => {
|
||||||
|
if (info.offset.y > 70 || info.velocity.y > 600) onOpenChange(false)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
className="drawer-handle"
|
||||||
|
type="button"
|
||||||
|
aria-label="Close avatar picker"
|
||||||
|
onPointerDown={event => drawerDragControls.start(event)}
|
||||||
|
>
|
||||||
<span />
|
<span />
|
||||||
</DrawerClose>
|
</button>
|
||||||
<div className="avatar-drawer-heading">
|
<div className="avatar-drawer-heading">
|
||||||
<div>
|
<div>
|
||||||
<DrawerTitle>Avatars</DrawerTitle>
|
<DrawerTitle>Avatars</DrawerTitle>
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
import { Code2 } from 'lucide-react'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
@ -20,6 +22,17 @@ export function StudioIdentity({ className = '', language, setLanguage, t }: Stu
|
|||||||
Bible Strong <em>Avatar Lab</em>
|
Bible Strong <em>Avatar Lab</em>
|
||||||
</div>
|
</div>
|
||||||
<div className="language-picker">
|
<div className="language-picker">
|
||||||
|
<a
|
||||||
|
className="source-link"
|
||||||
|
href="https://github.com/smontlouis/bible-strong-avatar-lab"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
aria-label="GitHub"
|
||||||
|
>
|
||||||
|
<Code2 aria-hidden="true" />
|
||||||
|
<span>GitHub</span>
|
||||||
|
</a>
|
||||||
|
<span className="identity-divider" aria-hidden="true" />
|
||||||
<span aria-hidden="true">{language === 'en' ? '🇬🇧' : language === 'fr' ? '🇫🇷' : '🇨🇳'}</span>
|
<span aria-hidden="true">{language === 'en' ? '🇬🇧' : language === 'fr' ? '🇫🇷' : '🇨🇳'}</span>
|
||||||
<Select
|
<Select
|
||||||
value={language}
|
value={language}
|
||||||
|
|||||||
@ -493,6 +493,7 @@ export function StudioLanguageProvider({ children }: { children: ReactNode }) {
|
|||||||
const [language, setLanguage] = useState<StudioLanguage>(readStoredStudioLanguage)
|
const [language, setLanguage] = useState<StudioLanguage>(readStoredStudioLanguage)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
document.documentElement.lang = language
|
||||||
try {
|
try {
|
||||||
window.localStorage.setItem(STUDIO_LANGUAGE_STORAGE_KEY, language)
|
window.localStorage.setItem(STUDIO_LANGUAGE_STORAGE_KEY, language)
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
import { Analytics } from '@vercel/analytics/react'
|
||||||
|
import { SpeedInsights } from '@vercel/speed-insights/react'
|
||||||
import { StrictMode } from 'react'
|
import { StrictMode } from 'react'
|
||||||
import { createRoot } from 'react-dom/client'
|
import { createRoot } from 'react-dom/client'
|
||||||
|
|
||||||
@ -7,5 +9,7 @@ import './app/styles.css'
|
|||||||
createRoot(document.getElementById('root')!).render(
|
createRoot(document.getElementById('root')!).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<App />
|
<App />
|
||||||
|
<Analytics />
|
||||||
|
<SpeedInsights />
|
||||||
</StrictMode>
|
</StrictMode>
|
||||||
)
|
)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user