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'
|
||||
|
||||
- name: Install pnpm
|
||||
run: npm install --global pnpm@11.21.0
|
||||
run: npm install --global pnpm@10.34.5
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
@ -4,6 +4,7 @@ legacy/
|
||||
LICENSE
|
||||
public/favicon.svg
|
||||
public/robots.txt
|
||||
public/sitemap.xml
|
||||
src/features/studio/defaultStudioDocument.json
|
||||
src/features/export/standaloneEngine.generated.ts
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
## What you can do
|
||||
@ -64,7 +66,7 @@ The JSON project file contains the complete current document: avatars, base and
|
||||
### Requirements
|
||||
|
||||
- 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
|
||||
|
||||
@ -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.
|
||||
|
||||
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
|
||||
|
||||
- **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."
|
||||
/>
|
||||
<meta name="application-name" content="Bible Strong Avatar Lab" />
|
||||
<meta name="author" content="Stéphane Montlouis-Calixte" />
|
||||
<meta name="robots" content="index, follow" />
|
||||
<meta property="og:type" content="website" />
|
||||
<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:description"
|
||||
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:description"
|
||||
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="apple-touch-icon" href="./apple-touch-icon.png" />
|
||||
<link rel="manifest" href="./site.webmanifest" />
|
||||
|
||||
13
package.json
13
package.json
@ -2,10 +2,19 @@
|
||||
"name": "bible-strong-avatar-lab",
|
||||
"version": "0.1.0",
|
||||
"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",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"packageManager": "pnpm@11.21.0",
|
||||
"packageManager": "pnpm@10.34.5",
|
||||
"engines": {
|
||||
"node": ">=22.12.0"
|
||||
},
|
||||
@ -24,6 +33,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@base-ui/react": "^1.7.0",
|
||||
"@vercel/analytics": "^2.0.1",
|
||||
"@vercel/speed-insights": "^2.0.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^1.31.0",
|
||||
|
||||
69
pnpm-lock.yaml
generated
69
pnpm-lock.yaml
generated
@ -11,6 +11,12 @@ importers:
|
||||
'@base-ui/react':
|
||||
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)
|
||||
'@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:
|
||||
specifier: ^0.7.1
|
||||
version: 0.7.1
|
||||
@ -440,6 +446,61 @@ packages:
|
||||
'@types/react@19.2.18':
|
||||
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':
|
||||
resolution: {integrity: sha512-BOVzne/NL162sMdResB25mUv+vWMF5NoAjNf09TeGlE7ZpszZWSD3winycicLJw72yeVsoCn/2kOhEuCvEShMA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
@ -1298,6 +1359,14 @@ snapshots:
|
||||
dependencies:
|
||||
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))':
|
||||
dependencies:
|
||||
'@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: *
|
||||
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;
|
||||
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'] {
|
||||
width: auto;
|
||||
height: 24px;
|
||||
@ -1317,7 +1343,7 @@ p {
|
||||
min-width: 0;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
border-radius: 12px;
|
||||
border-radius: 999px;
|
||||
color: #7b828e;
|
||||
font-size: 9px;
|
||||
font-weight: 750;
|
||||
@ -2386,6 +2412,9 @@ p {
|
||||
.stage-identity .language-picker [data-slot='select-trigger'] {
|
||||
min-width: 68px;
|
||||
}
|
||||
.stage-identity .source-link > span {
|
||||
display: none;
|
||||
}
|
||||
.avatar-wrap {
|
||||
width: min(42dvh, 78vw, 340px);
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
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 {
|
||||
@ -9,7 +9,7 @@ import {
|
||||
ContextMenuSeparator,
|
||||
ContextMenuTrigger,
|
||||
} 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 { ExpressionPreview } from '@/features/avatar/components/ExpressionWorkspace'
|
||||
import { defaultExpression } from '@/features/avatar/presets'
|
||||
@ -22,6 +22,7 @@ export function AvatarDrawer({
|
||||
controller: StudioController
|
||||
onOpenChange: (open: boolean) => void
|
||||
}) {
|
||||
const drawerDragControls = useDragControls()
|
||||
const {
|
||||
activateAvatar,
|
||||
activeAvatarId,
|
||||
@ -45,10 +46,30 @@ export function AvatarDrawer({
|
||||
} = controller
|
||||
|
||||
return (
|
||||
<DrawerContent className="avatar-drawer">
|
||||
<DrawerClose className="drawer-handle" aria-label="Close avatar picker">
|
||||
<DrawerContent
|
||||
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 />
|
||||
</DrawerClose>
|
||||
</button>
|
||||
<div className="avatar-drawer-heading">
|
||||
<div>
|
||||
<DrawerTitle>Avatars</DrawerTitle>
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import { Code2 } from 'lucide-react'
|
||||
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@ -20,6 +22,17 @@ export function StudioIdentity({ className = '', language, setLanguage, t }: Stu
|
||||
Bible Strong <em>Avatar Lab</em>
|
||||
</div>
|
||||
<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>
|
||||
<Select
|
||||
value={language}
|
||||
|
||||
@ -493,6 +493,7 @@ export function StudioLanguageProvider({ children }: { children: ReactNode }) {
|
||||
const [language, setLanguage] = useState<StudioLanguage>(readStoredStudioLanguage)
|
||||
|
||||
useEffect(() => {
|
||||
document.documentElement.lang = language
|
||||
try {
|
||||
window.localStorage.setItem(STUDIO_LANGUAGE_STORAGE_KEY, language)
|
||||
} catch {
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import { Analytics } from '@vercel/analytics/react'
|
||||
import { SpeedInsights } from '@vercel/speed-insights/react'
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
|
||||
@ -7,5 +9,7 @@ import './app/styles.css'
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
<Analytics />
|
||||
<SpeedInsights />
|
||||
</StrictMode>
|
||||
)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user