svelte and nx rewrite
This commit is contained in:
13
.gitignore
vendored
13
.gitignore
vendored
@@ -15,14 +15,7 @@ node_modules
|
|||||||
# Testing
|
# Testing
|
||||||
coverage
|
coverage
|
||||||
|
|
||||||
# Turbo
|
|
||||||
.turbo
|
|
||||||
|
|
||||||
# Vercel
|
|
||||||
.vercel
|
|
||||||
|
|
||||||
# Build Outputs
|
# Build Outputs
|
||||||
.next/
|
|
||||||
out/
|
out/
|
||||||
build
|
build
|
||||||
dist
|
dist
|
||||||
@@ -36,3 +29,9 @@ yarn-error.log*
|
|||||||
# Misc
|
# Misc
|
||||||
.DS_Store
|
.DS_Store
|
||||||
*.pem
|
*.pem
|
||||||
|
|
||||||
|
# NX
|
||||||
|
.nx/cache
|
||||||
|
.nx/workspace-data
|
||||||
|
.cursor/rules/nx-rules.mdc
|
||||||
|
.github/instructions/nx.instructions.md
|
||||||
10
README.md
10
README.md
@@ -1,6 +1,6 @@
|
|||||||
# ✨ Lumify Sanity Template
|
# ✨ Lumify Sanity Template
|
||||||
|
|
||||||
Official CLI and starter template for building modern web apps with Next.js, Sanity, Bun, and Shadcn UI — bundled into a single monorepo.
|
Official CLI and starter template for building modern web apps with Sveltekit, Sanity, Bun, and Shadcn UI — bundled into a single monorepo.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -8,9 +8,9 @@ Official CLI and starter template for building modern web apps with Next.js, San
|
|||||||
|
|
||||||
The `@lumify-systems/template-sanity` CLI sets up a fully functional monorepo with:
|
The `@lumify-systems/template-sanity` CLI sets up a fully functional monorepo with:
|
||||||
|
|
||||||
* ⚡ **Next.js App Router** for the frontend
|
* ⚡ **Sveltekit** for the frontend
|
||||||
* 📝 **Sanity Studio** CMS with custom schemas
|
* 📝 **Sanity Studio** CMS with custom schemas
|
||||||
* 🏗️ **TurboRepo** structure (client, studio, shared packages)
|
* 🏗️ **NX Monorepo** structure (client, studio, shared packages)
|
||||||
* 🎨 **Shadcn UI** components and theming pre-installed
|
* 🎨 **Shadcn UI** components and theming pre-installed
|
||||||
* 🧪 Preconfigured dev tools (Tailwind, TypeScript, ESLint, Prettier)
|
* 🧪 Preconfigured dev tools (Tailwind, TypeScript, ESLint, Prettier)
|
||||||
|
|
||||||
@@ -51,7 +51,7 @@ After that:
|
|||||||
|
|
||||||
```
|
```
|
||||||
apps/
|
apps/
|
||||||
client/ # Next.js frontend
|
client/ # Svelte frontend
|
||||||
studio/ # Sanity Studio CMS
|
studio/ # Sanity Studio CMS
|
||||||
packages/
|
packages/
|
||||||
ui/ # Shadcn components & design system
|
ui/ # Shadcn components & design system
|
||||||
@@ -59,7 +59,7 @@ packages/
|
|||||||
typescript-config/ # Shared TypeScript presets
|
typescript-config/ # Shared TypeScript presets
|
||||||
```
|
```
|
||||||
|
|
||||||
All apps and packages are connected with TurboRepo workspaces.
|
All apps and packages are connected with NX workspaces.
|
||||||
|
|
||||||
You can run commands from the root:
|
You can run commands from the root:
|
||||||
|
|
||||||
|
|||||||
17
debug.js
17
debug.js
@@ -1,17 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
import * as p from '@clack/prompts';
|
|
||||||
|
|
||||||
async function demoSpinner() {
|
|
||||||
await p.tasks([
|
|
||||||
{
|
|
||||||
title: 'Running demo task',
|
|
||||||
task: async () => {
|
|
||||||
await new Promise((r) => setTimeout(r, 1500));
|
|
||||||
return 'Demo completed!';
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
demoSpinner().catch(console.error);
|
|
||||||
12
index.js
12
index.js
@@ -201,6 +201,14 @@ async function main() {
|
|||||||
connFile = connFile.replace(/projectId: "[^"]+"/, `projectId: "${sanityJson.projectId}"`);
|
connFile = connFile.replace(/projectId: "[^"]+"/, `projectId: "${sanityJson.projectId}"`);
|
||||||
await fs.writeFile(connPath, connFile);
|
await fs.writeFile(connPath, connFile);
|
||||||
|
|
||||||
|
// Update wrangler.jsonc with project name
|
||||||
|
const wranglerPath = path.join(rootDir, "apps/client/wrangler.jsonc");
|
||||||
|
if (await fs.pathExists(wranglerPath)) {
|
||||||
|
let wranglerFile = await fs.readFile(wranglerPath, "utf8");
|
||||||
|
wranglerFile = wranglerFile.replace(/"name": "[^"]+"/, `"name": "${kebabName}"`);
|
||||||
|
await fs.writeFile(wranglerPath, wranglerFile);
|
||||||
|
}
|
||||||
|
|
||||||
return `Sanity project created with ID: ${sanityJson.projectId}`;
|
return `Sanity project created with ID: ${sanityJson.projectId}`;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -246,7 +254,7 @@ async function main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const corsOrigins = [
|
const corsOrigins = [
|
||||||
'http://localhost:3000',
|
'http://localhost:5173',
|
||||||
'https://*.api.sanity.io',
|
'https://*.api.sanity.io',
|
||||||
'wss://*.api.sanity.io',
|
'wss://*.api.sanity.io',
|
||||||
`https://${kebabName}.sanity.studio`,
|
`https://${kebabName}.sanity.studio`,
|
||||||
@@ -321,7 +329,7 @@ async function main() {
|
|||||||
` ${color.bold(color.green(pmRun))} ${color.dim('# Start your local dev server')} ${color.yellow('🖥️')}\n` +
|
` ${color.bold(color.green(pmRun))} ${color.dim('# Start your local dev server')} ${color.yellow('🖥️')}\n` +
|
||||||
` ${color.bold(color.green(pmDeploy))} ${color.dim('# Deploy your Sanity Studio')} ${color.yellow('🚀')}\n\n` +
|
` ${color.bold(color.green(pmDeploy))} ${color.dim('# Deploy your Sanity Studio')} ${color.yellow('🚀')}\n\n` +
|
||||||
color.dim('Local development:\n') +
|
color.dim('Local development:\n') +
|
||||||
` • App: ${color.cyan('http://localhost:3000')} ${color.yellow('🌐')}\n` +
|
` • App: ${color.cyan('http://localhost:5173')} ${color.yellow('🌐')}\n` +
|
||||||
` • Studio: ${color.cyan('http://localhost:3333')} ${color.yellow('🛠️')}\n\n` +
|
` • Studio: ${color.cyan('http://localhost:3333')} ${color.yellow('🛠️')}\n\n` +
|
||||||
color.dim('After deploying:\n') +
|
color.dim('After deploying:\n') +
|
||||||
` • Studio: ${color.cyan(`https://${kebabName}.sanity.studio`)} ${color.yellow('✨')}`
|
` • Studio: ${color.cyan(`https://${kebabName}.sanity.studio`)} ${color.yellow('✨')}`
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"access": "restricted",
|
"access": "restricted",
|
||||||
"registry": "https://npm.pkg.github.com"
|
"registry": "https://npm.pkg.github.com"
|
||||||
},
|
},
|
||||||
"description": "Template for Lumify Sanity and Next.js monorepo setup.",
|
"description": "Template for Lumify Sanity and Svelte monorepo setup.",
|
||||||
"bin": {
|
"bin": {
|
||||||
"template-sanity": "./index.js"
|
"template-sanity": "./index.js"
|
||||||
},
|
},
|
||||||
|
|||||||
14
template/.gitignore
vendored
14
template/.gitignore
vendored
@@ -15,19 +15,11 @@ node_modules
|
|||||||
# Testing
|
# Testing
|
||||||
coverage
|
coverage
|
||||||
|
|
||||||
# Turbo
|
|
||||||
.turbo
|
|
||||||
|
|
||||||
# Vercel
|
|
||||||
.vercel
|
|
||||||
|
|
||||||
# Build Outputs
|
# Build Outputs
|
||||||
.next/
|
|
||||||
out/
|
out/
|
||||||
build
|
build
|
||||||
dist
|
dist
|
||||||
|
|
||||||
|
|
||||||
# Debug
|
# Debug
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
@@ -36,3 +28,9 @@ yarn-error.log*
|
|||||||
# Misc
|
# Misc
|
||||||
.DS_Store
|
.DS_Store
|
||||||
*.pem
|
*.pem
|
||||||
|
|
||||||
|
# NX
|
||||||
|
.nx/cache
|
||||||
|
.nx/workspace-data
|
||||||
|
.cursor/rules/nx-rules.mdc
|
||||||
|
.github/instructions/nx.instructions.md
|
||||||
51
template/apps/client/.gitignore
vendored
51
template/apps/client/.gitignore
vendored
@@ -1,41 +1,22 @@
|
|||||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
node_modules
|
||||||
|
|
||||||
# dependencies
|
# Output
|
||||||
/node_modules
|
.output
|
||||||
/.pnp
|
.netlify
|
||||||
.pnp.*
|
.wrangler
|
||||||
.yarn/*
|
/.svelte-kit
|
||||||
!.yarn/patches
|
|
||||||
!.yarn/plugins
|
|
||||||
!.yarn/releases
|
|
||||||
!.yarn/versions
|
|
||||||
|
|
||||||
# testing
|
|
||||||
/coverage
|
|
||||||
|
|
||||||
# next.js
|
|
||||||
/.next/
|
|
||||||
/out/
|
|
||||||
|
|
||||||
# production
|
|
||||||
/build
|
/build
|
||||||
|
|
||||||
# misc
|
# OS
|
||||||
.DS_Store
|
.DS_Store
|
||||||
*.pem
|
Thumbs.db
|
||||||
|
|
||||||
# debug
|
# Env
|
||||||
npm-debug.log*
|
.env
|
||||||
yarn-debug.log*
|
.env.*
|
||||||
yarn-error.log*
|
!.env.example
|
||||||
.pnpm-debug.log*
|
!.env.test
|
||||||
|
|
||||||
# env files (can opt-in for committing if needed)
|
# Vite
|
||||||
.env*
|
vite.config.js.timestamp-*
|
||||||
|
vite.config.ts.timestamp-*
|
||||||
# vercel
|
|
||||||
.vercel
|
|
||||||
|
|
||||||
# typescript
|
|
||||||
*.tsbuildinfo
|
|
||||||
next-env.d.ts
|
|
||||||
|
|||||||
1
template/apps/client/.npmrc
Normal file
1
template/apps/client/.npmrc
Normal file
@@ -0,0 +1 @@
|
|||||||
|
engine-strict=true
|
||||||
9
template/apps/client/.prettierignore
Normal file
9
template/apps/client/.prettierignore
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# Package Managers
|
||||||
|
package-lock.json
|
||||||
|
pnpm-lock.yaml
|
||||||
|
yarn.lock
|
||||||
|
bun.lock
|
||||||
|
bun.lockb
|
||||||
|
|
||||||
|
# Miscellaneous
|
||||||
|
/static/
|
||||||
16
template/apps/client/.prettierrc
Normal file
16
template/apps/client/.prettierrc
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"useTabs": true,
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "none",
|
||||||
|
"printWidth": 100,
|
||||||
|
"plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": "*.svelte",
|
||||||
|
"options": {
|
||||||
|
"parser": "svelte"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tailwindStylesheet": "./src/app.css"
|
||||||
|
}
|
||||||
@@ -1,36 +1,38 @@
|
|||||||
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
|
# sv
|
||||||
|
|
||||||
## Getting Started
|
Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli).
|
||||||
|
|
||||||
First, run the development server:
|
## Creating a project
|
||||||
|
|
||||||
```bash
|
If you're seeing this, you've probably already done this step. Congrats!
|
||||||
npm run dev
|
|
||||||
# or
|
```sh
|
||||||
yarn dev
|
# create a new project in the current directory
|
||||||
# or
|
npx sv create
|
||||||
pnpm dev
|
|
||||||
# or
|
# create a new project in my-app
|
||||||
bun dev
|
npx sv create my-app
|
||||||
```
|
```
|
||||||
|
|
||||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
## Developing
|
||||||
|
|
||||||
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
|
||||||
|
|
||||||
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
|
```sh
|
||||||
|
npm run dev
|
||||||
|
|
||||||
## Learn More
|
# or start the server and open the app in a new browser tab
|
||||||
|
npm run dev -- --open
|
||||||
|
```
|
||||||
|
|
||||||
To learn more about Next.js, take a look at the following resources:
|
## Building
|
||||||
|
|
||||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
To create a production version of your app:
|
||||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
|
||||||
|
|
||||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
```sh
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
## Deploy on Vercel
|
You can preview the production build with `npm run preview`.
|
||||||
|
|
||||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment.
|
||||||
|
|
||||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|
|
||||||
|
|||||||
@@ -1,21 +1,16 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://ui.shadcn.com/schema.json",
|
"$schema": "https://shadcn-svelte.com/schema.json",
|
||||||
"style": "new-york",
|
|
||||||
"rsc": true,
|
|
||||||
"tsx": true,
|
|
||||||
"tailwind": {
|
"tailwind": {
|
||||||
"config": "tailwind.config.ts",
|
"css": "src/app.css",
|
||||||
"css": "src/app/globals.css",
|
"baseColor": "neutral"
|
||||||
"baseColor": "neutral",
|
|
||||||
"cssVariables": true,
|
|
||||||
"prefix": ""
|
|
||||||
},
|
},
|
||||||
"aliases": {
|
"aliases": {
|
||||||
"components": "@/components",
|
"components": "$lib/components",
|
||||||
"utils": "@/lib/utils",
|
"utils": "$lib/utils",
|
||||||
"ui": "@/components/ui",
|
"ui": "$lib/components/ui",
|
||||||
"lib": "@/lib",
|
"hooks": "$lib/hooks",
|
||||||
"hooks": "@/hooks"
|
"lib": "$lib"
|
||||||
},
|
},
|
||||||
"iconLibrary": "lucide"
|
"typescript": true,
|
||||||
|
"registry": "https://shadcn-svelte.com/registry"
|
||||||
}
|
}
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
import type { NextConfig } from "next";
|
|
||||||
|
|
||||||
const nextConfig: NextConfig = {
|
|
||||||
eslint: { ignoreDuringBuilds: true },
|
|
||||||
images: {
|
|
||||||
remotePatterns: [
|
|
||||||
{
|
|
||||||
hostname: "cdn.sanity.io",
|
|
||||||
pathname: "/images/**",
|
|
||||||
protocol: "https",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
hostname: "lh3.googleusercontent.com",
|
|
||||||
pathname: "**",
|
|
||||||
protocol: "https",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
headers: async () => {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
source: "/:path*",
|
|
||||||
headers: [
|
|
||||||
{
|
|
||||||
key: "X-Frame-Options",
|
|
||||||
value: "ALLOWALL",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default nextConfig;
|
|
||||||
@@ -1,45 +1,47 @@
|
|||||||
{
|
{
|
||||||
"name": "client",
|
"name": "client",
|
||||||
"version": "0.1.0",
|
|
||||||
"private": true,
|
"private": true,
|
||||||
"engines": {
|
"version": "0.0.1",
|
||||||
"bun": ">=1.2.12"
|
|
||||||
},
|
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"packageManager": "bun@1.2.12",
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev --turbopack",
|
"dev": "vite dev",
|
||||||
"build": "next build",
|
"build": "vite build",
|
||||||
"start": "next start",
|
"preview": "vite preview",
|
||||||
"lint": "next lint"
|
"prepare": "svelte-kit sync || echo ''",
|
||||||
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||||
|
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||||
|
"format": "prettier --write .",
|
||||||
|
"lint": "prettier --check ."
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/react-accordion": "^1.2.11",
|
"@portabletext/svelte": "^3.0.0",
|
||||||
"@radix-ui/react-slot": "^1.2.3",
|
|
||||||
"@sanity/client": "^7.8.1",
|
|
||||||
"@repo/sanity-connection": "*",
|
"@repo/sanity-connection": "*",
|
||||||
"@repo/typescript-config": "*",
|
"@repo/typescript-config": "*",
|
||||||
"@repo/ui": "*",
|
"@repo/ui": "*",
|
||||||
"class-variance-authority": "^0.7.1",
|
"@sanity/asset-utils": "^2.2.1",
|
||||||
"clsx": "^2.1.1",
|
"@sanity/client": "^7.8.1",
|
||||||
"framer-motion": "^12.23.9",
|
"@sanity/svelte-loader": "^1.13.47",
|
||||||
"lucide-react": "^0.525.0",
|
"@sanity/visual-editing": "^3.0.1",
|
||||||
"next": "15.4.3",
|
"@sveltejs/adapter-cloudflare-workers": "^2.9.0"
|
||||||
"next-sanity": "^10.0.4",
|
|
||||||
"react": "^19.1.0",
|
|
||||||
"react-dom": "^19.1.0",
|
|
||||||
"tailwind-merge": "^3.3.1"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3.3.1",
|
"@lucide/svelte": "^0.536.0",
|
||||||
"@tailwindcss/postcss": "^4.1.11",
|
"@sveltejs/kit": "^2.22.0",
|
||||||
"@types/node": "^24.1.0",
|
"@sveltejs/vite-plugin-svelte": "^6.0.0",
|
||||||
"@types/react": "^19.1.8",
|
"@tailwindcss/forms": "^0.5.9",
|
||||||
"@types/react-dom": "^19.1.6",
|
"@tailwindcss/typography": "^0.5.15",
|
||||||
"eslint": "^9.31.0",
|
"@tailwindcss/vite": "^4.0.0",
|
||||||
"eslint-config-next": "15.4.3",
|
"clsx": "^2.1.1",
|
||||||
"tailwindcss": "^4.1.11",
|
"prettier": "^3.4.2",
|
||||||
"tw-animate-css": "^1.3.5",
|
"prettier-plugin-svelte": "^3.3.3",
|
||||||
"typescript": "^5.8.3"
|
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||||
|
"svelte": "^5.0.0",
|
||||||
|
"svelte-check": "^4.0.0",
|
||||||
|
"tailwind-merge": "^3.3.1",
|
||||||
|
"tailwind-variants": "^1.0.0",
|
||||||
|
"tailwindcss": "^4.0.0",
|
||||||
|
"tw-animate-css": "^1.3.6",
|
||||||
|
"typescript": "^5.0.0",
|
||||||
|
"vite": "^7.0.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
const config = {
|
|
||||||
plugins: ["@tailwindcss/postcss"],
|
|
||||||
};
|
|
||||||
|
|
||||||
export default config;
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
{
|
|
||||||
"icons": [
|
|
||||||
{
|
|
||||||
"src": "/favicon-192.png",
|
|
||||||
"type": "image/png",
|
|
||||||
"sizes": "192x192"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "/favicon-512.png",
|
|
||||||
"type": "image/png",
|
|
||||||
"sizes": "512x512"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,16 +1,94 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
|
||||||
@import "tw-animate-css";
|
@import "tw-animate-css";
|
||||||
|
|
||||||
@custom-variant dark (&:is(.dark *));
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
@config "../../tailwind.config.ts";
|
@layer base {
|
||||||
|
:root {
|
||||||
|
--font-sans: 'Geist', ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--radius: 0.625rem;
|
||||||
|
--background: oklch(1 0 0);
|
||||||
|
--foreground: oklch(0.145 0 0);
|
||||||
|
--card: oklch(1 0 0);
|
||||||
|
--card-foreground: oklch(0.145 0 0);
|
||||||
|
--popover: oklch(1 0 0);
|
||||||
|
--popover-foreground: oklch(0.145 0 0);
|
||||||
|
--primary: oklch(0.205 0 0);
|
||||||
|
--primary-foreground: oklch(0.985 0 0);
|
||||||
|
--secondary: oklch(0.97 0 0);
|
||||||
|
--secondary-foreground: oklch(0.205 0 0);
|
||||||
|
--muted: oklch(0.97 0 0);
|
||||||
|
--muted-foreground: oklch(0.556 0 0);
|
||||||
|
--accent: oklch(0.97 0 0);
|
||||||
|
--accent-foreground: oklch(0.205 0 0);
|
||||||
|
--destructive: oklch(0.577 0.245 27.325);
|
||||||
|
--border: oklch(0.922 0 0);
|
||||||
|
--input: oklch(0.922 0 0);
|
||||||
|
--ring: oklch(0.708 0 0);
|
||||||
|
--chart-1: oklch(0.646 0.222 41.116);
|
||||||
|
--chart-2: oklch(0.6 0.118 184.704);
|
||||||
|
--chart-3: oklch(0.398 0.07 227.392);
|
||||||
|
--chart-4: oklch(0.828 0.189 84.429);
|
||||||
|
--chart-5: oklch(0.769 0.188 70.08);
|
||||||
|
--sidebar: oklch(0.985 0 0);
|
||||||
|
--sidebar-foreground: oklch(0.145 0 0);
|
||||||
|
--sidebar-primary: oklch(0.205 0 0);
|
||||||
|
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-accent: oklch(0.97 0 0);
|
||||||
|
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||||
|
--sidebar-border: oklch(0.922 0 0);
|
||||||
|
--sidebar-ring: oklch(0.708 0 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--background: oklch(0.145 0 0);
|
||||||
|
--foreground: oklch(0.985 0 0);
|
||||||
|
--card: oklch(0.205 0 0);
|
||||||
|
--card-foreground: oklch(0.985 0 0);
|
||||||
|
--popover: oklch(0.205 0 0);
|
||||||
|
--popover-foreground: oklch(0.985 0 0);
|
||||||
|
--primary: oklch(0.922 0 0);
|
||||||
|
--primary-foreground: oklch(0.205 0 0);
|
||||||
|
--secondary: oklch(0.269 0 0);
|
||||||
|
--secondary-foreground: oklch(0.985 0 0);
|
||||||
|
--muted: oklch(0.269 0 0);
|
||||||
|
--muted-foreground: oklch(0.708 0 0);
|
||||||
|
--accent: oklch(0.269 0 0);
|
||||||
|
--accent-foreground: oklch(0.985 0 0);
|
||||||
|
--destructive: oklch(0.704 0.191 22.216);
|
||||||
|
--border: oklch(1 0 0 / 10%);
|
||||||
|
--input: oklch(1 0 0 / 15%);
|
||||||
|
--ring: oklch(0.556 0 0);
|
||||||
|
--chart-1: oklch(0.488 0.243 264.376);
|
||||||
|
--chart-2: oklch(0.696 0.17 162.48);
|
||||||
|
--chart-3: oklch(0.769 0.188 70.08);
|
||||||
|
--chart-4: oklch(0.627 0.265 303.9);
|
||||||
|
--chart-5: oklch(0.645 0.246 16.439);
|
||||||
|
--sidebar: oklch(0.205 0 0);
|
||||||
|
--sidebar-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||||
|
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-accent: oklch(0.269 0 0);
|
||||||
|
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-border: oklch(1 0 0 / 10%);
|
||||||
|
--sidebar-ring: oklch(0.556 0 0);
|
||||||
|
}
|
||||||
|
|
||||||
@theme inline {
|
@theme inline {
|
||||||
--radius-sm: calc(var(--radius) - 4px);
|
--radius-sm: calc(var(--radius) - 4px);
|
||||||
--radius-md: calc(var(--radius) - 2px);
|
--radius-md: calc(var(--radius) - 2px);
|
||||||
--radius-lg: var(--radius);
|
--radius-lg: var(--radius);
|
||||||
--radius-xl: calc(var(--radius) + 4px);
|
--radius-xl: calc(var(--radius) + 4px);
|
||||||
--color-background: var(--bg-primary);
|
--color-background: var(--background);
|
||||||
--color-foreground: var(--foreground);
|
--color-foreground: var(--foreground);
|
||||||
--color-card: var(--card);
|
--color-card: var(--card);
|
||||||
--color-card-foreground: var(--card-foreground);
|
--color-card-foreground: var(--card-foreground);
|
||||||
@@ -43,62 +121,11 @@
|
|||||||
--color-sidebar-ring: var(--sidebar-ring);
|
--color-sidebar-ring: var(--sidebar-ring);
|
||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
|
||||||
--radius: 0.625rem;
|
|
||||||
--card: oklch(1 0 0);
|
|
||||||
--card-foreground: oklch(0.145 0 0);
|
|
||||||
--popover: oklch(1 0 0);
|
|
||||||
--popover-foreground: oklch(0.145 0 0);
|
|
||||||
--primary: oklch(0.205 0 0);
|
|
||||||
--primary-foreground: oklch(0.985 0 0);
|
|
||||||
--secondary: oklch(0.97 0 0);
|
|
||||||
--secondary-foreground: oklch(0.205 0 0);
|
|
||||||
--muted: oklch(0.97 0 0);
|
|
||||||
--muted-foreground: oklch(0.556 0 0);
|
|
||||||
--accent: oklch(0.97 0 0);
|
|
||||||
--accent-foreground: oklch(0.205 0 0);
|
|
||||||
--destructive: oklch(0.577 0.245 27.325);
|
|
||||||
--border: oklch(0.922 0 0);
|
|
||||||
--input: oklch(0.922 0 0);
|
|
||||||
--ring: oklch(0.708 0 0);
|
|
||||||
--chart-1: oklch(0.646 0.222 41.116);
|
|
||||||
--chart-2: oklch(0.6 0.118 184.704);
|
|
||||||
--chart-3: oklch(0.398 0.07 227.392);
|
|
||||||
--chart-4: oklch(0.828 0.189 84.429);
|
|
||||||
--chart-5: oklch(0.769 0.188 70.08);
|
|
||||||
--sidebar: oklch(0.985 0 0);
|
|
||||||
--sidebar-foreground: oklch(0.145 0 0);
|
|
||||||
--sidebar-primary: oklch(0.205 0 0);
|
|
||||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
||||||
--sidebar-accent: oklch(0.97 0 0);
|
|
||||||
--sidebar-accent-foreground: oklch(0.205 0 0);
|
|
||||||
--sidebar-border: oklch(0.922 0 0);
|
|
||||||
--sidebar-ring: oklch(0.708 0 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
* {
|
* {
|
||||||
@apply border-border outline-ring/50;
|
@apply border-border outline-ring/50;
|
||||||
}
|
}
|
||||||
body {
|
body {
|
||||||
@apply font-sans bg-background text-foreground text-lg;
|
@apply bg-background text-foreground;
|
||||||
}
|
|
||||||
h1, h2, h3, h4, h5, h6 {
|
|
||||||
@apply font-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
@apply text-4xl md:text-6xl uppercase font-bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
@apply text-3xl font-bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
@apply text-xl;
|
|
||||||
}
|
|
||||||
h4 {
|
|
||||||
@apply text-lg;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
9
template/apps/client/src/app.d.ts
vendored
Normal file
9
template/apps/client/src/app.d.ts
vendored
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import type {LoaderLocals} from '@sanity/svelte-loader'
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
namespace App {
|
||||||
|
interface Locals extends LoaderLocals {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {}
|
||||||
11
template/apps/client/src/app.html
Normal file
11
template/apps/client/src/app.html
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
%sveltekit.head%
|
||||||
|
</head>
|
||||||
|
<body data-sveltekit-preload-data="hover">
|
||||||
|
<div style="display: contents">%sveltekit.body%</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
import { defineQuery } from "next-sanity";
|
|
||||||
import { sanityFetch } from "@/sanity/live";
|
|
||||||
import type { Custom } from "@/sanity/sanity.types";
|
|
||||||
import type { Metadata } from "next";
|
|
||||||
import SanityBlock from "@/components/sanity-block";
|
|
||||||
import { notFound } from "next/navigation";
|
|
||||||
import { fetchSettings } from "@/sanity/settings";
|
|
||||||
|
|
||||||
const CUSTOMS_QUERY = defineQuery(`*[_type == "custom"]{ slug }`);
|
|
||||||
const CUSTOM_QUERY = defineQuery(
|
|
||||||
`*[_type == "custom" && slug.current == $slug][0]`,
|
|
||||||
);
|
|
||||||
|
|
||||||
type PageParams = Promise<{
|
|
||||||
slug: string;
|
|
||||||
}>;
|
|
||||||
|
|
||||||
export async function generateStaticParams() {
|
|
||||||
const customs: Custom[] = (
|
|
||||||
await sanityFetch({
|
|
||||||
query: CUSTOMS_QUERY,
|
|
||||||
stega: false,
|
|
||||||
perspective: "published",
|
|
||||||
})
|
|
||||||
).data;
|
|
||||||
return customs.map((custom) => ({
|
|
||||||
slug: custom.slug?.current ?? "",
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function generateMetadata(props: {
|
|
||||||
params: Promise<PageParams>;
|
|
||||||
}): Promise<Metadata> {
|
|
||||||
const params = await props.params;
|
|
||||||
if (!params.slug) {
|
|
||||||
return notFound();
|
|
||||||
}
|
|
||||||
const settings: { title: string } = await fetchSettings("title");
|
|
||||||
const custom: Custom = (
|
|
||||||
await sanityFetch({
|
|
||||||
query: CUSTOM_QUERY,
|
|
||||||
params,
|
|
||||||
stega: false,
|
|
||||||
perspective: "published",
|
|
||||||
})
|
|
||||||
).data;
|
|
||||||
const firstBlock = custom?.body ? custom.body[0] : undefined;
|
|
||||||
let description: string | undefined = undefined;
|
|
||||||
if (firstBlock && firstBlock._type === "block" && firstBlock.children) {
|
|
||||||
description = firstBlock.children.map((child) => child.text).join(" ");
|
|
||||||
}
|
|
||||||
|
|
||||||
const firstWord = custom.title ? custom.title.split(" ")[0] : "";
|
|
||||||
return {
|
|
||||||
title: `${firstWord} | ${settings.title}`,
|
|
||||||
description,
|
|
||||||
openGraph: {
|
|
||||||
title: `${firstWord} | ${settings.title}`,
|
|
||||||
description,
|
|
||||||
type: "article",
|
|
||||||
url: `/${custom.slug}`,
|
|
||||||
},
|
|
||||||
twitter: {
|
|
||||||
card: "summary_large_image",
|
|
||||||
title: `${firstWord} | ${settings.title}`,
|
|
||||||
description,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default async function CustomPage(props: {
|
|
||||||
params: Promise<PageParams>;
|
|
||||||
}) {
|
|
||||||
const params = await props.params;
|
|
||||||
const custom: Custom = (await sanityFetch({ query: CUSTOM_QUERY, params }))
|
|
||||||
.data;
|
|
||||||
if (!custom) {
|
|
||||||
return notFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<main className="container mx-auto min-h-screen max-w-3xl md:max-w-4xl p-8 flex flex-col gap-4">
|
|
||||||
<h2>{custom.title}</h2>
|
|
||||||
<div
|
|
||||||
className="items-start mt-2 mb-8 text-left"
|
|
||||||
style={{ maxWidth: "100%" }}
|
|
||||||
>
|
|
||||||
{custom.body && <SanityBlock body={custom.body} />}
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
'use server'
|
|
||||||
|
|
||||||
import {draftMode} from 'next/headers'
|
|
||||||
|
|
||||||
export async function disableDraftMode() {
|
|
||||||
const disable = (await draftMode()).disable()
|
|
||||||
const delay = new Promise((resolve) => setTimeout(resolve, 1000))
|
|
||||||
|
|
||||||
await Promise.allSettled([disable, delay]);
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import { client } from "@/sanity/client";
|
|
||||||
import { sanityConnection } from "@repo/sanity-connection";
|
|
||||||
import { defineEnableDraftMode } from "next-sanity/draft-mode";
|
|
||||||
|
|
||||||
export const { GET } = defineEnableDraftMode({
|
|
||||||
client: client.withConfig({
|
|
||||||
token: sanityConnection.publicViewerToken,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
import type { Metadata } from "next";
|
|
||||||
import { Geist, Noto_Sans } from "next/font/google";
|
|
||||||
import "./globals.css";
|
|
||||||
import { VisualEditing } from "next-sanity";
|
|
||||||
import { draftMode } from "next/headers";
|
|
||||||
import { SanityLive } from "@/sanity/live";
|
|
||||||
import { fetchSettings } from "@/sanity/settings";
|
|
||||||
import { getImage } from "@/lib/asset-to-url";
|
|
||||||
import Footer from "@/components/footer";
|
|
||||||
|
|
||||||
const sans = Geist({
|
|
||||||
variable: "--font-sans",
|
|
||||||
subsets: ["latin"],
|
|
||||||
});
|
|
||||||
|
|
||||||
export async function generateMetadata(): Promise<Metadata> {
|
|
||||||
const settings = await fetchSettings();
|
|
||||||
const logo = await getImage(settings?.logo?.asset?._ref);
|
|
||||||
|
|
||||||
return {
|
|
||||||
title: settings.title,
|
|
||||||
description: settings.description,
|
|
||||||
openGraph: {
|
|
||||||
title: settings.longTitle,
|
|
||||||
description: settings.description
|
|
||||||
},
|
|
||||||
twitter: {
|
|
||||||
title: settings.longTitle,
|
|
||||||
description: settings.description,
|
|
||||||
},
|
|
||||||
robots: {
|
|
||||||
index: true,
|
|
||||||
follow: true,
|
|
||||||
},
|
|
||||||
icons: {
|
|
||||||
icon: logo.url,
|
|
||||||
apple: logo.url,
|
|
||||||
},
|
|
||||||
manifest: "/manifest.webmanifest",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default async function RootLayout({
|
|
||||||
children,
|
|
||||||
}: Readonly<{
|
|
||||||
children: React.ReactNode;
|
|
||||||
}>) {
|
|
||||||
const settings = await fetchSettings();
|
|
||||||
return (
|
|
||||||
<html lang="en" className="scroll-smooth">
|
|
||||||
<body
|
|
||||||
className={`${sans.variable} antialiased text-text`}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
<Footer settings={settings} />
|
|
||||||
{(await draftMode()).isEnabled && <VisualEditing />}
|
|
||||||
<SanityLive />
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
import { sanityFetch } from "@/sanity/live";
|
|
||||||
import CTA from "@/components/section/cta";
|
|
||||||
import { getImage, getImages } from "@/lib/asset-to-url";
|
|
||||||
import { Home } from "@/sanity/sanity.types";
|
|
||||||
|
|
||||||
const HOME_QUERY = `*[_type == "home"][0]`;
|
|
||||||
|
|
||||||
export default async function IndexPage() {
|
|
||||||
const { data: home }: { data: Home } = await sanityFetch({
|
|
||||||
query: HOME_QUERY,
|
|
||||||
});
|
|
||||||
|
|
||||||
const background = await getImage(
|
|
||||||
home.headerSection?.backgroundImage?.asset?._ref
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<CTA
|
|
||||||
cta={home.headerSection}
|
|
||||||
background={background}
|
|
||||||
textColor="text-white"
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
export default function ColorTest() {
|
|
||||||
return (
|
|
||||||
<div className="p-8 space-y-4">
|
|
||||||
<h1 className="text-2xl font-bold mb-6">Color Test</h1>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<h2 className="text-lg font-semibold">Primitives (should work):</h2>
|
|
||||||
<div className="bg-primary1-500 text-white p-2 rounded">bg-primary1-500</div>
|
|
||||||
<div className="bg-primary2-500 text-white p-2 rounded">bg-primary2-500</div>
|
|
||||||
<div className="bg-secondary1-100 p-2 rounded border">bg-secondary1-100</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<h2 className="text-lg font-semibold">Variables (should work now):</h2>
|
|
||||||
<div className="bg-olive-green text-white p-2 rounded">bg-olive-green</div>
|
|
||||||
<div className="bg-pine-green text-white p-2 rounded">bg-pine-green</div>
|
|
||||||
<div className="bg-light-sage p-2 rounded border">bg-light-sage</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<h2 className="text-lg font-semibold">Brand Colors (should work now):</h2>
|
|
||||||
<div className="bg-bg-primary p-2 rounded border">bg-bg-primary</div>
|
|
||||||
<div className="bg-bg-secondary p-2 rounded border">bg-bg-secondary</div>
|
|
||||||
<div className="text-text p-2 rounded border">text-text (blue text)</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { Settings } from "@/sanity/sanity.types";
|
|
||||||
|
|
||||||
export default function Footer({ settings }: { settings: Settings }) {
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col gap-4">
|
|
||||||
<label className="text-sm pb-4 px-8">
|
|
||||||
{settings.footer?.replace(
|
|
||||||
"{YEAR}",
|
|
||||||
new Date().getFullYear().toString()
|
|
||||||
)}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
import Link from "next/link";
|
|
||||||
import { ArrowRight, ExternalLink, type LucideIcon } from "lucide-react";
|
|
||||||
import React, { createElement } from "react";
|
|
||||||
import { Button } from "./ui/button";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
export default function LinkButton({
|
|
||||||
text,
|
|
||||||
linkData,
|
|
||||||
variant = "default",
|
|
||||||
radius = "sm",
|
|
||||||
size = "default",
|
|
||||||
extraIcon,
|
|
||||||
showIcon = true,
|
|
||||||
className,
|
|
||||||
onPress,
|
|
||||||
...props
|
|
||||||
}: {
|
|
||||||
text: string;
|
|
||||||
linkData: { href: string; target: string } | null;
|
|
||||||
variant?:
|
|
||||||
| "ghost"
|
|
||||||
| "default"
|
|
||||||
| "secondary"
|
|
||||||
| "link"
|
|
||||||
| "destructive"
|
|
||||||
| "outline";
|
|
||||||
size?: "default" | "sm" | "lg" | "icon";
|
|
||||||
extraIcon?: LucideIcon;
|
|
||||||
showIcon?: boolean;
|
|
||||||
className?: string;
|
|
||||||
onPress?: () => void;
|
|
||||||
[key: string]: any;
|
|
||||||
}) {
|
|
||||||
const isExternal = linkData?.href?.startsWith("http");
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
asChild
|
|
||||||
variant={variant}
|
|
||||||
size={size}
|
|
||||||
rel={isExternal ? "noopener noreferrer" : undefined}
|
|
||||||
onClick={onPress}
|
|
||||||
className={cn(
|
|
||||||
className,
|
|
||||||
`
|
|
||||||
group
|
|
||||||
rounded-full
|
|
||||||
font-semibold
|
|
||||||
transition-all
|
|
||||||
duration-300
|
|
||||||
no-underline
|
|
||||||
flex
|
|
||||||
items-center
|
|
||||||
gap-2
|
|
||||||
${showIcon ? "pr-4" : "px-6"}
|
|
||||||
active:transform
|
|
||||||
active:translate-y-[1px]
|
|
||||||
`
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<Link
|
|
||||||
href={linkData?.href ?? ""}
|
|
||||||
target={linkData?.target ?? "_self"}
|
|
||||||
>
|
|
||||||
{extraIcon && (
|
|
||||||
<span className="transition-transform duration-300 group-hover:scale-110">
|
|
||||||
{createElement(extraIcon, { size: size === "lg" ? 24 : 20 })}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<span>{text}</span>
|
|
||||||
|
|
||||||
{showIcon && (
|
|
||||||
<span className="transition-all duration-300 group-hover:transform group-hover:translate-x-1">
|
|
||||||
{isExternal ? (
|
|
||||||
<ExternalLink size={size === "lg" ? 24 : 20} />
|
|
||||||
) : (
|
|
||||||
<ArrowRight strokeWidth={3} size={size === "lg" ? 24 : 20} />
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</Link>
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,243 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
||||||
import { PortableText, createDataAttribute, stegaClean } from "next-sanity";
|
|
||||||
import {
|
|
||||||
getFileAsset,
|
|
||||||
getImageAsset,
|
|
||||||
getImageDimensions,
|
|
||||||
SanityFileAsset,
|
|
||||||
} from "@sanity/asset-utils";
|
|
||||||
import type {
|
|
||||||
BlockContent,
|
|
||||||
Button as SanityButton,
|
|
||||||
SanityImageAsset,
|
|
||||||
} from "@/sanity/sanity.types";
|
|
||||||
import { client } from "@/sanity/client";
|
|
||||||
import LinkButton from "./link-button";
|
|
||||||
import Image from "next/image";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { DownloadIcon } from "lucide-react";
|
|
||||||
import { useDeconstructLink } from "@/lib/link-client";
|
|
||||||
import { dynamicHeight, generateImageUrl } from "@/lib/image-url";
|
|
||||||
import { Button } from "./ui/button";
|
|
||||||
import { sanityConnection } from "@repo/sanity-connection";
|
|
||||||
|
|
||||||
const { projectId, dataset } = client.config();
|
|
||||||
|
|
||||||
export function Callout(props: any) {
|
|
||||||
if (!props) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<div className="bg-secondary-500 text-secondary-50 shadow-lg rounded-md p-6 border my-6 border-gray-200">
|
|
||||||
<PortableText
|
|
||||||
value={props.value.content}
|
|
||||||
components={{
|
|
||||||
block: {
|
|
||||||
h1: ({ children }: any) => <h1>{children}</h1>,
|
|
||||||
h2: ({ children }: any) => <h2>{children}</h2>,
|
|
||||||
h3: ({ children }: any) => <h3>{children}</h3>,
|
|
||||||
h4: ({ children }: any) => <h4>{children}</h4>,
|
|
||||||
h5: ({ children }: any) => <h5>{children}</h5>,
|
|
||||||
h6: ({ children }: any) => <h6>{children}</h6>,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function PortableButton(value: { value: SanityButton }) {
|
|
||||||
const linkData = useDeconstructLink(value.value.link);
|
|
||||||
return (
|
|
||||||
<div className="flex justify-left">
|
|
||||||
<LinkButton
|
|
||||||
className="px-5"
|
|
||||||
text={value.value.text ?? ""}
|
|
||||||
color={"default"}
|
|
||||||
linkData={linkData}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function PortableImage({
|
|
||||||
value,
|
|
||||||
isInline,
|
|
||||||
}: {
|
|
||||||
value: SanityImageAsset;
|
|
||||||
isInline: boolean;
|
|
||||||
}) {
|
|
||||||
const { width, height } = getImageDimensions(value);
|
|
||||||
const imageAsset = getImageAsset(value, {
|
|
||||||
projectId: sanityConnection.projectId ?? "",
|
|
||||||
dataset: sanityConnection.dataset ?? "",
|
|
||||||
});
|
|
||||||
const attr = createDataAttribute({
|
|
||||||
id: imageAsset._id,
|
|
||||||
type: imageAsset._type,
|
|
||||||
workspace: "production",
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Image
|
|
||||||
data-sanity={attr("body")}
|
|
||||||
src={generateImageUrl(value)}
|
|
||||||
width={
|
|
||||||
isInline ? (width >= 100 ? 100 : width) : width >= 1200 ? 1200 : width
|
|
||||||
}
|
|
||||||
height={dynamicHeight(height, width, isInline)}
|
|
||||||
alt={value.altText || " "}
|
|
||||||
loading="lazy"
|
|
||||||
className="border rounded-md shadow-md"
|
|
||||||
style={{
|
|
||||||
display: isInline ? "inline-block" : "block",
|
|
||||||
aspectRatio: width / height,
|
|
||||||
maxHeight: "400px",
|
|
||||||
maxWidth: "100%",
|
|
||||||
width: "auto",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function PortableFile({ value }: { value: SanityFileAsset }) {
|
|
||||||
const fileAsset = getFileAsset(value, {
|
|
||||||
projectId: projectId ?? "",
|
|
||||||
dataset: dataset ?? "",
|
|
||||||
});
|
|
||||||
const attr = createDataAttribute({
|
|
||||||
id: fileAsset._id,
|
|
||||||
type: fileAsset._type,
|
|
||||||
workspace: "production",
|
|
||||||
});
|
|
||||||
|
|
||||||
const filename: string =
|
|
||||||
fileAsset?.originalFilename ||
|
|
||||||
`${fileAsset.extension.charAt(0).toUpperCase() + fileAsset.extension?.slice(1)} herunterladen`;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex justify-left">
|
|
||||||
<LinkButton
|
|
||||||
linkData={{ href: fileAsset.url, target: "" }}
|
|
||||||
data-sanity={attr("body")}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
download
|
|
||||||
showIcon={false}
|
|
||||||
extraIcon={DownloadIcon}
|
|
||||||
text={
|
|
||||||
filename.length > 20
|
|
||||||
? `${filename.slice(0, 10)}...${filename.slice(-10)}`
|
|
||||||
: filename
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function CustomLink(props: { children: React.ReactNode; value?: any }) {
|
|
||||||
const { value, children } = props;
|
|
||||||
|
|
||||||
let linkUrl = null;
|
|
||||||
let target = "_self";
|
|
||||||
|
|
||||||
if (value?.href) {
|
|
||||||
const href = value.href;
|
|
||||||
|
|
||||||
if (href.type === "external" && href.url) {
|
|
||||||
linkUrl = href.url;
|
|
||||||
target = href.blank ? "_blank" : "_self";
|
|
||||||
} else if (href.type === "internal" || href._type === "reference") {
|
|
||||||
const resolved = useDeconstructLink(href);
|
|
||||||
if (resolved) {
|
|
||||||
const ButtonComponent = Button as any;
|
|
||||||
return (
|
|
||||||
<ButtonComponent variant="link" className="p-0 h-auto font-normal" asChild>
|
|
||||||
<Link href={resolved}>
|
|
||||||
{children}
|
|
||||||
</Link>
|
|
||||||
</ButtonComponent>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else if (typeof href === "string") {
|
|
||||||
linkUrl = href;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (linkUrl) {
|
|
||||||
const ButtonComponent = Button as any;
|
|
||||||
return (
|
|
||||||
<ButtonComponent variant="link" className="p-0 h-auto font-normal" asChild>
|
|
||||||
<Link
|
|
||||||
href={linkUrl}
|
|
||||||
target={target}
|
|
||||||
rel={target === "_blank" ? "noopener noreferrer" : undefined}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</Link>
|
|
||||||
</ButtonComponent>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return <>{children}</>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const components = {
|
|
||||||
types: {
|
|
||||||
image: PortableImage,
|
|
||||||
button: PortableButton,
|
|
||||||
callout: Callout,
|
|
||||||
file: PortableFile,
|
|
||||||
},
|
|
||||||
block: {
|
|
||||||
h1: ({ children }: any) => <h1>{children}</h1>,
|
|
||||||
h2: ({ children }: any) => <h2>{children}</h2>,
|
|
||||||
h3: ({ children }: any) => <h3>{children}</h3>,
|
|
||||||
h4: ({ children }: any) => <h4>{children}</h4>,
|
|
||||||
h5: ({ children }: any) => <h5>{children}</h5>,
|
|
||||||
h6: ({ children }: any) => <h6>{children}</h6>,
|
|
||||||
},
|
|
||||||
marks: {
|
|
||||||
left: ({ children }: { children: React.ReactNode }) => (
|
|
||||||
<span style={{ textAlign: "left", width: "100%", display: "block" }}>
|
|
||||||
{children}
|
|
||||||
</span>
|
|
||||||
),
|
|
||||||
center: ({ children }: { children: React.ReactNode }) => (
|
|
||||||
<span style={{ textAlign: "center", width: "100%", display: "block" }}>
|
|
||||||
{children}
|
|
||||||
</span>
|
|
||||||
),
|
|
||||||
right: ({ children }: { children: React.ReactNode }) => (
|
|
||||||
<span style={{ textAlign: "right", width: "100%", display: "block" }}>
|
|
||||||
{children}
|
|
||||||
</span>
|
|
||||||
),
|
|
||||||
textColor: ({
|
|
||||||
children,
|
|
||||||
value = { value: "" },
|
|
||||||
}: {
|
|
||||||
children: React.ReactNode;
|
|
||||||
value?: { value: string };
|
|
||||||
}) => <span style={{ color: stegaClean(value.value) }}>{children}</span>,
|
|
||||||
highlightColor: ({
|
|
||||||
children,
|
|
||||||
value = { value: "" },
|
|
||||||
}: {
|
|
||||||
children: React.ReactNode;
|
|
||||||
value?: { value: string };
|
|
||||||
}) => (
|
|
||||||
<span style={{ background: stegaClean(value.value) }}>{children}</span>
|
|
||||||
),
|
|
||||||
link: CustomLink,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function SanityBlock({ body }: { body: BlockContent }) {
|
|
||||||
if (!body) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return <PortableText value={body} components={components} />;
|
|
||||||
}
|
|
||||||
@@ -1,126 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import LinkButton from "../link-button";
|
|
||||||
import { motion } from "framer-motion";
|
|
||||||
import { CtaSection } from "@/sanity/sanity.types";
|
|
||||||
import { useDeconstructLink } from "@/lib/link-client";
|
|
||||||
import { SimpleImage } from "@/lib/asset-to-url";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import { generateImageUrl } from "@/lib/image-url";
|
|
||||||
import SanityBlock from "../sanity-block";
|
|
||||||
|
|
||||||
interface CTAProps {
|
|
||||||
cta?: CtaSection;
|
|
||||||
sectionTitle?: string;
|
|
||||||
background: SimpleImage | any;
|
|
||||||
backgroundColor?: string;
|
|
||||||
textColor?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function CTA({
|
|
||||||
cta,
|
|
||||||
background,
|
|
||||||
sectionTitle,
|
|
||||||
backgroundColor = "bg-gray/80",
|
|
||||||
textColor,
|
|
||||||
}: CTAProps) {
|
|
||||||
const linkData = useDeconstructLink(cta?.button?.link);
|
|
||||||
|
|
||||||
const backgroundImageUrl = background
|
|
||||||
? (() => {
|
|
||||||
if (background.url) {
|
|
||||||
return generateImageUrl(
|
|
||||||
background,
|
|
||||||
background.dimensions?.width,
|
|
||||||
background.dimensions?.height
|
|
||||||
);
|
|
||||||
} else if (background.asset) {
|
|
||||||
const assetId = background.asset._ref || background.asset;
|
|
||||||
const match = assetId.match(/-(\d+)x(\d+)$/);
|
|
||||||
const width = match ? parseInt(match[1]) : 1920;
|
|
||||||
const height = match ? parseInt(match[2]) : 1080;
|
|
||||||
return generateImageUrl(background, width, height);
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
})()
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0 }}
|
|
||||||
animate={{ opacity: 1 }}
|
|
||||||
style={{
|
|
||||||
backgroundImage: backgroundImageUrl
|
|
||||||
? `url(${backgroundImageUrl})`
|
|
||||||
: undefined,
|
|
||||||
backgroundSize: "cover",
|
|
||||||
backgroundPosition: "center",
|
|
||||||
backgroundRepeat: "no-repeat",
|
|
||||||
}}
|
|
||||||
aria-label={background?.alt}
|
|
||||||
className={cn(
|
|
||||||
"md:min-h-screen flex flex-col md:flex-row w-full overflow-hidden relative",
|
|
||||||
textColor
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className={cn("absolute inset-0 z-0", backgroundColor)} />
|
|
||||||
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, x: -20 }}
|
|
||||||
animate={{ opacity: 1, x: 0 }}
|
|
||||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
|
||||||
className="z-10 flex flex-col items-center md:items-start justify-center md:justify-start mt-20 px-6 md:px-20 md:py-32 md:flex-1 space-y-12 min-h-screen md:min-h-0"
|
|
||||||
>
|
|
||||||
<motion.div>
|
|
||||||
<motion.label
|
|
||||||
initial={{ opacity: 0, y: -10 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ delay: 0.2, duration: 0.5 }}
|
|
||||||
className="text-sm"
|
|
||||||
>
|
|
||||||
{sectionTitle}
|
|
||||||
</motion.label>
|
|
||||||
<motion.h1
|
|
||||||
initial={{ opacity: 0, y: -10 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ delay: 0.2, duration: 0.5 }}
|
|
||||||
className="md:max-w-[60rem] text-6xl md:text-8xl"
|
|
||||||
>
|
|
||||||
{cta?.title}
|
|
||||||
</motion.h1>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0 }}
|
|
||||||
animate={{ opacity: 1 }}
|
|
||||||
transition={{ delay: 0.4, duration: 0.5 }}
|
|
||||||
className="flex flex-col items-start space-y-20 md:space-y-14 w-full max-w-xl"
|
|
||||||
>
|
|
||||||
{cta?.description && (
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0 }}
|
|
||||||
animate={{ opacity: 1 }}
|
|
||||||
transition={{ delay: 0.6, duration: 0.5 }}
|
|
||||||
>
|
|
||||||
<SanityBlock body={cta.description} />
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: 10 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ delay: 0.8, duration: 0.5 }}
|
|
||||||
>
|
|
||||||
<LinkButton
|
|
||||||
className="mb-20"
|
|
||||||
text={cta?.button?.text ?? ""}
|
|
||||||
color="primary"
|
|
||||||
linkData={linkData}
|
|
||||||
size="lg"
|
|
||||||
variant="default"
|
|
||||||
/>
|
|
||||||
</motion.div>
|
|
||||||
</motion.div>
|
|
||||||
</motion.div>
|
|
||||||
</motion.div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
import { Slot } from "@radix-ui/react-slot"
|
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
const buttonVariants = cva(
|
|
||||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
|
||||||
{
|
|
||||||
variants: {
|
|
||||||
variant: {
|
|
||||||
default:
|
|
||||||
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
|
|
||||||
destructive:
|
|
||||||
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
|
||||||
outline:
|
|
||||||
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
|
||||||
secondary:
|
|
||||||
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
|
|
||||||
ghost:
|
|
||||||
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
|
||||||
link: "text-primary underline-offset-4 hover:underline",
|
|
||||||
},
|
|
||||||
size: {
|
|
||||||
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
|
||||||
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
|
||||||
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
|
||||||
icon: "size-9",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
defaultVariants: {
|
|
||||||
variant: "default",
|
|
||||||
size: "default",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
interface ButtonProps
|
|
||||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
|
||||||
VariantProps<typeof buttonVariants> {
|
|
||||||
asChild?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
||||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
|
||||||
if (asChild) {
|
|
||||||
return (
|
|
||||||
<Slot
|
|
||||||
className={cn(buttonVariants({ variant, size, className }))}
|
|
||||||
{...(props as any)}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
className={cn(buttonVariants({ variant, size, className }))}
|
|
||||||
ref={ref}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
Button.displayName = "Button"
|
|
||||||
|
|
||||||
export { Button, buttonVariants }
|
|
||||||
6
template/apps/client/src/hooks.server.ts
Normal file
6
template/apps/client/src/hooks.server.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import {createRequestHandler, setServerClient} from '@sanity/svelte-loader'
|
||||||
|
import {serverClient} from '$lib/server/sanity'
|
||||||
|
|
||||||
|
setServerClient(serverClient)
|
||||||
|
|
||||||
|
export const handle = createRequestHandler()
|
||||||
51
template/apps/client/src/lib/asset-to-url-client.ts
Normal file
51
template/apps/client/src/lib/asset-to-url-client.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import { getImageDimensions, type SanityImageDimensions } from '@sanity/asset-utils';
|
||||||
|
import type { ImageWithAlt } from './sanity.types';
|
||||||
|
import { generateImageUrl } from './image-url';
|
||||||
|
import { client } from './sanity';
|
||||||
|
|
||||||
|
export type SimpleImage = {
|
||||||
|
url: string;
|
||||||
|
alt: string;
|
||||||
|
dimensions: SanityImageDimensions;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Internal helper to fetch image data from Sanity (client-side)
|
||||||
|
async function fetchImage(assetRef: string | undefined): Promise<ImageWithAlt | null> {
|
||||||
|
if (!assetRef) return null;
|
||||||
|
|
||||||
|
const image = await client.fetch(`*[_id == $id][0]`, { id: assetRef });
|
||||||
|
return image;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Internal helper to get dimensions safely
|
||||||
|
function getDimensions(image: ImageWithAlt | any): SanityImageDimensions {
|
||||||
|
try {
|
||||||
|
if (image._type === 'imageWithAlt') {
|
||||||
|
const compatibleImage = { ...image, asset: image.asset };
|
||||||
|
return getImageDimensions(compatibleImage as any);
|
||||||
|
}
|
||||||
|
return getImageDimensions(image);
|
||||||
|
} catch {
|
||||||
|
return { width: 1200, height: 800, aspectRatio: 1.5 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert a single asset reference to SimpleImage (client-side)
|
||||||
|
export async function getImageClient(assetRef: string | undefined): Promise<SimpleImage> {
|
||||||
|
const image = await fetchImage(assetRef);
|
||||||
|
if (!image)
|
||||||
|
return { url: '', alt: '', dimensions: { width: 0, height: 0, aspectRatio: 1 } };
|
||||||
|
|
||||||
|
const dimensions = getDimensions(image);
|
||||||
|
|
||||||
|
return {
|
||||||
|
url: generateImageUrl(image),
|
||||||
|
alt: image.alt || '',
|
||||||
|
dimensions
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert multiple asset references to SimpleImage array (client-side)
|
||||||
|
export async function getImagesClient(assetRefs: (string | undefined)[]): Promise<SimpleImage[]> {
|
||||||
|
return Promise.all(assetRefs.map((ref) => getImageClient(ref)));
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { sanityFetch } from "@/sanity/live";
|
import { getImageDimensions, type SanityImageDimensions } from '@sanity/asset-utils';
|
||||||
import { getImageDimensions, SanityImageDimensions } from "@sanity/asset-utils";
|
import type { ImageWithAlt } from './sanity.types';
|
||||||
import { ImageWithAlt } from "@/sanity/sanity.types";
|
import { generateImageUrl } from './image-url';
|
||||||
import { generateImageUrl } from "./image-url";
|
import { serverClient } from './server/sanity';
|
||||||
|
|
||||||
export type SimpleImage = {
|
export type SimpleImage = {
|
||||||
url: string;
|
url: string;
|
||||||
@@ -13,17 +13,14 @@ export type SimpleImage = {
|
|||||||
async function fetchImage(assetRef: string | undefined): Promise<ImageWithAlt | null> {
|
async function fetchImage(assetRef: string | undefined): Promise<ImageWithAlt | null> {
|
||||||
if (!assetRef) return null;
|
if (!assetRef) return null;
|
||||||
|
|
||||||
const { data: image } = await sanityFetch({
|
const image = await serverClient.fetch(`*[_id == $id][0]`, { id: assetRef });
|
||||||
query: `*[_id == $id][0]`,
|
|
||||||
params: { id: assetRef },
|
|
||||||
});
|
|
||||||
return image;
|
return image;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Internal helper to get dimensions safely
|
// Internal helper to get dimensions safely
|
||||||
function getDimensions(image: ImageWithAlt | any): SanityImageDimensions {
|
function getDimensions(image: ImageWithAlt | any): SanityImageDimensions {
|
||||||
try {
|
try {
|
||||||
if (image._type === "imageWithAlt") {
|
if (image._type === 'imageWithAlt') {
|
||||||
const compatibleImage = { ...image, asset: image.asset };
|
const compatibleImage = { ...image, asset: image.asset };
|
||||||
return getImageDimensions(compatibleImage as any);
|
return getImageDimensions(compatibleImage as any);
|
||||||
}
|
}
|
||||||
@@ -33,22 +30,22 @@ function getDimensions(image: ImageWithAlt | any): SanityImageDimensions {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Convert a single asset reference to SimpleImage
|
// Convert a single asset reference to SimpleImage
|
||||||
export async function getImage(assetRef: string | undefined): Promise<SimpleImage> {
|
export async function getImage(assetRef: string | undefined): Promise<SimpleImage> {
|
||||||
const image = await fetchImage(assetRef);
|
const image = await fetchImage(assetRef);
|
||||||
if (!image) return { url: "", alt: "", dimensions: { width: 0, height: 0, aspectRatio: 1 } };
|
if (!image)
|
||||||
|
return { url: '', alt: '', dimensions: { width: 0, height: 0, aspectRatio: 1 } };
|
||||||
|
|
||||||
const dimensions = getDimensions(image);
|
const dimensions = getDimensions(image);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
url: generateImageUrl(image),
|
url: generateImageUrl(image),
|
||||||
alt: image.alt || "",
|
alt: image.alt || '',
|
||||||
dimensions,
|
dimensions
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert multiple asset references to SimpleImage array
|
// Convert multiple asset references to SimpleImage array
|
||||||
export async function getImages(assetRefs: (string | undefined)[]): Promise<SimpleImage[]> {
|
export async function getImages(assetRefs: (string | undefined)[]): Promise<SimpleImage[]> {
|
||||||
return Promise.all(assetRefs.map(ref => getImage(ref)));
|
return Promise.all(assetRefs.map((ref) => getImage(ref)));
|
||||||
}
|
}
|
||||||
|
|||||||
1
template/apps/client/src/lib/assets/favicon.svg
Normal file
1
template/apps/client/src/lib/assets/favicon.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="107" height="128" viewBox="0 0 107 128"><title>svelte-logo</title><path d="M94.157 22.819c-10.4-14.885-30.94-19.297-45.792-9.835L22.282 29.608A29.92 29.92 0 0 0 8.764 49.65a31.5 31.5 0 0 0 3.108 20.231 30 30 0 0 0-4.477 11.183 31.9 31.9 0 0 0 5.448 24.116c10.402 14.887 30.942 19.297 45.791 9.835l26.083-16.624A29.92 29.92 0 0 0 98.235 78.35a31.53 31.53 0 0 0-3.105-20.232 30 30 0 0 0 4.474-11.182 31.88 31.88 0 0 0-5.447-24.116" style="fill:#ff3e00"/><path d="M45.817 106.582a20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.503 18 18 0 0 1 .624-2.435l.49-1.498 1.337.981a33.6 33.6 0 0 0 10.203 5.098l.97.294-.09.968a5.85 5.85 0 0 0 1.052 3.878 6.24 6.24 0 0 0 6.695 2.485 5.8 5.8 0 0 0 1.603-.704L69.27 76.28a5.43 5.43 0 0 0 2.45-3.631 5.8 5.8 0 0 0-.987-4.371 6.24 6.24 0 0 0-6.698-2.487 5.7 5.7 0 0 0-1.6.704l-9.953 6.345a19 19 0 0 1-5.296 2.326 20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.502 17.99 17.99 0 0 1 8.13-12.052l26.081-16.623a19 19 0 0 1 5.3-2.329 20.72 20.72 0 0 1 22.237 8.243 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-.624 2.435l-.49 1.498-1.337-.98a33.6 33.6 0 0 0-10.203-5.1l-.97-.294.09-.968a5.86 5.86 0 0 0-1.052-3.878 6.24 6.24 0 0 0-6.696-2.485 5.8 5.8 0 0 0-1.602.704L37.73 51.72a5.42 5.42 0 0 0-2.449 3.63 5.79 5.79 0 0 0 .986 4.372 6.24 6.24 0 0 0 6.698 2.486 5.8 5.8 0 0 0 1.602-.704l9.952-6.342a19 19 0 0 1 5.295-2.328 20.72 20.72 0 0 1 22.237 8.242 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-8.13 12.053l-26.081 16.622a19 19 0 0 1-5.3 2.328" style="fill:#fff"/></svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,12 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { PortableText } from '@portabletext/svelte';
|
||||||
|
|
||||||
|
let { portableText } = $props();
|
||||||
|
const { value } = portableText;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if value?.content}
|
||||||
|
<div class="bg-secondary text-secondary-foreground shadow-lg rounded-md p-6 border my-6 border-gray-200">
|
||||||
|
<PortableText value={value.content} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
let { portableText, children } = $props();
|
||||||
|
const { value } = portableText;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<a
|
||||||
|
href={value.href}
|
||||||
|
target={value.blank ? '_blank' : '_self'}
|
||||||
|
rel={value.blank ? 'noopener noreferrer' : undefined}
|
||||||
|
class="text-primary hover:underline"
|
||||||
|
>
|
||||||
|
{@render children()}
|
||||||
|
</a>
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { deconstructLink } from '$lib/link-helper';
|
||||||
|
import LinkButton from '../link-button.svelte';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
|
let { portableText } = $props();
|
||||||
|
const { value: button } = portableText;
|
||||||
|
|
||||||
|
let linkData: { href: string; target: string } | null = $state(null);
|
||||||
|
let mounted = $state(false);
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
if (button?.link) {
|
||||||
|
linkData = await deconstructLink(button.link);
|
||||||
|
}
|
||||||
|
mounted = true;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if mounted && linkData}
|
||||||
|
<LinkButton
|
||||||
|
text={button?.text || ''}
|
||||||
|
{linkData}
|
||||||
|
variant="default"
|
||||||
|
size="default"
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { getFileAsset } from '@sanity/asset-utils';
|
||||||
|
import { Download } from '@lucide/svelte';
|
||||||
|
import { client } from '$lib/sanity';
|
||||||
|
|
||||||
|
let { portableText } = $props();
|
||||||
|
const { value } = portableText;
|
||||||
|
const { projectId, dataset } = client.config();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if value?.asset}
|
||||||
|
{@const file = getFileAsset(value, { projectId, dataset })}
|
||||||
|
<a href={file.url} download class="inline-flex items-center gap-2 text-primary hover:underline">
|
||||||
|
<Download size={16} />
|
||||||
|
{value.title || 'Download file'}
|
||||||
|
</a>
|
||||||
|
{/if}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { getImageDimensions } from '@sanity/asset-utils';
|
||||||
|
import { generateImageUrl, dynamicHeight } from '$lib/image-url';
|
||||||
|
|
||||||
|
let { portableText } = $props();
|
||||||
|
const { value, isInline } = portableText;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if value?.asset}
|
||||||
|
{@const image = value}
|
||||||
|
{@const dimensions = getImageDimensions(image)}
|
||||||
|
{@const calculatedHeight = dynamicHeight(dimensions.height, dimensions.width, isInline ?? false)}
|
||||||
|
{@const imageUrl = generateImageUrl(image, dimensions.width, dimensions.height)}
|
||||||
|
|
||||||
|
<img
|
||||||
|
src={imageUrl}
|
||||||
|
alt={image.alt || ''}
|
||||||
|
width={isInline ? 100 : Math.min(dimensions.width, 1200)}
|
||||||
|
height={calculatedHeight}
|
||||||
|
loading="lazy"
|
||||||
|
class={isInline ? 'inline-block' : 'block mx-auto my-4'}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
11
template/apps/client/src/lib/components/footer.svelte
Normal file
11
template/apps/client/src/lib/components/footer.svelte
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { Settings } from '$lib/sanity.types';
|
||||||
|
|
||||||
|
let { settings }: { settings: Settings } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-4">
|
||||||
|
<p class="text-sm pb-4 px-8">
|
||||||
|
{settings?.footer?.replace('{YEAR}', new Date().getFullYear().toString())}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
106
template/apps/client/src/lib/components/link-button.svelte
Normal file
106
template/apps/client/src/lib/components/link-button.svelte
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { cn } from '$lib/utils';
|
||||||
|
import { ArrowRight, ExternalLink } from '@lucide/svelte';
|
||||||
|
|
||||||
|
let {
|
||||||
|
text,
|
||||||
|
linkData,
|
||||||
|
variant = 'default',
|
||||||
|
size = 'default',
|
||||||
|
extraIcon,
|
||||||
|
showIcon = true,
|
||||||
|
className,
|
||||||
|
onPress,
|
||||||
|
...restProps
|
||||||
|
}: {
|
||||||
|
text: string;
|
||||||
|
linkData: { href: string; target: string } | null;
|
||||||
|
variant?: 'ghost' | 'default' | 'secondary' | 'link' | 'destructive' | 'outline';
|
||||||
|
size?: 'default' | 'sm' | 'lg' | 'icon';
|
||||||
|
extraIcon?: any;
|
||||||
|
showIcon?: boolean;
|
||||||
|
className?: string;
|
||||||
|
onPress?: () => void;
|
||||||
|
[key: string]: any;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
const isExternal = linkData?.href?.startsWith('http');
|
||||||
|
|
||||||
|
const baseClasses = cn(
|
||||||
|
'group',
|
||||||
|
'rounded-full',
|
||||||
|
'font-semibold',
|
||||||
|
'transition-all',
|
||||||
|
'duration-300',
|
||||||
|
'no-underline',
|
||||||
|
'flex',
|
||||||
|
'items-center',
|
||||||
|
'gap-2',
|
||||||
|
'inline-flex',
|
||||||
|
'justify-center',
|
||||||
|
'whitespace-nowrap',
|
||||||
|
'text-sm',
|
||||||
|
'font-medium',
|
||||||
|
'ring-offset-background',
|
||||||
|
'transition-colors',
|
||||||
|
'focus-visible:outline-none',
|
||||||
|
'focus-visible:ring-2',
|
||||||
|
'focus-visible:ring-ring',
|
||||||
|
'focus-visible:ring-offset-2',
|
||||||
|
'disabled:pointer-events-none',
|
||||||
|
'disabled:opacity-50',
|
||||||
|
showIcon ? 'pr-4' : 'px-6',
|
||||||
|
'active:transform',
|
||||||
|
'active:translate-y-[1px]',
|
||||||
|
{
|
||||||
|
// Variant styles
|
||||||
|
'bg-primary text-primary-foreground hover:bg-primary/90': variant === 'default',
|
||||||
|
'bg-destructive text-destructive-foreground hover:bg-destructive/90':
|
||||||
|
variant === 'destructive',
|
||||||
|
'border border-input bg-background hover:bg-accent hover:text-accent-foreground':
|
||||||
|
variant === 'outline',
|
||||||
|
'bg-secondary text-secondary-foreground hover:bg-secondary/80': variant === 'secondary',
|
||||||
|
'hover:bg-accent hover:text-accent-foreground': variant === 'ghost',
|
||||||
|
'text-primary underline-offset-4 hover:underline': variant === 'link'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Size styles
|
||||||
|
'h-10 px-4 py-2': size === 'default',
|
||||||
|
'h-9 rounded-md px-3': size === 'sm',
|
||||||
|
'h-11 rounded-md px-8': size === 'lg',
|
||||||
|
'h-10 w-10': size === 'icon'
|
||||||
|
},
|
||||||
|
className
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if linkData}
|
||||||
|
<a
|
||||||
|
href={linkData.href}
|
||||||
|
target={linkData?.target ?? '_self'}
|
||||||
|
rel={isExternal ? 'noopener noreferrer' : undefined}
|
||||||
|
onclick={onPress}
|
||||||
|
class={baseClasses}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{#if extraIcon}
|
||||||
|
<span class="transition-transform duration-300 group-hover:scale-110">
|
||||||
|
{@render extraIcon({ size: size === 'lg' ? 24 : 20 })}
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<span>{text}</span>
|
||||||
|
|
||||||
|
{#if showIcon}
|
||||||
|
<span
|
||||||
|
class="transition-all duration-300 group-hover:transform group-hover:translate-x-1"
|
||||||
|
>
|
||||||
|
{#if isExternal}
|
||||||
|
<ExternalLink size={size === 'lg' ? 24 : 20} />
|
||||||
|
{:else}
|
||||||
|
<ArrowRight strokeWidth={3} size={size === 'lg' ? 24 : 20} />
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
</a>
|
||||||
|
{/if}
|
||||||
29
template/apps/client/src/lib/components/sanity-block.svelte
Normal file
29
template/apps/client/src/lib/components/sanity-block.svelte
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { PortableText } from '@portabletext/svelte';
|
||||||
|
import type { BlockContent } from '$lib/sanity.types';
|
||||||
|
import SanityImage from './blocks/sanity-image.svelte';
|
||||||
|
import SanityFile from './blocks/sanity-file.svelte';
|
||||||
|
import SanityButton from './blocks/sanity-button.svelte';
|
||||||
|
import Callout from './blocks/callout.svelte';
|
||||||
|
import LinkMark from './blocks/link-mark.svelte';
|
||||||
|
|
||||||
|
let { body }: { body: BlockContent } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="prose prose-lg max-w-none">
|
||||||
|
<PortableText
|
||||||
|
value={body}
|
||||||
|
components={{
|
||||||
|
types: {
|
||||||
|
callout: Callout,
|
||||||
|
image: SanityImage,
|
||||||
|
imageWithAlt: SanityImage,
|
||||||
|
file: SanityFile,
|
||||||
|
button: SanityButton
|
||||||
|
},
|
||||||
|
marks: {
|
||||||
|
link: LinkMark
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
84
template/apps/client/src/lib/components/section/cta.svelte
Normal file
84
template/apps/client/src/lib/components/section/cta.svelte
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import LinkButton from '../link-button.svelte';
|
||||||
|
import type { CtaSection } from '$lib/sanity.types';
|
||||||
|
import { deconstructLink } from '$lib/link-helper';
|
||||||
|
import type { SimpleImage } from '$lib/asset-to-url';
|
||||||
|
import { cn } from '$lib/utils';
|
||||||
|
import { generateImageUrl } from '$lib/image-url';
|
||||||
|
import SanityBlock from '../sanity-block.svelte';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
|
interface CTAProps {
|
||||||
|
cta?: CtaSection;
|
||||||
|
sectionTitle?: string;
|
||||||
|
background: SimpleImage | any;
|
||||||
|
backgroundColor?: string;
|
||||||
|
textColor?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
let {
|
||||||
|
cta,
|
||||||
|
background,
|
||||||
|
sectionTitle,
|
||||||
|
backgroundColor = 'bg-gray-900/80',
|
||||||
|
textColor = 'text-white'
|
||||||
|
}: CTAProps = $props();
|
||||||
|
|
||||||
|
let linkData: { href: string; target: string } | null = $state(null);
|
||||||
|
let mounted = $state(false);
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
if (cta?.button?.link) {
|
||||||
|
linkData = await deconstructLink(cta.button.link);
|
||||||
|
}
|
||||||
|
mounted = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<section
|
||||||
|
class={cn(
|
||||||
|
'min-h-screen flex flex-col md:flex-row w-full overflow-hidden relative',
|
||||||
|
textColor
|
||||||
|
)}
|
||||||
|
style:background-image={background?.url ? `url(${background.url})` : undefined}
|
||||||
|
style:background-size="cover"
|
||||||
|
style:background-position="center"
|
||||||
|
style:background-repeat="no-repeat"
|
||||||
|
aria-label={background?.alt}
|
||||||
|
>
|
||||||
|
<div class={cn('absolute inset-0 z-0', backgroundColor)}></div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="z-10 flex flex-col items-center md:items-start justify-center md:justify-start mt-20 px-6 md:px-20 md:py-32 md:flex-1 space-y-12 min-h-screen md:min-h-0"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
{#if sectionTitle}
|
||||||
|
<p class="text-sm mb-4">
|
||||||
|
{sectionTitle}
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
<h1 class="md:max-w-[60rem] text-6xl md:text-8xl font-bold leading-tight">
|
||||||
|
{cta?.title}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col items-start space-y-8 md:space-y-14 w-full max-w-xl">
|
||||||
|
{#if cta?.description}
|
||||||
|
<div class="text-lg">
|
||||||
|
<SanityBlock body={cta.description} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if mounted && linkData}
|
||||||
|
<div class="mb-20">
|
||||||
|
<LinkButton
|
||||||
|
text={cta?.button?.text ?? ''}
|
||||||
|
{linkData}
|
||||||
|
size="lg"
|
||||||
|
variant="default"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
<script lang="ts" module>
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
import type { HTMLAnchorAttributes, HTMLButtonAttributes } from "svelte/elements";
|
||||||
|
import { type VariantProps, tv } from "tailwind-variants";
|
||||||
|
|
||||||
|
export const buttonVariants = tv({
|
||||||
|
base: "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex shrink-0 items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium outline-none transition-all focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
|
||||||
|
destructive:
|
||||||
|
"bg-destructive shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60 text-white",
|
||||||
|
outline:
|
||||||
|
"bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 border",
|
||||||
|
secondary: "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
|
||||||
|
ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||||
|
link: "text-primary underline-offset-4 hover:underline",
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
||||||
|
sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5",
|
||||||
|
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||||
|
icon: "size-9",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
size: "default",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export type ButtonVariant = VariantProps<typeof buttonVariants>["variant"];
|
||||||
|
export type ButtonSize = VariantProps<typeof buttonVariants>["size"];
|
||||||
|
|
||||||
|
export type ButtonProps = WithElementRef<HTMLButtonAttributes> &
|
||||||
|
WithElementRef<HTMLAnchorAttributes> & {
|
||||||
|
variant?: ButtonVariant;
|
||||||
|
size?: ButtonSize;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
let {
|
||||||
|
class: className,
|
||||||
|
variant = "default",
|
||||||
|
size = "default",
|
||||||
|
ref = $bindable(null),
|
||||||
|
href = undefined,
|
||||||
|
type = "button",
|
||||||
|
disabled,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: ButtonProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if href}
|
||||||
|
<a
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="button"
|
||||||
|
class={cn(buttonVariants({ variant, size }), className)}
|
||||||
|
href={disabled ? undefined : href}
|
||||||
|
aria-disabled={disabled}
|
||||||
|
role={disabled ? "link" : undefined}
|
||||||
|
tabindex={disabled ? -1 : undefined}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</a>
|
||||||
|
{:else}
|
||||||
|
<button
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="button"
|
||||||
|
class={cn(buttonVariants({ variant, size }), className)}
|
||||||
|
{type}
|
||||||
|
{disabled}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
17
template/apps/client/src/lib/components/ui/button/index.ts
Normal file
17
template/apps/client/src/lib/components/ui/button/index.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import Root, {
|
||||||
|
type ButtonProps,
|
||||||
|
type ButtonSize,
|
||||||
|
type ButtonVariant,
|
||||||
|
buttonVariants,
|
||||||
|
} from "./button.svelte";
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
type ButtonProps as Props,
|
||||||
|
//
|
||||||
|
Root as Button,
|
||||||
|
buttonVariants,
|
||||||
|
type ButtonProps,
|
||||||
|
type ButtonSize,
|
||||||
|
type ButtonVariant,
|
||||||
|
};
|
||||||
21
template/apps/client/src/lib/image-helper.ts
Normal file
21
template/apps/client/src/lib/image-helper.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import type { SanityImageDimensions } from '@sanity/asset-utils';
|
||||||
|
|
||||||
|
export type SimpleImage = {
|
||||||
|
url: string;
|
||||||
|
alt: string;
|
||||||
|
dimensions: SanityImageDimensions;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getImageFromAsset(imageData: any): SimpleImage | null {
|
||||||
|
if (!imageData?.asset) return null;
|
||||||
|
|
||||||
|
if ('url' in imageData.asset) {
|
||||||
|
return {
|
||||||
|
url: imageData.asset.url,
|
||||||
|
alt: imageData.alt || '',
|
||||||
|
dimensions: imageData.asset.metadata?.dimensions || { width: 1920, height: 1080, aspectRatio: 1.78 }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
@@ -1,78 +1,58 @@
|
|||||||
import { client } from "@/sanity/client";
|
import { client } from './sanity';
|
||||||
import { ImageWithAlt, SanityImageAsset } from "@/sanity/sanity.types";
|
import type { ImageWithAlt, SanityImageAsset } from './sanity.types';
|
||||||
|
import imageUrlBuilder from '@sanity/image-url';
|
||||||
|
|
||||||
import imageUrlBuilder from "@sanity/image-url";
|
|
||||||
const { projectId, dataset } = client.config();
|
const { projectId, dataset } = client.config();
|
||||||
const builder = imageUrlBuilder({ projectId: projectId ?? "", dataset: dataset ?? "" });
|
const builder = imageUrlBuilder({ projectId: projectId ?? '', dataset: dataset ?? '' });
|
||||||
|
|
||||||
|
console.log('Image URL Builder initialized with:', { projectId, dataset });
|
||||||
|
|
||||||
export function generateImageUrl(image: ImageWithAlt | any, width?: number, height?: number): string {
|
export function generateImageUrl(image: ImageWithAlt | any, width?: number, height?: number): string {
|
||||||
if (!image || !projectId || !dataset) return "";
|
if (!image || !projectId || !dataset) {
|
||||||
if (image.url && image.crop && image.dimensions && width && height) {
|
console.log('No image, projectId, or dataset:', { image, projectId, dataset });
|
||||||
const imageRef = image.asset?._ref || image.asset;
|
return '';
|
||||||
|
|
||||||
if (imageRef) {
|
|
||||||
const crop = image.crop;
|
|
||||||
|
|
||||||
let imageBuilder = builder
|
|
||||||
.image(imageRef)
|
|
||||||
.fit("max")
|
|
||||||
.width(1920)
|
|
||||||
.format("webp")
|
|
||||||
.auto("format");
|
|
||||||
|
|
||||||
if (crop && (crop.top || crop.bottom || crop.left || crop.right)) {
|
|
||||||
const croppedWidth = Math.floor(width * (1 - (crop.right + crop.left)));
|
|
||||||
const croppedHeight = Math.floor(height * (1 - (crop.top + crop.bottom)));
|
|
||||||
|
|
||||||
const left = Math.floor(width * crop.left);
|
|
||||||
const top = Math.floor(height * crop.top);
|
|
||||||
|
|
||||||
imageBuilder = imageBuilder.rect(left, top, croppedWidth, croppedHeight);
|
|
||||||
} else {
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = imageBuilder.url();
|
// Handle direct URL
|
||||||
return url;
|
if (image.url && !image.asset) {
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (image.url && !image.crop) {
|
|
||||||
return image.url;
|
return image.url;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (image._type === "imageWithAlt" || image.asset) {
|
// Handle Sanity image object
|
||||||
const imageRef = image.asset?._ref || image.asset;
|
const imageRef = image.asset?._ref || image.asset?._id || image.asset;
|
||||||
const crop = image.crop;
|
|
||||||
|
|
||||||
|
if (!imageRef) {
|
||||||
|
console.log('No imageRef found:', image);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
let imageBuilder = builder
|
let imageBuilder = builder
|
||||||
.image(imageRef)
|
.image(imageRef)
|
||||||
.fit("max")
|
.fit('max')
|
||||||
.width(1920)
|
.width(width || 1920)
|
||||||
.format("webp")
|
.format('webp')
|
||||||
.auto("format");
|
.auto('format');
|
||||||
|
|
||||||
if (crop && (crop.top || crop.bottom || crop.left || crop.right) && width && height) {
|
// Apply cropping if available
|
||||||
|
if (image.crop && width && height) {
|
||||||
|
const crop = image.crop;
|
||||||
|
if (crop.top || crop.bottom || crop.left || crop.right) {
|
||||||
const croppedWidth = Math.floor(width * (1 - (crop.right + crop.left)));
|
const croppedWidth = Math.floor(width * (1 - (crop.right + crop.left)));
|
||||||
const croppedHeight = Math.floor(height * (1 - (crop.top + crop.bottom)));
|
const croppedHeight = Math.floor(height * (1 - (crop.top + crop.bottom)));
|
||||||
|
|
||||||
const left = Math.floor(width * crop.left);
|
const left = Math.floor(width * crop.left);
|
||||||
const top = Math.floor(height * crop.top);
|
const top = Math.floor(height * crop.top);
|
||||||
|
|
||||||
imageBuilder = imageBuilder.rect(left, top, croppedWidth, croppedHeight);
|
imageBuilder = imageBuilder.rect(left, top, croppedWidth, croppedHeight);
|
||||||
}
|
}
|
||||||
const url = imageBuilder.url();
|
|
||||||
return url;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (image._type === "imageWithAlt" && !image.asset) return "";
|
const url = imageBuilder.url();
|
||||||
|
console.log('Generated URL:', url);
|
||||||
return builder
|
return url;
|
||||||
.image(image as any)
|
} catch (error) {
|
||||||
.fit("max")
|
console.error('Error generating image URL:', error);
|
||||||
.width(1920)
|
return '';
|
||||||
.format("webp")
|
}
|
||||||
.auto("format")
|
|
||||||
.url();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function dynamicHeight(
|
export function dynamicHeight(
|
||||||
|
|||||||
1
template/apps/client/src/lib/index.ts
Normal file
1
template/apps/client/src/lib/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
// place files you want to import through the `$lib` alias in this folder.
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { deconstructLink, resolveHref } from "./link-helper";
|
|
||||||
import { Link } from "@/sanity/sanity.types";
|
|
||||||
|
|
||||||
interface InternalLink {
|
|
||||||
_ref: string;
|
|
||||||
_type: string;
|
|
||||||
_weak?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const useResolveHref = (internalLink: InternalLink) => {
|
|
||||||
const [href, setHref] = useState<{ type: string; slug?: string } | null>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchHref = async () => {
|
|
||||||
const resolvedHref = await resolveHref(internalLink);
|
|
||||||
setHref(resolvedHref);
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchHref();
|
|
||||||
}, [internalLink]);
|
|
||||||
|
|
||||||
return href;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const useDeconstructLink = (link?: Link) => {
|
|
||||||
const [deconstLink, setdeconstLink] = useState<{
|
|
||||||
href: string;
|
|
||||||
target: string;
|
|
||||||
} | null>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!link) return;
|
|
||||||
|
|
||||||
const fetchHref = async () => {
|
|
||||||
const resolvedLink = await deconstructLink(link);
|
|
||||||
setdeconstLink(resolvedLink);
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchHref();
|
|
||||||
}, [link]);
|
|
||||||
|
|
||||||
return deconstLink;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export { useResolveHref, useDeconstructLink };
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import type { Link } from "@/sanity/sanity.types";
|
import type { Link } from './sanity.types';
|
||||||
import { client } from "@/sanity/client";
|
import { client } from './sanity';
|
||||||
import { defineQuery } from "next-sanity";
|
|
||||||
|
|
||||||
interface InternalLink {
|
interface InternalLink {
|
||||||
_ref: string;
|
_ref: string;
|
||||||
@@ -11,52 +10,53 @@ interface InternalLink {
|
|||||||
export const resolveHref = async (internalLink: InternalLink) => {
|
export const resolveHref = async (internalLink: InternalLink) => {
|
||||||
if (!internalLink || !internalLink._ref) return null;
|
if (!internalLink || !internalLink._ref) return null;
|
||||||
|
|
||||||
const QUERY = defineQuery('*[_id == $ref][0]{ _type, slug }');
|
const QUERY = '*[_id == $ref][0]{ _type, slug }';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result: any = await client.fetch(QUERY, { ref: internalLink._ref });
|
const result: any = await client.fetch(QUERY, { ref: internalLink._ref });
|
||||||
return result ? { type: result._type, slug: result.slug?.current } : null;
|
return result ? { type: result._type, slug: result.slug?.current } : null;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Sanity query failed:", error);
|
console.error('Sanity query failed:', error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function deconstructLink(
|
export async function deconstructLink(
|
||||||
link?: Link,
|
link?: Link
|
||||||
): Promise<{ href: string; target: string } | null> {
|
): Promise<{ href: string; target: string } | null> {
|
||||||
if (!link) return null;
|
if (!link) return null;
|
||||||
|
|
||||||
const { type, value, anchor, parameters, blank, url, email, phone, internalLink } = link;
|
const { type, value, anchor, parameters, blank, url, email, phone, internalLink } = link;
|
||||||
const target = blank ? "_blank" : "";
|
const target = blank ? '_blank' : '';
|
||||||
let href = "";
|
let href: string | null = null;
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case "static":
|
case 'static':
|
||||||
href = `${value || ""}${anchor || ""}${parameters || ""}`;
|
href = `${value || ''}${anchor || ''}${parameters || ''}`;
|
||||||
break;
|
break;
|
||||||
case "external":
|
case 'external':
|
||||||
href = `${url || ""}${anchor || ""}${parameters || ""}`;
|
href = `${url || ''}${anchor || ''}${parameters || ''}`;
|
||||||
break;
|
break;
|
||||||
case "email":
|
case 'email':
|
||||||
href = `mailto:${email || ""}`;
|
href = `mailto:${email || ''}`;
|
||||||
break;
|
break;
|
||||||
case "phone":
|
case 'phone':
|
||||||
href = `tel:${phone || ""}`;
|
href = `tel:${phone || ''}`;
|
||||||
break;
|
break;
|
||||||
case "internal":
|
case 'internal':
|
||||||
if (internalLink) {
|
if (internalLink) {
|
||||||
const resolved = await resolveHref(internalLink)
|
const resolved = await resolveHref(internalLink);
|
||||||
if (resolved) {
|
if (resolved) {
|
||||||
href = resolved.type === "custom"
|
href =
|
||||||
? `/${resolved.slug || ""}`
|
resolved.type === 'custom'
|
||||||
: `/${resolved.type}/${resolved.slug || ""}`;
|
? `/${resolved.slug || ''}`
|
||||||
|
: `/${resolved.type}/${resolved.slug || ''}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
if (!href) return null;
|
||||||
return { href, target };
|
return { href, target };
|
||||||
}
|
}
|
||||||
53
template/apps/client/src/lib/queries.ts
Normal file
53
template/apps/client/src/lib/queries.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import type { Home } from './sanity.types'
|
||||||
|
|
||||||
|
export type HomeResult = Home
|
||||||
|
|
||||||
|
export const homeQuery = `*[_type == "home"][0]{
|
||||||
|
_id,
|
||||||
|
_type,
|
||||||
|
_createdAt,
|
||||||
|
_updatedAt,
|
||||||
|
_rev,
|
||||||
|
title,
|
||||||
|
headerSection{
|
||||||
|
_type,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
backgroundImage{
|
||||||
|
asset->{
|
||||||
|
_id,
|
||||||
|
_ref,
|
||||||
|
_type,
|
||||||
|
url,
|
||||||
|
metadata {
|
||||||
|
dimensions {
|
||||||
|
width,
|
||||||
|
height
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
crop,
|
||||||
|
hotspot,
|
||||||
|
alt
|
||||||
|
},
|
||||||
|
button{
|
||||||
|
_type,
|
||||||
|
text,
|
||||||
|
link{
|
||||||
|
_type,
|
||||||
|
type,
|
||||||
|
value,
|
||||||
|
anchor,
|
||||||
|
parameters,
|
||||||
|
blank,
|
||||||
|
url,
|
||||||
|
email,
|
||||||
|
phone,
|
||||||
|
internalLink->{
|
||||||
|
_ref,
|
||||||
|
_type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`
|
||||||
15
template/apps/client/src/lib/sanity.ts
Normal file
15
template/apps/client/src/lib/sanity.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { sanityConnection } from '@repo/sanity-connection';
|
||||||
|
import { createClient } from '@sanity/client';
|
||||||
|
|
||||||
|
export const client = createClient({
|
||||||
|
projectId: sanityConnection.projectId,
|
||||||
|
dataset: sanityConnection.dataset,
|
||||||
|
apiVersion: '2024-12-01',
|
||||||
|
useCdn: false,
|
||||||
|
token: sanityConnection.publicViewerToken,
|
||||||
|
ignoreBrowserTokenWarning: true,
|
||||||
|
stega: {
|
||||||
|
enabled: true,
|
||||||
|
studioUrl: sanityConnection.studioUrl
|
||||||
|
}
|
||||||
|
});
|
||||||
575
template/apps/client/src/lib/sanity.types.ts
Normal file
575
template/apps/client/src/lib/sanity.types.ts
Normal file
@@ -0,0 +1,575 @@
|
|||||||
|
/**
|
||||||
|
* ---------------------------------------------------------------------------------
|
||||||
|
* This file has been generated by Sanity TypeGen.
|
||||||
|
* Command: `sanity typegen generate`
|
||||||
|
*
|
||||||
|
* Any modifications made directly to this file will be overwritten the next time
|
||||||
|
* the TypeScript definitions are generated. Please make changes to the Sanity
|
||||||
|
* schema definitions and/or GROQ queries if you need to update these types.
|
||||||
|
*
|
||||||
|
* For more information on how to use Sanity TypeGen, visit the official documentation:
|
||||||
|
* https://www.sanity.io/docs/sanity-typegen
|
||||||
|
* ---------------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Source: schema.json
|
||||||
|
export type Home = {
|
||||||
|
_id: string;
|
||||||
|
_type: 'home';
|
||||||
|
_createdAt: string;
|
||||||
|
_updatedAt: string;
|
||||||
|
_rev: string;
|
||||||
|
title?: string;
|
||||||
|
headerSection?: CtaSection;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FaqSection = {
|
||||||
|
_type: 'faqSection';
|
||||||
|
sectionTitle?: string;
|
||||||
|
title?: string;
|
||||||
|
faqs?: Array<
|
||||||
|
{
|
||||||
|
_key: string;
|
||||||
|
} & Faq
|
||||||
|
>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CtaSection = {
|
||||||
|
_type: 'ctaSection';
|
||||||
|
backgroundImage?: {
|
||||||
|
asset?: {
|
||||||
|
_ref: string;
|
||||||
|
_type: 'reference';
|
||||||
|
_weak?: boolean;
|
||||||
|
[internalGroqTypeReferenceTo]?: 'sanity.imageAsset';
|
||||||
|
};
|
||||||
|
media?: unknown;
|
||||||
|
hotspot?: SanityImageHotspot;
|
||||||
|
crop?: SanityImageCrop;
|
||||||
|
alt?: string;
|
||||||
|
_type: 'imageWithAlt';
|
||||||
|
};
|
||||||
|
title?: string;
|
||||||
|
description?: Array<
|
||||||
|
| {
|
||||||
|
children?: Array<{
|
||||||
|
marks?: Array<string>;
|
||||||
|
text?: string;
|
||||||
|
_type: 'span';
|
||||||
|
_key: string;
|
||||||
|
}>;
|
||||||
|
style?: 'normal' | 'h1' | 'h2' | 'h3' | 'h4' | 'blockquote';
|
||||||
|
listItem?: 'bullet';
|
||||||
|
markDefs?: Array<
|
||||||
|
| {
|
||||||
|
href?: Link;
|
||||||
|
_type: 'link';
|
||||||
|
_key: string;
|
||||||
|
}
|
||||||
|
| ({
|
||||||
|
_key: string;
|
||||||
|
} & TextColor)
|
||||||
|
| ({
|
||||||
|
_key: string;
|
||||||
|
} & HighlightColor)
|
||||||
|
>;
|
||||||
|
level?: number;
|
||||||
|
_type: 'block';
|
||||||
|
_key: string;
|
||||||
|
}
|
||||||
|
| ({
|
||||||
|
_key: string;
|
||||||
|
} & Button)
|
||||||
|
| {
|
||||||
|
asset?: {
|
||||||
|
_ref: string;
|
||||||
|
_type: 'reference';
|
||||||
|
_weak?: boolean;
|
||||||
|
[internalGroqTypeReferenceTo]?: 'sanity.imageAsset';
|
||||||
|
};
|
||||||
|
media?: unknown;
|
||||||
|
hotspot?: SanityImageHotspot;
|
||||||
|
crop?: SanityImageCrop;
|
||||||
|
_type: 'image';
|
||||||
|
_key: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
asset?: {
|
||||||
|
_ref: string;
|
||||||
|
_type: 'reference';
|
||||||
|
_weak?: boolean;
|
||||||
|
[internalGroqTypeReferenceTo]?: 'sanity.fileAsset';
|
||||||
|
};
|
||||||
|
media?: unknown;
|
||||||
|
_type: 'file';
|
||||||
|
_key: string;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
button?: Button;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ContactSection = {
|
||||||
|
_type: 'contactSection';
|
||||||
|
title?: string;
|
||||||
|
contactMethods?: Array<{
|
||||||
|
type?: 'email' | 'phone' | 'address' | 'social';
|
||||||
|
label?: string;
|
||||||
|
value?: string;
|
||||||
|
link?: Link;
|
||||||
|
_key: string;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ImageWithAlt = {
|
||||||
|
_type: 'imageWithAlt';
|
||||||
|
asset?: {
|
||||||
|
_ref: string;
|
||||||
|
_type: 'reference';
|
||||||
|
_weak?: boolean;
|
||||||
|
[internalGroqTypeReferenceTo]?: 'sanity.imageAsset';
|
||||||
|
};
|
||||||
|
media?: unknown;
|
||||||
|
hotspot?: SanityImageHotspot;
|
||||||
|
crop?: SanityImageCrop;
|
||||||
|
alt?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Faq = {
|
||||||
|
_type: 'faq';
|
||||||
|
question?: string;
|
||||||
|
answer?: Array<
|
||||||
|
| {
|
||||||
|
children?: Array<{
|
||||||
|
marks?: Array<string>;
|
||||||
|
text?: string;
|
||||||
|
_type: 'span';
|
||||||
|
_key: string;
|
||||||
|
}>;
|
||||||
|
style?: 'normal' | 'h1' | 'h2' | 'h3' | 'h4' | 'blockquote';
|
||||||
|
listItem?: 'bullet';
|
||||||
|
markDefs?: Array<
|
||||||
|
| {
|
||||||
|
href?: Link;
|
||||||
|
_type: 'link';
|
||||||
|
_key: string;
|
||||||
|
}
|
||||||
|
| ({
|
||||||
|
_key: string;
|
||||||
|
} & TextColor)
|
||||||
|
| ({
|
||||||
|
_key: string;
|
||||||
|
} & HighlightColor)
|
||||||
|
>;
|
||||||
|
level?: number;
|
||||||
|
_type: 'block';
|
||||||
|
_key: string;
|
||||||
|
}
|
||||||
|
| ({
|
||||||
|
_key: string;
|
||||||
|
} & Button)
|
||||||
|
| {
|
||||||
|
asset?: {
|
||||||
|
_ref: string;
|
||||||
|
_type: 'reference';
|
||||||
|
_weak?: boolean;
|
||||||
|
[internalGroqTypeReferenceTo]?: 'sanity.imageAsset';
|
||||||
|
};
|
||||||
|
media?: unknown;
|
||||||
|
hotspot?: SanityImageHotspot;
|
||||||
|
crop?: SanityImageCrop;
|
||||||
|
_type: 'image';
|
||||||
|
_key: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
asset?: {
|
||||||
|
_ref: string;
|
||||||
|
_type: 'reference';
|
||||||
|
_weak?: boolean;
|
||||||
|
[internalGroqTypeReferenceTo]?: 'sanity.fileAsset';
|
||||||
|
};
|
||||||
|
media?: unknown;
|
||||||
|
_type: 'file';
|
||||||
|
_key: string;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Button = {
|
||||||
|
_type: 'button';
|
||||||
|
text?: string;
|
||||||
|
link?: Link;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Settings = {
|
||||||
|
_id: string;
|
||||||
|
_type: 'settings';
|
||||||
|
_createdAt: string;
|
||||||
|
_updatedAt: string;
|
||||||
|
_rev: string;
|
||||||
|
title?: string;
|
||||||
|
longTitle?: string;
|
||||||
|
description?: string;
|
||||||
|
logo?: {
|
||||||
|
asset?: {
|
||||||
|
_ref: string;
|
||||||
|
_type: 'reference';
|
||||||
|
_weak?: boolean;
|
||||||
|
[internalGroqTypeReferenceTo]?: 'sanity.imageAsset';
|
||||||
|
};
|
||||||
|
media?: unknown;
|
||||||
|
hotspot?: SanityImageHotspot;
|
||||||
|
crop?: SanityImageCrop;
|
||||||
|
_type: 'image';
|
||||||
|
};
|
||||||
|
favicon?: {
|
||||||
|
asset?: {
|
||||||
|
_ref: string;
|
||||||
|
_type: 'reference';
|
||||||
|
_weak?: boolean;
|
||||||
|
[internalGroqTypeReferenceTo]?: 'sanity.imageAsset';
|
||||||
|
};
|
||||||
|
media?: unknown;
|
||||||
|
hotspot?: SanityImageHotspot;
|
||||||
|
crop?: SanityImageCrop;
|
||||||
|
_type: 'image';
|
||||||
|
};
|
||||||
|
footer?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BlockContent = Array<
|
||||||
|
| {
|
||||||
|
children?: Array<{
|
||||||
|
marks?: Array<string>;
|
||||||
|
text?: string;
|
||||||
|
_type: 'span';
|
||||||
|
_key: string;
|
||||||
|
}>;
|
||||||
|
style?: 'normal' | 'h1' | 'h2' | 'h3' | 'h4' | 'blockquote';
|
||||||
|
listItem?: 'bullet';
|
||||||
|
markDefs?: Array<
|
||||||
|
| {
|
||||||
|
href?: Link;
|
||||||
|
_type: 'link';
|
||||||
|
_key: string;
|
||||||
|
}
|
||||||
|
| ({
|
||||||
|
_key: string;
|
||||||
|
} & TextColor)
|
||||||
|
| ({
|
||||||
|
_key: string;
|
||||||
|
} & HighlightColor)
|
||||||
|
>;
|
||||||
|
level?: number;
|
||||||
|
_type: 'block';
|
||||||
|
_key: string;
|
||||||
|
}
|
||||||
|
| ({
|
||||||
|
_key: string;
|
||||||
|
} & Button)
|
||||||
|
| {
|
||||||
|
asset?: {
|
||||||
|
_ref: string;
|
||||||
|
_type: 'reference';
|
||||||
|
_weak?: boolean;
|
||||||
|
[internalGroqTypeReferenceTo]?: 'sanity.imageAsset';
|
||||||
|
};
|
||||||
|
media?: unknown;
|
||||||
|
hotspot?: SanityImageHotspot;
|
||||||
|
crop?: SanityImageCrop;
|
||||||
|
_type: 'image';
|
||||||
|
_key: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
asset?: {
|
||||||
|
_ref: string;
|
||||||
|
_type: 'reference';
|
||||||
|
_weak?: boolean;
|
||||||
|
[internalGroqTypeReferenceTo]?: 'sanity.fileAsset';
|
||||||
|
};
|
||||||
|
media?: unknown;
|
||||||
|
_type: 'file';
|
||||||
|
_key: string;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type HighlightColor = {
|
||||||
|
_type: 'highlightColor';
|
||||||
|
label?: string;
|
||||||
|
value?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TextColor = {
|
||||||
|
_type: 'textColor';
|
||||||
|
label?: string;
|
||||||
|
value?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SimplerColor = {
|
||||||
|
_type: 'simplerColor';
|
||||||
|
label?: string;
|
||||||
|
value?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MetaTag = {
|
||||||
|
_type: 'metaTag';
|
||||||
|
metaAttributes?: Array<
|
||||||
|
{
|
||||||
|
_key: string;
|
||||||
|
} & MetaAttribute
|
||||||
|
>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MetaAttribute = {
|
||||||
|
_type: 'metaAttribute';
|
||||||
|
attributeKey?: string;
|
||||||
|
attributeType?: 'string' | 'image';
|
||||||
|
attributeValueImage?: {
|
||||||
|
asset?: {
|
||||||
|
_ref: string;
|
||||||
|
_type: 'reference';
|
||||||
|
_weak?: boolean;
|
||||||
|
[internalGroqTypeReferenceTo]?: 'sanity.imageAsset';
|
||||||
|
};
|
||||||
|
media?: unknown;
|
||||||
|
hotspot?: SanityImageHotspot;
|
||||||
|
crop?: SanityImageCrop;
|
||||||
|
_type: 'image';
|
||||||
|
};
|
||||||
|
attributeValueString?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SeoMetaFields = {
|
||||||
|
_type: 'seoMetaFields';
|
||||||
|
nofollowAttributes?: boolean;
|
||||||
|
metaTitle?: string;
|
||||||
|
metaDescription?: string;
|
||||||
|
metaImage?: {
|
||||||
|
asset?: {
|
||||||
|
_ref: string;
|
||||||
|
_type: 'reference';
|
||||||
|
_weak?: boolean;
|
||||||
|
[internalGroqTypeReferenceTo]?: 'sanity.imageAsset';
|
||||||
|
};
|
||||||
|
media?: unknown;
|
||||||
|
hotspot?: SanityImageHotspot;
|
||||||
|
crop?: SanityImageCrop;
|
||||||
|
_type: 'image';
|
||||||
|
};
|
||||||
|
seoKeywords?: Array<string>;
|
||||||
|
openGraph?: OpenGraph;
|
||||||
|
additionalMetaTags?: Array<
|
||||||
|
{
|
||||||
|
_key: string;
|
||||||
|
} & MetaTag
|
||||||
|
>;
|
||||||
|
twitter?: Twitter;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Twitter = {
|
||||||
|
_type: 'twitter';
|
||||||
|
cardType?: string;
|
||||||
|
creator?: string;
|
||||||
|
site?: string;
|
||||||
|
handle?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type OpenGraph = {
|
||||||
|
_type: 'openGraph';
|
||||||
|
url?: string;
|
||||||
|
image?: {
|
||||||
|
asset?: {
|
||||||
|
_ref: string;
|
||||||
|
_type: 'reference';
|
||||||
|
_weak?: boolean;
|
||||||
|
[internalGroqTypeReferenceTo]?: 'sanity.imageAsset';
|
||||||
|
};
|
||||||
|
media?: unknown;
|
||||||
|
hotspot?: SanityImageHotspot;
|
||||||
|
crop?: SanityImageCrop;
|
||||||
|
_type: 'image';
|
||||||
|
};
|
||||||
|
title?: string;
|
||||||
|
description?: string;
|
||||||
|
siteName?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Custom = {
|
||||||
|
_id: string;
|
||||||
|
_type: 'custom';
|
||||||
|
_createdAt: string;
|
||||||
|
_updatedAt: string;
|
||||||
|
_rev: string;
|
||||||
|
title?: string;
|
||||||
|
slug?: Slug;
|
||||||
|
body?: BlockContent;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Link = {
|
||||||
|
_type: 'link';
|
||||||
|
text?: string;
|
||||||
|
type?: string;
|
||||||
|
internalLink?: {
|
||||||
|
_ref: string;
|
||||||
|
_type: 'reference';
|
||||||
|
_weak?: boolean;
|
||||||
|
[internalGroqTypeReferenceTo]?: 'custom';
|
||||||
|
};
|
||||||
|
url?: string;
|
||||||
|
email?: string;
|
||||||
|
phone?: string;
|
||||||
|
value?: string;
|
||||||
|
blank?: boolean;
|
||||||
|
parameters?: string;
|
||||||
|
anchor?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SanityImagePaletteSwatch = {
|
||||||
|
_type: 'sanity.imagePaletteSwatch';
|
||||||
|
background?: string;
|
||||||
|
foreground?: string;
|
||||||
|
population?: number;
|
||||||
|
title?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SanityImagePalette = {
|
||||||
|
_type: 'sanity.imagePalette';
|
||||||
|
darkMuted?: SanityImagePaletteSwatch;
|
||||||
|
lightVibrant?: SanityImagePaletteSwatch;
|
||||||
|
darkVibrant?: SanityImagePaletteSwatch;
|
||||||
|
vibrant?: SanityImagePaletteSwatch;
|
||||||
|
dominant?: SanityImagePaletteSwatch;
|
||||||
|
lightMuted?: SanityImagePaletteSwatch;
|
||||||
|
muted?: SanityImagePaletteSwatch;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SanityImageDimensions = {
|
||||||
|
_type: 'sanity.imageDimensions';
|
||||||
|
height?: number;
|
||||||
|
width?: number;
|
||||||
|
aspectRatio?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SanityImageHotspot = {
|
||||||
|
_type: 'sanity.imageHotspot';
|
||||||
|
x?: number;
|
||||||
|
y?: number;
|
||||||
|
height?: number;
|
||||||
|
width?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SanityImageCrop = {
|
||||||
|
_type: 'sanity.imageCrop';
|
||||||
|
top?: number;
|
||||||
|
bottom?: number;
|
||||||
|
left?: number;
|
||||||
|
right?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SanityFileAsset = {
|
||||||
|
_id: string;
|
||||||
|
_type: 'sanity.fileAsset';
|
||||||
|
_createdAt: string;
|
||||||
|
_updatedAt: string;
|
||||||
|
_rev: string;
|
||||||
|
originalFilename?: string;
|
||||||
|
label?: string;
|
||||||
|
title?: string;
|
||||||
|
description?: string;
|
||||||
|
altText?: string;
|
||||||
|
sha1hash?: string;
|
||||||
|
extension?: string;
|
||||||
|
mimeType?: string;
|
||||||
|
size?: number;
|
||||||
|
assetId?: string;
|
||||||
|
uploadId?: string;
|
||||||
|
path?: string;
|
||||||
|
url?: string;
|
||||||
|
source?: SanityAssetSourceData;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SanityImageAsset = {
|
||||||
|
_id: string;
|
||||||
|
_type: 'sanity.imageAsset';
|
||||||
|
_createdAt: string;
|
||||||
|
_updatedAt: string;
|
||||||
|
_rev: string;
|
||||||
|
originalFilename?: string;
|
||||||
|
label?: string;
|
||||||
|
title?: string;
|
||||||
|
description?: string;
|
||||||
|
altText?: string;
|
||||||
|
sha1hash?: string;
|
||||||
|
extension?: string;
|
||||||
|
mimeType?: string;
|
||||||
|
size?: number;
|
||||||
|
assetId?: string;
|
||||||
|
uploadId?: string;
|
||||||
|
path?: string;
|
||||||
|
url?: string;
|
||||||
|
metadata?: SanityImageMetadata;
|
||||||
|
source?: SanityAssetSourceData;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SanityImageMetadata = {
|
||||||
|
_type: 'sanity.imageMetadata';
|
||||||
|
location?: Geopoint;
|
||||||
|
dimensions?: SanityImageDimensions;
|
||||||
|
palette?: SanityImagePalette;
|
||||||
|
lqip?: string;
|
||||||
|
blurHash?: string;
|
||||||
|
hasAlpha?: boolean;
|
||||||
|
isOpaque?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Geopoint = {
|
||||||
|
_type: 'geopoint';
|
||||||
|
lat?: number;
|
||||||
|
lng?: number;
|
||||||
|
alt?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Slug = {
|
||||||
|
_type: 'slug';
|
||||||
|
current?: string;
|
||||||
|
source?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SanityAssetSourceData = {
|
||||||
|
_type: 'sanity.assetSourceData';
|
||||||
|
name?: string;
|
||||||
|
id?: string;
|
||||||
|
url?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AllSanitySchemaTypes =
|
||||||
|
| Home
|
||||||
|
| FaqSection
|
||||||
|
| CtaSection
|
||||||
|
| ContactSection
|
||||||
|
| ImageWithAlt
|
||||||
|
| Faq
|
||||||
|
| Button
|
||||||
|
| Settings
|
||||||
|
| BlockContent
|
||||||
|
| HighlightColor
|
||||||
|
| TextColor
|
||||||
|
| SimplerColor
|
||||||
|
| MetaTag
|
||||||
|
| MetaAttribute
|
||||||
|
| SeoMetaFields
|
||||||
|
| Twitter
|
||||||
|
| OpenGraph
|
||||||
|
| Custom
|
||||||
|
| Link
|
||||||
|
| SanityImagePaletteSwatch
|
||||||
|
| SanityImagePalette
|
||||||
|
| SanityImageDimensions
|
||||||
|
| SanityImageHotspot
|
||||||
|
| SanityImageCrop
|
||||||
|
| SanityFileAsset
|
||||||
|
| SanityImageAsset
|
||||||
|
| SanityImageMetadata
|
||||||
|
| Geopoint
|
||||||
|
| Slug
|
||||||
|
| SanityAssetSourceData;
|
||||||
|
export declare const internalGroqTypeReferenceTo: unique symbol;
|
||||||
6
template/apps/client/src/lib/server/sanity.ts
Normal file
6
template/apps/client/src/lib/server/sanity.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import {client} from '$lib/sanity'
|
||||||
|
import { sanityConnection } from '@repo/sanity-connection'
|
||||||
|
|
||||||
|
export const serverClient = client.withConfig({
|
||||||
|
token: sanityConnection.publicViewerToken,
|
||||||
|
})
|
||||||
16
template/apps/client/src/lib/settings.ts
Normal file
16
template/apps/client/src/lib/settings.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { serverClient } from './server/sanity';
|
||||||
|
|
||||||
|
export async function fetchSettings(prop?: string | undefined) {
|
||||||
|
const settings = await serverClient.fetch(`*[_type == "settings"][0]{
|
||||||
|
${
|
||||||
|
prop ||
|
||||||
|
`title,
|
||||||
|
longTitle,
|
||||||
|
footer,
|
||||||
|
description,
|
||||||
|
logo,
|
||||||
|
favicon`
|
||||||
|
}
|
||||||
|
}`);
|
||||||
|
return settings;
|
||||||
|
}
|
||||||
@@ -1,6 +1,13 @@
|
|||||||
import { clsx, type ClassValue } from "clsx"
|
import { clsx, type ClassValue } from "clsx";
|
||||||
import { twMerge } from "tailwind-merge"
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
export function cn(...inputs: ClassValue[]) {
|
export function cn(...inputs: ClassValue[]) {
|
||||||
return twMerge(clsx(inputs))
|
return twMerge(clsx(inputs));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
export type WithoutChild<T> = T extends { child?: any } ? Omit<T, "child"> : T;
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
export type WithoutChildren<T> = T extends { children?: any } ? Omit<T, "children"> : T;
|
||||||
|
export type WithoutChildrenOrChild<T> = WithoutChildren<WithoutChild<T>>;
|
||||||
|
export type WithElementRef<T, U extends HTMLElement = HTMLElement> = T & { ref?: U | null };
|
||||||
|
|||||||
14
template/apps/client/src/routes/+layout.server.ts
Normal file
14
template/apps/client/src/routes/+layout.server.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import type {LayoutServerLoad} from './$types'
|
||||||
|
import { fetchSettings } from '$lib/settings'
|
||||||
|
import { getImage } from '$lib/asset-to-url'
|
||||||
|
|
||||||
|
export const load: LayoutServerLoad = async ({locals: {preview}}) => {
|
||||||
|
const settings = await fetchSettings()
|
||||||
|
const logo = settings?.logo?.asset?._ref ? await getImage(settings.logo.asset._ref) : null
|
||||||
|
|
||||||
|
return {
|
||||||
|
preview,
|
||||||
|
settings,
|
||||||
|
logo
|
||||||
|
}
|
||||||
|
}
|
||||||
35
template/apps/client/src/routes/+layout.svelte
Normal file
35
template/apps/client/src/routes/+layout.svelte
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {VisualEditing} from '@sanity/visual-editing/svelte'
|
||||||
|
import {LiveMode} from '@sanity/svelte-loader'
|
||||||
|
import {client} from '$lib/sanity'
|
||||||
|
import Footer from '$lib/components/footer.svelte'
|
||||||
|
import '../app.css'
|
||||||
|
|
||||||
|
let {children, data} = $props();
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>{data.settings?.title || 'Website'}</title>
|
||||||
|
<meta name="description" content={data.settings?.description || ''} />
|
||||||
|
<meta property="og:title" content={data.settings?.longTitle || data.settings?.title || ''} />
|
||||||
|
<meta property="og:description" content={data.settings?.description || ''} />
|
||||||
|
<meta name="twitter:title" content={data.settings?.longTitle || data.settings?.title || ''} />
|
||||||
|
<meta name="twitter:description" content={data.settings?.description || ''} />
|
||||||
|
<meta name="robots" content="index, follow" />
|
||||||
|
{#if data.logo}
|
||||||
|
<link rel="icon" href={data.logo.url} />
|
||||||
|
<link rel="apple-touch-icon" href={data.logo.url} />
|
||||||
|
{/if}
|
||||||
|
<link rel="manifest" href="/manifest.webmanifest" />
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<div class="scroll-smooth antialiased font-sans">
|
||||||
|
{@render children()}
|
||||||
|
{#if data.settings}
|
||||||
|
<Footer settings={data.settings} />
|
||||||
|
{/if}
|
||||||
|
{#if data.preview}
|
||||||
|
<VisualEditing />
|
||||||
|
<LiveMode {client} />
|
||||||
|
{/if}
|
||||||
11
template/apps/client/src/routes/+page.server.ts
Normal file
11
template/apps/client/src/routes/+page.server.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import {homeQuery as query, type HomeResult} from '$lib/queries'
|
||||||
|
import type {PageServerLoad} from './$types'
|
||||||
|
|
||||||
|
export const load: PageServerLoad = async ({locals: {loadQuery}}) => {
|
||||||
|
const initial = await loadQuery<HomeResult>(query)
|
||||||
|
|
||||||
|
return {
|
||||||
|
query,
|
||||||
|
options: {initial}
|
||||||
|
}
|
||||||
|
}
|
||||||
20
template/apps/client/src/routes/+page.svelte
Normal file
20
template/apps/client/src/routes/+page.svelte
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import {useQuery} from '@sanity/svelte-loader'
|
||||||
|
import type { PageData } from './$types';
|
||||||
|
import CTA from '$lib/components/section/cta.svelte';
|
||||||
|
import { getImageFromAsset } from '$lib/image-helper';
|
||||||
|
|
||||||
|
export let data: PageData
|
||||||
|
const query = useQuery(data)
|
||||||
|
$: ({data: home} = $query)
|
||||||
|
|
||||||
|
$: ctaBackground = getImageFromAsset(home?.headerSection?.backgroundImage);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if home?.headerSection}
|
||||||
|
<CTA
|
||||||
|
cta={home.headerSection}
|
||||||
|
background={ctaBackground}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import { sanityConnection } from "@repo/sanity-connection";
|
|
||||||
import { createClient } from "next-sanity";
|
|
||||||
|
|
||||||
export const client = createClient({
|
|
||||||
projectId: sanityConnection.projectId,
|
|
||||||
dataset: sanityConnection.dataset,
|
|
||||||
apiVersion: "2024-12-01",
|
|
||||||
useCdn: false,
|
|
||||||
token: sanityConnection.publicViewerToken,
|
|
||||||
ignoreBrowserTokenWarning: true,
|
|
||||||
stega: {
|
|
||||||
studioUrl: sanityConnection.studioUrl,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import { defineLive } from "next-sanity";
|
|
||||||
import { client } from "./client";
|
|
||||||
import { sanityConnection } from "@repo/sanity-connection";
|
|
||||||
|
|
||||||
export const { sanityFetch, SanityLive } = defineLive({
|
|
||||||
client,
|
|
||||||
serverToken: sanityConnection.publicViewerToken,
|
|
||||||
browserToken: sanityConnection.publicViewerToken,
|
|
||||||
});
|
|
||||||
@@ -1,512 +0,0 @@
|
|||||||
/**
|
|
||||||
* ---------------------------------------------------------------------------------
|
|
||||||
* This file has been generated by Sanity TypeGen.
|
|
||||||
* Command: `sanity typegen generate`
|
|
||||||
*
|
|
||||||
* Any modifications made directly to this file will be overwritten the next time
|
|
||||||
* the TypeScript definitions are generated. Please make changes to the Sanity
|
|
||||||
* schema definitions and/or GROQ queries if you need to update these types.
|
|
||||||
*
|
|
||||||
* For more information on how to use Sanity TypeGen, visit the official documentation:
|
|
||||||
* https://www.sanity.io/docs/sanity-typegen
|
|
||||||
* ---------------------------------------------------------------------------------
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Source: schema.json
|
|
||||||
export type Home = {
|
|
||||||
_id: string;
|
|
||||||
_type: "home";
|
|
||||||
_createdAt: string;
|
|
||||||
_updatedAt: string;
|
|
||||||
_rev: string;
|
|
||||||
title?: string;
|
|
||||||
headerSection?: CtaSection;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type FaqSection = {
|
|
||||||
_type: "faqSection";
|
|
||||||
sectionTitle?: string;
|
|
||||||
title?: string;
|
|
||||||
faqs?: Array<{
|
|
||||||
_key: string;
|
|
||||||
} & Faq>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type CtaSection = {
|
|
||||||
_type: "ctaSection";
|
|
||||||
backgroundImage?: {
|
|
||||||
asset?: {
|
|
||||||
_ref: string;
|
|
||||||
_type: "reference";
|
|
||||||
_weak?: boolean;
|
|
||||||
[internalGroqTypeReferenceTo]?: "sanity.imageAsset";
|
|
||||||
};
|
|
||||||
media?: unknown;
|
|
||||||
hotspot?: SanityImageHotspot;
|
|
||||||
crop?: SanityImageCrop;
|
|
||||||
alt?: string;
|
|
||||||
_type: "imageWithAlt";
|
|
||||||
};
|
|
||||||
title?: string;
|
|
||||||
description?: Array<{
|
|
||||||
children?: Array<{
|
|
||||||
marks?: Array<string>;
|
|
||||||
text?: string;
|
|
||||||
_type: "span";
|
|
||||||
_key: string;
|
|
||||||
}>;
|
|
||||||
style?: "normal" | "h1" | "h2" | "h3" | "h4" | "blockquote";
|
|
||||||
listItem?: "bullet";
|
|
||||||
markDefs?: Array<{
|
|
||||||
href?: Link;
|
|
||||||
_type: "link";
|
|
||||||
_key: string;
|
|
||||||
} | {
|
|
||||||
_key: string;
|
|
||||||
} & TextColor | {
|
|
||||||
_key: string;
|
|
||||||
} & HighlightColor>;
|
|
||||||
level?: number;
|
|
||||||
_type: "block";
|
|
||||||
_key: string;
|
|
||||||
} | {
|
|
||||||
_key: string;
|
|
||||||
} & Button | {
|
|
||||||
asset?: {
|
|
||||||
_ref: string;
|
|
||||||
_type: "reference";
|
|
||||||
_weak?: boolean;
|
|
||||||
[internalGroqTypeReferenceTo]?: "sanity.imageAsset";
|
|
||||||
};
|
|
||||||
media?: unknown;
|
|
||||||
hotspot?: SanityImageHotspot;
|
|
||||||
crop?: SanityImageCrop;
|
|
||||||
_type: "image";
|
|
||||||
_key: string;
|
|
||||||
} | {
|
|
||||||
asset?: {
|
|
||||||
_ref: string;
|
|
||||||
_type: "reference";
|
|
||||||
_weak?: boolean;
|
|
||||||
[internalGroqTypeReferenceTo]?: "sanity.fileAsset";
|
|
||||||
};
|
|
||||||
media?: unknown;
|
|
||||||
_type: "file";
|
|
||||||
_key: string;
|
|
||||||
}>;
|
|
||||||
button?: Button;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ContactSection = {
|
|
||||||
_type: "contactSection";
|
|
||||||
title?: string;
|
|
||||||
contactMethods?: Array<{
|
|
||||||
type?: "email" | "phone" | "address" | "social";
|
|
||||||
label?: string;
|
|
||||||
value?: string;
|
|
||||||
link?: Link;
|
|
||||||
_key: string;
|
|
||||||
}>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ImageWithAlt = {
|
|
||||||
_type: "imageWithAlt";
|
|
||||||
asset?: {
|
|
||||||
_ref: string;
|
|
||||||
_type: "reference";
|
|
||||||
_weak?: boolean;
|
|
||||||
[internalGroqTypeReferenceTo]?: "sanity.imageAsset";
|
|
||||||
};
|
|
||||||
media?: unknown;
|
|
||||||
hotspot?: SanityImageHotspot;
|
|
||||||
crop?: SanityImageCrop;
|
|
||||||
alt?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type Faq = {
|
|
||||||
_type: "faq";
|
|
||||||
question?: string;
|
|
||||||
answer?: Array<{
|
|
||||||
children?: Array<{
|
|
||||||
marks?: Array<string>;
|
|
||||||
text?: string;
|
|
||||||
_type: "span";
|
|
||||||
_key: string;
|
|
||||||
}>;
|
|
||||||
style?: "normal" | "h1" | "h2" | "h3" | "h4" | "blockquote";
|
|
||||||
listItem?: "bullet";
|
|
||||||
markDefs?: Array<{
|
|
||||||
href?: Link;
|
|
||||||
_type: "link";
|
|
||||||
_key: string;
|
|
||||||
} | {
|
|
||||||
_key: string;
|
|
||||||
} & TextColor | {
|
|
||||||
_key: string;
|
|
||||||
} & HighlightColor>;
|
|
||||||
level?: number;
|
|
||||||
_type: "block";
|
|
||||||
_key: string;
|
|
||||||
} | {
|
|
||||||
_key: string;
|
|
||||||
} & Button | {
|
|
||||||
asset?: {
|
|
||||||
_ref: string;
|
|
||||||
_type: "reference";
|
|
||||||
_weak?: boolean;
|
|
||||||
[internalGroqTypeReferenceTo]?: "sanity.imageAsset";
|
|
||||||
};
|
|
||||||
media?: unknown;
|
|
||||||
hotspot?: SanityImageHotspot;
|
|
||||||
crop?: SanityImageCrop;
|
|
||||||
_type: "image";
|
|
||||||
_key: string;
|
|
||||||
} | {
|
|
||||||
asset?: {
|
|
||||||
_ref: string;
|
|
||||||
_type: "reference";
|
|
||||||
_weak?: boolean;
|
|
||||||
[internalGroqTypeReferenceTo]?: "sanity.fileAsset";
|
|
||||||
};
|
|
||||||
media?: unknown;
|
|
||||||
_type: "file";
|
|
||||||
_key: string;
|
|
||||||
}>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type Button = {
|
|
||||||
_type: "button";
|
|
||||||
text?: string;
|
|
||||||
link?: Link;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type Settings = {
|
|
||||||
_id: string;
|
|
||||||
_type: "settings";
|
|
||||||
_createdAt: string;
|
|
||||||
_updatedAt: string;
|
|
||||||
_rev: string;
|
|
||||||
title?: string;
|
|
||||||
longTitle?: string;
|
|
||||||
description?: string;
|
|
||||||
logo?: {
|
|
||||||
asset?: {
|
|
||||||
_ref: string;
|
|
||||||
_type: "reference";
|
|
||||||
_weak?: boolean;
|
|
||||||
[internalGroqTypeReferenceTo]?: "sanity.imageAsset";
|
|
||||||
};
|
|
||||||
media?: unknown;
|
|
||||||
hotspot?: SanityImageHotspot;
|
|
||||||
crop?: SanityImageCrop;
|
|
||||||
_type: "image";
|
|
||||||
};
|
|
||||||
favicon?: {
|
|
||||||
asset?: {
|
|
||||||
_ref: string;
|
|
||||||
_type: "reference";
|
|
||||||
_weak?: boolean;
|
|
||||||
[internalGroqTypeReferenceTo]?: "sanity.imageAsset";
|
|
||||||
};
|
|
||||||
media?: unknown;
|
|
||||||
hotspot?: SanityImageHotspot;
|
|
||||||
crop?: SanityImageCrop;
|
|
||||||
_type: "image";
|
|
||||||
};
|
|
||||||
footer?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type BlockContent = Array<{
|
|
||||||
children?: Array<{
|
|
||||||
marks?: Array<string>;
|
|
||||||
text?: string;
|
|
||||||
_type: "span";
|
|
||||||
_key: string;
|
|
||||||
}>;
|
|
||||||
style?: "normal" | "h1" | "h2" | "h3" | "h4" | "blockquote";
|
|
||||||
listItem?: "bullet";
|
|
||||||
markDefs?: Array<{
|
|
||||||
href?: Link;
|
|
||||||
_type: "link";
|
|
||||||
_key: string;
|
|
||||||
} | {
|
|
||||||
_key: string;
|
|
||||||
} & TextColor | {
|
|
||||||
_key: string;
|
|
||||||
} & HighlightColor>;
|
|
||||||
level?: number;
|
|
||||||
_type: "block";
|
|
||||||
_key: string;
|
|
||||||
} | {
|
|
||||||
_key: string;
|
|
||||||
} & Button | {
|
|
||||||
asset?: {
|
|
||||||
_ref: string;
|
|
||||||
_type: "reference";
|
|
||||||
_weak?: boolean;
|
|
||||||
[internalGroqTypeReferenceTo]?: "sanity.imageAsset";
|
|
||||||
};
|
|
||||||
media?: unknown;
|
|
||||||
hotspot?: SanityImageHotspot;
|
|
||||||
crop?: SanityImageCrop;
|
|
||||||
_type: "image";
|
|
||||||
_key: string;
|
|
||||||
} | {
|
|
||||||
asset?: {
|
|
||||||
_ref: string;
|
|
||||||
_type: "reference";
|
|
||||||
_weak?: boolean;
|
|
||||||
[internalGroqTypeReferenceTo]?: "sanity.fileAsset";
|
|
||||||
};
|
|
||||||
media?: unknown;
|
|
||||||
_type: "file";
|
|
||||||
_key: string;
|
|
||||||
}>;
|
|
||||||
|
|
||||||
export type HighlightColor = {
|
|
||||||
_type: "highlightColor";
|
|
||||||
label?: string;
|
|
||||||
value?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type TextColor = {
|
|
||||||
_type: "textColor";
|
|
||||||
label?: string;
|
|
||||||
value?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type SimplerColor = {
|
|
||||||
_type: "simplerColor";
|
|
||||||
label?: string;
|
|
||||||
value?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type MetaTag = {
|
|
||||||
_type: "metaTag";
|
|
||||||
metaAttributes?: Array<{
|
|
||||||
_key: string;
|
|
||||||
} & MetaAttribute>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type MetaAttribute = {
|
|
||||||
_type: "metaAttribute";
|
|
||||||
attributeKey?: string;
|
|
||||||
attributeType?: "string" | "image";
|
|
||||||
attributeValueImage?: {
|
|
||||||
asset?: {
|
|
||||||
_ref: string;
|
|
||||||
_type: "reference";
|
|
||||||
_weak?: boolean;
|
|
||||||
[internalGroqTypeReferenceTo]?: "sanity.imageAsset";
|
|
||||||
};
|
|
||||||
media?: unknown;
|
|
||||||
hotspot?: SanityImageHotspot;
|
|
||||||
crop?: SanityImageCrop;
|
|
||||||
_type: "image";
|
|
||||||
};
|
|
||||||
attributeValueString?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type SeoMetaFields = {
|
|
||||||
_type: "seoMetaFields";
|
|
||||||
nofollowAttributes?: boolean;
|
|
||||||
metaTitle?: string;
|
|
||||||
metaDescription?: string;
|
|
||||||
metaImage?: {
|
|
||||||
asset?: {
|
|
||||||
_ref: string;
|
|
||||||
_type: "reference";
|
|
||||||
_weak?: boolean;
|
|
||||||
[internalGroqTypeReferenceTo]?: "sanity.imageAsset";
|
|
||||||
};
|
|
||||||
media?: unknown;
|
|
||||||
hotspot?: SanityImageHotspot;
|
|
||||||
crop?: SanityImageCrop;
|
|
||||||
_type: "image";
|
|
||||||
};
|
|
||||||
seoKeywords?: Array<string>;
|
|
||||||
openGraph?: OpenGraph;
|
|
||||||
additionalMetaTags?: Array<{
|
|
||||||
_key: string;
|
|
||||||
} & MetaTag>;
|
|
||||||
twitter?: Twitter;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type Twitter = {
|
|
||||||
_type: "twitter";
|
|
||||||
cardType?: string;
|
|
||||||
creator?: string;
|
|
||||||
site?: string;
|
|
||||||
handle?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type OpenGraph = {
|
|
||||||
_type: "openGraph";
|
|
||||||
url?: string;
|
|
||||||
image?: {
|
|
||||||
asset?: {
|
|
||||||
_ref: string;
|
|
||||||
_type: "reference";
|
|
||||||
_weak?: boolean;
|
|
||||||
[internalGroqTypeReferenceTo]?: "sanity.imageAsset";
|
|
||||||
};
|
|
||||||
media?: unknown;
|
|
||||||
hotspot?: SanityImageHotspot;
|
|
||||||
crop?: SanityImageCrop;
|
|
||||||
_type: "image";
|
|
||||||
};
|
|
||||||
title?: string;
|
|
||||||
description?: string;
|
|
||||||
siteName?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type Custom = {
|
|
||||||
_id: string;
|
|
||||||
_type: "custom";
|
|
||||||
_createdAt: string;
|
|
||||||
_updatedAt: string;
|
|
||||||
_rev: string;
|
|
||||||
title?: string;
|
|
||||||
slug?: Slug;
|
|
||||||
body?: BlockContent;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type Link = {
|
|
||||||
_type: "link";
|
|
||||||
text?: string;
|
|
||||||
type?: string;
|
|
||||||
internalLink?: {
|
|
||||||
_ref: string;
|
|
||||||
_type: "reference";
|
|
||||||
_weak?: boolean;
|
|
||||||
[internalGroqTypeReferenceTo]?: "custom";
|
|
||||||
};
|
|
||||||
url?: string;
|
|
||||||
email?: string;
|
|
||||||
phone?: string;
|
|
||||||
value?: string;
|
|
||||||
blank?: boolean;
|
|
||||||
parameters?: string;
|
|
||||||
anchor?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type SanityImagePaletteSwatch = {
|
|
||||||
_type: "sanity.imagePaletteSwatch";
|
|
||||||
background?: string;
|
|
||||||
foreground?: string;
|
|
||||||
population?: number;
|
|
||||||
title?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type SanityImagePalette = {
|
|
||||||
_type: "sanity.imagePalette";
|
|
||||||
darkMuted?: SanityImagePaletteSwatch;
|
|
||||||
lightVibrant?: SanityImagePaletteSwatch;
|
|
||||||
darkVibrant?: SanityImagePaletteSwatch;
|
|
||||||
vibrant?: SanityImagePaletteSwatch;
|
|
||||||
dominant?: SanityImagePaletteSwatch;
|
|
||||||
lightMuted?: SanityImagePaletteSwatch;
|
|
||||||
muted?: SanityImagePaletteSwatch;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type SanityImageDimensions = {
|
|
||||||
_type: "sanity.imageDimensions";
|
|
||||||
height?: number;
|
|
||||||
width?: number;
|
|
||||||
aspectRatio?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type SanityImageHotspot = {
|
|
||||||
_type: "sanity.imageHotspot";
|
|
||||||
x?: number;
|
|
||||||
y?: number;
|
|
||||||
height?: number;
|
|
||||||
width?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type SanityImageCrop = {
|
|
||||||
_type: "sanity.imageCrop";
|
|
||||||
top?: number;
|
|
||||||
bottom?: number;
|
|
||||||
left?: number;
|
|
||||||
right?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type SanityFileAsset = {
|
|
||||||
_id: string;
|
|
||||||
_type: "sanity.fileAsset";
|
|
||||||
_createdAt: string;
|
|
||||||
_updatedAt: string;
|
|
||||||
_rev: string;
|
|
||||||
originalFilename?: string;
|
|
||||||
label?: string;
|
|
||||||
title?: string;
|
|
||||||
description?: string;
|
|
||||||
altText?: string;
|
|
||||||
sha1hash?: string;
|
|
||||||
extension?: string;
|
|
||||||
mimeType?: string;
|
|
||||||
size?: number;
|
|
||||||
assetId?: string;
|
|
||||||
uploadId?: string;
|
|
||||||
path?: string;
|
|
||||||
url?: string;
|
|
||||||
source?: SanityAssetSourceData;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type SanityImageAsset = {
|
|
||||||
_id: string;
|
|
||||||
_type: "sanity.imageAsset";
|
|
||||||
_createdAt: string;
|
|
||||||
_updatedAt: string;
|
|
||||||
_rev: string;
|
|
||||||
originalFilename?: string;
|
|
||||||
label?: string;
|
|
||||||
title?: string;
|
|
||||||
description?: string;
|
|
||||||
altText?: string;
|
|
||||||
sha1hash?: string;
|
|
||||||
extension?: string;
|
|
||||||
mimeType?: string;
|
|
||||||
size?: number;
|
|
||||||
assetId?: string;
|
|
||||||
uploadId?: string;
|
|
||||||
path?: string;
|
|
||||||
url?: string;
|
|
||||||
metadata?: SanityImageMetadata;
|
|
||||||
source?: SanityAssetSourceData;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type SanityImageMetadata = {
|
|
||||||
_type: "sanity.imageMetadata";
|
|
||||||
location?: Geopoint;
|
|
||||||
dimensions?: SanityImageDimensions;
|
|
||||||
palette?: SanityImagePalette;
|
|
||||||
lqip?: string;
|
|
||||||
blurHash?: string;
|
|
||||||
hasAlpha?: boolean;
|
|
||||||
isOpaque?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type Geopoint = {
|
|
||||||
_type: "geopoint";
|
|
||||||
lat?: number;
|
|
||||||
lng?: number;
|
|
||||||
alt?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type Slug = {
|
|
||||||
_type: "slug";
|
|
||||||
current?: string;
|
|
||||||
source?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type SanityAssetSourceData = {
|
|
||||||
_type: "sanity.assetSourceData";
|
|
||||||
name?: string;
|
|
||||||
id?: string;
|
|
||||||
url?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type AllSanitySchemaTypes = Home | FaqSection | CtaSection | ContactSection | ImageWithAlt | Faq | Button | Settings | BlockContent | HighlightColor | TextColor | SimplerColor | MetaTag | MetaAttribute | SeoMetaFields | Twitter | OpenGraph | Custom | Link | SanityImagePaletteSwatch | SanityImagePalette | SanityImageDimensions | SanityImageHotspot | SanityImageCrop | SanityFileAsset | SanityImageAsset | SanityImageMetadata | Geopoint | Slug | SanityAssetSourceData;
|
|
||||||
export declare const internalGroqTypeReferenceTo: unique symbol;
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import { sanityFetch } from "./live";
|
|
||||||
|
|
||||||
export async function fetchSettings(prop?: string | undefined) {
|
|
||||||
const settingsFetch = await sanityFetch({
|
|
||||||
query: `*[_type == "settings"][0]{
|
|
||||||
${
|
|
||||||
prop ||
|
|
||||||
`title,
|
|
||||||
longTitle,
|
|
||||||
footer,
|
|
||||||
description,
|
|
||||||
logo,
|
|
||||||
favicon`
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
});
|
|
||||||
return settingsFetch.data;
|
|
||||||
}
|
|
||||||
3
template/apps/client/static/robots.txt
Normal file
3
template/apps/client/static/robots.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# allow crawling everything by default
|
||||||
|
User-agent: *
|
||||||
|
Disallow:
|
||||||
12
template/apps/client/svelte.config.js
Normal file
12
template/apps/client/svelte.config.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import adapter from '@sveltejs/adapter-cloudflare-workers';
|
||||||
|
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
||||||
|
|
||||||
|
/** @type {import('@sveltejs/kit').Config} */
|
||||||
|
const config = {
|
||||||
|
// Consult https://svelte.dev/docs/kit/integrations
|
||||||
|
// for more information about preprocessors
|
||||||
|
preprocess: vitePreprocess(),
|
||||||
|
kit: { adapter: adapter() }
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
import { config } from "@repo/ui";
|
|
||||||
|
|
||||||
export default config;
|
|
||||||
@@ -1,20 +1,19 @@
|
|||||||
{
|
{
|
||||||
"extends": "@repo/typescript-config/nextjs.json",
|
"extends": "./.svelte-kit/tsconfig.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"paths": {
|
"allowJs": true,
|
||||||
"@/*": [
|
"checkJs": true,
|
||||||
"./src/*"
|
"esModuleInterop": true,
|
||||||
]
|
"forceConsistentCasingInFileNames": true,
|
||||||
},
|
"resolveJsonModule": true,
|
||||||
},
|
"skipLibCheck": true,
|
||||||
"include": [
|
"sourceMap": true,
|
||||||
"next-env.d.ts",
|
"strict": true,
|
||||||
"**/*.ts",
|
"moduleResolution": "bundler"
|
||||||
"**/*.tsx",
|
}
|
||||||
".next/types/**/*.ts",
|
// Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
|
||||||
"../../packages/ui/src/tailwind.config.ts"
|
// except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
|
||||||
, "../../packages/ui/src/components/logo.tsx" ],
|
//
|
||||||
"exclude": [
|
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
|
||||||
"node_modules"
|
// from the referenced tsconfig.json - TypeScript does not merge them in
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|||||||
7
template/apps/client/vite.config.ts
Normal file
7
template/apps/client/vite.config.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import tailwindcss from '@tailwindcss/vite';
|
||||||
|
import { sveltekit } from '@sveltejs/kit/vite';
|
||||||
|
import { defineConfig } from 'vite';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [tailwindcss(), sveltekit()]
|
||||||
|
});
|
||||||
13
template/apps/client/wrangler.jsonc
Normal file
13
template/apps/client/wrangler.jsonc
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"name": "template-sanity",
|
||||||
|
"account_id": "7709703dd5c5923bd03a32fefb04f32d", // vaporvee
|
||||||
|
"main": "./.cloudflare/worker.js",
|
||||||
|
"site": {
|
||||||
|
"bucket": "./.cloudflare/public"
|
||||||
|
},
|
||||||
|
"build": {
|
||||||
|
"command": "bun run build"
|
||||||
|
},
|
||||||
|
"compatibility_date": "2021-11-12",
|
||||||
|
"compatibility_flags": ["nodejs_compat"]
|
||||||
|
}
|
||||||
@@ -5,7 +5,5 @@ Congratulations, you have now installed the Sanity Content Studio, an open-sourc
|
|||||||
Now you can do the following things:
|
Now you can do the following things:
|
||||||
|
|
||||||
- [Read “getting started” in the docs](https://www.sanity.io/docs/introduction/getting-started?utm_source=readme)
|
- [Read “getting started” in the docs](https://www.sanity.io/docs/introduction/getting-started?utm_source=readme)
|
||||||
- Check out the example frontend: [React/Next.js](https://github.com/sanity-io/tutorial-sanity-blog-react-next)
|
|
||||||
- [Read the blog post about this template](https://www.sanity.io/blog/build-your-own-blog-with-sanity-and-next-js?utm_source=readme)
|
|
||||||
- [Join the Sanity community](https://www.sanity.io/community/join?utm_source=readme)
|
- [Join the Sanity community](https://www.sanity.io/community/join?utm_source=readme)
|
||||||
- [Extend and build plugins](https://www.sanity.io/docs/content-studio/extending?utm_source=readme)
|
- [Extend and build plugins](https://www.sanity.io/docs/content-studio/extending?utm_source=readme)
|
||||||
|
|||||||
@@ -19,10 +19,10 @@
|
|||||||
"@repo/sanity-connection": "*",
|
"@repo/sanity-connection": "*",
|
||||||
"@repo/ui": "*",
|
"@repo/ui": "*",
|
||||||
"@sanity/document-internationalization": "^3.3.3",
|
"@sanity/document-internationalization": "^3.3.3",
|
||||||
"@sanity/vision": "^4.1.1",
|
"@sanity/vision": "^4.2.0",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
"sanity": "^4.1.1",
|
"sanity": "^4.2.0",
|
||||||
"sanity-plugin-link-field": "^1.4.0",
|
"sanity-plugin-link-field": "^1.4.0",
|
||||||
"sanity-plugin-media": "^3.0.4",
|
"sanity-plugin-media": "^3.0.4",
|
||||||
"sanity-plugin-seo": "^1.3.1",
|
"sanity-plugin-seo": "^1.3.1",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"generates": "../client/src/sanity/sanity.types.ts",
|
"generates": "../client-svelte/src/lib/sanity.types.ts",
|
||||||
"path": "./schemaTypes/*.ts"
|
"path": "./schemaTypes/*.ts"
|
||||||
}
|
}
|
||||||
@@ -39,15 +39,17 @@ export default defineConfig({
|
|||||||
.child(S.document().schemaType('settings').documentId('settings').title('Settings')),
|
.child(S.document().schemaType('settings').documentId('settings').title('Settings')),
|
||||||
]),
|
]),
|
||||||
}),
|
}),
|
||||||
|
// @ts-ignore
|
||||||
linkField({
|
linkField({
|
||||||
linkableSchemaTypes: ['custom'],
|
linkableSchemaTypes: ['custom'],
|
||||||
}),
|
}),
|
||||||
presentationTool({
|
presentationTool({
|
||||||
previewUrl: {
|
previewUrl: {
|
||||||
origin: sanityConnection.previewUrl ?? 'http://localhost:3000',
|
origin: sanityConnection.previewUrl ?? 'http://localhost:5173',
|
||||||
preview: '/',
|
preview: '/',
|
||||||
previewMode: {
|
previewMode: {
|
||||||
enable: '/api/draft-mode/enable',
|
enable: '/preview/enable',
|
||||||
|
disable: '/preview/disable',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -10,11 +10,10 @@ export default defineField({
|
|||||||
title: 'Alt Text',
|
title: 'Alt Text',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'Important for SEO and accessibility',
|
description: 'Important for SEO and accessibility',
|
||||||
validation: (Rule) => Rule.required().min(1).max(100),
|
validation: (Rule) => Rule.max(100),
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
options: {
|
options: {
|
||||||
hotspot: true,
|
hotspot: true,
|
||||||
},
|
},
|
||||||
validation: (Rule) => Rule.required()
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export default defineType({
|
|||||||
defineField({
|
defineField({
|
||||||
name: 'title',
|
name: 'title',
|
||||||
title: 'Page Title',
|
title: 'Page Title',
|
||||||
|
initialValue: 'Home',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
validation: (Rule) => Rule.required(),
|
validation: (Rule) => Rule.required(),
|
||||||
group: 'header',
|
group: 'header',
|
||||||
|
|||||||
@@ -10,24 +10,26 @@ export default defineType({
|
|||||||
defineField({
|
defineField({
|
||||||
name: 'backgroundImage',
|
name: 'backgroundImage',
|
||||||
title: 'Background Image',
|
title: 'Background Image',
|
||||||
type: 'imageWithAlt',
|
type: 'imageWithAlt'
|
||||||
validation: (Rule) => Rule.required(),
|
|
||||||
}),
|
}),
|
||||||
defineField({
|
defineField({
|
||||||
name: 'title',
|
name: 'title',
|
||||||
title: 'Title',
|
title: 'Title',
|
||||||
|
initialValue: 'Welcome to Our Site',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
validation: (Rule) => Rule.required().min(1).max(100),
|
validation: (Rule) => Rule.required().min(1).max(100),
|
||||||
}),
|
}),
|
||||||
defineField({
|
defineField({
|
||||||
name: 'description',
|
name: 'description',
|
||||||
title: 'Description',
|
title: 'Description',
|
||||||
type: 'blockContent',
|
type: 'blockContent'
|
||||||
validation: (Rule) => Rule.required(),
|
|
||||||
}),
|
}),
|
||||||
defineField({
|
defineField({
|
||||||
name: 'button',
|
name: 'button',
|
||||||
title: 'Button',
|
title: 'Button',
|
||||||
|
initialValue: {
|
||||||
|
text: 'Get Started',
|
||||||
|
},
|
||||||
type: 'button',
|
type: 'button',
|
||||||
validation: (Rule) => Rule.required(),
|
validation: (Rule) => Rule.required(),
|
||||||
}),
|
}),
|
||||||
|
|||||||
3294
template/bun.lock
3294
template/bun.lock
File diff suppressed because it is too large
Load Diff
40
template/nx.json
Normal file
40
template/nx.json
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"$schema": "./node_modules/nx/schemas/nx-schema.json",
|
||||||
|
"targetDefaults": {
|
||||||
|
"build": {
|
||||||
|
"dependsOn": [
|
||||||
|
"^build",
|
||||||
|
"^generate"
|
||||||
|
],
|
||||||
|
"inputs": [
|
||||||
|
"{projectRoot}/**/*",
|
||||||
|
"{projectRoot}/.env*"
|
||||||
|
],
|
||||||
|
"outputs": [
|
||||||
|
"{projectRoot}/dist/**"
|
||||||
|
],
|
||||||
|
"cache": true
|
||||||
|
},
|
||||||
|
"lint": {
|
||||||
|
"dependsOn": [
|
||||||
|
"^lint"
|
||||||
|
],
|
||||||
|
"cache": true
|
||||||
|
},
|
||||||
|
"check-types": {
|
||||||
|
"dependsOn": [
|
||||||
|
"^check-types"
|
||||||
|
],
|
||||||
|
"cache": true
|
||||||
|
},
|
||||||
|
"deploy": {
|
||||||
|
"cache": true
|
||||||
|
},
|
||||||
|
"generate": {
|
||||||
|
"cache": true
|
||||||
|
},
|
||||||
|
"dev": {
|
||||||
|
"cache": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,18 +2,18 @@
|
|||||||
"name": "web",
|
"name": "web",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "turbo run build",
|
"build": "nx run-many -t build",
|
||||||
"dev": "turbo run dev",
|
"dev": "nx run-many -t dev",
|
||||||
"lint": "turbo run lint",
|
"lint": "nx run-many -t lint",
|
||||||
"deploy": "turbo run deploy",
|
"deploy": "nx run-many -t deploy",
|
||||||
"generate": "turbo run generate",
|
"generate": "nx run-many -t generate",
|
||||||
"format": "prettier --write \"**/*.{ts,tsx,md}\"",
|
"format": "prettier --write \"**/*.{ts,tsx,md}\"",
|
||||||
"check-types": "turbo run check-types"
|
"check-types": "nx run-many -t check-types"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"prettier": "^3.6.2",
|
"prettier": "^3.6.2",
|
||||||
"turbo": "^2.5.5",
|
"typescript": "5.8.3",
|
||||||
"typescript": "5.8.3"
|
"nx": "21.3.11"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"bun": ">=1.2.12"
|
"bun": ">=1.2.12"
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
export const sanityConnection = {
|
export const sanityConnection = {
|
||||||
pageTitle: "lumify template",
|
pageTitle: "lumify template",
|
||||||
publicViewerToken: "skX06BHIWzgWfkt52091aJgLmYEwFJ7ufghsXi0wRXRkW2Nom8mzfYnFpSVJH1toNy0e34Hot2yTwjLxHCjhsWLZiC0qjR19WI6b9WEFj04shHMZCiS09LgRuzd9BnEgewAQpJUAbhdSwg3NOg8rOhZXpHyciAoYwLqhaTHP6g0FDpiZFrb5",
|
publicViewerToken: "skIwDMWqhZ3YBpwVE4abnq3JsW0Ncru5rTbYbE1Qwa5aSIOT0mzktYHUKAowoneBQix1fxuAB9yYzQFkKuhA4tByjELI0i9pRCko8cxnseXOW36hWFwUnY57GKyhCQnWCB5Ky4YlwXRJWnXfygfk0by87R7hTQigMy0bNs0gDgwCu2a4JoEl",
|
||||||
studioHost: "vaporvee",
|
studioHost: "web-svelte",
|
||||||
studioUrl: "https://vaporvee.sanity.studio", // normaly https://<studioHost>.sanity.studio
|
studioUrl: "https://web-svelte.sanity.studio", // normaly https://<studioHost>.sanity.studio
|
||||||
projectId: "ax04yw0e",
|
projectId: "mp2psn1f",
|
||||||
previewUrl: "http://localhost:3000",
|
previewUrl: "http://localhost:5173",
|
||||||
dataset: "production", // leave as "production" for the main dataset
|
dataset: "production", // leave as "production" for the main dataset
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "https://json.schemastore.org/tsconfig",
|
|
||||||
"extends": "./base.json",
|
|
||||||
"compilerOptions": {
|
|
||||||
"plugins": [
|
|
||||||
{
|
|
||||||
"name": "next"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"allowJs": true,
|
|
||||||
"noEmit": true,
|
|
||||||
"jsx": "preserve"
|
|
||||||
},
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "https://json.schemastore.org/tsconfig",
|
|
||||||
"extends": "./base.json",
|
|
||||||
"compilerOptions": {
|
|
||||||
"jsx": "react-jsx"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -9,7 +9,6 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"lint": "eslint . --max-warnings 0",
|
"lint": "eslint . --max-warnings 0",
|
||||||
"generate:component": "turbo gen react-component",
|
|
||||||
"check-types": "tsc --noEmit"
|
"check-types": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -1,37 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "https://turborepo.com/schema.json",
|
|
||||||
"ui": "tui",
|
|
||||||
"tasks": {
|
|
||||||
"build": {
|
|
||||||
"dependsOn": [
|
|
||||||
"^build",
|
|
||||||
"^generate"
|
|
||||||
],
|
|
||||||
"inputs": [
|
|
||||||
"$TURBO_DEFAULT$",
|
|
||||||
".env*"
|
|
||||||
],
|
|
||||||
"outputs": [
|
|
||||||
".next/**",
|
|
||||||
"!.next/cache/**",
|
|
||||||
"dist/**"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"lint": {
|
|
||||||
"dependsOn": [
|
|
||||||
"^lint"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"check-types": {
|
|
||||||
"dependsOn": [
|
|
||||||
"^check-types"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"deploy": {},
|
|
||||||
"generate": {},
|
|
||||||
"dev": {
|
|
||||||
"cache": false,
|
|
||||||
"persistent": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user