init commit
This commit is contained in:
parent
9e531aabce
commit
6c11574836
@ -1,2 +1,3 @@
|
|||||||
DATABASE_URI=postgres://postgres:<password>@127.0.0.1:5432/your-database-name
|
DATABASE_URI=postgres://postgres:<password>@127.0.0.1:5432/your-database-name
|
||||||
PAYLOAD_SECRET=YOUR_SECRET_HERE
|
PAYLOAD_SECRET=YOUR_SECRET_HERE
|
||||||
|
PORT=3000
|
||||||
|
|||||||
37
README.md
37
README.md
@ -1,38 +1,11 @@
|
|||||||
# Payload Blank Template
|
# Payload Personal Portfolio
|
||||||
|
|
||||||
This template comes configured with the bare minimum to get started on anything you need.
|
|
||||||
|
|
||||||
## Quick start
|
|
||||||
|
|
||||||
This template can be deployed directly from our Cloud hosting and it will setup MongoDB and cloud S3 object storage for media.
|
|
||||||
|
|
||||||
## Quick Start - local setup
|
|
||||||
|
|
||||||
To spin up this template locally, follow these steps:
|
|
||||||
|
|
||||||
### Clone
|
|
||||||
|
|
||||||
After you click the `Deploy` button above, you'll want to have standalone copy of this repo on your machine. If you've already cloned this repo, skip to [Development](#development).
|
|
||||||
|
|
||||||
### Development
|
### Development
|
||||||
|
|
||||||
1. First [clone the repo](#clone) if you have not done so already
|
1. `cp .env.example .env` to copy the example environment variables.
|
||||||
2. `cd my-project && cp .env.example .env` to copy the example environment variables. You'll need to add the `MONGODB_URI` from your Cloud project to your `.env` if you want to use S3 storage and the MongoDB database that was created for you.
|
2. `npm install && npm run dev` to install dependencies and start the dev server
|
||||||
|
3. open `http://localhost:{process.env.PORT}` to open the app in your browser
|
||||||
|
|
||||||
3. `pnpm install && pnpm dev` to install dependencies and start the dev server
|
|
||||||
4. open `http://localhost:3000` to open the app in your browser
|
|
||||||
|
|
||||||
That's it! Changes made in `./src` will be reflected in your app. Follow the on-screen instructions to login and create your first admin user. Then check out [Production](#production) once you're ready to build and serve your app, and [Deployment](#deployment) when you're ready to go live.
|
|
||||||
|
|
||||||
#### Docker (Optional)
|
|
||||||
|
|
||||||
If you prefer to use Docker for local development instead of a local MongoDB instance, the provided docker-compose.yml file can be used.
|
|
||||||
|
|
||||||
To do so, follow these steps:
|
|
||||||
|
|
||||||
- Modify the `MONGODB_URI` in your `.env` file to `mongodb://127.0.0.1/<dbname>`
|
|
||||||
- Modify the `docker-compose.yml` file's `MONGODB_URI` to match the above `<dbname>`
|
|
||||||
- Run `docker-compose up` to start the database, optionally pass `-d` to run in the background.
|
|
||||||
|
|
||||||
## How it works
|
## How it works
|
||||||
|
|
||||||
@ -62,6 +35,4 @@ Alternatively, you can use [Docker](https://www.docker.com) to spin up this temp
|
|||||||
|
|
||||||
That's it! The Docker instance will help you get up and running quickly while also standardizing the development environment across your teams.
|
That's it! The Docker instance will help you get up and running quickly while also standardizing the development environment across your teams.
|
||||||
|
|
||||||
## Questions
|
|
||||||
|
|
||||||
If you have any issues or questions, reach out to us on [Discord](https://discord.com/invite/payload) or start a [GitHub discussion](https://github.com/payloadcms/payload/discussions).
|
|
||||||
|
|||||||
21
components.json
Normal file
21
components.json
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://ui.shadcn.com/schema.json",
|
||||||
|
"style": "new-york",
|
||||||
|
"rsc": true,
|
||||||
|
"tsx": true,
|
||||||
|
"tailwind": {
|
||||||
|
"config": "tailwind.config.js",
|
||||||
|
"css": "src/app/globals.css",
|
||||||
|
"baseColor": "stone",
|
||||||
|
"cssVariables": true,
|
||||||
|
"prefix": ""
|
||||||
|
},
|
||||||
|
"aliases": {
|
||||||
|
"components": "@/components",
|
||||||
|
"utils": "@/lib/utils",
|
||||||
|
"ui": "@/components/ui",
|
||||||
|
"lib": "@/lib",
|
||||||
|
"hooks": "@/hooks"
|
||||||
|
},
|
||||||
|
"iconLibrary": "lucide"
|
||||||
|
}
|
||||||
@ -1,32 +1,30 @@
|
|||||||
version: '3'
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
payload:
|
payload:
|
||||||
image: node:18-alpine
|
image: node:18-alpine
|
||||||
ports:
|
ports:
|
||||||
- '3000:3000'
|
- "${PORT}:${PORT}"
|
||||||
volumes:
|
volumes:
|
||||||
- .:/home/node/app
|
- .:/home/node/app
|
||||||
- node_modules:/home/node/app/node_modules
|
- node_modules:/home/node/app/node_modules
|
||||||
working_dir: /home/node/app/
|
working_dir: /home/node/app/
|
||||||
command: sh -c "corepack enable && corepack prepare pnpm@latest --activate && pnpm install && pnpm dev"
|
command: sh -c "corepack enable && corepack prepare pnpm@latest --activate && pnpm install && pnpm dev"
|
||||||
depends_on:
|
# depends_on:
|
||||||
- mongo
|
# - mongo
|
||||||
# - postgres
|
# - postgres
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
|
|
||||||
# Ensure your DATABASE_URI uses 'mongo' as the hostname ie. mongodb://mongo/my-db-name
|
# Ensure your DATABASE_URI uses 'mongo' as the hostname ie. mongodb://mongo/my-db-name
|
||||||
mongo:
|
# mongo:
|
||||||
image: mongo:latest
|
# image: mongo:latest
|
||||||
ports:
|
# ports:
|
||||||
- '27017:27017'
|
# - '27017:27017'
|
||||||
command:
|
# command:
|
||||||
- --storageEngine=wiredTiger
|
# - --storageEngine=wiredTiger
|
||||||
volumes:
|
# volumes:
|
||||||
- data:/data/db
|
# - data:/data/db
|
||||||
logging:
|
# logging:
|
||||||
driver: none
|
# driver: none
|
||||||
|
|
||||||
# Uncomment the following to use postgres
|
# Uncomment the following to use postgres
|
||||||
# postgres:
|
# postgres:
|
||||||
|
|||||||
@ -15,7 +15,7 @@ const eslintConfig = [
|
|||||||
rules: {
|
rules: {
|
||||||
'@typescript-eslint/ban-ts-comment': 'warn',
|
'@typescript-eslint/ban-ts-comment': 'warn',
|
||||||
'@typescript-eslint/no-empty-object-type': 'warn',
|
'@typescript-eslint/no-empty-object-type': 'warn',
|
||||||
'@typescript-eslint/no-explicit-any': 'warn',
|
'@typescript-eslint/no-explicit-any': 'off',
|
||||||
'@typescript-eslint/no-unused-vars': [
|
'@typescript-eslint/no-unused-vars': [
|
||||||
'warn',
|
'warn',
|
||||||
{
|
{
|
||||||
|
|||||||
1689
package-lock.json
generated
1689
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
20
package.json
20
package.json
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "ysandler-work",
|
"name": "ysandler-work",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "A blank template to get started with Payload 3.0",
|
"description": "Personal Portfolio Site",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@ -15,26 +15,42 @@
|
|||||||
"start": "cross-env NODE_OPTIONS=--no-deprecation next start"
|
"start": "cross-env NODE_OPTIONS=--no-deprecation next start"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@payloadcms/db-postgres": "3.33.0",
|
||||||
"@payloadcms/next": "3.33.0",
|
"@payloadcms/next": "3.33.0",
|
||||||
"@payloadcms/payload-cloud": "3.33.0",
|
"@payloadcms/payload-cloud": "3.33.0",
|
||||||
"@payloadcms/richtext-lexical": "3.33.0",
|
"@payloadcms/richtext-lexical": "3.33.0",
|
||||||
|
"@radix-ui/react-avatar": "^1.1.7",
|
||||||
|
"@radix-ui/react-icons": "^1.3.2",
|
||||||
|
"@radix-ui/react-separator": "^1.1.4",
|
||||||
|
"@radix-ui/react-tooltip": "^1.2.4",
|
||||||
|
"@tailwindcss/postcss": "^4.1.4",
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"graphql": "^16.8.1",
|
"graphql": "^16.8.1",
|
||||||
|
"lucide-react": "^0.503.0",
|
||||||
|
"motion": "^12.7.4",
|
||||||
"next": "15.3.0",
|
"next": "15.3.0",
|
||||||
|
"next-themes": "^0.4.6",
|
||||||
"payload": "3.33.0",
|
"payload": "3.33.0",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"react-dom": "19.1.0",
|
"react-dom": "19.1.0",
|
||||||
|
"react-markdown": "^10.1.0",
|
||||||
"sharp": "0.32.6",
|
"sharp": "0.32.6",
|
||||||
"@payloadcms/db-postgres": "3.33.0"
|
"tailwind-merge": "^3.2.0",
|
||||||
|
"tw-animate-css": "^1.2.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3.2.0",
|
"@eslint/eslintrc": "^3.2.0",
|
||||||
"@types/node": "^22.5.4",
|
"@types/node": "^22.5.4",
|
||||||
"@types/react": "19.1.0",
|
"@types/react": "19.1.0",
|
||||||
"@types/react-dom": "19.1.2",
|
"@types/react-dom": "19.1.2",
|
||||||
|
"autoprefixer": "^10.4.21",
|
||||||
"eslint": "^9.16.0",
|
"eslint": "^9.16.0",
|
||||||
"eslint-config-next": "15.3.0",
|
"eslint-config-next": "15.3.0",
|
||||||
|
"postcss": "^8.5.3",
|
||||||
"prettier": "^3.4.2",
|
"prettier": "^3.4.2",
|
||||||
|
"tailwindcss": "^4.1.4",
|
||||||
"typescript": "5.7.3"
|
"typescript": "5.7.3"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|||||||
6
postcss.config.mjs
Normal file
6
postcss.config.mjs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
const config = {
|
||||||
|
plugins: {
|
||||||
|
'@tailwindcss/postcss': {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
export default config
|
||||||
8
src/app/(frontend)/[slug]/page.client.tsx
Normal file
8
src/app/(frontend)/[slug]/page.client.tsx
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
'use client'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
const PageClient: React.FC = () => {
|
||||||
|
return <React.Fragment />
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PageClient
|
||||||
87
src/app/(frontend)/[slug]/page.tsx
Normal file
87
src/app/(frontend)/[slug]/page.tsx
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
import configPromise from '@payload-config'
|
||||||
|
import { getPayload, type RequiredDataFromCollectionSlug } from 'payload'
|
||||||
|
import { draftMode } from "next/headers";
|
||||||
|
import { cache } from "react";
|
||||||
|
import { RenderBlocks } from "@/blocks/RenderBlocks";
|
||||||
|
import PageClient from "./page.client";
|
||||||
|
|
||||||
|
|
||||||
|
export async function generateStaticParams() {
|
||||||
|
const payload = await getPayload({ config: configPromise })
|
||||||
|
const pages = await payload.find({
|
||||||
|
collection: 'pages',
|
||||||
|
draft: false,
|
||||||
|
limit: 1000,
|
||||||
|
overrideAccess: false,
|
||||||
|
pagination: false,
|
||||||
|
select: {
|
||||||
|
slug: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const params = pages.docs
|
||||||
|
?.filter((doc) => {
|
||||||
|
return doc.slug !== 'home'
|
||||||
|
})
|
||||||
|
.map(({ slug }) => {
|
||||||
|
return { slug }
|
||||||
|
})
|
||||||
|
|
||||||
|
return params
|
||||||
|
}
|
||||||
|
|
||||||
|
const queryPageBySlug = cache(async ({ slug }: { slug: string }) => {
|
||||||
|
const { isEnabled: draft } = await draftMode()
|
||||||
|
|
||||||
|
const payload = await getPayload({ config: configPromise })
|
||||||
|
|
||||||
|
const result = await payload.find({
|
||||||
|
collection: 'pages',
|
||||||
|
draft,
|
||||||
|
limit: 1,
|
||||||
|
pagination: false,
|
||||||
|
overrideAccess: draft,
|
||||||
|
where: {
|
||||||
|
slug: {
|
||||||
|
equals: slug,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return result.docs?.[0] || null
|
||||||
|
})
|
||||||
|
|
||||||
|
type Args = {
|
||||||
|
params: Promise<{
|
||||||
|
slug?: string
|
||||||
|
}>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function Page({ params: paramsPromise }: Args) {
|
||||||
|
const { slug = 'home' } = await paramsPromise
|
||||||
|
|
||||||
|
//const { isEnabled: draft } = await draftMode()
|
||||||
|
//const url = '/' + slug
|
||||||
|
|
||||||
|
const page: RequiredDataFromCollectionSlug<'pages'> | null = await queryPageBySlug({
|
||||||
|
slug,
|
||||||
|
}) || null
|
||||||
|
|
||||||
|
if (!page) {
|
||||||
|
return <div /> // return <PayloadRedirects url={url} />
|
||||||
|
}
|
||||||
|
|
||||||
|
const { layout } = page
|
||||||
|
|
||||||
|
return (
|
||||||
|
<article className="flex flex-col min-h-[100dvh] space-y-10" >
|
||||||
|
<PageClient />
|
||||||
|
{/* Allows redirects for valid pages too */}
|
||||||
|
{/*<PayloadRedirects disableNotFound url={url} />*/}
|
||||||
|
{/*draft && <LivePreviewListener />*/}
|
||||||
|
|
||||||
|
<RenderBlocks blocks={layout} />
|
||||||
|
</article>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@ -1,18 +1,82 @@
|
|||||||
import React from 'react'
|
import { ThemeProvider } from '@/components/theme-provider'
|
||||||
import './styles.css'
|
import { TooltipProvider } from '@/components/ui/tooltip'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
import type { Metadata } from 'next'
|
||||||
|
import { Inter as FontSans } from 'next/font/google'
|
||||||
|
import { getPayload } from 'payload'
|
||||||
|
import configPromise from '@payload-config'
|
||||||
|
import Navbar from '@/globals/Nav/component'
|
||||||
|
|
||||||
export const metadata = {
|
const fontSans = FontSans({
|
||||||
description: 'A blank template using Payload in a Next.js app.',
|
subsets: ['latin'],
|
||||||
title: 'Payload Blank Template',
|
variable: '--font-sans',
|
||||||
|
})
|
||||||
|
|
||||||
|
const payload = await getPayload({ config: configPromise })
|
||||||
|
|
||||||
|
const metaProps = await payload.findGlobal({
|
||||||
|
slug: 'meta',
|
||||||
|
})
|
||||||
|
|
||||||
|
const navProps = await payload.findGlobal({
|
||||||
|
slug: 'nav',
|
||||||
|
})
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
metadataBase: new URL(metaProps.url || ''),
|
||||||
|
title: {
|
||||||
|
default: metaProps.name || '',
|
||||||
|
template: `%s | ${metaProps.name}`,
|
||||||
|
},
|
||||||
|
description: metaProps.description,
|
||||||
|
openGraph: {
|
||||||
|
title: `${metaProps.name}`,
|
||||||
|
description: metaProps.description || '',
|
||||||
|
url: metaProps.url || '',
|
||||||
|
siteName: `${metaProps.name}`,
|
||||||
|
locale: 'en_US',
|
||||||
|
type: 'website',
|
||||||
|
},
|
||||||
|
robots: {
|
||||||
|
index: true,
|
||||||
|
follow: true,
|
||||||
|
googleBot: {
|
||||||
|
index: true,
|
||||||
|
follow: true,
|
||||||
|
'max-video-preview': -1,
|
||||||
|
'max-image-preview': 'large',
|
||||||
|
'max-snippet': -1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
twitter: {
|
||||||
|
title: `${metaProps.name}`,
|
||||||
|
card: 'summary_large_image',
|
||||||
|
},
|
||||||
|
verification: {
|
||||||
|
google: `${metaProps.googleVerification}`,
|
||||||
|
yandex: '',
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function RootLayout(props: { children: React.ReactNode }) {
|
export default function RootLayout({
|
||||||
const { children } = props
|
children,
|
||||||
|
}: Readonly<{
|
||||||
|
children: React.ReactNode
|
||||||
|
}>) {
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en" suppressHydrationWarning>
|
||||||
<body>
|
<body
|
||||||
<main>{children}</main>
|
className={cn(
|
||||||
|
'min-h-screen bg-background font-sans antialiased max-w-2xl mx-auto py-12 sm:py-24 px-6',
|
||||||
|
fontSans.variable,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<ThemeProvider attribute="class" defaultTheme="light">
|
||||||
|
<TooltipProvider delayDuration={0}>
|
||||||
|
<main>{children}</main>
|
||||||
|
<Navbar {...navProps} />
|
||||||
|
</TooltipProvider>
|
||||||
|
</ThemeProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,59 +1,3 @@
|
|||||||
import { headers as getHeaders } from 'next/headers.js'
|
import PageTemplate from './[slug]/page'
|
||||||
import Image from 'next/image'
|
|
||||||
import { getPayload } from 'payload'
|
|
||||||
import React from 'react'
|
|
||||||
import { fileURLToPath } from 'url'
|
|
||||||
|
|
||||||
import config from '@/payload.config'
|
export default PageTemplate
|
||||||
import './styles.css'
|
|
||||||
|
|
||||||
export default async function HomePage() {
|
|
||||||
const headers = await getHeaders()
|
|
||||||
const payloadConfig = await config
|
|
||||||
const payload = await getPayload({ config: payloadConfig })
|
|
||||||
const { user } = await payload.auth({ headers })
|
|
||||||
|
|
||||||
const fileURL = `vscode://file/${fileURLToPath(import.meta.url)}`
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="home">
|
|
||||||
<div className="content">
|
|
||||||
<picture>
|
|
||||||
<source srcSet="https://raw.githubusercontent.com/payloadcms/payload/main/packages/ui/src/assets/payload-favicon.svg" />
|
|
||||||
<Image
|
|
||||||
alt="Payload Logo"
|
|
||||||
height={65}
|
|
||||||
src="https://raw.githubusercontent.com/payloadcms/payload/main/packages/ui/src/assets/payload-favicon.svg"
|
|
||||||
width={65}
|
|
||||||
/>
|
|
||||||
</picture>
|
|
||||||
{!user && <h1>Welcome to your new project.</h1>}
|
|
||||||
{user && <h1>Welcome back, {user.email}</h1>}
|
|
||||||
<div className="links">
|
|
||||||
<a
|
|
||||||
className="admin"
|
|
||||||
href={payloadConfig.routes.admin}
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
Go to admin panel
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
className="docs"
|
|
||||||
href="https://payloadcms.com/docs"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
Documentation
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="footer">
|
|
||||||
<p>Update this page by editing</p>
|
|
||||||
<a className="codeLink" href={fileURL}>
|
|
||||||
<code>app/(frontend)/page.tsx</code>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,164 +1 @@
|
|||||||
:root {
|
|
||||||
--font-mono: 'Roboto Mono', monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
* {
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
html {
|
|
||||||
font-size: 18px;
|
|
||||||
line-height: 32px;
|
|
||||||
|
|
||||||
background: rgb(0, 0, 0);
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
}
|
|
||||||
|
|
||||||
html,
|
|
||||||
body,
|
|
||||||
#app {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: system-ui;
|
|
||||||
font-size: 18px;
|
|
||||||
line-height: 32px;
|
|
||||||
|
|
||||||
margin: 0;
|
|
||||||
color: rgb(1000, 1000, 1000);
|
|
||||||
|
|
||||||
@media (max-width: 1024px) {
|
|
||||||
font-size: 15px;
|
|
||||||
line-height: 24px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
img {
|
|
||||||
max-width: 100%;
|
|
||||||
height: auto;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
margin: 40px 0;
|
|
||||||
font-size: 64px;
|
|
||||||
line-height: 70px;
|
|
||||||
font-weight: bold;
|
|
||||||
|
|
||||||
@media (max-width: 1024px) {
|
|
||||||
margin: 24px 0;
|
|
||||||
font-size: 42px;
|
|
||||||
line-height: 42px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
font-size: 38px;
|
|
||||||
line-height: 38px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 400px) {
|
|
||||||
font-size: 32px;
|
|
||||||
line-height: 32px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
margin: 24px 0;
|
|
||||||
|
|
||||||
@media (max-width: 1024px) {
|
|
||||||
margin: calc(var(--base) * 0.75) 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: currentColor;
|
|
||||||
|
|
||||||
&:focus {
|
|
||||||
opacity: 0.8;
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
opacity: 0.7;
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
svg {
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
.home {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
height: 100vh;
|
|
||||||
padding: 45px;
|
|
||||||
max-width: 1024px;
|
|
||||||
margin: 0 auto;
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
@media (max-width: 400px) {
|
|
||||||
padding: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
flex-grow: 1;
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.links {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
|
|
||||||
a {
|
|
||||||
text-decoration: none;
|
|
||||||
padding: 0.25rem 0.5rem;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin {
|
|
||||||
color: rgb(0, 0, 0);
|
|
||||||
background: rgb(1000, 1000, 1000);
|
|
||||||
border: 1px solid rgb(0, 0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
.docs {
|
|
||||||
color: rgb(1000, 1000, 1000);
|
|
||||||
background: rgb(0, 0, 0);
|
|
||||||
border: 1px solid rgb(1000, 1000, 1000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
|
|
||||||
@media (max-width: 1024px) {
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.codeLink {
|
|
||||||
text-decoration: none;
|
|
||||||
padding: 0 0.5rem;
|
|
||||||
background: rgb(60, 60, 60);
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1 +1,37 @@
|
|||||||
export const importMap = {}
|
import { RscEntryLexicalCell as RscEntryLexicalCell_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc'
|
||||||
|
import { RscEntryLexicalField as RscEntryLexicalField_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc'
|
||||||
|
import { LexicalDiffComponent as LexicalDiffComponent_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc'
|
||||||
|
import { BlocksFeatureClient as BlocksFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
|
import { LinkFeatureClient as LinkFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
|
import { HeadingFeatureClient as HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
|
import { InlineCodeFeatureClient as InlineCodeFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
|
import { IndentFeatureClient as IndentFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
|
import { OrderedListFeatureClient as OrderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
|
import { UnorderedListFeatureClient as UnorderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
|
import { BlockquoteFeatureClient as BlockquoteFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
|
import { AlignFeatureClient as AlignFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
|
import { BoldFeatureClient as BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
|
import { ItalicFeatureClient as ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
|
import { UnderlineFeatureClient as UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
|
import { ParagraphFeatureClient as ParagraphFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
|
import { FixedToolbarFeatureClient as FixedToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
|
|
||||||
|
export const importMap = {
|
||||||
|
"@payloadcms/richtext-lexical/rsc#RscEntryLexicalCell": RscEntryLexicalCell_44fe37237e0ebf4470c9990d8cb7b07e,
|
||||||
|
"@payloadcms/richtext-lexical/rsc#RscEntryLexicalField": RscEntryLexicalField_44fe37237e0ebf4470c9990d8cb7b07e,
|
||||||
|
"@payloadcms/richtext-lexical/rsc#LexicalDiffComponent": LexicalDiffComponent_44fe37237e0ebf4470c9990d8cb7b07e,
|
||||||
|
"@payloadcms/richtext-lexical/client#BlocksFeatureClient": BlocksFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@payloadcms/richtext-lexical/client#LinkFeatureClient": LinkFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@payloadcms/richtext-lexical/client#HeadingFeatureClient": HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@payloadcms/richtext-lexical/client#InlineCodeFeatureClient": InlineCodeFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@payloadcms/richtext-lexical/client#IndentFeatureClient": IndentFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@payloadcms/richtext-lexical/client#OrderedListFeatureClient": OrderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@payloadcms/richtext-lexical/client#UnorderedListFeatureClient": UnorderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@payloadcms/richtext-lexical/client#BlockquoteFeatureClient": BlockquoteFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@payloadcms/richtext-lexical/client#AlignFeatureClient": AlignFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@payloadcms/richtext-lexical/client#BoldFeatureClient": BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@payloadcms/richtext-lexical/client#ItalicFeatureClient": ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@payloadcms/richtext-lexical/client#UnderlineFeatureClient": UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@payloadcms/richtext-lexical/client#ParagraphFeatureClient": ParagraphFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@payloadcms/richtext-lexical/client#FixedToolbarFeatureClient": FixedToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864
|
||||||
|
}
|
||||||
|
|||||||
@ -23,9 +23,13 @@ const serverFunction: ServerFunctionClient = async function (args) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const Layout = ({ children }: Args) => (
|
const Layout = ({ children }: Args) => (
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
<RootLayout config={config} importMap={importMap} serverFunction={serverFunction}>
|
<RootLayout config={config} importMap={importMap} serverFunction={serverFunction}>
|
||||||
{children}
|
{children}
|
||||||
</RootLayout>
|
</RootLayout>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
)
|
)
|
||||||
|
|
||||||
export default Layout
|
export default Layout
|
||||||
|
|||||||
120
src/app/globals.css
Normal file
120
src/app/globals.css
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
|
@import "tw-animate-css";
|
||||||
|
|
||||||
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
|
@theme inline {
|
||||||
|
--radius-sm: calc(var(--radius) - 4px);
|
||||||
|
--radius-md: calc(var(--radius) - 2px);
|
||||||
|
--radius-lg: var(--radius);
|
||||||
|
--radius-xl: calc(var(--radius) + 4px);
|
||||||
|
--color-background: var(--background);
|
||||||
|
--color-foreground: var(--foreground);
|
||||||
|
--color-card: var(--card);
|
||||||
|
--color-card-foreground: var(--card-foreground);
|
||||||
|
--color-popover: var(--popover);
|
||||||
|
--color-popover-foreground: var(--popover-foreground);
|
||||||
|
--color-primary: var(--primary);
|
||||||
|
--color-primary-foreground: var(--primary-foreground);
|
||||||
|
--color-secondary: var(--secondary);
|
||||||
|
--color-secondary-foreground: var(--secondary-foreground);
|
||||||
|
--color-muted: var(--muted);
|
||||||
|
--color-muted-foreground: var(--muted-foreground);
|
||||||
|
--color-accent: var(--accent);
|
||||||
|
--color-accent-foreground: var(--accent-foreground);
|
||||||
|
--color-destructive: var(--destructive);
|
||||||
|
--color-border: var(--border);
|
||||||
|
--color-input: var(--input);
|
||||||
|
--color-ring: var(--ring);
|
||||||
|
--color-chart-1: var(--chart-1);
|
||||||
|
--color-chart-2: var(--chart-2);
|
||||||
|
--color-chart-3: var(--chart-3);
|
||||||
|
--color-chart-4: var(--chart-4);
|
||||||
|
--color-chart-5: var(--chart-5);
|
||||||
|
--color-sidebar: var(--sidebar);
|
||||||
|
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||||
|
--color-sidebar-primary: var(--sidebar-primary);
|
||||||
|
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||||
|
--color-sidebar-accent: var(--sidebar-accent);
|
||||||
|
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||||
|
--color-sidebar-border: var(--sidebar-border);
|
||||||
|
--color-sidebar-ring: var(--sidebar-ring);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--radius: 0.625rem;
|
||||||
|
--background: oklch(1 0 0);
|
||||||
|
--foreground: oklch(0.147 0.004 49.25);
|
||||||
|
--card: oklch(1 0 0);
|
||||||
|
--card-foreground: oklch(0.147 0.004 49.25);
|
||||||
|
--popover: oklch(1 0 0);
|
||||||
|
--popover-foreground: oklch(0.147 0.004 49.25);
|
||||||
|
--primary: oklch(0.216 0.006 56.043);
|
||||||
|
--primary-foreground: oklch(0.985 0.001 106.423);
|
||||||
|
--secondary: oklch(0.97 0.001 106.424);
|
||||||
|
--secondary-foreground: oklch(0.216 0.006 56.043);
|
||||||
|
--muted: oklch(0.97 0.001 106.424);
|
||||||
|
--muted-foreground: oklch(0.553 0.013 58.071);
|
||||||
|
--accent: oklch(0.97 0.001 106.424);
|
||||||
|
--accent-foreground: oklch(0.216 0.006 56.043);
|
||||||
|
--destructive: oklch(0.577 0.245 27.325);
|
||||||
|
--border: oklch(0.923 0.003 48.717);
|
||||||
|
--input: oklch(0.923 0.003 48.717);
|
||||||
|
--ring: oklch(0.709 0.01 56.259);
|
||||||
|
--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.001 106.423);
|
||||||
|
--sidebar-foreground: oklch(0.147 0.004 49.25);
|
||||||
|
--sidebar-primary: oklch(0.216 0.006 56.043);
|
||||||
|
--sidebar-primary-foreground: oklch(0.985 0.001 106.423);
|
||||||
|
--sidebar-accent: oklch(0.97 0.001 106.424);
|
||||||
|
--sidebar-accent-foreground: oklch(0.216 0.006 56.043);
|
||||||
|
--sidebar-border: oklch(0.923 0.003 48.717);
|
||||||
|
--sidebar-ring: oklch(0.709 0.01 56.259);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--background: oklch(0.147 0.004 49.25);
|
||||||
|
--foreground: oklch(0.985 0.001 106.423);
|
||||||
|
--card: oklch(0.216 0.006 56.043);
|
||||||
|
--card-foreground: oklch(0.985 0.001 106.423);
|
||||||
|
--popover: oklch(0.216 0.006 56.043);
|
||||||
|
--popover-foreground: oklch(0.985 0.001 106.423);
|
||||||
|
--primary: oklch(0.923 0.003 48.717);
|
||||||
|
--primary-foreground: oklch(0.216 0.006 56.043);
|
||||||
|
--secondary: oklch(0.268 0.007 34.298);
|
||||||
|
--secondary-foreground: oklch(0.985 0.001 106.423);
|
||||||
|
--muted: oklch(0.268 0.007 34.298);
|
||||||
|
--muted-foreground: oklch(0.709 0.01 56.259);
|
||||||
|
--accent: oklch(0.268 0.007 34.298);
|
||||||
|
--accent-foreground: oklch(0.985 0.001 106.423);
|
||||||
|
--destructive: oklch(0.704 0.191 22.216);
|
||||||
|
--border: oklch(1 0 0 / 10%);
|
||||||
|
--input: oklch(1 0 0 / 15%);
|
||||||
|
--ring: oklch(0.553 0.013 58.071);
|
||||||
|
--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.216 0.006 56.043);
|
||||||
|
--sidebar-foreground: oklch(0.985 0.001 106.423);
|
||||||
|
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||||
|
--sidebar-primary-foreground: oklch(0.985 0.001 106.423);
|
||||||
|
--sidebar-accent: oklch(0.268 0.007 34.298);
|
||||||
|
--sidebar-accent-foreground: oklch(0.985 0.001 106.423);
|
||||||
|
--sidebar-border: oklch(1 0 0 / 10%);
|
||||||
|
--sidebar-ring: oklch(0.553 0.013 58.071);
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
@apply border-border outline-ring/50;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
@apply bg-background text-foreground;
|
||||||
|
}
|
||||||
|
}
|
||||||
12
src/app/layout.tsx
Normal file
12
src/app/layout.tsx
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { ReactNode } from 'react'
|
||||||
|
import './globals.css'
|
||||||
|
|
||||||
|
type LayoutProps = {
|
||||||
|
children: ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
const Layout = ({ children }: LayoutProps) => {
|
||||||
|
return children
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Layout
|
||||||
33
src/blocks/BadgeList/component.tsx
Normal file
33
src/blocks/BadgeList/component.tsx
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import { Badge } from '@/components/ui/badge'
|
||||||
|
import BlurFade from '@/components/ui/blur-fade'
|
||||||
|
import { BadgeListBlock as BadgeListProps } from '@/payload-types'
|
||||||
|
import clsx from 'clsx'
|
||||||
|
|
||||||
|
const BLUR_FADE_DELAY = 0.04
|
||||||
|
|
||||||
|
type Props = { className?: string } & BadgeListProps
|
||||||
|
export const BadgeList = (props: Props) => {
|
||||||
|
return (
|
||||||
|
<section id={props.blockName || ''} className="mt-1">
|
||||||
|
<div className="flex flex-col gap-y-3">
|
||||||
|
{!props.shouldHideListHeader && (
|
||||||
|
<BlurFade delay={BLUR_FADE_DELAY * 9}>
|
||||||
|
<h2 className="text-xl font-bold">{props.listHeader}</h2>
|
||||||
|
</BlurFade>
|
||||||
|
)}
|
||||||
|
<div className="flex flex-wrap gap-1">
|
||||||
|
{props.badges?.map((b, id) => (
|
||||||
|
<BlurFade key={b.id} delay={BLUR_FADE_DELAY * 10 + id * 0.05}>
|
||||||
|
<Badge
|
||||||
|
className={clsx(props.textSize ? props.textSize : '')}
|
||||||
|
variant={props.listVariant || 'default'}
|
||||||
|
>
|
||||||
|
{b.value}
|
||||||
|
</Badge>
|
||||||
|
</BlurFade>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
72
src/blocks/BadgeList/config.ts
Normal file
72
src/blocks/BadgeList/config.ts
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import { Block } from "payload";
|
||||||
|
|
||||||
|
export const BadgeList: Block = {
|
||||||
|
slug: 'badgeList',
|
||||||
|
interfaceName: 'BadgeListBlock',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'listHeader',
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'shouldHideListHeader',
|
||||||
|
type: 'checkbox',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'defaultColor',
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'defaultTextColor',
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'textSize',
|
||||||
|
type: 'text',
|
||||||
|
admin: {
|
||||||
|
description: 'A valid CSS text size string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'listVariant',
|
||||||
|
type: 'select',
|
||||||
|
options: [
|
||||||
|
'default',
|
||||||
|
'secondary',
|
||||||
|
'destructive',
|
||||||
|
'outline',
|
||||||
|
],
|
||||||
|
defaultValue: 'default',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'badges',
|
||||||
|
type: 'array',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'value',
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'color',
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'textColor',
|
||||||
|
type: 'text',
|
||||||
|
admin: {
|
||||||
|
description: 'String of a valid CSS color',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'icon',
|
||||||
|
type: 'relationship',
|
||||||
|
relationTo: 'media'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'href',
|
||||||
|
type: 'text',
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
41
src/blocks/ProfileBrief/component.tsx
Normal file
41
src/blocks/ProfileBrief/component.tsx
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||||
|
import BlurFade from '@/components/ui/blur-fade'
|
||||||
|
import BlurFadeText from '@/components/ui/blur-fade-text'
|
||||||
|
import type { Media, ProfileBriefBlock as ProfileBriefProps } from '@/payload-types'
|
||||||
|
|
||||||
|
const BLUR_FADE_DELAY = 0.04
|
||||||
|
const INITITALS = 'YS'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
className?: string
|
||||||
|
} & ProfileBriefProps
|
||||||
|
export const ProfileBrief = (props: Props) => {
|
||||||
|
const avatar = (props.avatar as Media) || undefined
|
||||||
|
return (
|
||||||
|
<section id="hero">
|
||||||
|
<div className="mx-auto w-full max-w-2xl space-y-8">
|
||||||
|
<div className="gap-2 flex justify-between">
|
||||||
|
<div className="flex-col flex flex-1 space-y-1.5">
|
||||||
|
<BlurFadeText
|
||||||
|
delay={BLUR_FADE_DELAY}
|
||||||
|
className="text-3xl font-bold tracking-tighter sm:text-5xl xl:text-6xl/none"
|
||||||
|
yOffset={8}
|
||||||
|
text={props.heading || ''}
|
||||||
|
/>
|
||||||
|
<BlurFadeText
|
||||||
|
className="max-w-[600px] md:text-xl"
|
||||||
|
delay={BLUR_FADE_DELAY}
|
||||||
|
text={props.subheading || ''}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<BlurFade delay={BLUR_FADE_DELAY}>
|
||||||
|
<Avatar className="size-28 border">
|
||||||
|
<AvatarImage alt={avatar?.alt || ''} src={avatar?.url || ''} />
|
||||||
|
<AvatarFallback>{INITITALS}</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
</BlurFade>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
21
src/blocks/ProfileBrief/config.ts
Normal file
21
src/blocks/ProfileBrief/config.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { Block } from "payload";
|
||||||
|
|
||||||
|
export const ProfileBrief: Block = {
|
||||||
|
slug: 'profileBrief',
|
||||||
|
interfaceName: 'ProfileBriefBlock',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'heading',
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'subheading',
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'avatar',
|
||||||
|
type: 'relationship',
|
||||||
|
relationTo: 'media'
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
46
src/blocks/RenderBlocks.tsx
Normal file
46
src/blocks/RenderBlocks.tsx
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import React, { Fragment } from 'react'
|
||||||
|
|
||||||
|
import type { Page } from '@/payload-types'
|
||||||
|
import { SimpleBrief } from './SimpleBrief/component'
|
||||||
|
import { SimpleList } from './SimpleList/component'
|
||||||
|
import { Showcase } from './Showcase/component'
|
||||||
|
import { ProfileBrief } from './ProfileBrief/component'
|
||||||
|
import { BadgeList } from './BadgeList/component'
|
||||||
|
|
||||||
|
const blockComponents = {
|
||||||
|
simpleBrief: SimpleBrief,
|
||||||
|
simpleList: SimpleList,
|
||||||
|
showcase: Showcase,
|
||||||
|
profileBrief: ProfileBrief,
|
||||||
|
badgeList: BadgeList,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RenderBlocks: React.FC<{
|
||||||
|
blocks: Page['layout']
|
||||||
|
}> = (props) => {
|
||||||
|
const blocks = Array.from(props.blocks?.values() || [])
|
||||||
|
const hasBlocks = blocks && Array.isArray(blocks) && blocks.length > 0
|
||||||
|
if (!hasBlocks) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
{blocks.map((block, index) => {
|
||||||
|
const { blockType } = block
|
||||||
|
|
||||||
|
if (blockType && blockType in blockComponents) {
|
||||||
|
const Block = blockComponents[blockType]
|
||||||
|
|
||||||
|
if (Block) {
|
||||||
|
return (
|
||||||
|
<div key={index}>
|
||||||
|
{/* @ts-expect-error there may be some mismatch between the expected types here */}
|
||||||
|
<Block {...block} disableInnerContainer />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
})}
|
||||||
|
</Fragment>
|
||||||
|
)
|
||||||
|
}
|
||||||
56
src/blocks/Showcase/component.tsx
Normal file
56
src/blocks/Showcase/component.tsx
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import { ProjectCard } from '@/components/project-card'
|
||||||
|
import BlurFade from '@/components/ui/blur-fade'
|
||||||
|
import { BadgeListBlock, Media, ShowcaseBlock as ShowcaseProps } from '@/payload-types'
|
||||||
|
import { ShowcaseCardBlock as ShowcaseCardProps } from '@/payload-types'
|
||||||
|
import { DefaultTypedEditorState } from '@payloadcms/richtext-lexical'
|
||||||
|
|
||||||
|
const BLUR_FADE_DELAY = 0.04
|
||||||
|
|
||||||
|
type CardProps = { className?: string; index: number } & ShowcaseCardProps
|
||||||
|
export const ShowcaseCard = (props: CardProps) => {
|
||||||
|
const realtedImage = props.image as Media | undefined
|
||||||
|
const realtedVideo = props.video as Media | undefined
|
||||||
|
const description = props.description as DefaultTypedEditorState | undefined
|
||||||
|
const tags = (props.tags as BadgeListBlock[]) || []
|
||||||
|
const links = (props.links as BadgeListBlock[]) || []
|
||||||
|
return (
|
||||||
|
<BlurFade delay={BLUR_FADE_DELAY * 12 + props.index * 0.05}>
|
||||||
|
<ProjectCard
|
||||||
|
href={props.mainLink || ''}
|
||||||
|
title={props.title || ''}
|
||||||
|
description={description}
|
||||||
|
dates={props.unstructuredDate || ''}
|
||||||
|
tags={tags}
|
||||||
|
image={realtedImage}
|
||||||
|
video={realtedVideo}
|
||||||
|
links={links}
|
||||||
|
/>
|
||||||
|
</BlurFade>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ShowcaseComponentProps = { className: string } & ShowcaseProps
|
||||||
|
export const Showcase = (props: ShowcaseComponentProps) => {
|
||||||
|
return (
|
||||||
|
<section id={props.blockName || ''}>
|
||||||
|
<div className="space-y-12 w-full py-12">
|
||||||
|
<BlurFade delay={BLUR_FADE_DELAY * 11}>
|
||||||
|
<div className="flex flex-col items-center justify-center space-y-4 text-center">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h2 className="inline-block rounded-lg bg-foreground text-background px-3 py-1 text-sm">
|
||||||
|
{props.listName}
|
||||||
|
</h2>
|
||||||
|
<h3 className="text-3xl font-bold tracking-tighter sm:text-5xl">{props.heading}</h3>
|
||||||
|
<p className="text-muted-foreground md:text-xl/relaxed lg:text-base/relaxed xl:text-xl/relaxed">
|
||||||
|
{props.subheading}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</BlurFade>
|
||||||
|
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 max-w-[800px] mx-auto">
|
||||||
|
{props.lineItems?.map((item, i) => <ShowcaseCard key={item.title} index={i} {...item} />)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
71
src/blocks/Showcase/config.ts
Normal file
71
src/blocks/Showcase/config.ts
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import { Block } from "payload";
|
||||||
|
import { BadgeList } from "../BadgeList/config";
|
||||||
|
|
||||||
|
export const ShowcaseCard: Block = {
|
||||||
|
slug: 'showcaseCard',
|
||||||
|
interfaceName: 'ShowcaseCardBlock',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'image',
|
||||||
|
type: 'relationship',
|
||||||
|
relationTo: 'media'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'video',
|
||||||
|
type: 'relationship',
|
||||||
|
relationTo: 'media'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'mainLink',
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'title',
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'unstructuredDate',
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'description',
|
||||||
|
type: 'richText'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'tags',
|
||||||
|
label: 'Add Tag Section',
|
||||||
|
type: 'blocks',
|
||||||
|
blocks: [BadgeList],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'links',
|
||||||
|
label: 'Add Link Section',
|
||||||
|
type: 'blocks',
|
||||||
|
blocks: [BadgeList],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Showcase: Block = {
|
||||||
|
slug: 'showcase',
|
||||||
|
interfaceName: 'ShowcaseBlock',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'listName',
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'heading',
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'subheading',
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'lineItems',
|
||||||
|
type: 'blocks',
|
||||||
|
blocks: [ShowcaseCard]
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
23
src/blocks/SimpleBrief/component.tsx
Normal file
23
src/blocks/SimpleBrief/component.tsx
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { RichText } from '@/components/RichText'
|
||||||
|
import BlurFade from '@/components/ui/blur-fade'
|
||||||
|
import { SimpleBriefBlock } from '@/payload-types'
|
||||||
|
import { DefaultTypedEditorState } from '@payloadcms/richtext-lexical'
|
||||||
|
|
||||||
|
const BLUR_FADE_DELAY = 0.04
|
||||||
|
|
||||||
|
type Props = { className: string } & SimpleBriefBlock
|
||||||
|
export const SimpleBrief = (props: Props) => {
|
||||||
|
return (
|
||||||
|
<section id={props.blockName || ''}>
|
||||||
|
<BlurFade delay={BLUR_FADE_DELAY * 3}>
|
||||||
|
<h2 className="text-xl font-bold">{props.title}</h2>
|
||||||
|
</BlurFade>
|
||||||
|
<BlurFade delay={BLUR_FADE_DELAY * 4}>
|
||||||
|
<RichText
|
||||||
|
data={props.content as DefaultTypedEditorState}
|
||||||
|
className="prose max-w-full text-pretty font-sans text-sm text-muted-foreground dark:prose-invert"
|
||||||
|
/>
|
||||||
|
</BlurFade>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
16
src/blocks/SimpleBrief/config.ts
Normal file
16
src/blocks/SimpleBrief/config.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { Block } from "payload";
|
||||||
|
|
||||||
|
export const SimpleBrief: Block = {
|
||||||
|
slug: 'simpleBrief',
|
||||||
|
interfaceName: 'simpleBriefBlock',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'title',
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'content',
|
||||||
|
type: 'richText'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
39
src/blocks/SimpleList/component.tsx
Normal file
39
src/blocks/SimpleList/component.tsx
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import { ResumeCard } from '@/components/resume-card'
|
||||||
|
import BlurFade from '@/components/ui/blur-fade'
|
||||||
|
import { Media, SimpleListBlock } from '@/payload-types'
|
||||||
|
import { DefaultTypedEditorState } from '@payloadcms/richtext-lexical'
|
||||||
|
|
||||||
|
const BLUR_FADE_DELAY = 0.04
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
className: string
|
||||||
|
} & SimpleListBlock
|
||||||
|
export const SimpleList = (props: Props) => {
|
||||||
|
return (
|
||||||
|
<section id="work">
|
||||||
|
<div className="flex min-h-0 flex-col gap-y-3">
|
||||||
|
<BlurFade delay={BLUR_FADE_DELAY * 5}>
|
||||||
|
<h2 className="text-xl font-bold">{props.listHeader}</h2>
|
||||||
|
</BlurFade>
|
||||||
|
{props.lineItems?.map((item, id) => {
|
||||||
|
const avatar = item.avatar as Media | undefined
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BlurFade key={item.title || '' + id} delay={BLUR_FADE_DELAY * 6 + id * 0.05}>
|
||||||
|
<ResumeCard
|
||||||
|
logoUrl={avatar?.url || ''}
|
||||||
|
altText={avatar?.alt || ''}
|
||||||
|
title={item.title || ''}
|
||||||
|
subtitle={item.subtitle || ''}
|
||||||
|
href={item.link || ''}
|
||||||
|
badges={[]}
|
||||||
|
period={item.extraDetail || ''}
|
||||||
|
description={item.description as DefaultTypedEditorState}
|
||||||
|
/>
|
||||||
|
</BlurFade>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
51
src/blocks/SimpleList/config.ts
Normal file
51
src/blocks/SimpleList/config.ts
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import { Block } from "payload";
|
||||||
|
|
||||||
|
export const SimpleList: Block = {
|
||||||
|
slug: 'simpleList',
|
||||||
|
interfaceName: 'SimpleListBlock',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'listHeader',
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'lineItems',
|
||||||
|
type: 'array',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'title',
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'subtitle',
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'description',
|
||||||
|
type: 'richText',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'extraDetail',
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'avatar',
|
||||||
|
type: 'relationship',
|
||||||
|
relationTo: 'media',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'initials',
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'link',
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'isInitiallyHidden',
|
||||||
|
type: 'checkbox',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
27
src/collections/Pages.ts
Normal file
27
src/collections/Pages.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { BadgeList } from "@/blocks/BadgeList/config";
|
||||||
|
import { ProfileBrief } from "@/blocks/ProfileBrief/config";
|
||||||
|
import { Showcase } from "@/blocks/Showcase/config";
|
||||||
|
import { SimpleBrief } from "@/blocks/SimpleBrief/config";
|
||||||
|
import { SimpleList } from "@/blocks/SimpleList/config";
|
||||||
|
import { CollectionConfig } from "payload";
|
||||||
|
|
||||||
|
export const Pages: CollectionConfig = {
|
||||||
|
slug: 'pages',
|
||||||
|
admin: {
|
||||||
|
useAsTitle: 'slug',
|
||||||
|
},
|
||||||
|
access: {
|
||||||
|
read: () => true
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'slug',
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'layout',
|
||||||
|
type: 'blocks',
|
||||||
|
blocks: [SimpleList, Showcase, ProfileBrief, SimpleBrief, BadgeList],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
60
src/components/RichText.tsx
Normal file
60
src/components/RichText.tsx
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
import {
|
||||||
|
DefaultNodeTypes,
|
||||||
|
SerializedBlockNode,
|
||||||
|
SerializedLinkNode,
|
||||||
|
type DefaultTypedEditorState,
|
||||||
|
} from '@payloadcms/richtext-lexical'
|
||||||
|
import {
|
||||||
|
JSXConvertersFunction,
|
||||||
|
LinkJSXConverter,
|
||||||
|
RichText as ConvertRichText,
|
||||||
|
} from '@payloadcms/richtext-lexical/react'
|
||||||
|
import { BadgeList } from '@/blocks/BadgeList/component'
|
||||||
|
import { BadgeListBlock } from '@/payload-types'
|
||||||
|
|
||||||
|
type NodeTypes = DefaultNodeTypes | SerializedBlockNode<BadgeListBlock>
|
||||||
|
|
||||||
|
const internalDocToHref = ({ linkNode }: { linkNode: SerializedLinkNode }) => {
|
||||||
|
const { value, relationTo } = linkNode.fields.doc!
|
||||||
|
if (typeof value !== 'object') {
|
||||||
|
throw new Error('Expected value to be an object')
|
||||||
|
}
|
||||||
|
const slug = value.slug
|
||||||
|
return relationTo === 'posts' ? `/posts/${slug}` : `/${slug}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const jsxConverters: JSXConvertersFunction<NodeTypes> = ({ defaultConverters }) => {
|
||||||
|
return {
|
||||||
|
...defaultConverters,
|
||||||
|
...LinkJSXConverter({ internalDocToHref }),
|
||||||
|
blocks: {
|
||||||
|
badgeList: ({ node }) => <BadgeList {...node.fields} />,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
data: DefaultTypedEditorState
|
||||||
|
enableGutter?: boolean
|
||||||
|
enableProse?: boolean
|
||||||
|
} & React.HTMLAttributes<HTMLDivElement>
|
||||||
|
|
||||||
|
export const RichText = (props: Props) => {
|
||||||
|
const { className, enableProse = true, enableGutter = true, ...rest } = props
|
||||||
|
return (
|
||||||
|
<ConvertRichText
|
||||||
|
converters={jsxConverters}
|
||||||
|
className={cn(
|
||||||
|
'payload-richtext',
|
||||||
|
{
|
||||||
|
container: enableGutter,
|
||||||
|
'max-w-none': !enableGutter,
|
||||||
|
'mx-auto prose md:prose-md dark:prose-invert': enableProse,
|
||||||
|
},
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
62
src/components/hackathon-card.tsx
Normal file
62
src/components/hackathon-card.tsx
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
dates: string;
|
||||||
|
location: string;
|
||||||
|
image?: string;
|
||||||
|
links?: readonly {
|
||||||
|
icon: React.ReactNode;
|
||||||
|
title: string;
|
||||||
|
href: string;
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function HackathonCard({
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
dates,
|
||||||
|
location,
|
||||||
|
image,
|
||||||
|
links,
|
||||||
|
}: Props) {
|
||||||
|
return (
|
||||||
|
<li className="relative ml-10 py-4">
|
||||||
|
<div className="absolute -left-16 top-2 flex items-center justify-center bg-white rounded-full">
|
||||||
|
<Avatar className="border size-12 m-auto">
|
||||||
|
<AvatarImage src={image} alt={title} className="object-contain" />
|
||||||
|
<AvatarFallback>{title[0]}</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-1 flex-col justify-start gap-1">
|
||||||
|
{dates && (
|
||||||
|
<time className="text-xs text-muted-foreground">{dates}</time>
|
||||||
|
)}
|
||||||
|
<h2 className="font-semibold leading-none">{title}</h2>
|
||||||
|
{location && (
|
||||||
|
<p className="text-sm text-muted-foreground">{location}</p>
|
||||||
|
)}
|
||||||
|
{description && (
|
||||||
|
<span className="prose dark:prose-invert text-sm text-muted-foreground">
|
||||||
|
{description}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{links && links.length > 0 && (
|
||||||
|
<div className="mt-2 flex flex-row flex-wrap items-start gap-2">
|
||||||
|
{links?.map((link, idx) => (
|
||||||
|
<Link href={link.href} key={idx}>
|
||||||
|
<Badge key={idx} title={link.title} className="flex gap-2">
|
||||||
|
{link.icon}
|
||||||
|
{link.title}
|
||||||
|
</Badge>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
223
src/components/icons.tsx
Normal file
223
src/components/icons.tsx
Normal file
@ -0,0 +1,223 @@
|
|||||||
|
import { GlobeIcon, MailIcon } from "lucide-react";
|
||||||
|
|
||||||
|
export type IconProps = React.HTMLAttributes<SVGElement>;
|
||||||
|
|
||||||
|
export const Icons = {
|
||||||
|
globe: (props: IconProps) => <GlobeIcon {...props} />,
|
||||||
|
email: (props: IconProps) => <MailIcon {...props} />,
|
||||||
|
linkedin: (props: IconProps) => (
|
||||||
|
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||||
|
<title>LinkedIn</title>
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
x: (props: IconProps) => (
|
||||||
|
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||||
|
<title>X</title>
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M18.901 1.153h3.68l-8.04 9.19L24 22.846h-7.406l-5.8-7.584-6.638 7.584H.474l8.6-9.83L0 1.154h7.594l5.243 6.932ZM17.61 20.644h2.039L6.486 3.24H4.298Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
youtube: (props: IconProps) => (
|
||||||
|
<svg
|
||||||
|
width="32px"
|
||||||
|
height="32px"
|
||||||
|
viewBox="0 0 32 32"
|
||||||
|
fill="currentColor"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<title>youtube</title>
|
||||||
|
<path d="M29.41,9.26a3.5,3.5,0,0,0-2.47-2.47C24.76,6.2,16,6.2,16,6.2s-8.76,0-10.94.59A3.5,3.5,0,0,0,2.59,9.26,36.13,36.13,0,0,0,2,16a36.13,36.13,0,0,0,.59,6.74,3.5,3.5,0,0,0,2.47,2.47C7.24,25.8,16,25.8,16,25.8s8.76,0,10.94-.59a3.5,3.5,0,0,0,2.47-2.47A36.13,36.13,0,0,0,30,16,36.13,36.13,0,0,0,29.41,9.26ZM13.2,20.2V11.8L20.47,16Z" />
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
nextjs: (props: IconProps) => (
|
||||||
|
<svg
|
||||||
|
role="img"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
className="size-8"
|
||||||
|
fill="currentColor"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<title>Next.js</title>
|
||||||
|
<path d="M11.5725 0c-.1763 0-.3098.0013-.3584.0067-.0516.0053-.2159.021-.3636.0328-3.4088.3073-6.6017 2.1463-8.624 4.9728C1.1004 6.584.3802 8.3666.1082 10.255c-.0962.659-.108.8537-.108 1.7474s.012 1.0884.108 1.7476c.652 4.506 3.8591 8.2919 8.2087 9.6945.7789.2511 1.6.4223 2.5337.5255.3636.04 1.9354.04 2.299 0 1.6117-.1783 2.9772-.577 4.3237-1.2643.2065-.1056.2464-.1337.2183-.1573-.0188-.0139-.8987-1.1938-1.9543-2.62l-1.919-2.592-2.4047-3.5583c-1.3231-1.9564-2.4117-3.556-2.4211-3.556-.0094-.0026-.0187 1.5787-.0235 3.509-.0067 3.3802-.0093 3.5162-.0516 3.596-.061.115-.108.1618-.2064.2134-.075.0374-.1408.0445-.495.0445h-.406l-.1078-.068a.4383.4383 0 01-.1572-.1712l-.0493-.1056.0053-4.703.0067-4.7054.0726-.0915c.0376-.0493.1174-.1125.1736-.143.0962-.047.1338-.0517.5396-.0517.4787 0 .5584.0187.6827.1547.0353.0377 1.3373 1.9987 2.895 4.3608a10760.433 10760.433 0 004.7344 7.1706l1.9002 2.8782.096-.0633c.8518-.5536 1.7525-1.3418 2.4657-2.1627 1.5179-1.7429 2.4963-3.868 2.8247-6.134.0961-.6591.1078-.854.1078-1.7475 0-.8937-.012-1.0884-.1078-1.7476-.6522-4.506-3.8592-8.2919-8.2087-9.6945-.7672-.2487-1.5836-.42-2.4985-.5232-.169-.0176-1.0835-.0366-1.6123-.037zm4.0685 7.217c.3473 0 .4082.0053.4857.047.1127.0562.204.1642.237.2767.0186.061.0234 1.3653.0186 4.3044l-.0067 4.2175-.7436-1.14-.7461-1.14v-3.066c0-1.982.0093-3.0963.0234-3.1502.0375-.1313.1196-.2346.2323-.2955.0961-.0494.1313-.054.4997-.054z" />
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
framermotion: (props: IconProps) => (
|
||||||
|
<svg
|
||||||
|
role="img"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
className="size-8"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<title>Framer Motion</title>
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||||
|
<path d="M12 12l-8 -8v16l16 -16v16l-4 -4" />
|
||||||
|
<path d="M20 12l-8 8l-4 -4" />
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
tailwindcss: (props: IconProps) => (
|
||||||
|
<svg
|
||||||
|
role="img"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
className="size-8"
|
||||||
|
fill="currentColor"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<title>Tailwind CSS</title>
|
||||||
|
<path d="m12.001 4.8c-3.2 0-5.2 1.6-6 4.8 1.2-1.6 2.6-2.2 4.2-1.8.913.228 1.565.89 2.288 1.624 1.177 1.194 2.538 2.576 5.512 2.576 3.2 0 5.2-1.6 6-4.8-1.2 1.6-2.6 2.2-4.2 1.8-.913-.228-1.565-.89-2.288-1.624-1.176-1.194-2.537-2.576-5.512-2.576zm-6 7.2c-3.2 0-5.2 1.6-6 4.8 1.2-1.6 2.6-2.2 4.2-1.8.913.228 1.565.89 2.288 1.624 1.177 1.194 2.538 2.576 5.512 2.576 3.2 0 5.2-1.6 6-4.8-1.2 1.6-2.6 2.2-4.2 1.8-.913-.228-1.565-.89-2.288-1.624-1.176-1.194-2.537-2.576-5.512-2.576z" />
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
typescript: (props: IconProps) => (
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 32 32"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
className="size-8"
|
||||||
|
fill="currentColor"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path d="m0 16v16h32v-32h-32zm25.786-1.276c.813.203 1.432.568 2.005 1.156.292.312.729.885.766 1.026.01.042-1.38.974-2.224 1.495-.031.021-.156-.109-.292-.313-.411-.599-.844-.859-1.505-.906-.969-.063-1.594.443-1.589 1.292-.005.208.042.417.135.599.214.443.615.708 1.854 1.245 2.292.984 3.271 1.635 3.88 2.557.682 1.031.833 2.677.375 3.906-.51 1.328-1.771 2.234-3.542 2.531-.547.099-1.849.083-2.438-.026-1.286-.229-2.505-.865-3.255-1.698-.297-.323-.87-1.172-.833-1.229.016-.021.146-.104.292-.188s.682-.396 1.188-.688l.922-.536.193.286c.271.411.859.974 1.214 1.161 1.021.542 2.422.464 3.115-.156.281-.234.438-.594.417-.958 0-.37-.047-.536-.24-.813-.25-.354-.755-.656-2.198-1.281-1.651-.714-2.365-1.151-3.01-1.854-.406-.464-.708-1.01-.88-1.599-.12-.453-.151-1.589-.057-2.042.339-1.599 1.547-2.708 3.281-3.036.563-.109 1.875-.068 2.427.068zm-7.51 1.339.01 1.307h-4.167v11.839h-2.948v-11.839h-4.161v-1.281c0-.714.016-1.307.036-1.323.016-.021 2.547-.031 5.62-.026l5.594.016z" />
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
react: (props: IconProps) => (
|
||||||
|
<svg
|
||||||
|
role="img"
|
||||||
|
viewBox="0 0 32 32"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
className="size-8"
|
||||||
|
fill="currentColor"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<title>React</title>
|
||||||
|
<path d="m16 13.146c-1.573 0-2.854 1.281-2.854 2.854s1.281 2.854 2.854 2.854 2.854-1.281 2.854-2.854-1.281-2.854-2.854-2.854zm-7.99 8.526-.63-.156c-4.688-1.188-7.38-3.198-7.38-5.521s2.693-4.333 7.38-5.521l.63-.156.177.625c.474 1.635 1.083 3.229 1.818 4.771l.135.281-.135.286c-.734 1.536-1.344 3.13-1.818 4.771zm-.921-9.74c-3.563 1-5.75 2.536-5.75 4.063s2.188 3.057 5.75 4.063c.438-1.391.964-2.745 1.578-4.063-.615-1.318-1.141-2.672-1.578-4.063zm16.901 9.74-.177-.625c-.474-1.635-1.083-3.229-1.818-4.766l-.135-.286.135-.286c.734-1.536 1.344-3.13 1.818-4.771l.177-.62.63.156c4.688 1.188 7.38 3.198 7.38 5.521s-2.693 4.333-7.38 5.521zm-.657-5.677c.641 1.385 1.172 2.745 1.578 4.063 3.568-1.005 5.75-2.536 5.75-4.063s-2.188-3.057-5.75-4.063c-.438 1.385-.964 2.745-1.578 4.063zm-16.255-4.068-.177-.625c-1.318-4.646-.917-7.979 1.099-9.141 1.979-1.141 5.151.208 8.479 3.625l.453.464-.453.464c-1.182 1.229-2.26 2.552-3.229 3.958l-.182.255-.313.026c-1.703.135-3.391.406-5.047.813zm2.531-8.838c-.359 0-.677.073-.943.229-1.323.766-1.557 3.422-.646 7.005 1.422-.318 2.859-.542 4.313-.672.833-1.188 1.75-2.323 2.734-3.391-2.078-2.026-4.047-3.172-5.458-3.172zm12.787 27.145c-.005 0-.005 0 0 0-1.901 0-4.344-1.427-6.875-4.031l-.453-.464.453-.464c1.182-1.229 2.26-2.552 3.229-3.958l.177-.255.313-.031c1.703-.13 3.391-.401 5.052-.813l.63-.156.177.625c1.318 4.646.917 7.974-1.099 9.135-.49.281-1.042.422-1.604.411zm-5.464-4.505c2.078 2.026 4.047 3.172 5.458 3.172h.005c.354 0 .672-.078.938-.229 1.323-.766 1.563-3.422.646-7.005-1.422.318-2.865.542-4.313.667-.833 1.193-1.75 2.323-2.734 3.396zm7.99-13.802-.63-.161c-1.661-.406-3.349-.677-5.052-.813l-.313-.026-.177-.255c-.969-1.406-2.047-2.729-3.229-3.958l-.453-.464.453-.464c3.328-3.417 6.5-4.766 8.479-3.625 2.016 1.161 2.417 4.495 1.099 9.141zm-5.255-2.276c1.521.141 2.969.365 4.313.672.917-3.583.677-6.24-.646-7.005-1.318-.76-3.797.406-6.401 2.943.984 1.073 1.896 2.203 2.734 3.391zm-10.058 20.583c-.563.01-1.12-.13-1.609-.411-2.016-1.161-2.417-4.49-1.099-9.135l.177-.625.63.156c1.542.391 3.24.661 5.047.813l.313.031.177.255c.969 1.406 2.047 2.729 3.229 3.958l.453.464-.453.464c-2.526 2.604-4.969 4.031-6.865 4.031zm-1.588-8.567c-.917 3.583-.677 6.24.646 7.005 1.318.75 3.792-.406 6.401-2.943-.984-1.073-1.901-2.203-2.734-3.396-1.453-.125-2.891-.349-4.313-.667zm7.979.838c-1.099 0-2.224-.047-3.354-.141l-.313-.026-.182-.26c-.635-.917-1.24-1.859-1.797-2.828-.563-.969-1.078-1.958-1.557-2.969l-.135-.286.135-.286c.479-1.01.995-2 1.557-2.969.552-.953 1.156-1.906 1.797-2.828l.182-.26.313-.026c2.234-.188 4.479-.188 6.708 0l.313.026.182.26c1.276 1.833 2.401 3.776 3.354 5.797l.135.286-.135.286c-.953 2.021-2.073 3.964-3.354 5.797l-.182.26-.313.026c-1.125.094-2.255.141-3.354.141zm-2.927-1.448c1.969.151 3.885.151 5.859 0 1.099-1.609 2.078-3.302 2.927-5.063-.844-1.76-1.823-3.453-2.932-5.063-1.948-.151-3.906-.151-5.854 0-1.109 1.609-2.089 3.302-2.932 5.063.849 1.76 1.828 3.453 2.932 5.063z" />
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
github: (props: IconProps) => (
|
||||||
|
<svg viewBox="0 0 438.549 438.549" {...props}>
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M409.132 114.573c-19.608-33.596-46.205-60.194-79.798-79.8-33.598-19.607-70.277-29.408-110.063-29.408-39.781 0-76.472 9.804-110.063 29.408-33.596 19.605-60.192 46.204-79.8 79.8C9.803 148.168 0 184.854 0 224.63c0 47.78 13.94 90.745 41.827 128.906 27.884 38.164 63.906 64.572 108.063 79.227 5.14.954 8.945.283 11.419-1.996 2.475-2.282 3.711-5.14 3.711-8.562 0-.571-.049-5.708-.144-15.417a2549.81 2549.81 0 01-.144-25.406l-6.567 1.136c-4.187.767-9.469 1.092-15.846 1-6.374-.089-12.991-.757-19.842-1.999-6.854-1.231-13.229-4.086-19.13-8.559-5.898-4.473-10.085-10.328-12.56-17.556l-2.855-6.57c-1.903-4.374-4.899-9.233-8.992-14.559-4.093-5.331-8.232-8.945-12.419-10.848l-1.999-1.431c-1.332-.951-2.568-2.098-3.711-3.429-1.142-1.331-1.997-2.663-2.568-3.997-.572-1.335-.098-2.43 1.427-3.289 1.525-.859 4.281-1.276 8.28-1.276l5.708.853c3.807.763 8.516 3.042 14.133 6.851 5.614 3.806 10.229 8.754 13.846 14.842 4.38 7.806 9.657 13.754 15.846 17.847 6.184 4.093 12.419 6.136 18.699 6.136 6.28 0 11.704-.476 16.274-1.423 4.565-.952 8.848-2.383 12.847-4.285 1.713-12.758 6.377-22.559 13.988-29.41-10.848-1.14-20.601-2.857-29.264-5.14-8.658-2.286-17.605-5.996-26.835-11.14-9.235-5.137-16.896-11.516-22.985-19.126-6.09-7.614-11.088-17.61-14.987-29.979-3.901-12.374-5.852-26.648-5.852-42.826 0-23.035 7.52-42.637 22.557-58.817-7.044-17.318-6.379-36.732 1.997-58.24 5.52-1.715 13.706-.428 24.554 3.853 10.85 4.283 18.794 7.952 23.84 10.994 5.046 3.041 9.089 5.618 12.135 7.708 17.705-4.947 35.976-7.421 54.818-7.421s37.117 2.474 54.823 7.421l10.849-6.849c7.419-4.57 16.18-8.758 26.262-12.565 10.088-3.805 17.802-4.853 23.134-3.138 8.562 21.509 9.325 40.922 2.279 58.24 15.036 16.18 22.559 35.787 22.559 58.817 0 16.178-1.958 30.497-5.853 42.966-3.9 12.471-8.941 22.457-15.125 29.979-6.191 7.521-13.901 13.85-23.131 18.986-9.232 5.14-18.182 8.85-26.84 11.136-8.662 2.286-18.415 4.004-29.263 5.146 9.894 8.562 14.842 22.077 14.842 40.539v60.237c0 3.422 1.19 6.279 3.572 8.562 2.379 2.279 6.136 2.95 11.276 1.995 44.163-14.653 80.185-41.062 108.068-79.226 27.88-38.161 41.825-81.126 41.825-128.906-.01-39.771-9.818-76.454-29.414-110.049z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
notion: (props: IconProps) => (
|
||||||
|
<svg
|
||||||
|
width="100"
|
||||||
|
height="100"
|
||||||
|
viewBox="0 0 100 100"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M6.017 4.313l55.333 -4.087c6.797 -0.583 8.543 -0.19 12.817 2.917l17.663 12.443c2.913 2.14 3.883 2.723 3.883 5.053v68.243c0 4.277 -1.553 6.807 -6.99 7.193L24.467 99.967c-4.08 0.193 -6.023 -0.39 -8.16 -3.113L3.3 79.94c-2.333 -3.113 -3.3 -5.443 -3.3 -8.167V11.113c0 -3.497 1.553 -6.413 6.017 -6.8z"
|
||||||
|
fill="#fff"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M61.35 0.227l-55.333 4.087C1.553 4.7 0 7.617 0 11.113v60.66c0 2.723 0.967 5.053 3.3 8.167l13.007 16.913c2.137 2.723 4.08 3.307 8.16 3.113l64.257 -3.89c5.433 -0.387 6.99 -2.917 6.99 -7.193V20.64c0 -2.21 -0.873 -2.847 -3.443 -4.733L74.167 3.143c-4.273 -3.107 -6.02 -3.5 -12.817 -2.917zM25.92 19.523c-5.247 0.353 -6.437 0.433 -9.417 -1.99L8.927 11.507c-0.77 -0.78 -0.383 -1.753 1.557 -1.947l53.193 -3.887c4.467 -0.39 6.793 1.167 8.54 2.527l9.123 6.61c0.39 0.197 1.36 1.36 0.193 1.36l-54.933 3.307 -0.68 0.047zM19.803 88.3V30.367c0 -2.53 0.777 -3.697 3.103 -3.893L86 22.78c2.14 -0.193 3.107 1.167 3.107 3.693v57.547c0 2.53 -0.39 4.67 -3.883 4.863l-60.377 3.5c-3.493 0.193 -5.043 -0.97 -5.043 -4.083zm59.6 -54.827c0.387 1.75 0 3.5 -1.75 3.7l-2.91 0.577v42.773c-2.527 1.36 -4.853 2.137 -6.797 2.137 -3.107 0 -3.883 -0.973 -6.21 -3.887l-19.03 -29.94v28.967l6.02 1.363s0 3.5 -4.857 3.5l-13.39 0.777c-0.39 -0.78 0 -2.723 1.357 -3.11l3.497 -0.97v-38.3L30.48 40.667c-0.39 -1.75 0.58 -4.277 3.3 -4.473l14.367 -0.967 19.8 30.327v-26.83l-5.047 -0.58c-0.39 -2.143 1.163 -3.7 3.103 -3.89l13.4 -0.78z"
|
||||||
|
fill="#000"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
openai: (props: IconProps) => (
|
||||||
|
<svg role="img" viewBox="0 0 24 24" {...props}>
|
||||||
|
<path d="M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z" />
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
googleDrive: (props: IconProps) => (
|
||||||
|
<svg viewBox="0 0 87.3 78" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||||
|
<path
|
||||||
|
d="m6.6 66.85 3.85 6.65c.8 1.4 1.95 2.5 3.3 3.3l13.75-23.8h-27.5c0 1.55.4 3.1 1.2 4.5z"
|
||||||
|
fill="#0066da"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="m43.65 25-13.75-23.8c-1.35.8-2.5 1.9-3.3 3.3l-25.4 44a9.06 9.06 0 0 0 -1.2 4.5h27.5z"
|
||||||
|
fill="#00ac47"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="m73.55 76.8c1.35-.8 2.5-1.9 3.3-3.3l1.6-2.75 7.65-13.25c.8-1.4 1.2-2.95 1.2-4.5h-27.502l5.852 11.5z"
|
||||||
|
fill="#ea4335"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="m43.65 25 13.75-23.8c-1.35-.8-2.9-1.2-4.5-1.2h-18.5c-1.6 0-3.15.45-4.5 1.2z"
|
||||||
|
fill="#00832d"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="m59.8 53h-32.3l-13.75 23.8c1.35.8 2.9 1.2 4.5 1.2h50.8c1.6 0 3.15-.45 4.5-1.2z"
|
||||||
|
fill="#2684fc"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="m73.4 26.5-12.7-22c-.8-1.4-1.95-2.5-3.3-3.3l-13.75 23.8 16.15 28h27.45c0-1.55-.4-3.1-1.2-4.5z"
|
||||||
|
fill="#ffba00"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
whatsapp: (props: IconProps) => (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 175.216 175.552"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<defs>
|
||||||
|
<linearGradient
|
||||||
|
id="b"
|
||||||
|
x1="85.915"
|
||||||
|
x2="86.535"
|
||||||
|
y1="32.567"
|
||||||
|
y2="137.092"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
>
|
||||||
|
<stop offset="0" stopColor="#57d163" />
|
||||||
|
<stop offset="1" stopColor="#23b33a" />
|
||||||
|
</linearGradient>
|
||||||
|
<filter
|
||||||
|
id="a"
|
||||||
|
width="1.115"
|
||||||
|
height="1.114"
|
||||||
|
x="-.057"
|
||||||
|
y="-.057"
|
||||||
|
colorInterpolationFilters="sRGB"
|
||||||
|
>
|
||||||
|
<feGaussianBlur stdDeviation="3.531" />
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
<path
|
||||||
|
fill="#b3b3b3"
|
||||||
|
d="m54.532 138.45 2.235 1.324c9.387 5.571 20.15 8.518 31.126 8.523h.023c33.707 0 61.139-27.426 61.153-61.135.006-16.335-6.349-31.696-17.895-43.251A60.75 60.75 0 0 0 87.94 25.983c-33.733 0-61.166 27.423-61.178 61.13a60.98 60.98 0 0 0 9.349 32.535l1.455 2.312-6.179 22.558zm-40.811 23.544L24.16 123.88c-6.438-11.154-9.825-23.808-9.821-36.772.017-40.556 33.021-73.55 73.578-73.55 19.681.01 38.154 7.669 52.047 21.572s21.537 32.383 21.53 52.037c-.018 40.553-33.027 73.553-73.578 73.553h-.032c-12.313-.005-24.412-3.094-35.159-8.954zm0 0"
|
||||||
|
filter="url(#a)"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill="#fff"
|
||||||
|
d="m12.966 161.238 10.439-38.114a73.42 73.42 0 0 1-9.821-36.772c.017-40.556 33.021-73.55 73.578-73.55 19.681.01 38.154 7.669 52.047 21.572s21.537 32.383 21.53 52.037c-.018 40.553-33.027 73.553-73.578 73.553h-.032c-12.313-.005-24.412-3.094-35.159-8.954z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill="url(#linearGradient1780)"
|
||||||
|
d="M87.184 25.227c-33.733 0-61.166 27.423-61.178 61.13a60.98 60.98 0 0 0 9.349 32.535l1.455 2.312-6.179 22.559 23.146-6.069 2.235 1.324c9.387 5.571 20.15 8.518 31.126 8.524h.023c33.707 0 61.14-27.426 61.153-61.135a60.75 60.75 0 0 0-17.895-43.251 60.75 60.75 0 0 0-43.235-17.929z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill="url(#b)"
|
||||||
|
d="M87.184 25.227c-33.733 0-61.166 27.423-61.178 61.13a60.98 60.98 0 0 0 9.349 32.535l1.455 2.313-6.179 22.558 23.146-6.069 2.235 1.324c9.387 5.571 20.15 8.517 31.126 8.523h.023c33.707 0 61.14-27.426 61.153-61.135a60.75 60.75 0 0 0-17.895-43.251 60.75 60.75 0 0 0-43.235-17.928z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill="#fff"
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M68.772 55.603c-1.378-3.061-2.828-3.123-4.137-3.176l-3.524-.043c-1.226 0-3.218.46-4.902 2.3s-6.435 6.287-6.435 15.332 6.588 17.785 7.506 19.013 12.718 20.381 31.405 27.75c15.529 6.124 18.689 4.906 22.061 4.6s10.877-4.447 12.408-8.74 1.532-7.971 1.073-8.74-1.685-1.226-3.525-2.146-10.877-5.367-12.562-5.981-2.91-.919-4.137.921-4.746 5.979-5.819 7.206-2.144 1.381-3.984.462-7.76-2.861-14.784-9.124c-5.465-4.873-9.154-10.891-10.228-12.73s-.114-2.835.808-3.751c.825-.824 1.838-2.147 2.759-3.22s1.224-1.84 1.836-3.065.307-2.301-.153-3.22-4.032-10.011-5.666-13.647"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
};
|
||||||
22
src/components/mode-toggle.tsx
Normal file
22
src/components/mode-toggle.tsx
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { MoonIcon, SunIcon } from "@radix-ui/react-icons";
|
||||||
|
import { useTheme } from "next-themes";
|
||||||
|
|
||||||
|
export function ModeToggle() {
|
||||||
|
const { theme, setTheme } = useTheme();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
type="button"
|
||||||
|
size="icon"
|
||||||
|
className="px-2"
|
||||||
|
onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
|
||||||
|
>
|
||||||
|
<SunIcon className="h-[1.2rem] w-[1.2rem] text-neutral-800 dark:hidden dark:text-neutral-200" />
|
||||||
|
<MoonIcon className="hidden h-[1.2rem] w-[1.2rem] text-neutral-800 dark:block dark:text-neutral-200" />
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
123
src/components/project-card.tsx
Normal file
123
src/components/project-card.tsx
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
import { Badge } from '@/components/ui/badge'
|
||||||
|
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
import { BadgeListBlock, Media } from '@/payload-types'
|
||||||
|
import { DefaultTypedEditorState } from '@payloadcms/richtext-lexical'
|
||||||
|
import Image from 'next/image'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import { RichText } from './RichText'
|
||||||
|
import clsx from 'clsx'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
title: string
|
||||||
|
href?: string
|
||||||
|
description?: DefaultTypedEditorState
|
||||||
|
dates: string
|
||||||
|
tags?: BadgeListBlock[]
|
||||||
|
link?: string
|
||||||
|
image?: Media
|
||||||
|
video?: Media
|
||||||
|
links?: BadgeListBlock[]
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ProjectCard({
|
||||||
|
title,
|
||||||
|
href,
|
||||||
|
description,
|
||||||
|
dates,
|
||||||
|
link,
|
||||||
|
image,
|
||||||
|
video,
|
||||||
|
className,
|
||||||
|
...rest
|
||||||
|
}: Props) {
|
||||||
|
const tags = rest?.tags?.length ? rest.tags[0] : null
|
||||||
|
const links = rest?.links?.length ? rest.links[0] : null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
className={
|
||||||
|
'flex flex-col overflow-hidden border hover:shadow-lg transition-all duration-300 ease-out h-full'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Link href={href || '#'} className={cn('block cursor-pointer', className)}>
|
||||||
|
{video && (
|
||||||
|
<video
|
||||||
|
src={video.url || ''}
|
||||||
|
autoPlay
|
||||||
|
loop
|
||||||
|
muted
|
||||||
|
playsInline
|
||||||
|
className="pointer-events-none mx-auto h-40 w-full object-cover object-top" // needed because random black line at bottom of video
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{image && (
|
||||||
|
<Image
|
||||||
|
src={image.url || ''}
|
||||||
|
alt={image.alt || ''}
|
||||||
|
width={500}
|
||||||
|
height={300}
|
||||||
|
className="h-40 w-full overflow-hidden object-cover object-top"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
<CardHeader className="px-2">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<CardTitle className="mt-1 text-base">{title}</CardTitle>
|
||||||
|
<time className="font-sans text-xs">{dates}</time>
|
||||||
|
<div className="hidden font-sans text-xs underline print:visible">
|
||||||
|
{link?.replace('https://', '').replace('www.', '').replace('/', '')}
|
||||||
|
</div>
|
||||||
|
{!!description && (
|
||||||
|
<RichText
|
||||||
|
className="prose max-w-full text-pretty font-sans text-xs text-muted-foreground dark:prose-invert"
|
||||||
|
data={description}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="mt-auto flex flex-col px-2">
|
||||||
|
{tags?.badges && tags.badges.length > 0 && (
|
||||||
|
<div className="mt-2 flex flex-wrap gap-1">
|
||||||
|
{tags.badges.map((b, i) => (
|
||||||
|
<Badge
|
||||||
|
key={b.value || '' + i}
|
||||||
|
variant="secondary"
|
||||||
|
className={clsx(
|
||||||
|
'px-1 py-0 text-[10px]',
|
||||||
|
b.color || tags.defaultColor ? `bg-[${b.color || tags.defaultColor}]` : '',
|
||||||
|
b.textColor || tags.defaultTextColor
|
||||||
|
? `text-[${b.textColor || tags.defaultTextColor}]`
|
||||||
|
: '',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{b.value}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
<CardFooter className="px-2 pb-2">
|
||||||
|
{links?.badges && links.badges.length > 0 && (
|
||||||
|
<div className="flex flex-row flex-wrap items-start gap-1">
|
||||||
|
{links.badges?.map((link, idx) => {
|
||||||
|
const icon = link.icon as Media | undefined
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link href={link?.href || ''} key={idx} target="_blank">
|
||||||
|
<Badge key={idx} className="flex gap-2 px-2 py-1 text-[10px]">
|
||||||
|
{!!icon && icon.url && (
|
||||||
|
<Image src={icon.url} alt={icon.alt || ''} width={16} height={16} />
|
||||||
|
)}
|
||||||
|
{link.value}
|
||||||
|
</Badge>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
112
src/components/resume-card.tsx
Normal file
112
src/components/resume-card.tsx
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||||
|
import { Badge } from '@/components/ui/badge'
|
||||||
|
import { Card, CardHeader } from '@/components/ui/card'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
import { DefaultTypedEditorState } from '@payloadcms/richtext-lexical'
|
||||||
|
import { motion } from 'framer-motion'
|
||||||
|
import { ChevronRightIcon } from 'lucide-react'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import React from 'react'
|
||||||
|
import { RichText } from './RichText'
|
||||||
|
|
||||||
|
type ResumeCardProps = {
|
||||||
|
logoUrl: string
|
||||||
|
altText: string
|
||||||
|
title: string
|
||||||
|
subtitle?: string
|
||||||
|
href?: string
|
||||||
|
badges?: string[]
|
||||||
|
period: string
|
||||||
|
description?: DefaultTypedEditorState
|
||||||
|
}
|
||||||
|
export const ResumeCard = ({
|
||||||
|
logoUrl,
|
||||||
|
altText,
|
||||||
|
title,
|
||||||
|
subtitle,
|
||||||
|
href,
|
||||||
|
badges,
|
||||||
|
period,
|
||||||
|
description,
|
||||||
|
}: ResumeCardProps) => {
|
||||||
|
const [isExpanded, setIsExpanded] = React.useState(false)
|
||||||
|
|
||||||
|
const handleClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
|
||||||
|
if (description) {
|
||||||
|
e.preventDefault()
|
||||||
|
setIsExpanded(!isExpanded)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDoubleClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
|
||||||
|
e.preventDefault()
|
||||||
|
if (description) setIsExpanded(true)
|
||||||
|
|
||||||
|
if (href && window) window.open(href, '_blank')
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
href={href || '#'}
|
||||||
|
className="block cursor-pointer"
|
||||||
|
onClick={handleClick}
|
||||||
|
onDoubleClick={handleDoubleClick}
|
||||||
|
>
|
||||||
|
<Card className="flex">
|
||||||
|
<div className="flex-none">
|
||||||
|
<Avatar className="border size-12 m-auto bg-muted-background dark:bg-foreground">
|
||||||
|
<AvatarImage src={logoUrl} alt={altText} className="object-contain" />
|
||||||
|
<AvatarFallback>{altText[0]}</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
</div>
|
||||||
|
<div className="flex-grow ml-4 items-center flex-col group">
|
||||||
|
<CardHeader>
|
||||||
|
<div className="flex items-center justify-between gap-x-2 text-base">
|
||||||
|
<h3 className="inline-flex items-center justify-center font-semibold leading-none text-xs sm:text-sm">
|
||||||
|
{title}
|
||||||
|
{badges && (
|
||||||
|
<span className="inline-flex gap-x-1">
|
||||||
|
{badges.map((badge, index) => (
|
||||||
|
<Badge variant="secondary" className="align-middle text-xs" key={index}>
|
||||||
|
{badge}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<ChevronRightIcon
|
||||||
|
className={cn(
|
||||||
|
'size-4 translate-x-0 transform opacity-0 transition-all duration-300 ease-out group-hover:translate-x-1 group-hover:opacity-100',
|
||||||
|
isExpanded ? 'rotate-90' : 'rotate-0',
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</h3>
|
||||||
|
<div className="text-xs sm:text-sm tabular-nums text-muted-foreground text-right">
|
||||||
|
{period}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{subtitle && <div className="font-sans text-xs">{subtitle}</div>}
|
||||||
|
</CardHeader>
|
||||||
|
{description && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, height: 0 }}
|
||||||
|
animate={{
|
||||||
|
opacity: isExpanded ? 1 : 0,
|
||||||
|
|
||||||
|
height: isExpanded ? 'auto' : 0,
|
||||||
|
}}
|
||||||
|
transition={{
|
||||||
|
duration: 0.7,
|
||||||
|
ease: [0.16, 1, 0.3, 1],
|
||||||
|
}}
|
||||||
|
className="mt-2 text-xs sm:text-sm"
|
||||||
|
>
|
||||||
|
<RichText data={description} />
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
}
|
||||||
7
src/components/theme-provider.tsx
Normal file
7
src/components/theme-provider.tsx
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { ThemeProvider as NextThemesProvider, ThemeProviderProps } from 'next-themes'
|
||||||
|
|
||||||
|
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
||||||
|
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
|
||||||
|
}
|
||||||
50
src/components/ui/avatar.tsx
Normal file
50
src/components/ui/avatar.tsx
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as AvatarPrimitive from "@radix-ui/react-avatar"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const Avatar = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AvatarPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AvatarPrimitive.Root
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
Avatar.displayName = AvatarPrimitive.Root.displayName
|
||||||
|
|
||||||
|
const AvatarImage = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AvatarPrimitive.Image>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AvatarPrimitive.Image
|
||||||
|
ref={ref}
|
||||||
|
className={cn("aspect-square h-full w-full", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
AvatarImage.displayName = AvatarPrimitive.Image.displayName
|
||||||
|
|
||||||
|
const AvatarFallback = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AvatarPrimitive.Fallback>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AvatarPrimitive.Fallback
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"flex h-full w-full items-center justify-center rounded-full bg-muted",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
|
||||||
|
|
||||||
|
export { Avatar, AvatarImage, AvatarFallback }
|
||||||
36
src/components/ui/badge.tsx
Normal file
36
src/components/ui/badge.tsx
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const badgeVariants = cva(
|
||||||
|
"inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default:
|
||||||
|
"border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
|
||||||
|
secondary:
|
||||||
|
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||||
|
destructive:
|
||||||
|
"border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
|
||||||
|
outline: "text-foreground",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export interface BadgeProps
|
||||||
|
extends React.HTMLAttributes<HTMLDivElement>,
|
||||||
|
VariantProps<typeof badgeVariants> {}
|
||||||
|
|
||||||
|
function Badge({ className, variant, ...props }: BadgeProps) {
|
||||||
|
return (
|
||||||
|
<div className={cn(badgeVariants({ variant }), className)} {...props} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Badge, badgeVariants }
|
||||||
85
src/components/ui/blur-fade-text.tsx
Normal file
85
src/components/ui/blur-fade-text.tsx
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { AnimatePresence, motion, Variants } from "framer-motion";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
|
||||||
|
interface BlurFadeTextProps {
|
||||||
|
text: string;
|
||||||
|
className?: string;
|
||||||
|
variant?: {
|
||||||
|
hidden: { y: number };
|
||||||
|
visible: { y: number };
|
||||||
|
};
|
||||||
|
duration?: number;
|
||||||
|
characterDelay?: number;
|
||||||
|
delay?: number;
|
||||||
|
yOffset?: number;
|
||||||
|
animateByCharacter?: boolean;
|
||||||
|
}
|
||||||
|
const BlurFadeText = ({
|
||||||
|
text,
|
||||||
|
className,
|
||||||
|
variant,
|
||||||
|
characterDelay = 0.03,
|
||||||
|
delay = 0,
|
||||||
|
yOffset = 8,
|
||||||
|
animateByCharacter = false,
|
||||||
|
}: BlurFadeTextProps) => {
|
||||||
|
const defaultVariants: Variants = {
|
||||||
|
hidden: { y: yOffset, opacity: 0, filter: "blur(8px)" },
|
||||||
|
visible: { y: -yOffset, opacity: 1, filter: "blur(0px)" },
|
||||||
|
};
|
||||||
|
const combinedVariants = variant || defaultVariants;
|
||||||
|
const characters = useMemo(() => Array.from(text), [text]);
|
||||||
|
|
||||||
|
if (animateByCharacter) {
|
||||||
|
return (
|
||||||
|
<div className="flex">
|
||||||
|
<AnimatePresence>
|
||||||
|
{characters.map((char, i) => (
|
||||||
|
<motion.span
|
||||||
|
key={i}
|
||||||
|
initial="hidden"
|
||||||
|
animate="visible"
|
||||||
|
exit="hidden"
|
||||||
|
variants={combinedVariants}
|
||||||
|
transition={{
|
||||||
|
yoyo: Infinity,
|
||||||
|
delay: delay + i * characterDelay,
|
||||||
|
ease: "easeOut",
|
||||||
|
}}
|
||||||
|
className={cn("inline-block", className)}
|
||||||
|
style={{ width: char.trim() === "" ? "0.2em" : "auto" }}
|
||||||
|
>
|
||||||
|
{char}
|
||||||
|
</motion.span>
|
||||||
|
))}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex">
|
||||||
|
<AnimatePresence>
|
||||||
|
<motion.span
|
||||||
|
initial="hidden"
|
||||||
|
animate="visible"
|
||||||
|
exit="hidden"
|
||||||
|
variants={combinedVariants}
|
||||||
|
transition={{
|
||||||
|
yoyo: Infinity,
|
||||||
|
delay,
|
||||||
|
ease: "easeOut",
|
||||||
|
}}
|
||||||
|
className={cn("inline-block", className)}
|
||||||
|
>
|
||||||
|
{text}
|
||||||
|
</motion.span>
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BlurFadeText;
|
||||||
61
src/components/ui/blur-fade.tsx
Normal file
61
src/components/ui/blur-fade.tsx
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { AnimatePresence, motion, useInView, Variants } from 'framer-motion'
|
||||||
|
import { useRef } from 'react'
|
||||||
|
|
||||||
|
interface BlurFadeProps {
|
||||||
|
children: React.ReactNode
|
||||||
|
className?: string
|
||||||
|
variant?: {
|
||||||
|
hidden: { y: number }
|
||||||
|
visible: { y: number }
|
||||||
|
}
|
||||||
|
duration?: number
|
||||||
|
delay?: number
|
||||||
|
yOffset?: number
|
||||||
|
inView?: boolean
|
||||||
|
inViewMargin?: string
|
||||||
|
blur?: string
|
||||||
|
}
|
||||||
|
const BlurFade = ({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
variant,
|
||||||
|
duration = 0.4,
|
||||||
|
delay = 0,
|
||||||
|
yOffset = 6,
|
||||||
|
inView = false,
|
||||||
|
inViewMargin = '-50px',
|
||||||
|
blur = '6px',
|
||||||
|
}: BlurFadeProps) => {
|
||||||
|
const ref = useRef(null)
|
||||||
|
// @ts-ignore
|
||||||
|
const inViewResult = useInView(ref, { once: true, margin: inViewMargin })
|
||||||
|
const isInView = !inView || inViewResult
|
||||||
|
const defaultVariants: Variants = {
|
||||||
|
hidden: { y: yOffset, opacity: 0, filter: `blur(${blur})` },
|
||||||
|
visible: { y: -yOffset, opacity: 1, filter: `blur(0px)` },
|
||||||
|
}
|
||||||
|
const combinedVariants = variant || defaultVariants
|
||||||
|
return (
|
||||||
|
<AnimatePresence>
|
||||||
|
<motion.div
|
||||||
|
ref={ref}
|
||||||
|
initial="hidden"
|
||||||
|
animate={isInView ? 'visible' : 'hidden'}
|
||||||
|
exit="hidden"
|
||||||
|
variants={combinedVariants}
|
||||||
|
transition={{
|
||||||
|
delay: 0.04 + delay,
|
||||||
|
duration,
|
||||||
|
ease: 'easeOut',
|
||||||
|
}}
|
||||||
|
className={className}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</motion.div>
|
||||||
|
</AnimatePresence>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BlurFade
|
||||||
57
src/components/ui/button.tsx
Normal file
57
src/components/ui/button.tsx
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import { Slot } from "@radix-ui/react-slot";
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority";
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const buttonVariants = cva(
|
||||||
|
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default:
|
||||||
|
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
|
||||||
|
destructive:
|
||||||
|
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
|
||||||
|
outline:
|
||||||
|
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
|
||||||
|
secondary:
|
||||||
|
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
|
||||||
|
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||||
|
link: "text-primary underline-offset-4 hover:underline",
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: "h-9 px-4 py-2",
|
||||||
|
sm: "h-8 rounded-md px-3 text-xs",
|
||||||
|
lg: "h-10 rounded-md px-8",
|
||||||
|
icon: "h-9 w-9 rounded-full",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
size: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export interface ButtonProps
|
||||||
|
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||||
|
VariantProps<typeof buttonVariants> {
|
||||||
|
asChild?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||||
|
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||||
|
const Comp = asChild ? Slot : "button";
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
className={cn(buttonVariants({ variant, size, className }))}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
Button.displayName = "Button";
|
||||||
|
|
||||||
|
export { Button, buttonVariants };
|
||||||
86
src/components/ui/card.tsx
Normal file
86
src/components/ui/card.tsx
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const Card = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn("rounded-lg bg-card text-card-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
Card.displayName = "Card";
|
||||||
|
|
||||||
|
const CardHeader = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div ref={ref} className={cn("flex flex-col", className)} {...props} />
|
||||||
|
));
|
||||||
|
CardHeader.displayName = "CardHeader";
|
||||||
|
|
||||||
|
const CardTitle = React.forwardRef<
|
||||||
|
HTMLParagraphElement,
|
||||||
|
React.HTMLAttributes<HTMLHeadingElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<h3
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"text-2xl font-semibold leading-none tracking-tight",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
CardTitle.displayName = "CardTitle";
|
||||||
|
|
||||||
|
const CardDescription = React.forwardRef<
|
||||||
|
HTMLParagraphElement,
|
||||||
|
React.HTMLAttributes<HTMLParagraphElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<p
|
||||||
|
ref={ref}
|
||||||
|
className={cn("text-sm text-muted-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
CardDescription.displayName = "CardDescription";
|
||||||
|
|
||||||
|
const CardContent = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"text-pretty font-sans text-sm text-muted-foreground",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
CardContent.displayName = "CardContent";
|
||||||
|
|
||||||
|
const CardFooter = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn("flex items-center pt-2", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
CardFooter.displayName = "CardFooter";
|
||||||
|
|
||||||
|
export {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardFooter,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
};
|
||||||
112
src/components/ui/dock.tsx
Normal file
112
src/components/ui/dock.tsx
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
import { cva, type VariantProps } from 'class-variance-authority'
|
||||||
|
import { motion, useMotionValue, useSpring, useTransform } from 'framer-motion'
|
||||||
|
import React, { PropsWithChildren, useRef } from 'react'
|
||||||
|
|
||||||
|
export interface DockProps extends VariantProps<typeof dockVariants> {
|
||||||
|
className?: string
|
||||||
|
magnification?: number
|
||||||
|
distance?: number
|
||||||
|
children: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_MAGNIFICATION = 60
|
||||||
|
const DEFAULT_DISTANCE = 140
|
||||||
|
|
||||||
|
const dockVariants = cva('mx-auto w-max h-full p-2 flex items-end rounded-full border')
|
||||||
|
|
||||||
|
const Dock = React.forwardRef<HTMLDivElement, DockProps>(
|
||||||
|
(
|
||||||
|
{
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
magnification = DEFAULT_MAGNIFICATION,
|
||||||
|
distance = DEFAULT_DISTANCE,
|
||||||
|
...props
|
||||||
|
},
|
||||||
|
ref,
|
||||||
|
) => {
|
||||||
|
const mousex = useMotionValue(Infinity)
|
||||||
|
|
||||||
|
const renderChildren = () => {
|
||||||
|
return React.Children.map(children, (child: React.ReactNode) => {
|
||||||
|
if (React.isValidElement(child)) {
|
||||||
|
return React.cloneElement(child, {
|
||||||
|
mousex,
|
||||||
|
magnification,
|
||||||
|
distance,
|
||||||
|
} as DockIconProps)
|
||||||
|
}
|
||||||
|
return child
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
ref={ref}
|
||||||
|
onMouseMove={(e) => mousex.set(e.pageX)}
|
||||||
|
onMouseLeave={() => mousex.set(Infinity)}
|
||||||
|
{...props}
|
||||||
|
className={cn(dockVariants({ className }))}
|
||||||
|
>
|
||||||
|
{renderChildren()}
|
||||||
|
</motion.div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
Dock.displayName = 'Dock'
|
||||||
|
|
||||||
|
export interface DockIconProps {
|
||||||
|
size?: number
|
||||||
|
magnification?: number
|
||||||
|
distance?: number
|
||||||
|
mousex?: any
|
||||||
|
className?: string
|
||||||
|
children?: React.ReactNode
|
||||||
|
props?: PropsWithChildren
|
||||||
|
}
|
||||||
|
|
||||||
|
const DockIcon = ({
|
||||||
|
magnification = DEFAULT_MAGNIFICATION,
|
||||||
|
distance = DEFAULT_DISTANCE,
|
||||||
|
mousex,
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: DockIconProps) => {
|
||||||
|
const ref = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
const distanceCalc = useTransform(mousex, (val: number) => {
|
||||||
|
const bounds = ref.current?.getBoundingClientRect() ?? { x: 0, width: 0 }
|
||||||
|
return val - bounds.x - bounds.width / 2
|
||||||
|
})
|
||||||
|
|
||||||
|
const widthSync = useTransform(distanceCalc, [-distance, 0, distance], [40, magnification, 40])
|
||||||
|
|
||||||
|
const width = useSpring(widthSync, {
|
||||||
|
mass: 0.1,
|
||||||
|
stiffness: 150,
|
||||||
|
damping: 12,
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
ref={ref}
|
||||||
|
style={{ width }}
|
||||||
|
className={cn(
|
||||||
|
'flex aspect-square cursor-pointer items-center justify-center rounded-full',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</motion.div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
DockIcon.displayName = 'DockIcon'
|
||||||
|
|
||||||
|
export { Dock, DockIcon, dockVariants }
|
||||||
31
src/components/ui/separator.tsx
Normal file
31
src/components/ui/separator.tsx
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as SeparatorPrimitive from "@radix-ui/react-separator"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const Separator = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SeparatorPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
|
||||||
|
>(
|
||||||
|
(
|
||||||
|
{ className, orientation = "horizontal", decorative = true, ...props },
|
||||||
|
ref
|
||||||
|
) => (
|
||||||
|
<SeparatorPrimitive.Root
|
||||||
|
ref={ref}
|
||||||
|
decorative={decorative}
|
||||||
|
orientation={orientation}
|
||||||
|
className={cn(
|
||||||
|
"shrink-0 bg-border",
|
||||||
|
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Separator.displayName = SeparatorPrimitive.Root.displayName
|
||||||
|
|
||||||
|
export { Separator }
|
||||||
119
src/components/ui/terminal.tsx
Normal file
119
src/components/ui/terminal.tsx
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { motion, MotionProps } from "motion/react";
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
|
interface AnimatedSpanProps extends MotionProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
delay?: number;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AnimatedSpan = ({
|
||||||
|
children,
|
||||||
|
delay = 0,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: AnimatedSpanProps) => (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: -5 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.3, delay: delay / 1000 }}
|
||||||
|
className={cn("grid text-sm font-normal tracking-tight", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
|
||||||
|
interface TypingAnimationProps extends MotionProps {
|
||||||
|
children: string;
|
||||||
|
className?: string;
|
||||||
|
duration?: number;
|
||||||
|
delay?: number;
|
||||||
|
as?: React.ElementType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TypingAnimation = ({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
duration = 60,
|
||||||
|
delay = 0,
|
||||||
|
as: Component = "span",
|
||||||
|
...props
|
||||||
|
}: TypingAnimationProps) => {
|
||||||
|
if (typeof children !== "string") {
|
||||||
|
throw new Error("TypingAnimation: children must be a string. Received:");
|
||||||
|
}
|
||||||
|
|
||||||
|
const MotionComponent = motion.create(Component, {
|
||||||
|
forwardMotionProps: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [displayedText, setDisplayedText] = useState<string>("");
|
||||||
|
const [started, setStarted] = useState(false);
|
||||||
|
const elementRef = useRef<HTMLElement | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const startTimeout = setTimeout(() => {
|
||||||
|
setStarted(true);
|
||||||
|
}, delay);
|
||||||
|
return () => clearTimeout(startTimeout);
|
||||||
|
}, [delay]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!started) return;
|
||||||
|
|
||||||
|
let i = 0;
|
||||||
|
const typingEffect = setInterval(() => {
|
||||||
|
if (i < children.length) {
|
||||||
|
setDisplayedText(children.substring(0, i + 1));
|
||||||
|
i++;
|
||||||
|
} else {
|
||||||
|
clearInterval(typingEffect);
|
||||||
|
}
|
||||||
|
}, duration);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearInterval(typingEffect);
|
||||||
|
};
|
||||||
|
}, [children, duration, started]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MotionComponent
|
||||||
|
ref={elementRef}
|
||||||
|
className={cn("text-sm font-normal tracking-tight", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{displayedText}
|
||||||
|
</MotionComponent>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface TerminalProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Terminal = ({ children, className }: TerminalProps) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"z-0 h-full max-h-[400px] w-full max-w-lg rounded-xl border border-border bg-background",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-y-2 border-b border-border p-4">
|
||||||
|
<div className="flex flex-row gap-x-2">
|
||||||
|
<div className="h-2 w-2 rounded-full bg-red-500"></div>
|
||||||
|
<div className="h-2 w-2 rounded-full bg-yellow-500"></div>
|
||||||
|
<div className="h-2 w-2 rounded-full bg-green-500"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<pre className="p-4">
|
||||||
|
<code className="grid gap-y-1 overflow-auto">{children}</code>
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
30
src/components/ui/tooltip.tsx
Normal file
30
src/components/ui/tooltip.tsx
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const TooltipProvider = TooltipPrimitive.Provider;
|
||||||
|
|
||||||
|
const Tooltip = TooltipPrimitive.Root;
|
||||||
|
|
||||||
|
const TooltipTrigger = TooltipPrimitive.Trigger;
|
||||||
|
|
||||||
|
const TooltipContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof TooltipPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
||||||
|
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||||
|
<TooltipPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
sideOffset={sideOffset}
|
||||||
|
className={cn(
|
||||||
|
"z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
|
||||||
|
|
||||||
|
export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger };
|
||||||
68
src/fields/defaultLexical.ts
Normal file
68
src/fields/defaultLexical.ts
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import type { TextFieldSingleValidation } from 'payload'
|
||||||
|
import {
|
||||||
|
BoldFeature,
|
||||||
|
ItalicFeature,
|
||||||
|
LinkFeature,
|
||||||
|
ParagraphFeature,
|
||||||
|
lexicalEditor,
|
||||||
|
UnderlineFeature,
|
||||||
|
type LinkFields,
|
||||||
|
UnorderedListFeature,
|
||||||
|
AlignFeature,
|
||||||
|
BlockquoteFeature,
|
||||||
|
InlineCodeFeature,
|
||||||
|
HeadingFeature,
|
||||||
|
OrderedListFeature,
|
||||||
|
IndentFeature,
|
||||||
|
FixedToolbarFeature,
|
||||||
|
BlocksFeature,
|
||||||
|
} from '@payloadcms/richtext-lexical'
|
||||||
|
import { BadgeList } from '@/blocks/BadgeList/config'
|
||||||
|
|
||||||
|
export const defaultLexical = lexicalEditor({
|
||||||
|
features: [
|
||||||
|
FixedToolbarFeature(),
|
||||||
|
ParagraphFeature(),
|
||||||
|
UnderlineFeature(),
|
||||||
|
BoldFeature(),
|
||||||
|
ItalicFeature(),
|
||||||
|
AlignFeature(),
|
||||||
|
BlockquoteFeature(),
|
||||||
|
UnorderedListFeature(),
|
||||||
|
OrderedListFeature(),
|
||||||
|
IndentFeature(),
|
||||||
|
InlineCodeFeature(),
|
||||||
|
HeadingFeature(),
|
||||||
|
LinkFeature({
|
||||||
|
enabledCollections: ['pages',],
|
||||||
|
fields: ({ defaultFields }) => {
|
||||||
|
const defaultFieldsWithoutUrl = defaultFields.filter((field) => {
|
||||||
|
if ('name' in field && field.name === 'url') return false
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
return [
|
||||||
|
...defaultFieldsWithoutUrl,
|
||||||
|
{
|
||||||
|
name: 'url',
|
||||||
|
type: 'text',
|
||||||
|
admin: {
|
||||||
|
condition: (_data, siblingData) => siblingData?.linkType !== 'internal',
|
||||||
|
},
|
||||||
|
label: ({ t }) => t('fields:enterURL'),
|
||||||
|
required: true,
|
||||||
|
validate: ((value, options) => {
|
||||||
|
if ((options?.siblingData as LinkFields)?.linkType === 'internal') {
|
||||||
|
return true // no validation needed, as no url should exist for internal links
|
||||||
|
}
|
||||||
|
return value ? true : 'URL is required'
|
||||||
|
}) as TextFieldSingleValidation,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
BlocksFeature({
|
||||||
|
blocks: [BadgeList],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})
|
||||||
23
src/globals/Meta/config.ts
Normal file
23
src/globals/Meta/config.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { GlobalConfig } from "payload";
|
||||||
|
|
||||||
|
export const MetaGlobal: GlobalConfig = {
|
||||||
|
slug: 'meta',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'name',
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'url',
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'description',
|
||||||
|
type: 'textarea',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'googleVerification',
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
79
src/globals/Nav/component.tsx
Normal file
79
src/globals/Nav/component.tsx
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import { Dock, DockIcon } from '@/components/ui/dock'
|
||||||
|
import { ModeToggle } from '@/components/mode-toggle'
|
||||||
|
import { buttonVariants } from '@/components/ui/button'
|
||||||
|
import { Separator } from '@/components/ui/separator'
|
||||||
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import { Nav } from '@/payload-types'
|
||||||
|
import Image from 'next/image'
|
||||||
|
|
||||||
|
type Props = { className?: string } & Nav
|
||||||
|
|
||||||
|
export default function Navbar(props: Props) {
|
||||||
|
return (
|
||||||
|
<div className="pointer-events-none fixed inset-x-0 bottom-0 z-30 mx-auto mb-4 flex origin-bottom h-full max-h-14">
|
||||||
|
<div className="fixed bottom-0 inset-x-0 h-16 w-full bg-background to-transparent backdrop-blur-lg [-webkit-mask-image:linear-gradient(to_top,black,transparent)] dark:bg-background"></div>
|
||||||
|
<Dock className="z-50 pointer-events-auto relative mx-auto flex min-h-full h-full items-center px-1 bg-background [box-shadow:0_0_0_1px_rgba(0,0,0,.03),0_2px_4px_rgba(0,0,0,.05),0_12px_24px_rgba(0,0,0,.05)] transform-gpu dark:[border:1px_solid_rgba(255,255,255,.1)] dark:[box-shadow:0_-20px_80px_-20px_#ffffff1f_inset] ">
|
||||||
|
{props.internalLinks?.map((item) => (
|
||||||
|
<DockIcon key={item.href}>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Link
|
||||||
|
href={item.href || ''}
|
||||||
|
className={cn(buttonVariants({ variant: 'ghost', size: 'icon' }), 'size-12')}
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
className="w-[1.2rem]"
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
src={item.iconSrc || ''}
|
||||||
|
alt={''}
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>{item.label}</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</DockIcon>
|
||||||
|
))}
|
||||||
|
<Separator orientation="vertical" className="h-full" />
|
||||||
|
{props.contact?.map((c) => (
|
||||||
|
<DockIcon key={c.href}>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Link
|
||||||
|
href={c.href || ''}
|
||||||
|
className={cn(buttonVariants({ variant: 'ghost', size: 'icon' }), 'size-12')}
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
className="w-[1.2rem]"
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
src={c.iconSrc || ''}
|
||||||
|
alt={''}
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>{c.label}</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</DockIcon>
|
||||||
|
))}
|
||||||
|
<Separator orientation="vertical" className="h-full py-2" />
|
||||||
|
<DockIcon>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<ModeToggle />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>Theme</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</DockIcon>
|
||||||
|
</Dock>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
43
src/globals/Nav/config.ts
Normal file
43
src/globals/Nav/config.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import { GlobalConfig } from "payload";
|
||||||
|
|
||||||
|
export const NavGlobal: GlobalConfig = {
|
||||||
|
slug: 'nav',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'internalLinks',
|
||||||
|
type: 'array',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'href',
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'iconSrc',
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'label',
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'contact',
|
||||||
|
type: 'array',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'href',
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'iconSrc',
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'label',
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
37
src/lib/utils.ts
Normal file
37
src/lib/utils.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { clsx, type ClassValue } from "clsx"
|
||||||
|
import { twMerge } from "tailwind-merge"
|
||||||
|
|
||||||
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatDate(date: string) {
|
||||||
|
const currentDate = new Date().getTime();
|
||||||
|
if (!date.includes("T")) {
|
||||||
|
date = `${date}T00:00:00`;
|
||||||
|
}
|
||||||
|
const targetDate = new Date(date).getTime();
|
||||||
|
const timeDifference = Math.abs(currentDate - targetDate);
|
||||||
|
const daysAgo = Math.floor(timeDifference / (1000 * 60 * 60 * 24));
|
||||||
|
|
||||||
|
const fullDate = new Date(date).toLocaleString("en-us", {
|
||||||
|
month: "long",
|
||||||
|
day: "numeric",
|
||||||
|
year: "numeric",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (daysAgo < 1) {
|
||||||
|
return "Today";
|
||||||
|
} else if (daysAgo < 7) {
|
||||||
|
return `${fullDate} (${daysAgo}d ago)`;
|
||||||
|
} else if (daysAgo < 30) {
|
||||||
|
const weeksAgo = Math.floor(daysAgo / 7);
|
||||||
|
return `${fullDate} (${weeksAgo}w ago)`;
|
||||||
|
} else if (daysAgo < 365) {
|
||||||
|
const monthsAgo = Math.floor(daysAgo / 30);
|
||||||
|
return `${fullDate} (${monthsAgo}mo ago)`;
|
||||||
|
} else {
|
||||||
|
const yearsAgo = Math.floor(daysAgo / 365);
|
||||||
|
return `${fullDate} (${yearsAgo}y ago)`;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -6,24 +6,102 @@
|
|||||||
* and re-run `payload generate:types` to regenerate this file.
|
* and re-run `payload generate:types` to regenerate this file.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Supported timezones in IANA format.
|
||||||
|
*
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "supportedTimezones".
|
||||||
|
*/
|
||||||
|
export type SupportedTimezones =
|
||||||
|
| 'Pacific/Midway'
|
||||||
|
| 'Pacific/Niue'
|
||||||
|
| 'Pacific/Honolulu'
|
||||||
|
| 'Pacific/Rarotonga'
|
||||||
|
| 'America/Anchorage'
|
||||||
|
| 'Pacific/Gambier'
|
||||||
|
| 'America/Los_Angeles'
|
||||||
|
| 'America/Tijuana'
|
||||||
|
| 'America/Denver'
|
||||||
|
| 'America/Phoenix'
|
||||||
|
| 'America/Chicago'
|
||||||
|
| 'America/Guatemala'
|
||||||
|
| 'America/New_York'
|
||||||
|
| 'America/Bogota'
|
||||||
|
| 'America/Caracas'
|
||||||
|
| 'America/Santiago'
|
||||||
|
| 'America/Buenos_Aires'
|
||||||
|
| 'America/Sao_Paulo'
|
||||||
|
| 'Atlantic/South_Georgia'
|
||||||
|
| 'Atlantic/Azores'
|
||||||
|
| 'Atlantic/Cape_Verde'
|
||||||
|
| 'Europe/London'
|
||||||
|
| 'Europe/Berlin'
|
||||||
|
| 'Africa/Lagos'
|
||||||
|
| 'Europe/Athens'
|
||||||
|
| 'Africa/Cairo'
|
||||||
|
| 'Europe/Moscow'
|
||||||
|
| 'Asia/Riyadh'
|
||||||
|
| 'Asia/Dubai'
|
||||||
|
| 'Asia/Baku'
|
||||||
|
| 'Asia/Karachi'
|
||||||
|
| 'Asia/Tashkent'
|
||||||
|
| 'Asia/Calcutta'
|
||||||
|
| 'Asia/Dhaka'
|
||||||
|
| 'Asia/Almaty'
|
||||||
|
| 'Asia/Jakarta'
|
||||||
|
| 'Asia/Bangkok'
|
||||||
|
| 'Asia/Shanghai'
|
||||||
|
| 'Asia/Singapore'
|
||||||
|
| 'Asia/Tokyo'
|
||||||
|
| 'Asia/Seoul'
|
||||||
|
| 'Australia/Brisbane'
|
||||||
|
| 'Australia/Sydney'
|
||||||
|
| 'Pacific/Guam'
|
||||||
|
| 'Pacific/Noumea'
|
||||||
|
| 'Pacific/Auckland'
|
||||||
|
| 'Pacific/Fiji';
|
||||||
|
|
||||||
export interface Config {
|
export interface Config {
|
||||||
auth: {
|
auth: {
|
||||||
users: UserAuthOperations;
|
users: UserAuthOperations;
|
||||||
};
|
};
|
||||||
|
blocks: {};
|
||||||
collections: {
|
collections: {
|
||||||
users: User;
|
users: User;
|
||||||
media: Media;
|
media: Media;
|
||||||
|
pages: Page;
|
||||||
|
'payload-locked-documents': PayloadLockedDocument;
|
||||||
'payload-preferences': PayloadPreference;
|
'payload-preferences': PayloadPreference;
|
||||||
'payload-migrations': PayloadMigration;
|
'payload-migrations': PayloadMigration;
|
||||||
};
|
};
|
||||||
db: {
|
collectionsJoins: {};
|
||||||
defaultIDType: string;
|
collectionsSelect: {
|
||||||
|
users: UsersSelect<false> | UsersSelect<true>;
|
||||||
|
media: MediaSelect<false> | MediaSelect<true>;
|
||||||
|
pages: PagesSelect<false> | PagesSelect<true>;
|
||||||
|
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
|
||||||
|
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
|
||||||
|
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
|
||||||
|
};
|
||||||
|
db: {
|
||||||
|
defaultIDType: number;
|
||||||
|
};
|
||||||
|
globals: {
|
||||||
|
meta: Meta;
|
||||||
|
nav: Nav;
|
||||||
|
};
|
||||||
|
globalsSelect: {
|
||||||
|
meta: MetaSelect<false> | MetaSelect<true>;
|
||||||
|
nav: NavSelect<false> | NavSelect<true>;
|
||||||
};
|
};
|
||||||
globals: {};
|
|
||||||
locale: null;
|
locale: null;
|
||||||
user: User & {
|
user: User & {
|
||||||
collection: 'users';
|
collection: 'users';
|
||||||
};
|
};
|
||||||
|
jobs: {
|
||||||
|
tasks: unknown;
|
||||||
|
workflows: unknown;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
export interface UserAuthOperations {
|
export interface UserAuthOperations {
|
||||||
forgotPassword: {
|
forgotPassword: {
|
||||||
@ -48,7 +126,7 @@ export interface UserAuthOperations {
|
|||||||
* via the `definition` "users".
|
* via the `definition` "users".
|
||||||
*/
|
*/
|
||||||
export interface User {
|
export interface User {
|
||||||
id: string;
|
id: number;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
email: string;
|
email: string;
|
||||||
@ -65,7 +143,7 @@ export interface User {
|
|||||||
* via the `definition` "media".
|
* via the `definition` "media".
|
||||||
*/
|
*/
|
||||||
export interface Media {
|
export interface Media {
|
||||||
id: string;
|
id: number;
|
||||||
alt: string;
|
alt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
@ -79,15 +157,202 @@ export interface Media {
|
|||||||
focalX?: number | null;
|
focalX?: number | null;
|
||||||
focalY?: number | null;
|
focalY?: number | null;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "pages".
|
||||||
|
*/
|
||||||
|
export interface Page {
|
||||||
|
id: number;
|
||||||
|
slug?: string | null;
|
||||||
|
layout?: (SimpleListBlock | ShowcaseBlock | ProfileBriefBlock | SimpleBriefBlock | BadgeListBlock)[] | null;
|
||||||
|
updatedAt: string;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "SimpleListBlock".
|
||||||
|
*/
|
||||||
|
export interface SimpleListBlock {
|
||||||
|
listHeader?: string | null;
|
||||||
|
lineItems?:
|
||||||
|
| {
|
||||||
|
title?: string | null;
|
||||||
|
subtitle?: string | null;
|
||||||
|
description?: {
|
||||||
|
root: {
|
||||||
|
type: string;
|
||||||
|
children: {
|
||||||
|
type: string;
|
||||||
|
version: number;
|
||||||
|
[k: string]: unknown;
|
||||||
|
}[];
|
||||||
|
direction: ('ltr' | 'rtl') | null;
|
||||||
|
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
|
||||||
|
indent: number;
|
||||||
|
version: number;
|
||||||
|
};
|
||||||
|
[k: string]: unknown;
|
||||||
|
} | null;
|
||||||
|
extraDetail?: string | null;
|
||||||
|
avatar?: (number | null) | Media;
|
||||||
|
initials?: string | null;
|
||||||
|
link?: string | null;
|
||||||
|
isInitiallyHidden?: boolean | null;
|
||||||
|
id?: string | null;
|
||||||
|
}[]
|
||||||
|
| null;
|
||||||
|
id?: string | null;
|
||||||
|
blockName?: string | null;
|
||||||
|
blockType: 'simpleList';
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "ShowcaseBlock".
|
||||||
|
*/
|
||||||
|
export interface ShowcaseBlock {
|
||||||
|
listName?: string | null;
|
||||||
|
heading?: string | null;
|
||||||
|
subheading?: string | null;
|
||||||
|
lineItems?: ShowcaseCardBlock[] | null;
|
||||||
|
id?: string | null;
|
||||||
|
blockName?: string | null;
|
||||||
|
blockType: 'showcase';
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "ShowcaseCardBlock".
|
||||||
|
*/
|
||||||
|
export interface ShowcaseCardBlock {
|
||||||
|
image?: (number | null) | Media;
|
||||||
|
video?: (number | null) | Media;
|
||||||
|
mainLink?: string | null;
|
||||||
|
title?: string | null;
|
||||||
|
unstructuredDate?: string | null;
|
||||||
|
description?: {
|
||||||
|
root: {
|
||||||
|
type: string;
|
||||||
|
children: {
|
||||||
|
type: string;
|
||||||
|
version: number;
|
||||||
|
[k: string]: unknown;
|
||||||
|
}[];
|
||||||
|
direction: ('ltr' | 'rtl') | null;
|
||||||
|
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
|
||||||
|
indent: number;
|
||||||
|
version: number;
|
||||||
|
};
|
||||||
|
[k: string]: unknown;
|
||||||
|
} | null;
|
||||||
|
tags?: BadgeListBlock[] | null;
|
||||||
|
links?: BadgeListBlock[] | null;
|
||||||
|
id?: string | null;
|
||||||
|
blockName?: string | null;
|
||||||
|
blockType: 'showcaseCard';
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "BadgeListBlock".
|
||||||
|
*/
|
||||||
|
export interface BadgeListBlock {
|
||||||
|
listHeader?: string | null;
|
||||||
|
shouldHideListHeader?: boolean | null;
|
||||||
|
defaultColor?: string | null;
|
||||||
|
defaultTextColor?: string | null;
|
||||||
|
/**
|
||||||
|
* A valid CSS text size string
|
||||||
|
*/
|
||||||
|
textSize?: string | null;
|
||||||
|
listVariant?: ('default' | 'secondary' | 'destructive' | 'outline') | null;
|
||||||
|
badges?:
|
||||||
|
| {
|
||||||
|
value?: string | null;
|
||||||
|
color?: string | null;
|
||||||
|
/**
|
||||||
|
* String of a valid CSS color
|
||||||
|
*/
|
||||||
|
textColor?: string | null;
|
||||||
|
icon?: (number | null) | Media;
|
||||||
|
href?: string | null;
|
||||||
|
id?: string | null;
|
||||||
|
}[]
|
||||||
|
| null;
|
||||||
|
id?: string | null;
|
||||||
|
blockName?: string | null;
|
||||||
|
blockType: 'badgeList';
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "ProfileBriefBlock".
|
||||||
|
*/
|
||||||
|
export interface ProfileBriefBlock {
|
||||||
|
heading?: string | null;
|
||||||
|
subheading?: string | null;
|
||||||
|
avatar?: (number | null) | Media;
|
||||||
|
id?: string | null;
|
||||||
|
blockName?: string | null;
|
||||||
|
blockType: 'profileBrief';
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "simpleBriefBlock".
|
||||||
|
*/
|
||||||
|
export interface SimpleBriefBlock {
|
||||||
|
title?: string | null;
|
||||||
|
content?: {
|
||||||
|
root: {
|
||||||
|
type: string;
|
||||||
|
children: {
|
||||||
|
type: string;
|
||||||
|
version: number;
|
||||||
|
[k: string]: unknown;
|
||||||
|
}[];
|
||||||
|
direction: ('ltr' | 'rtl') | null;
|
||||||
|
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
|
||||||
|
indent: number;
|
||||||
|
version: number;
|
||||||
|
};
|
||||||
|
[k: string]: unknown;
|
||||||
|
} | null;
|
||||||
|
id?: string | null;
|
||||||
|
blockName?: string | null;
|
||||||
|
blockType: 'simpleBrief';
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "payload-locked-documents".
|
||||||
|
*/
|
||||||
|
export interface PayloadLockedDocument {
|
||||||
|
id: number;
|
||||||
|
document?:
|
||||||
|
| ({
|
||||||
|
relationTo: 'users';
|
||||||
|
value: number | User;
|
||||||
|
} | null)
|
||||||
|
| ({
|
||||||
|
relationTo: 'media';
|
||||||
|
value: number | Media;
|
||||||
|
} | null)
|
||||||
|
| ({
|
||||||
|
relationTo: 'pages';
|
||||||
|
value: number | Page;
|
||||||
|
} | null);
|
||||||
|
globalSlug?: string | null;
|
||||||
|
user: {
|
||||||
|
relationTo: 'users';
|
||||||
|
value: number | User;
|
||||||
|
};
|
||||||
|
updatedAt: string;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* This interface was referenced by `Config`'s JSON-Schema
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
* via the `definition` "payload-preferences".
|
* via the `definition` "payload-preferences".
|
||||||
*/
|
*/
|
||||||
export interface PayloadPreference {
|
export interface PayloadPreference {
|
||||||
id: string;
|
id: number;
|
||||||
user: {
|
user: {
|
||||||
relationTo: 'users';
|
relationTo: 'users';
|
||||||
value: string | User;
|
value: number | User;
|
||||||
};
|
};
|
||||||
key?: string | null;
|
key?: string | null;
|
||||||
value?:
|
value?:
|
||||||
@ -107,12 +372,278 @@ export interface PayloadPreference {
|
|||||||
* via the `definition` "payload-migrations".
|
* via the `definition` "payload-migrations".
|
||||||
*/
|
*/
|
||||||
export interface PayloadMigration {
|
export interface PayloadMigration {
|
||||||
id: string;
|
id: number;
|
||||||
name?: string | null;
|
name?: string | null;
|
||||||
batch?: number | null;
|
batch?: number | null;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "users_select".
|
||||||
|
*/
|
||||||
|
export interface UsersSelect<T extends boolean = true> {
|
||||||
|
updatedAt?: T;
|
||||||
|
createdAt?: T;
|
||||||
|
email?: T;
|
||||||
|
resetPasswordToken?: T;
|
||||||
|
resetPasswordExpiration?: T;
|
||||||
|
salt?: T;
|
||||||
|
hash?: T;
|
||||||
|
loginAttempts?: T;
|
||||||
|
lockUntil?: T;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "media_select".
|
||||||
|
*/
|
||||||
|
export interface MediaSelect<T extends boolean = true> {
|
||||||
|
alt?: T;
|
||||||
|
updatedAt?: T;
|
||||||
|
createdAt?: T;
|
||||||
|
url?: T;
|
||||||
|
thumbnailURL?: T;
|
||||||
|
filename?: T;
|
||||||
|
mimeType?: T;
|
||||||
|
filesize?: T;
|
||||||
|
width?: T;
|
||||||
|
height?: T;
|
||||||
|
focalX?: T;
|
||||||
|
focalY?: T;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "pages_select".
|
||||||
|
*/
|
||||||
|
export interface PagesSelect<T extends boolean = true> {
|
||||||
|
slug?: T;
|
||||||
|
layout?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
simpleList?: T | SimpleListBlockSelect<T>;
|
||||||
|
showcase?: T | ShowcaseBlockSelect<T>;
|
||||||
|
profileBrief?: T | ProfileBriefBlockSelect<T>;
|
||||||
|
simpleBrief?: T | SimpleBriefBlockSelect<T>;
|
||||||
|
badgeList?: T | BadgeListBlockSelect<T>;
|
||||||
|
};
|
||||||
|
updatedAt?: T;
|
||||||
|
createdAt?: T;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "SimpleListBlock_select".
|
||||||
|
*/
|
||||||
|
export interface SimpleListBlockSelect<T extends boolean = true> {
|
||||||
|
listHeader?: T;
|
||||||
|
lineItems?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
title?: T;
|
||||||
|
subtitle?: T;
|
||||||
|
description?: T;
|
||||||
|
extraDetail?: T;
|
||||||
|
avatar?: T;
|
||||||
|
initials?: T;
|
||||||
|
link?: T;
|
||||||
|
isInitiallyHidden?: T;
|
||||||
|
id?: T;
|
||||||
|
};
|
||||||
|
id?: T;
|
||||||
|
blockName?: T;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "ShowcaseBlock_select".
|
||||||
|
*/
|
||||||
|
export interface ShowcaseBlockSelect<T extends boolean = true> {
|
||||||
|
listName?: T;
|
||||||
|
heading?: T;
|
||||||
|
subheading?: T;
|
||||||
|
lineItems?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
showcaseCard?: T | ShowcaseCardBlockSelect<T>;
|
||||||
|
};
|
||||||
|
id?: T;
|
||||||
|
blockName?: T;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "ShowcaseCardBlock_select".
|
||||||
|
*/
|
||||||
|
export interface ShowcaseCardBlockSelect<T extends boolean = true> {
|
||||||
|
image?: T;
|
||||||
|
video?: T;
|
||||||
|
mainLink?: T;
|
||||||
|
title?: T;
|
||||||
|
unstructuredDate?: T;
|
||||||
|
description?: T;
|
||||||
|
tags?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
badgeList?: T | BadgeListBlockSelect<T>;
|
||||||
|
};
|
||||||
|
links?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
badgeList?: T | BadgeListBlockSelect<T>;
|
||||||
|
};
|
||||||
|
id?: T;
|
||||||
|
blockName?: T;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "BadgeListBlock_select".
|
||||||
|
*/
|
||||||
|
export interface BadgeListBlockSelect<T extends boolean = true> {
|
||||||
|
listHeader?: T;
|
||||||
|
shouldHideListHeader?: T;
|
||||||
|
defaultColor?: T;
|
||||||
|
defaultTextColor?: T;
|
||||||
|
textSize?: T;
|
||||||
|
listVariant?: T;
|
||||||
|
badges?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
value?: T;
|
||||||
|
color?: T;
|
||||||
|
textColor?: T;
|
||||||
|
icon?: T;
|
||||||
|
href?: T;
|
||||||
|
id?: T;
|
||||||
|
};
|
||||||
|
id?: T;
|
||||||
|
blockName?: T;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "ProfileBriefBlock_select".
|
||||||
|
*/
|
||||||
|
export interface ProfileBriefBlockSelect<T extends boolean = true> {
|
||||||
|
heading?: T;
|
||||||
|
subheading?: T;
|
||||||
|
avatar?: T;
|
||||||
|
id?: T;
|
||||||
|
blockName?: T;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "simpleBriefBlock_select".
|
||||||
|
*/
|
||||||
|
export interface SimpleBriefBlockSelect<T extends boolean = true> {
|
||||||
|
title?: T;
|
||||||
|
content?: T;
|
||||||
|
id?: T;
|
||||||
|
blockName?: T;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "payload-locked-documents_select".
|
||||||
|
*/
|
||||||
|
export interface PayloadLockedDocumentsSelect<T extends boolean = true> {
|
||||||
|
document?: T;
|
||||||
|
globalSlug?: T;
|
||||||
|
user?: T;
|
||||||
|
updatedAt?: T;
|
||||||
|
createdAt?: T;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "payload-preferences_select".
|
||||||
|
*/
|
||||||
|
export interface PayloadPreferencesSelect<T extends boolean = true> {
|
||||||
|
user?: T;
|
||||||
|
key?: T;
|
||||||
|
value?: T;
|
||||||
|
updatedAt?: T;
|
||||||
|
createdAt?: T;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "payload-migrations_select".
|
||||||
|
*/
|
||||||
|
export interface PayloadMigrationsSelect<T extends boolean = true> {
|
||||||
|
name?: T;
|
||||||
|
batch?: T;
|
||||||
|
updatedAt?: T;
|
||||||
|
createdAt?: T;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "meta".
|
||||||
|
*/
|
||||||
|
export interface Meta {
|
||||||
|
id: number;
|
||||||
|
name?: string | null;
|
||||||
|
url?: string | null;
|
||||||
|
description?: string | null;
|
||||||
|
googleVerification?: string | null;
|
||||||
|
updatedAt?: string | null;
|
||||||
|
createdAt?: string | null;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "nav".
|
||||||
|
*/
|
||||||
|
export interface Nav {
|
||||||
|
id: number;
|
||||||
|
internalLinks?:
|
||||||
|
| {
|
||||||
|
href?: string | null;
|
||||||
|
iconSrc?: string | null;
|
||||||
|
label?: string | null;
|
||||||
|
id?: string | null;
|
||||||
|
}[]
|
||||||
|
| null;
|
||||||
|
contact?:
|
||||||
|
| {
|
||||||
|
href?: string | null;
|
||||||
|
iconSrc?: string | null;
|
||||||
|
label?: string | null;
|
||||||
|
id?: string | null;
|
||||||
|
}[]
|
||||||
|
| null;
|
||||||
|
updatedAt?: string | null;
|
||||||
|
createdAt?: string | null;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "meta_select".
|
||||||
|
*/
|
||||||
|
export interface MetaSelect<T extends boolean = true> {
|
||||||
|
name?: T;
|
||||||
|
url?: T;
|
||||||
|
description?: T;
|
||||||
|
googleVerification?: T;
|
||||||
|
updatedAt?: T;
|
||||||
|
createdAt?: T;
|
||||||
|
globalType?: T;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "nav_select".
|
||||||
|
*/
|
||||||
|
export interface NavSelect<T extends boolean = true> {
|
||||||
|
internalLinks?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
href?: T;
|
||||||
|
iconSrc?: T;
|
||||||
|
label?: T;
|
||||||
|
id?: T;
|
||||||
|
};
|
||||||
|
contact?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
href?: T;
|
||||||
|
iconSrc?: T;
|
||||||
|
label?: T;
|
||||||
|
id?: T;
|
||||||
|
};
|
||||||
|
updatedAt?: T;
|
||||||
|
createdAt?: T;
|
||||||
|
globalType?: T;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* This interface was referenced by `Config`'s JSON-Schema
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
* via the `definition` "auth".
|
* via the `definition` "auth".
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
// storage-adapter-import-placeholder
|
// storage-adapter-import-placeholder
|
||||||
import { postgresAdapter } from '@payloadcms/db-postgres'
|
import { postgresAdapter } from '@payloadcms/db-postgres'
|
||||||
import { payloadCloudPlugin } from '@payloadcms/payload-cloud'
|
import { payloadCloudPlugin } from '@payloadcms/payload-cloud'
|
||||||
import { lexicalEditor } from '@payloadcms/richtext-lexical'
|
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { buildConfig } from 'payload'
|
import { buildConfig } from 'payload'
|
||||||
import { fileURLToPath } from 'url'
|
import { fileURLToPath } from 'url'
|
||||||
@ -9,6 +8,10 @@ import sharp from 'sharp'
|
|||||||
|
|
||||||
import { Users } from './collections/Users'
|
import { Users } from './collections/Users'
|
||||||
import { Media } from './collections/Media'
|
import { Media } from './collections/Media'
|
||||||
|
import { Pages } from './collections/Pages'
|
||||||
|
import { defaultLexical } from './fields/defaultLexical'
|
||||||
|
import { NavGlobal } from './globals/Nav/config'
|
||||||
|
import { MetaGlobal } from './globals/Meta/config'
|
||||||
|
|
||||||
const filename = fileURLToPath(import.meta.url)
|
const filename = fileURLToPath(import.meta.url)
|
||||||
const dirname = path.dirname(filename)
|
const dirname = path.dirname(filename)
|
||||||
@ -20,8 +23,9 @@ export default buildConfig({
|
|||||||
baseDir: path.resolve(dirname),
|
baseDir: path.resolve(dirname),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
collections: [Users, Media],
|
collections: [Users, Media, Pages],
|
||||||
editor: lexicalEditor(),
|
globals: [MetaGlobal, NavGlobal],
|
||||||
|
editor: defaultLexical,
|
||||||
secret: process.env.PAYLOAD_SECRET || '',
|
secret: process.env.PAYLOAD_SECRET || '',
|
||||||
typescript: {
|
typescript: {
|
||||||
outputFile: path.resolve(dirname, 'payload-types.ts'),
|
outputFile: path.resolve(dirname, 'payload-types.ts'),
|
||||||
|
|||||||
13
tailwind.config.js
Normal file
13
tailwind.config.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
export default {
|
||||||
|
content: ['./src /**/*.{jsx,tsx
|
||||||
|
}'
|
||||||
|
], // tell tailwind where to look
|
||||||
|
darkMode: ['selector', '[data-theme="dark"
|
||||||
|
]', '.dark'
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user