9
sites/vibes/.dockerignore
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
node_modules
|
||||
dist
|
||||
.astro
|
||||
.git
|
||||
.vscode
|
||||
.DS_Store
|
||||
npm-debug.log*
|
||||
Dockerfile
|
||||
docker-compose*.yml
|
||||
5
sites/vibes/.prettierignore
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
node_modules
|
||||
pnpm-lock.yaml
|
||||
package-lock.json
|
||||
src/components/ui
|
||||
.astro
|
||||
6
sites/vibes/.prettierrc
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"plugins": ["prettier-plugin-tailwindcss"],
|
||||
"printWidth": 80,
|
||||
"tabWidth": 2,
|
||||
"tailwindStylesheet": "./src/app/globals.css"
|
||||
}
|
||||
20
sites/vibes/Dockerfile
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
FROM node:22-alpine AS build
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm ci
|
||||
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
FROM nginx:1.27-alpine AS runtime
|
||||
|
||||
COPY deploy/nginx.conf /etc/nginx/conf.d/default.conf
|
||||
COPY deploy/blog-locations.conf /etc/nginx/snippets/blog-locations.conf
|
||||
COPY --from=build /app/dist /usr/share/nginx/html
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
|
||||
CMD wget -q --spider http://127.0.0.1/nginx-health || exit 1
|
||||
22
sites/vibes/astro.config.mjs
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
// @ts-check
|
||||
import { defineConfig } from "astro/config";
|
||||
import mdx from "@astrojs/mdx";
|
||||
import sitemap from "@astrojs/sitemap";
|
||||
|
||||
import react from "@astrojs/react";
|
||||
// @ts-ignore - TailwindCSS v4 Vite plugin has type compatibility issues with Astro
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
import { ACTIVE_BLOG_SITE } from "./src/blog-sites.js";
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
site: ACTIVE_BLOG_SITE.url,
|
||||
integrations: [mdx(), sitemap(), react()],
|
||||
output: "static",
|
||||
outDir: "dist",
|
||||
|
||||
vite: {
|
||||
// @ts-ignore - Suppress TypeScript error for TailwindCSS v4 plugin compatibility
|
||||
plugins: [tailwindcss()],
|
||||
},
|
||||
});
|
||||
21
sites/vibes/components.json
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "new-york",
|
||||
"rsc": false,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "",
|
||||
"css": "src/styles/global.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
},
|
||||
"iconLibrary": "lucide"
|
||||
}
|
||||
23
sites/vibes/deploy/blog-locations.conf
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
index index.html;
|
||||
|
||||
access_log /var/log/nginx/access.log;
|
||||
error_log /var/log/nginx/error.log warn;
|
||||
|
||||
location = /nginx-health {
|
||||
access_log off;
|
||||
return 204;
|
||||
}
|
||||
|
||||
location ^~ /dist/ {
|
||||
rewrite ^/dist/[^/]+/?(.*)$ /$1 last;
|
||||
}
|
||||
|
||||
location ~* \.(?:css|js|mjs|png|jpg|jpeg|gif|svg|webp|ico|woff2?)$ {
|
||||
try_files $uri =404;
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
}
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/index.html /index.html /404.html;
|
||||
}
|
||||
48
sites/vibes/deploy/nginx.conf
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
server {
|
||||
listen 80 default_server;
|
||||
server_name research.neeldhara.blog research.neeldhara.email _;
|
||||
root /usr/share/nginx/html;
|
||||
include /etc/nginx/snippets/blog-locations.conf;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name vibes.neeldhara.blog vibes.neeldhara.email;
|
||||
root /usr/share/nginx/html;
|
||||
include /etc/nginx/snippets/blog-locations.conf;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name puzzles.neeldhara.blog puzzles.neeldhara.email;
|
||||
root /usr/share/nginx/html;
|
||||
include /etc/nginx/snippets/blog-locations.conf;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name reflections.neeldhara.blog reflections.neeldhara.email;
|
||||
root /usr/share/nginx/html;
|
||||
include /etc/nginx/snippets/blog-locations.conf;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name poetry.neeldhara.blog poetry.neeldhara.email;
|
||||
root /usr/share/nginx/html;
|
||||
include /etc/nginx/snippets/blog-locations.conf;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name reviews.neeldhara.blog reviews.neeldhara.email;
|
||||
root /usr/share/nginx/html;
|
||||
include /etc/nginx/snippets/blog-locations.conf;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name art.neeldhara.blog art.neeldhara.email;
|
||||
root /usr/share/nginx/html;
|
||||
include /etc/nginx/snippets/blog-locations.conf;
|
||||
}
|
||||
9144
sites/vibes/package-lock.json
generated
Normal file
66
sites/vibes/package.json
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
{
|
||||
"name": "charter-astro-template",
|
||||
"type": "module",
|
||||
"version": "1.0.0",
|
||||
"author": "Shadcnblocks.com",
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"dev:site": "astro dev",
|
||||
"build": "astro build",
|
||||
"build:site": "astro build",
|
||||
"build:research": "BLOG_SITE=research astro build",
|
||||
"build:vibes": "BLOG_SITE=vibes astro build",
|
||||
"build:puzzles": "BLOG_SITE=puzzles astro build",
|
||||
"build:reflections": "BLOG_SITE=reflections astro build",
|
||||
"build:poetry": "BLOG_SITE=poetry astro build",
|
||||
"build:reviews": "BLOG_SITE=reviews astro build",
|
||||
"build:art": "BLOG_SITE=art astro build",
|
||||
"preview": "astro preview",
|
||||
"astro": "astro",
|
||||
"format": "prettier -w ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/check": "^0.9.4",
|
||||
"@astrojs/mdx": "^4.3.3",
|
||||
"@astrojs/react": "^4.3.0",
|
||||
"@astrojs/rss": "^4.0.12",
|
||||
"@astrojs/sitemap": "^3.4.2",
|
||||
"@hookform/resolvers": "^3.9.1",
|
||||
"@nobie-org/tailwindcss-animate": "^1.0.0",
|
||||
"@radix-ui/react-accordion": "^1.2.2",
|
||||
"@radix-ui/react-avatar": "^1.1.3",
|
||||
"@radix-ui/react-checkbox": "^1.1.3",
|
||||
"@radix-ui/react-collapsible": "^1.1.2",
|
||||
"@radix-ui/react-label": "^2.1.1",
|
||||
"@radix-ui/react-navigation-menu": "^1.2.3",
|
||||
"@radix-ui/react-separator": "^1.1.2",
|
||||
"@radix-ui/react-slot": "^1.1.1",
|
||||
"@radix-ui/react-switch": "^1.1.2",
|
||||
"@radix-ui/react-tabs": "^1.1.3",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"@tailwindcss/vite": "^4.0.12",
|
||||
"@types/react": "^19.0.10",
|
||||
"@types/react-dom": "^19.0.4",
|
||||
"astro": "^5.12.9",
|
||||
"astro-themes": "^0.2.5",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"embla-carousel-autoplay": "^8.5.2",
|
||||
"embla-carousel-react": "^8.5.2",
|
||||
"lucide-react": "^0.479.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-fast-marquee": "^1.6.5",
|
||||
"react-hook-form": "^7.54.2",
|
||||
"react-icons": "^5.4.0",
|
||||
"tailwind-merge": "^3.0.2",
|
||||
"tailwindcss": "^4.0.12",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"typescript": "^5.9.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"prettier": "^3.5.3",
|
||||
"prettier-plugin-tailwindcss": "^0.6.11"
|
||||
}
|
||||
}
|
||||
BIN
sites/vibes/public/avatar/avatar1.png
Normal file
|
After Width: | Height: | Size: 127 KiB |
BIN
sites/vibes/public/avatar/avatar2.png
Normal file
|
After Width: | Height: | Size: 126 KiB |
BIN
sites/vibes/public/favicon/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
sites/vibes/public/favicon/favicon-96x96.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
sites/vibes/public/favicon/favicon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
1
sites/vibes/public/favicon/favicon.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.dev/svgjs" width="1000" height="1000"><g clip-path="url(#SvgjsClipPath1046)"><rect width="1000" height="1000" fill="#ffffff"></rect><g transform="matrix(8.860759493670885,0,0,8.860759493670885,150.00000000000006,189.873417721519)"><svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.dev/svgjs" width="79" height="70" viewBox="0 0 79 70"><image width="79" height="70" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAE8AAABGCAYAAACNO8lOAAAACXBIWXMAACE4AAAhOAFFljFgAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAGZSURBVHgB7dzdTYNQGIfxF2SAjuAIjmBHcAJDTLxuJ7BOoPemgRXcoCN0BDaQe1E8GC6MCaTw5yMcnl+CN3r1xLenbyE1Q2+B2fFg6CFMq3iloYfvbWjojXgC4gmIJyCeIGr+VfludrUzNAjzlnjBh1mcGRoxtoLo8j9NNu7HxlCLsw7xyld33Rtqb2wYCuIJiCcgnoB4gg6nbatnW50oGyjew8FWiLEVEE8w1GveH8mN20RezHvBfoR41f5b3pr3vjaMrYB4AuIJiCcY48Bok9eXF6aOt3fbSGqeYGwFxBNMPbb/VNvIYm8qnWeOV61xS91GeMRMQjwB8QTEExBPMPNpW7iNI1joW5Vo7rcqj2dbMMZWQDzB1GPrNorjk3khvJs6nkcPSBbcAFIQT0A8AfEEIxwYn+4GT3Qy75X5CPF+t4atrQBjKyCeYKCxXeP3FITpUK95nqxcXRQnxlZAPAHxBMQTEE/Q4bQNdu46GGphl/Us9urBxCEwtoK2/zz3cXlybWgQZ3yLWW88YiYhnoB4AuIJiCf4AdfbOrITOpkLAAAAAElFTkSuQmCC"></image></svg></g></g><defs><clipPath id="SvgjsClipPath1046"><rect width="1000" height="1000" x="0" y="0" rx="200" ry="200"></rect></clipPath></defs></svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
21
sites/vibes/public/favicon/site.webmanifest
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"name": "MyWebSite",
|
||||
"short_name": "MySite",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/web-app-manifest-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
},
|
||||
{
|
||||
"src": "/web-app-manifest-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
}
|
||||
],
|
||||
"theme_color": "#ffffff",
|
||||
"background_color": "#ffffff",
|
||||
"display": "standalone"
|
||||
}
|
||||
BIN
sites/vibes/public/favicon/web-app-manifest-192x192.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
sites/vibes/public/favicon/web-app-manifest-512x512.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
sites/vibes/public/images/about/1.webp
Normal file
|
After Width: | Height: | Size: 80 KiB |
BIN
sites/vibes/public/images/about/2.webp
Normal file
|
After Width: | Height: | Size: 64 KiB |
BIN
sites/vibes/public/images/about/3.webp
Normal file
|
After Width: | Height: | Size: 56 KiB |
BIN
sites/vibes/public/images/about/4.webp
Normal file
|
After Width: | Height: | Size: 70 KiB |
BIN
sites/vibes/public/images/homepage/code-snippet.webp
Normal file
|
After Width: | Height: | Size: 61 KiB |
BIN
sites/vibes/public/images/homepage/features-tabs/1.webp
Normal file
|
After Width: | Height: | Size: 175 KiB |
BIN
sites/vibes/public/images/homepage/features-tabs/2.webp
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
sites/vibes/public/images/homepage/features-tabs/3.webp
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
sites/vibes/public/images/homepage/features-tabs/4.webp
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
sites/vibes/public/images/homepage/hero/1.webp
Normal file
|
After Width: | Height: | Size: 211 KiB |
BIN
sites/vibes/public/images/homepage/hero/2.webp
Normal file
|
After Width: | Height: | Size: 192 KiB |
BIN
sites/vibes/public/images/homepage/hero/3.webp
Normal file
|
After Width: | Height: | Size: 212 KiB |
BIN
sites/vibes/public/images/homepage/hero/4.webp
Normal file
|
After Width: | Height: | Size: 213 KiB |
BIN
sites/vibes/public/images/homepage/why-charter/1.webp
Normal file
|
After Width: | Height: | Size: 34 KiB |
10
sites/vibes/public/images/homepage/why-charter/2.svg
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<svg width="289" height="49" viewBox="0 0 289 49" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M44.5 38.5H26.5C18.958 38.5 15.186 38.5 12.844 36.156C10.502 33.812 10.5 30.042 10.5 22.5V4.5" stroke="#00009D" stroke-width="3" stroke-linecap="round"/>
|
||||
<path opacity="0.5" d="M4.5 10.5H22.5C30.042 10.5 33.814 10.5 36.156 12.844C38.498 15.188 38.5 18.958 38.5 26.5V44.5" stroke="#00009D" stroke-width="3" stroke-linecap="round"/>
|
||||
<path d="M90.5 28.5L94.5 24.5M94.5 24.5L90.5 20.5M94.5 24.5H74.5" stroke="#00009D" stroke-opacity="0.5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M130.5 3C130.898 3 131.279 3.15804 131.561 3.43934C131.842 3.72064 132 4.10218 132 4.5V22.5C132 26.314 132.004 29.024 132.28 31.08C132.55 33.09 133.058 34.25 133.904 35.096C134.75 35.942 135.91 36.45 137.922 36.72C139.978 36.996 142.686 37 146.5 37H164.5C164.898 37 165.279 37.158 165.561 37.4393C165.842 37.7206 166 38.1022 166 38.5C166 38.8978 165.842 39.2794 165.561 39.5607C165.279 39.842 164.898 40 164.5 40H146.388C142.712 40 139.8 40 137.522 39.694C135.178 39.378 133.28 38.714 131.782 37.218C130.286 35.72 129.622 33.822 129.306 31.478C129 29.198 129 26.288 129 22.612V4.5C129 4.10218 129.158 3.72064 129.439 3.43934C129.721 3.15804 130.102 3 130.5 3Z" fill="#00009D"/>
|
||||
<path opacity="0.5" d="M124.5 12H129V9H124.5C124.102 9 123.721 9.15804 123.439 9.43934C123.158 9.72064 123 10.1022 123 10.5C123 10.8978 123.158 11.2794 123.439 11.5607C123.721 11.842 124.102 12 124.5 12ZM160 44.5V40H157V44.5C157 44.8978 157.158 45.2794 157.439 45.5607C157.721 45.842 158.102 46 158.5 46C158.898 46 159.279 45.842 159.561 45.5607C159.842 45.2794 160 44.8978 160 44.5ZM151.08 12.28C149.024 12.004 146.314 12 142.5 12H136.5C136.102 12 135.721 11.842 135.439 11.5607C135.158 11.2794 135 10.8978 135 10.5C135 10.1022 135.158 9.72064 135.439 9.43934C135.721 9.15804 136.102 9 136.5 9H142.612C146.288 9 149.2 9 151.478 9.306C153.822 9.622 155.72 10.286 157.218 11.782C158.714 13.28 159.378 15.178 159.694 17.522C160 19.802 160 22.712 160 26.388V32.5C160 32.8978 159.842 33.2794 159.561 33.5607C159.279 33.842 158.898 34 158.5 34C158.102 34 157.721 33.842 157.439 33.5607C157.158 33.2794 157 32.8978 157 32.5V26.5C157 22.686 156.996 19.978 156.72 17.92C156.45 15.91 155.942 14.75 155.096 13.904C154.25 13.058 153.09 12.55 151.078 12.28" fill="#00009D"/>
|
||||
<path d="M137.5 23.5001C137.5 20.6721 137.5 19.2581 138.38 18.3801C139.258 17.5001 140.672 17.5001 143.5 17.5001H145.5C148.328 17.5001 149.742 17.5001 150.62 18.3801C151.5 19.2581 151.5 20.6721 151.5 23.5001V25.5001C151.5 28.3281 151.5 29.7421 150.62 30.6201C149.742 31.5001 148.328 31.5001 145.5 31.5001H143.5C140.672 31.5001 139.258 31.5001 138.38 30.6201C137.5 29.7421 137.5 28.3281 137.5 25.5001V23.5001Z" fill="#00009D"/>
|
||||
<path d="M210.5 28.5L214.5 24.5M214.5 24.5L210.5 20.5M214.5 24.5H194.5" stroke="#00009D" stroke-opacity="0.5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M246.844 6.844C244.5 9.186 244.5 12.958 244.5 20.5V28.5C244.5 36.042 244.5 39.814 246.844 42.156C249.188 44.498 252.958 44.5 260.5 44.5H268.5C276.042 44.5 279.814 44.5 282.156 42.156C284.498 39.812 284.5 36.042 284.5 28.5V20.5C284.5 12.958 284.5 9.186 282.156 6.844C279.812 4.502 276.042 4.5 268.5 4.5H260.5C252.958 4.5 249.186 4.5 246.844 6.844ZM258 12.5C258 12.1022 257.842 11.7206 257.561 11.4393C257.279 11.158 256.898 11 256.5 11C256.102 11 255.721 11.158 255.439 11.4393C255.158 11.7206 255 12.1022 255 12.5V15H252.5C252.102 15 251.721 15.158 251.439 15.4393C251.158 15.7206 251 16.1022 251 16.5C251 16.8978 251.158 17.2794 251.439 17.5607C251.721 17.842 252.102 18 252.5 18H255V24.604C255 26.402 255 27.9 255.16 29.088C255.328 30.344 255.7 31.478 256.61 32.388C257.522 33.3 258.656 33.672 259.91 33.84C261.1 34 262.6 34 264.396 34H271V36.5C271 36.8978 271.158 37.2794 271.439 37.5607C271.721 37.842 272.102 38 272.5 38C272.898 38 273.279 37.842 273.561 37.5607C273.842 37.2794 274 36.8978 274 36.5V34H276.5C276.898 34 277.279 33.842 277.561 33.5607C277.842 33.2794 278 32.8978 278 32.5C278 32.1022 277.842 31.7206 277.561 31.4393C277.279 31.158 276.898 31 276.5 31H264.5C262.572 31 261.276 30.996 260.31 30.868C259.388 30.742 258.992 30.528 258.732 30.268C258.472 30.008 258.258 29.612 258.132 28.688C258.004 27.724 258 26.428 258 24.5V12.5ZM264.5 18C266.428 18 267.724 18.004 268.69 18.134C269.612 18.258 270.008 18.472 270.268 18.734C270.528 18.996 270.742 19.388 270.868 20.31C270.996 21.276 271 22.572 271 24.5V27.44C271 27.8378 271.158 28.2194 271.439 28.5007C271.721 28.782 272.102 28.94 272.5 28.94C272.898 28.94 273.279 28.782 273.561 28.5007C273.842 28.2194 274 27.8378 274 27.44V24.396C274 22.598 274 21.1 273.84 19.912C273.672 18.656 273.3 17.522 272.388 16.612C271.478 15.7 270.344 15.328 269.088 15.16C267.9 15 266.4 15 264.604 15H261.558C261.16 15 260.779 15.158 260.497 15.4393C260.216 15.7206 260.058 16.1022 260.058 16.5C260.058 16.8978 260.216 17.2794 260.497 17.5607C260.779 17.842 261.16 18 261.558 18H264.5Z" fill="#00009D"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5 KiB |
9
sites/vibes/public/images/homepage/why-charter/3.svg
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<svg width="288" height="49" viewBox="0 0 288 49" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path opacity="0.6" d="M17.9995 30.4999L3.99951 44.4999M3.99951 44.4999H15.7135M3.99951 44.4999V32.7859" stroke="#00009D" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M30 18.4999L44 4.49988M44 4.49988H32.286M44 4.49988V16.2139" stroke="#00009D" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M90 28.5L94 24.5M94 24.5L90 20.5M94 24.5H74" stroke="#00009D" stroke-opacity="0.5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M159.999 8.49976L147.999 20.4998M147.999 20.4998H155.499M147.999 20.4998V12.9998M127.999 40.4998L139.999 28.4998M139.999 28.4998H132.499M139.999 28.4998V35.9998" stroke="#00009D" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path opacity="0.5" d="M124 24.4999C124 15.0719 124 10.3579 126.928 7.42788C129.86 4.49988 134.572 4.49988 144 4.49988C153.428 4.49988 158.142 4.49988 161.07 7.42788C164 10.3599 164 15.0719 164 24.4999C164 33.9279 164 38.6419 161.07 41.5699C158.144 44.4999 153.428 44.4999 144 44.4999C134.572 44.4999 129.858 44.4999 126.928 41.5699C124 38.6439 124 33.9279 124 24.4999Z" stroke="#00009D" stroke-width="3"/>
|
||||
<path d="M210 28.5L214 24.5M214 24.5L210 20.5M214 24.5H194" stroke="#00009D" stroke-opacity="0.5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M244 24.5C244 15.072 244 10.358 246.928 7.428C249.86 4.5 254.572 4.5 264 4.5C273.428 4.5 278.142 4.5 281.07 7.428C284 10.36 284 15.072 284 24.5C284 33.928 284 38.642 281.07 41.57C278.144 44.5 273.428 44.5 264 44.5C254.572 44.5 249.858 44.5 246.928 41.57C244 38.644 244 33.928 244 24.5ZM252.94 35.56C252.659 35.2788 252.501 34.8975 252.501 34.5C252.501 34.1025 252.659 33.7212 252.94 33.44L257.38 29H255C254.602 29 254.221 28.842 253.939 28.5607C253.658 28.2794 253.5 27.8978 253.5 27.5C253.5 27.1022 253.658 26.7206 253.939 26.4393C254.221 26.158 254.602 26 255 26H261C261.398 26 261.779 26.158 262.061 26.4393C262.342 26.7206 262.5 27.1022 262.5 27.5V33.5C262.5 33.8978 262.342 34.2794 262.061 34.5607C261.779 34.842 261.398 35 261 35C260.602 35 260.221 34.842 259.939 34.5607C259.658 34.2794 259.5 33.8978 259.5 33.5V31.12L255.06 35.56C254.779 35.8409 254.398 35.9987 254 35.9987C253.602 35.9987 253.221 35.8409 252.94 35.56ZM273 23C273.398 23 273.779 22.842 274.061 22.5607C274.342 22.2794 274.5 21.8978 274.5 21.5C274.5 21.1022 274.342 20.7206 274.061 20.4393C273.779 20.158 273.398 20 273 20H270.62L275.06 15.56C275.325 15.2757 275.469 14.8996 275.462 14.511C275.455 14.1224 275.298 13.7516 275.023 13.4768C274.748 13.2019 274.378 13.0445 273.989 13.0377C273.6 13.0308 273.224 13.175 272.94 13.44L268.5 17.88V15.5C268.5 15.1022 268.342 14.7206 268.061 14.4393C267.779 14.158 267.398 14 267 14C266.602 14 266.221 14.158 265.939 14.4393C265.658 14.7206 265.5 15.1022 265.5 15.5V21.5C265.5 22.328 266.172 23 267 23H273Z" fill="#00009D"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.9 KiB |
25
sites/vibes/public/images/layout/circles.svg
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
<svg width="240" height="80" viewBox="0 0 240 80" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_1_910)">
|
||||
<g opacity="0.3">
|
||||
<path d="M55.2258 40C55.2258 61.6638 37.6638 79.2258 16 79.2258C-5.66382 79.2258 -23.2258 61.6638 -23.2258 40C-23.2258 18.3362 -5.66382 0.774192 16 0.774192C37.6638 0.774192 55.2258 18.3362 55.2258 40Z" stroke="white" stroke-width="1.54838"/>
|
||||
<ellipse cx="15.7419" cy="39.7421" rx="5.16129" ry="5.16129" fill="white"/>
|
||||
<line x1="16.0003" y1="1.12803e-08" x2="16.0002" y2="78.9677" stroke="white" stroke-width="0.516128"/>
|
||||
<line x1="-23.4839" y1="39.4836" x2="55.4838" y2="39.4836" stroke="white" stroke-width="0.516128"/>
|
||||
</g>
|
||||
<path d="M159.226 40C159.226 61.6638 141.664 79.2258 120 79.2258C98.3362 79.2258 80.7742 61.6638 80.7742 40C80.7742 18.3362 98.3362 0.774192 120 0.774192C141.664 0.774192 159.226 18.3362 159.226 40Z" stroke="white" stroke-width="1.54838"/>
|
||||
<ellipse cx="119.742" cy="39.7421" rx="5.16129" ry="5.16129" fill="white"/>
|
||||
<line x1="120" y1="1.12803e-08" x2="120" y2="78.9677" stroke="white" stroke-width="0.516128"/>
|
||||
<line x1="80.5161" y1="39.4836" x2="159.484" y2="39.4836" stroke="white" stroke-width="0.516128"/>
|
||||
<g opacity="0.3">
|
||||
<path d="M263.226 40C263.226 61.6638 245.664 79.2258 224 79.2258C202.336 79.2258 184.774 61.6638 184.774 40C184.774 18.3362 202.336 0.774192 224 0.774192C245.664 0.774192 263.226 18.3362 263.226 40Z" stroke="white" stroke-width="1.54838"/>
|
||||
<ellipse cx="223.742" cy="39.7421" rx="5.16129" ry="5.16129" fill="white"/>
|
||||
<line x1="224" y1="1.12803e-08" x2="224" y2="78.9677" stroke="white" stroke-width="0.516128"/>
|
||||
<line x1="184.516" y1="39.4836" x2="263.484" y2="39.4836" stroke="white" stroke-width="0.516128"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_1_910">
|
||||
<rect width="288" height="80" fill="white" transform="translate(-24)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
13
sites/vibes/public/images/layout/logo.svg
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<svg width="128" height="23" viewBox="0 0 128 23" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="26.1032" height="2.19048" fill="#00009D"/>
|
||||
<rect x="3.46825" y="3.46826" width="22.6349" height="2.19048" fill="#00009D"/>
|
||||
<rect x="6.93651" y="6.93652" width="19.1667" height="2.19048" fill="#00009D"/>
|
||||
<rect x="10.4048" y="10.4048" width="15.6984" height="2.19048" fill="#00009D"/>
|
||||
<rect x="6.93651" y="13.873" width="19.1667" height="2.19048" fill="#00009D"/>
|
||||
<rect x="3.46825" y="17.3412" width="22.6349" height="2.19048" fill="#00009D"/>
|
||||
<rect y="20.8095" width="26.1032" height="2.19048" fill="#00009D"/>
|
||||
<rect x="2.19045" y="-0.00012207" width="23" height="2.19047" transform="rotate(90 2.19045 -0.00012207)" fill="#00009D"/>
|
||||
<rect x="5.65871" y="3.46802" width="16.0635" height="2.19048" transform="rotate(90 5.65871 3.46802)" fill="#00009D"/>
|
||||
<rect x="9.12695" y="6.93634" width="9.12698" height="2.19048" transform="rotate(90 9.12695 6.93634)" fill="#00009D"/>
|
||||
<path d="M43.2132 18.68C41.7972 18.68 40.6152 18.41 39.6672 17.87C38.7192 17.33 38.0052 16.562 37.5252 15.566C37.0452 14.558 36.8052 13.364 36.8052 11.984V11.93C36.8052 10.826 36.9552 9.86 37.2552 9.032C37.5672 8.192 37.9992 7.49 38.5512 6.926C39.1032 6.362 39.7752 5.936 40.5672 5.648C41.3592 5.36 42.2412 5.216 43.2132 5.216C44.3172 5.216 45.2952 5.408 46.1472 5.792C47.0112 6.176 47.7012 6.722 48.2172 7.43C48.7452 8.126 49.0572 8.942 49.1532 9.878H46.1292C46.0332 9.422 45.8412 9.038 45.5532 8.726C45.2652 8.402 44.9172 8.162 44.5092 8.006C44.1132 7.85 43.6812 7.772 43.2132 7.772C42.4692 7.772 41.8512 7.91 41.3592 8.186C40.8672 8.45 40.5012 8.822 40.2612 9.302C40.0212 9.77 39.9012 10.31 39.9012 10.922V12.974C39.9012 13.478 39.9732 13.928 40.1172 14.324C40.2732 14.708 40.4952 15.038 40.7832 15.314C41.0712 15.578 41.4192 15.782 41.8272 15.926C42.2352 16.058 42.6972 16.124 43.2132 16.124C43.6932 16.124 44.1372 16.04 44.5452 15.872C44.9532 15.704 45.2952 15.464 45.5712 15.152C45.8592 14.828 46.0452 14.438 46.1292 13.982H49.1532C49.0812 14.69 48.8832 15.332 48.5592 15.908C48.2352 16.484 47.8092 16.982 47.2812 17.402C46.7532 17.81 46.1412 18.128 45.4452 18.356C44.7612 18.572 44.0172 18.68 43.2132 18.68ZM59.7313 18.5V5.378H62.6653V18.5H59.7313ZM51.3433 18.5V5.378H54.2953V18.5H51.3433ZM54.0433 13.1V10.598H59.9833V13.1H54.0433ZM64.1759 18.5L68.5859 5.378H72.6719L77.0999 18.5H74.1839L73.3019 15.872H67.9019L67.0199 18.5H64.1759ZM68.5859 13.622H72.6179L71.3219 9.698L70.6559 7.664H70.5479L69.8819 9.698L68.5859 13.622ZM78.6069 18.5V5.378H84.8349C85.8789 5.378 86.7429 5.552 87.4269 5.9C88.1229 6.236 88.6449 6.722 88.9929 7.358C89.3409 7.982 89.5149 8.72 89.5149 9.572C89.5149 10.268 89.3769 10.892 89.1009 11.444C88.8369 11.996 88.4469 12.446 87.9309 12.794C87.4149 13.142 86.8029 13.364 86.0949 13.46L86.2749 12.992L89.6769 18.5H86.2389L83.4669 13.838H81.3069V11.624H84.3129C84.7689 11.624 85.1529 11.564 85.4649 11.444C85.7769 11.312 86.0109 11.132 86.1669 10.904C86.3349 10.664 86.4189 10.376 86.4189 10.04V9.482C86.4189 9.146 86.3289 8.858 86.1489 8.618C85.9809 8.378 85.7409 8.198 85.4289 8.078C85.1169 7.958 84.7449 7.898 84.3129 7.898H81.0729L81.5589 7.394V18.5H78.6069ZM94.365 7.646H97.299V18.5H94.365V7.646ZM101.223 7.898H90.441V5.378H101.223V7.898ZM102.882 18.5V5.378H112.872V7.88H105.834V10.67H111.954V13.01H105.834V15.998H112.872V18.5H102.882ZM115.31 18.5V5.378H121.538C122.582 5.378 123.446 5.552 124.13 5.9C124.826 6.236 125.348 6.722 125.696 7.358C126.044 7.982 126.218 8.72 126.218 9.572C126.218 10.268 126.08 10.892 125.804 11.444C125.54 11.996 125.15 12.446 124.634 12.794C124.118 13.142 123.506 13.364 122.798 13.46L122.978 12.992L126.38 18.5H122.942L120.17 13.838H118.01V11.624H121.016C121.472 11.624 121.856 11.564 122.168 11.444C122.48 11.312 122.714 11.132 122.87 10.904C123.038 10.664 123.122 10.376 123.122 10.04V9.482C123.122 9.146 123.032 8.858 122.852 8.618C122.684 8.378 122.444 8.198 122.132 8.078C121.82 7.958 121.448 7.898 121.016 7.898H117.776L118.262 7.394V18.5H115.31Z" fill="#00009D"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.9 KiB |
12
sites/vibes/public/images/layout/logomark.svg
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<svg width="27" height="24" viewBox="0 0 27 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect y="0.5" width="26.1032" height="2.19048" fill="#09090B"/>
|
||||
<rect x="3.46826" y="3.96826" width="22.6349" height="2.19048" fill="#09090B"/>
|
||||
<rect x="6.93652" y="7.43652" width="19.1667" height="2.19048" fill="#09090B"/>
|
||||
<rect x="10.4048" y="10.9048" width="15.6984" height="2.19048" fill="#09090B"/>
|
||||
<rect x="6.93652" y="14.373" width="19.1667" height="2.19048" fill="#09090B"/>
|
||||
<rect x="3.46826" y="17.8413" width="22.6349" height="2.19048" fill="#09090B"/>
|
||||
<rect y="21.3096" width="26.1032" height="2.19048" fill="#09090B"/>
|
||||
<rect x="2.19043" y="0.499756" width="23" height="2.19047" transform="rotate(90 2.19043 0.499756)" fill="#09090B"/>
|
||||
<rect x="5.65869" y="3.96802" width="16.0635" height="2.19048" transform="rotate(90 5.65869 3.96802)" fill="#09090B"/>
|
||||
<rect x="9.12695" y="7.43628" width="9.12698" height="2.19048" transform="rotate(90 9.12695 7.43628)" fill="#09090B"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 988 B |
3
sites/vibes/public/images/logos/arc.svg
Normal file
|
After Width: | Height: | Size: 8.2 KiB |
5
sites/vibes/public/images/logos/asana.svg
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<svg width="57" height="53" viewBox="0 0 57 53" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="Group 7">
|
||||
<path id="Vector" fill-rule="evenodd" clip-rule="evenodd" d="M44.5711 27.9387C37.8447 27.9387 32.3922 33.3916 32.3922 40.1182C32.3922 46.8445 37.8447 52.2973 44.5711 52.2973C51.2974 52.2973 56.75 46.8445 56.75 40.1182C56.75 33.3916 51.2974 27.9387 44.5711 27.9387ZM12.929 27.9398C6.20278 27.9398 0.75 33.3916 0.75 40.1182C0.75 46.8445 6.20278 52.2973 12.929 52.2973C19.6555 52.2973 25.1085 46.8445 25.1085 40.1182C25.1085 33.3916 19.6555 27.9398 12.929 27.9398ZM40.9288 12.7146C40.9288 19.4414 35.4762 24.8947 28.7501 24.8947C22.0236 24.8947 16.571 19.4414 16.571 12.7146C16.571 5.9892 22.0236 0.535889 28.7501 0.535889C35.4762 0.535889 40.9288 5.9892 40.9288 12.7146Z" fill="#F06A6A"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 812 B |
18
sites/vibes/public/images/logos/confluence.svg
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<svg width="56" height="54" viewBox="0 0 56 54" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="Group 5">
|
||||
<path id="Vector" d="M2.336 40.9876C1.7641 41.9202 1.12181 43.0024 0.620299 43.8647C0.385584 44.2613 0.315845 44.7342 0.426077 45.1817C0.536308 45.6292 0.817709 46.0156 1.20979 46.2579L12.6478 53.2966C12.8466 53.4194 13.0678 53.5013 13.2985 53.5376C13.5293 53.574 13.7649 53.564 13.9918 53.5083C14.2187 53.4526 14.4321 53.3523 14.6198 53.2132C14.8075 53.0742 14.9657 52.8991 15.085 52.6983C15.5337 51.9329 16.1232 50.9387 16.7655 49.874C21.2967 42.3953 25.8631 43.3104 34.0721 47.2345L45.4133 52.628C45.6259 52.7291 45.8567 52.7866 46.0919 52.797C46.3271 52.8074 46.5621 52.7705 46.7828 52.6885C47.0035 52.6064 47.2055 52.4809 47.3768 52.3194C47.5481 52.1579 47.6852 51.9636 47.7801 51.7481L53.2264 39.4302C53.4113 39.0074 53.4227 38.5288 53.2582 38.0976C53.0936 37.6664 52.7662 37.3171 52.3465 37.125C49.9533 35.9988 45.1934 33.7464 40.9085 31.6876C25.4584 24.1737 12.3575 24.6664 2.336 40.9876Z" fill="url(#paint0_linear_54_3758)"/>
|
||||
<path id="Vector_2" d="M53.6223 13.0084C54.1942 12.0758 54.8365 10.9848 55.382 10.1313C55.6159 9.73362 55.6843 9.25996 55.5724 8.81234C55.4605 8.36472 55.1773 7.97895 54.7837 7.73813L43.3457 0.699354C43.1473 0.578026 42.9268 0.497234 42.6969 0.461644C42.4671 0.426054 42.2325 0.436377 42.0067 0.492012C41.7809 0.547648 41.5683 0.64749 41.3814 0.785774C41.1944 0.924057 41.0366 1.09804 40.9173 1.29765C40.4598 2.06312 39.8703 3.05734 39.228 4.12196C34.6968 11.6007 30.1392 10.6856 21.9214 6.7615L10.545 1.39443C10.3328 1.29464 10.1028 1.23825 9.86858 1.22858C9.63432 1.2189 9.40048 1.25614 9.18081 1.33811C8.96114 1.42007 8.76007 1.54511 8.5894 1.70588C8.41873 1.86664 8.28192 2.0599 8.18698 2.27428L2.74073 14.5921C2.55578 15.015 2.54436 15.4936 2.70894 15.9248C2.87352 16.356 3.20092 16.7053 3.62058 16.8973C6.01376 18.0236 10.7825 20.2672 15.0586 22.3348C30.4999 29.7871 43.6008 29.2944 53.6223 13.0084Z" fill="url(#paint1_linear_54_3758)"/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_54_3758" x1="52.8832" y1="56.904" x2="18.2788" y2="37.0107" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#0050D3"/>
|
||||
<stop offset="0.94" stop-color="#007FFC"/>
|
||||
<stop offset="1" stop-color="#0082FF"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_54_3758" x1="3.07507" y1="-2.93441" x2="37.6795" y2="16.9501" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#0050D3"/>
|
||||
<stop offset="0.94" stop-color="#007FFC"/>
|
||||
<stop offset="1" stop-color="#0082FF"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.5 KiB |
3
sites/vibes/public/images/logos/descript.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<svg width="112" height="27" viewBox="0 0 112 27" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M47.969 12.838C48.287 11.347 49.314 10.588 50.665 10.588C52.003 10.588 52.982 11.381 53.295 12.838H47.969ZM50.738 8.006C47.3 8.006 45.161 10.66 45.161 13.907C45.161 17.27 47.446 19.764 50.843 19.764C53.185 19.764 54.606 18.717 55.485 17.399L53.3 16.05C52.75 16.776 51.87 17.16 50.928 17.16C49.385 17.16 48.352 16.462 48 15.066H53.772L53.77 15.072L55.874 15.084C55.954 14.716 56.001 14.337 56.001 13.908C56.001 11.08 54.198 8.007 50.739 8.007L50.738 8.006ZM37.484 17.111C35.585 17.111 34.444 15.75 34.444 14.071C34.444 12.391 35.607 11.03 37.484 11.03C39.36 11.03 40.524 12.391 40.524 14.07C40.524 15.75 39.382 17.111 37.484 17.111ZM40.524 9.925C39.71 8.965 38.555 8.377 37.122 8.377C33.917 8.377 31.612 10.927 31.612 14.071C31.612 17.215 33.896 19.764 37.122 19.764C38.555 19.764 39.71 19.176 40.523 18.216V19.484H43.355V4.388H40.523L40.524 9.925ZM64.478 13.65C65.269 14.19 65.771 15.003 65.771 16.074C65.771 18.24 64.281 19.742 61.493 19.742C58.87 19.742 57.463 18.426 56.903 17.097L59.311 15.721C59.311 15.721 59.635 17.157 61.45 17.157C62.573 17.157 62.919 16.671 62.919 16.242C62.919 16.042 62.854 15.677 62.055 15.367C60.449 14.746 57.345 14.467 57.345 11.633C57.345 9.313 59.202 8.076 61.493 8.076C63.635 8.076 65.011 9.381 65.533 10.396L63.178 11.744C63.178 11.744 62.725 10.617 61.515 10.617C60.305 10.617 60.197 11.302 60.197 11.501C60.197 12.081 60.801 12.319 62.098 12.694C62.866 12.916 63.655 13.118 64.48 13.647L64.478 13.65ZM112 11.124H109.558V15.72C109.558 17.156 110.984 16.847 112 16.847V19.432C112 19.432 111.465 19.52 110.531 19.52C108.457 19.52 106.771 18.901 106.771 15.72V11.124H104.891V8.384H106.771V5.291H109.558V8.384H112V11.124ZM72.7 16.957C73.857 16.957 74.746 16.393 75.196 15.467L77.612 16.884C76.665 18.582 74.901 19.763 72.7 19.763C69.362 19.763 67.029 17.152 67.029 13.93C67.029 10.709 69.384 8.097 72.7 8.097C74.869 8.097 76.626 9.259 77.583 10.934L75.172 12.384C74.714 11.47 73.832 10.904 72.7 10.904C71.014 10.904 69.816 12.258 69.816 13.93C69.816 15.602 71.003 16.957 72.7 16.957ZM98.21 17.447C96.38 17.447 95.279 16.05 95.279 14.326C95.279 12.602 96.401 11.206 98.21 11.206C100.019 11.206 101.141 12.602 101.141 14.326C101.141 16.05 100.041 17.447 98.21 17.447ZM98.559 8.482C101.649 8.482 103.871 11.099 103.871 14.326C103.871 17.553 101.67 20.169 98.559 20.169C97.177 20.169 96.064 19.566 95.279 18.581V24.286H92.549V8.792H95.279V10.071C96.064 9.086 97.177 8.482 98.559 8.482ZM87.301 19.882V8.815H90.031V19.882H87.301ZM82.214 10.705C82.741 9.364 83.814 8.597 85.43 8.593V11.781C83.535 11.593 82.166 12.579 82.216 14.907L82.214 14.902V19.882H79.484V8.815H82.214V10.705ZM87.301 7.338V4.388H90.121V7.338H87.301ZM0 24.567C0 26.043 0.942001 27 2.394 27H9.388C13.56 27 16.974 25.565 19.294 23.053H0V24.567ZM9.388 0H2.394C0.942001 0 0 0.957001 0 2.433V3.947H19.294C16.974 1.435 13.56 0 9.388 0ZM15.492 17.347C15.492 18.541 16.257 19.316 17.437 19.316H21.627C22.103 18.117 22.422 16.8 22.57 15.379H17.437C16.257 15.379 15.492 16.153 15.492 17.347ZM9.682 9.663C9.682 10.857 10.448 11.631 11.628 11.631H22.57C22.4375 10.2818 22.1205 8.9571 21.628 7.694H11.628C10.448 7.694 9.682 8.469 9.682 9.663ZM11.626 17.347C11.626 16.153 10.861 15.379 9.681 15.379H0V19.316H9.681C10.861 19.316 11.626 18.541 11.626 17.347ZM5.817 9.663C5.817 8.469 5.052 7.694 3.872 7.694H0V11.631H3.872C5.052 11.631 5.817 10.857 5.817 9.663Z" fill="#7D7D7D"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.4 KiB |
10
sites/vibes/public/images/logos/drive.svg
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<svg width="58" height="51" viewBox="0 0 58 51" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="Group 3">
|
||||
<path id="Vector" d="M4.66744 43.5191L7.16994 47.8416C7.68994 48.7516 8.43744 49.4666 9.31494 49.9866L18.2524 34.5166H0.377441C0.377441 35.5241 0.637441 36.5316 1.15744 37.4416L4.66744 43.5191Z" fill="#0066DA"/>
|
||||
<path id="Vector_2" d="M28.7499 16.3167L19.8124 0.84668C18.9349 1.36668 18.1874 2.08168 17.6674 2.99168L1.15744 31.5917C0.647005 32.4821 0.378121 33.4904 0.377441 34.5167H18.2524L28.7499 16.3167Z" fill="#00AC47"/>
|
||||
<path id="Vector_3" d="M48.1849 49.9866C49.0624 49.4666 49.8099 48.7516 50.3299 47.8416L51.3699 46.0541L56.3424 37.4416C56.8624 36.5316 57.1224 35.5241 57.1224 34.5166H39.2461L43.0499 41.9916L48.1849 49.9866Z" fill="#EA4335"/>
|
||||
<path id="Vector_4" d="M28.75 16.3167L37.6875 0.84665C36.81 0.32665 35.8025 0.0666504 34.7625 0.0666504H22.7375C21.6975 0.0666504 20.69 0.35915 19.8125 0.84665L28.75 16.3167Z" fill="#00832D"/>
|
||||
<path id="Vector_5" d="M39.2474 34.5166H18.2524L9.31494 49.9866C10.1924 50.5066 11.1999 50.7666 12.2399 50.7666H45.2599C46.2999 50.7666 47.3074 50.4741 48.1849 49.9866L39.2474 34.5166Z" fill="#2684FC"/>
|
||||
<path id="Vector_6" d="M48.0875 17.2917L39.8325 2.99168C39.3125 2.08168 38.565 1.36668 37.6875 0.84668L28.75 16.3167L39.2475 34.5167H57.09C57.09 33.5092 56.83 32.5017 56.31 31.5917L48.0875 17.2917Z" fill="#FFBA00"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
22
sites/vibes/public/images/logos/excel.svg
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
<svg width="66" height="62" viewBox="0 0 66 62" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="Group 2">
|
||||
<path id="Vector" d="M41.3139 29.4884L15.6162 24.9535V58.4617C15.6162 59.992 16.8567 61.2326 18.387 61.2326H62.7291C64.2594 61.2326 65.4999 59.992 65.4999 58.4617V46.1163L41.3139 29.4884Z" fill="#185C37"/>
|
||||
<path id="Vector_2" d="M41.3139 0.767456H18.387C16.8567 0.767456 15.6162 2.00798 15.6162 3.53826V15.8837L41.3139 31L54.9185 35.5349L65.4999 31V15.8837L41.3139 0.767456Z" fill="#21A366"/>
|
||||
<path id="Vector_3" d="M15.6162 15.8838H41.3139V31.0001H15.6162V15.8838Z" fill="#107C41"/>
|
||||
<path id="Vector_4" opacity="0.1" d="M34.0082 12.8605H15.6162V50.6512H34.0082C35.5364 50.6462 36.774 49.4086 36.779 47.8804V15.6313C36.774 14.1031 35.5364 12.8654 34.0082 12.8605Z" fill="black"/>
|
||||
<path id="Vector_5" opacity="0.2" d="M32.4966 14.3721H15.6162V52.1628H32.4966C34.0248 52.1578 35.2624 50.9202 35.2674 49.392V17.1429C35.2624 15.6147 34.0248 14.377 32.4966 14.3721Z" fill="black"/>
|
||||
<path id="Vector_6" opacity="0.2" d="M32.4966 14.3721H15.6162V49.1395H32.4966C34.0248 49.1345 35.2624 47.8969 35.2674 46.3687V17.1429C35.2624 15.6147 34.0248 14.377 32.4966 14.3721Z" fill="black"/>
|
||||
<path id="Vector_7" opacity="0.2" d="M30.9849 14.3721H15.6162V49.1395H30.9849C32.5131 49.1345 33.7508 47.8969 33.7557 46.3687V17.1429C33.7508 15.6147 32.5131 14.377 30.9849 14.3721Z" fill="black"/>
|
||||
<path id="Vector_8" d="M3.27081 14.3721H30.985C32.5153 14.3721 33.7558 15.6126 33.7558 17.1429V44.857C33.7558 46.3873 32.5153 47.6278 30.985 47.6278H3.27081C1.74053 47.6279 0.5 46.3873 0.5 44.8571V17.1429C0.5 15.6126 1.74053 14.3721 3.27081 14.3721Z" fill="url(#paint0_linear_54_3761)"/>
|
||||
<path id="Vector_9" d="M9.08154 40.0063L14.9104 30.9743L9.56981 21.9922H13.8658L16.7803 27.7364C17.0493 28.2821 17.2337 28.6872 17.3335 28.9547H17.3713C17.5628 28.5194 17.7643 28.0966 17.976 27.6865L21.0914 21.9952H25.0352L19.5586 30.9244L25.1743 40.0062H20.9781L17.6117 33.7013C17.4531 33.433 17.3185 33.1512 17.2096 32.8593H17.1597C17.0611 33.1453 16.9303 33.4192 16.7697 33.6756L13.3035 40.0063H9.08154Z" fill="white"/>
|
||||
<path id="Vector_10" d="M62.7292 0.767456H41.314V15.8837H65.5V3.53826C65.5 2.00798 64.2595 0.767456 62.7292 0.767456Z" fill="#33C481"/>
|
||||
<path id="Vector_11" d="M41.314 31H65.5V46.1163H41.314V31Z" fill="#107C41"/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_54_3761" x1="6.27721" y1="12.207" x2="27.9786" y2="49.7929" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#18884F"/>
|
||||
<stop offset="0.5" stop-color="#117E43"/>
|
||||
<stop offset="1" stop-color="#0B6631"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.5 KiB |
17
sites/vibes/public/images/logos/jira.svg
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<svg width="58" height="59" viewBox="0 0 58 59" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="Group 4">
|
||||
<path id="Vector" d="M56.3559 27.7959L31.1962 2.63629L28.7729 0.166626L9.81828 19.1058L1.15905 27.7959C0.725471 28.23 0.481934 28.8185 0.481934 29.4321C0.481934 30.0456 0.725471 30.6341 1.15905 31.0682L18.4621 48.3712L28.7729 58.6666L47.7121 39.7274L48.0053 39.4342L56.3559 31.0373C56.7801 30.6045 57.0177 30.0227 57.0177 29.4166C57.0177 28.8106 56.7801 28.2287 56.3559 27.7959ZM28.7729 38.045L20.1291 29.4012L28.7729 20.7574L37.4167 29.4012L28.7729 38.045Z" fill="#0082FF"/>
|
||||
<path id="Vector_2" d="M28.7728 20.7574C26.0542 18.0385 24.5218 14.3543 24.5102 10.5095C24.4987 6.66458 26.0089 2.97131 28.711 0.236084L9.80273 19.1444L20.0981 29.4398L28.7728 20.7574Z" fill="url(#paint0_linear_54_3798)"/>
|
||||
<path id="Vector_3" d="M37.4245 29.3781L28.7729 38.045C30.1301 39.3973 31.2069 41.0042 31.9417 42.7736C32.6765 44.5429 33.0547 46.4399 33.0547 48.3558C33.0547 50.2717 32.6765 52.1687 31.9417 53.9381C31.2069 55.7074 30.1301 57.3143 28.7729 58.6666L47.7353 39.7043L37.4245 29.3781Z" fill="url(#paint1_linear_54_3798)"/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_54_3798" x1="27.2138" y1="12.0133" x2="15.1434" y2="24.0837" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.18" stop-color="#0050D3"/>
|
||||
<stop offset="1" stop-color="#0082FF"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_54_3798" x1="30.4091" y1="46.6888" x2="42.4564" y2="34.6415" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.18" stop-color="#0050D3"/>
|
||||
<stop offset="1" stop-color="#0082FF"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
3
sites/vibes/public/images/logos/mercury.svg
Normal file
|
After Width: | Height: | Size: 12 KiB |
7
sites/vibes/public/images/logos/monday.svg
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<svg width="66" height="40" viewBox="0 0 66 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="Group 6">
|
||||
<path id="path841" d="M8.33572 39.8068C6.89885 39.8099 5.48689 39.4316 4.2441 38.7104C3.00131 37.9893 1.97223 36.9511 1.26199 35.702C0.562617 34.4618 0.213823 33.0546 0.252967 31.6313C0.292111 30.208 0.717718 28.8221 1.4842 27.6222L16.0514 4.74674C16.797 3.51732 17.8555 2.50782 19.1189 1.82137C20.3823 1.13491 21.8052 0.796092 23.2424 0.839523C24.6785 0.873937 26.0797 1.28928 27.3025 2.04305C28.5253 2.79682 29.526 3.86195 30.202 5.12944C31.5599 7.69722 31.3902 10.7866 29.7637 13.197L15.2058 36.0724C14.4675 37.2228 13.4508 38.1682 12.2499 38.821C11.0489 39.4738 9.70261 39.8129 8.33572 39.8068Z" fill="#F62B54"/>
|
||||
<path id="path843" d="M33.3225 39.8062C30.3843 39.8062 27.6808 38.2352 26.2611 35.7107C25.5633 34.4738 25.2153 33.0703 25.2544 31.6507C25.2936 30.2311 25.7184 28.849 26.4833 27.6524L41.0227 4.82943C41.7573 3.58272 42.8121 2.55524 44.0776 1.85356C45.3431 1.15188 46.7734 0.801539 48.2199 0.838884C51.1827 0.903696 53.8678 2.56102 55.2257 5.15349C56.5744 7.74596 56.3769 10.8538 54.7011 13.258L40.1647 36.081C39.4288 37.2265 38.4162 38.1682 37.2204 38.8193C36.0245 39.4703 34.684 39.8097 33.3225 39.8062Z" fill="#FFCC00"/>
|
||||
<path id="ellipse845" d="M57.7506 39.9975C61.8926 39.9975 65.2503 36.7102 65.2503 32.6552C65.2503 28.6002 61.8926 25.313 57.7506 25.313C53.6087 25.313 50.251 28.6002 50.251 32.6552C50.251 36.7102 53.6087 39.9975 57.7506 39.9975Z" fill="#00CA72"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
3
sites/vibes/public/images/logos/monzo.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<svg width="104" height="19" viewBox="0 0 104 19" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 18.52V0.479978H4.645V2.89798C5.10303 2.03361 5.79327 1.31428 6.638 0.820977C7.47083 0.335411 8.42591 0.100028 9.389 0.142978C10.3788 0.112533 11.3566 0.366947 12.206 0.875978C13.0674 1.39375 13.7679 2.14093 14.229 3.03398C16.049 1.04998 17.445 0.143977 19.623 0.143977C23.553 0.143977 25.763 2.42598 25.763 6.62798V18.52H20.759V8.74498C20.759 6.22498 20.24 5.08198 18.323 5.08198C16.374 5.08198 15.334 6.32498 15.334 8.74498V18.52H10.3V8.74498C10.3 6.22498 9.78 5.08198 7.863 5.08198C5.914 5.08198 4.874 6.32498 4.874 8.74498V18.52H0ZM31.688 2.82998C32.9487 1.50496 34.5785 0.589366 36.366 0.201978C38.142 -0.178284 39.9918 -0.00733316 41.668 0.691978C43.364 1.40339 44.8102 2.60285 45.823 4.13798C46.8539 5.6967 47.4161 7.51838 47.443 9.38698C47.4771 10.6423 47.2612 11.8918 46.808 13.063C46.3595 14.2241 45.6805 15.2824 44.812 16.174C43.9583 17.0822 42.926 17.804 41.78 18.294C40.6442 18.7787 39.4188 19.0183 38.184 18.997C36.9467 19.0314 35.7172 18.7909 34.584 18.293C33.4394 17.7893 32.4179 17.0431 31.59 16.106C29.922 14.325 29 11.934 29.023 9.45298C29.0178 8.22325 29.2508 7.00416 29.709 5.86298C30.1628 4.73165 30.8353 3.70097 31.688 2.82998ZM38.218 13.953C39.077 13.991 39.928 13.761 40.66 13.294C41.3983 12.8204 41.979 12.1377 42.328 11.333C42.5933 10.7241 42.7233 10.0649 42.709 9.40086C42.6946 8.73684 42.5363 8.08385 42.245 7.48698C41.9571 6.89687 41.5414 6.3783 41.028 5.96898C40.5221 5.565 39.9302 5.28242 39.298 5.14298C38.4561 4.95792 37.5776 5.03791 36.783 5.37198C35.9793 5.71146 35.2963 6.28501 34.823 7.01798C34.3418 7.76269 34.0878 8.63135 34.092 9.51798C34.082 10.09 34.18 10.658 34.382 11.19C34.582 11.722 34.884 12.208 35.267 12.62C35.65 13.032 36.107 13.362 36.614 13.59C37.1182 13.8184 37.6646 13.9419 38.218 13.953ZM50.706 18.52V0.479978H55.578L55.514 2.89898C56.0148 2.01342 56.751 1.28381 57.641 0.790978C58.5186 0.306119 59.5163 0.0816692 60.517 0.143977C64.123 0.143977 66.461 2.49498 66.461 6.62798V18.52H61.329V8.74498C61.329 6.22498 60.809 5.08198 58.827 5.08198C56.813 5.08198 55.741 6.32498 55.741 8.74498V18.52H50.706ZM88.242 2.82798C89.5026 1.50195 91.1329 0.585618 92.921 0.197978C94.6976 -0.182702 96.5482 -0.0117452 98.225 0.687978C99.9211 1.39913 101.367 2.59865 102.38 4.13398C103.41 5.69314 103.972 7.51528 103.997 9.38398C104.03 10.6392 103.815 11.8887 103.362 13.06C102.913 14.221 102.234 15.2793 101.366 16.171C100.512 17.0792 99.4793 17.8009 98.333 18.291C97.1974 18.7753 95.9724 19.0148 94.738 18.994C93.5004 19.0284 92.2705 18.788 91.137 18.29C89.9927 17.786 88.9712 17.0398 88.143 16.103C86.475 14.322 85.553 11.932 85.576 9.45098C85.5709 8.22126 85.8039 7.00219 86.262 5.86098C86.7153 4.72966 87.3875 3.69896 88.24 2.82798H88.242ZM94.772 13.951C95.6348 13.9875 96.4879 13.757 97.215 13.291C97.9533 12.8173 98.5339 12.1347 98.883 11.33C99.2366 10.515 99.3467 9.61514 99.2 8.73898C99.0561 7.86732 98.6593 7.05713 98.059 6.40898C97.4696 5.77104 96.6997 5.32801 95.852 5.13898C95.01 4.95384 94.1314 5.03419 93.337 5.36898C92.5335 5.7089 91.8509 6.28279 91.378 7.01598C90.8971 7.76083 90.6432 8.6294 90.647 9.51598C90.637 10.088 90.735 10.656 90.937 11.188C91.138 11.72 91.439 12.206 91.822 12.618C92.205 13.03 92.663 13.359 93.169 13.588C93.6732 13.8163 94.2186 13.9398 94.772 13.951ZM70.202 18.521V14.757L77.382 5.14398H70.202V0.477979H83.164V4.74598L76.829 13.852H83.294V18.519H70.203L70.202 18.521Z" fill="#7D7D7D"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.4 KiB |
6
sites/vibes/public/images/logos/notion.svg
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<svg width="63" height="66" viewBox="0 0 63 66" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="Group 8">
|
||||
<path id="Vector" d="M4.41105 3.30349L40.3775 0.646943C44.7955 0.267993 45.9304 0.523443 48.7085 2.54299L60.1895 10.6309C62.0829 12.0219 62.7134 12.4009 62.7134 13.9154V58.2733C62.7134 61.0534 61.704 62.6979 58.1699 62.9488L16.4035 65.4786C13.7515 65.604 12.4886 65.2251 11.0995 63.4551L2.645 52.461C1.12855 50.4376 0.5 48.9231 0.5 47.1525V7.72349C0.5 5.45044 1.50945 3.55504 4.41105 3.30349Z" fill="white"/>
|
||||
<path id="Vector_2" fill-rule="evenodd" clip-rule="evenodd" d="M40.3775 0.647593L4.41105 3.30414C1.50945 3.55504 0.5 5.45109 0.5 7.72349V47.1525C0.5 48.9224 1.12855 50.4369 2.645 52.461L11.0995 63.4545C12.4886 65.2244 13.7515 65.604 16.4035 65.4779L58.1706 62.9494C61.702 62.6979 62.7141 61.0534 62.7141 58.274V13.916C62.7141 12.4795 62.1466 12.0655 60.4761 10.8396L48.7085 2.54299C45.9311 0.523443 44.7955 0.267993 40.3775 0.646943V0.647593ZM17.348 13.19C13.9374 13.4194 13.1639 13.4714 11.2269 11.8965L6.30255 7.97959C5.80205 7.47259 6.0536 6.84014 7.3146 6.71404L41.89 4.18749C44.7936 3.93399 46.3055 4.94604 47.441 5.83004L53.371 10.1265C53.6245 10.2546 54.255 11.0105 53.4964 11.0105L17.79 13.1601L17.348 13.19ZM13.3719 57.895V20.2386C13.3719 18.5941 13.877 17.8355 15.3889 17.7081L56.4 15.307C57.791 15.1816 58.4195 16.0656 58.4195 17.7075V55.113C58.4195 56.7575 58.166 58.1485 55.8956 58.274L16.6505 60.549C14.3801 60.6744 13.3726 59.9185 13.3726 57.895H13.3719ZM52.1119 22.2575C52.3635 23.395 52.1119 24.5325 50.9744 24.6625L49.0829 25.0375V52.84C47.4404 53.724 45.9285 54.229 44.6649 54.229C42.6453 54.229 42.1409 53.5966 40.6284 51.7025L28.2589 32.2415V51.07L32.1719 51.956C32.1719 51.956 32.1719 54.231 29.0148 54.231L20.3113 54.736C20.0578 54.229 20.3113 52.9661 21.1934 52.7145L23.4664 52.084V27.189L20.312 26.9336C20.0585 25.7961 20.689 24.1535 22.457 24.0261L31.7955 23.3976L44.6655 43.1101V25.6706L41.385 25.2936C41.1315 23.9007 42.1409 22.8886 43.4019 22.7651L52.1119 22.2575Z" fill="black"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2 KiB |
3
sites/vibes/public/images/logos/perplexity.svg
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
3
sites/vibes/public/images/logos/ramp.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<svg width="105" height="28" viewBox="0 0 105 28" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7.249 9.464C4.75 9.464 3.524 11.671 3.524 14.618V22.003H0V6.419H3.462V10.462H3.522C4.262 7.976 5.735 5.967 8.007 5.967C9.606 5.967 10.28 6.527 10.28 6.527L8.69 9.737C8.69 9.737 8.183 9.464 7.249 9.464ZM49.845 11.604V22H46.413V12.87C46.413 10.248 45.593 8.86 43.497 8.86C41.327 8.86 40.277 10.616 40.277 13.984V22H36.875V12.869C36.875 10.349 36.065 8.859 33.99 8.859C31.622 8.859 30.71 10.939 30.71 13.983V22H27.248V6.419H30.71V9.948H30.741C31.281 7.505 32.752 5.998 35.236 5.998C37.699 5.998 39.304 7.325 39.913 9.677C40.493 7.413 42.036 5.997 44.408 5.997C47.718 5.997 49.846 8.077 49.846 11.603L49.845 11.604ZM17.205 5.967C14.02 5.967 11.935 7.467 10.985 10.173L13.917 11.242C14.452 9.609 15.567 8.679 17.267 8.679C19.181 8.679 20.305 9.524 20.305 10.819C20.305 12.145 19.41 12.422 17.389 12.749C15.141 13.112 9.794 13.231 9.794 17.752C9.794 20.4 12.004 22.393 15.323 22.393C17.818 22.393 19.517 21.371 20.303 19.469H20.334V22H23.767V12.416C23.765 8.223 21.66 5.967 17.205 5.967ZM20.395 14.667C20.395 17.934 18.781 20.033 16.203 20.033C14.38 20.033 13.287 19.007 13.287 17.529C13.287 16.143 14.411 15.18 16.567 14.784C18.773 14.378 19.884 13.876 20.394 12.674V14.666L20.395 14.667ZM62.056 5.997C59.43 5.997 57.696 7.444 56.953 9.617V6.419H53.307V28H56.922V18.804H56.952C57.757 21.158 59.432 22.453 62.056 22.453C66.216 22.453 69.194 19.017 69.194 14.163C69.192 9.343 66.216 5.997 62.056 5.997ZM61.159 19.56C58.277 19.56 56.679 17.465 56.679 14.21C56.679 10.955 58.467 8.86 61.159 8.86C63.849 8.86 65.639 11.057 65.639 14.21C65.639 17.365 63.851 19.56 61.159 19.56ZM105 21.93V22.028L90.892 22.033V21.931C92.927 20.779 94.331 19.606 95.594 18.381H101.387L105 21.93ZM101.504 3.516L97.928 0H97.825C97.825 0 97.885 6.552 91.881 12.51C86.006 18.342 79.097 18.356 79.097 18.356V18.458L82.74 22.035C82.74 22.035 89.548 22.102 95.562 16.189C101.555 10.297 101.504 3.516 101.504 3.516Z" fill="#7D7D7D"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2 KiB |
4
sites/vibes/public/images/logos/raycast.svg
Normal file
|
After Width: | Height: | Size: 6.2 KiB |
3
sites/vibes/public/images/logos/retool.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<svg width="114" height="22" viewBox="0 0 114 22" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M48.54 21.539H44.385L35.306 12.308H33.614V21.538H30.536V0H40.846C45.186 0 47.771 2.308 47.771 6.154C47.771 10 45.186 12.308 40.846 12.308H39.307L48.54 21.538V21.539ZM44.693 6.154C44.693 3.969 43.463 2.615 40.846 2.615H33.614V9.692H40.846C43.462 9.692 44.693 8.338 44.693 6.154ZM56.42 22C51.496 22 48.418 18.339 48.418 13.538C48.418 8.738 51.496 5.077 56.266 5.077C60.113 5.077 62.422 7.231 63.437 10.431C63.807 11.631 63.96 12.954 63.96 14.308V14.615H51.373C51.465 17.2 53.373 19.692 56.42 19.692C59.036 19.692 60.237 18.185 60.483 17.077H63.653C62.914 19.692 60.452 22 56.42 22ZM51.373 12.308H61.006C60.852 9.538 59.375 7.385 56.266 7.385C53.158 7.385 51.466 9.692 51.373 12.308ZM64.113 8V5.54H67.96V1.847H70.914V5.54H75.069V8H70.914V17.693C70.914 18.77 71.222 19.139 72.299 19.139H75.069V21.539H71.684C68.883 21.539 67.96 20.154 67.96 17.693V8H64.113ZM83.563 5.077C88.488 5.077 91.719 8.738 91.719 13.538C91.719 18.338 88.488 22 83.563 22C78.639 22 75.408 18.339 75.408 13.538C75.408 8.738 78.639 5.077 83.563 5.077ZM83.563 7.538C80.209 7.538 78.362 10.308 78.362 13.538C78.362 16.769 80.209 19.538 83.563 19.538C86.918 19.538 88.765 16.769 88.765 13.538C88.765 10.308 86.918 7.538 83.563 7.538ZM101.18 5.077C106.105 5.077 109.336 8.738 109.336 13.538C109.336 18.338 106.105 22 101.18 22C96.256 22 93.025 18.339 93.025 13.538C93.025 8.738 96.256 5.077 101.18 5.077ZM101.18 7.538C97.826 7.538 95.979 10.308 95.979 13.538C95.979 16.769 97.826 19.538 101.18 19.538C104.535 19.538 106.382 16.769 106.382 13.538C106.382 10.308 104.535 7.538 101.18 7.538ZM114 0V21.539H111.045V0H114ZM13.534 1.01C13.534 0.452 13.08 0 12.519 0H1.015C0.454999 0 0 0.452 0 1.01V5.72C0 6.278 0.454999 6.73 1.015 6.73H12.519C13.079 6.73 13.534 7.182 13.534 7.74V9.086C13.534 9.644 13.08 10.096 12.519 10.096H5.075C4.515 10.096 4.06 10.548 4.06 11.106V15.816C4.06 16.374 4.515 16.826 5.075 16.826H12.519C13.079 16.826 13.534 17.278 13.534 17.836V20.528C13.534 21.085 13.988 21.538 14.549 21.538H19.286C19.846 21.538 20.301 21.085 20.301 20.528V15.816C20.301 15.259 19.846 14.806 19.286 14.806H14.549C13.989 14.806 13.534 14.355 13.534 13.797V12.451C13.534 11.894 13.988 11.441 14.549 11.441H19.286C19.846 11.441 20.301 10.99 20.301 10.432V5.721C20.301 5.163 19.846 4.711 19.286 4.711H14.549C13.989 4.711 13.534 4.259 13.534 3.701V1.01Z" fill="#7D7D7D"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
3
sites/vibes/public/images/logos/watershed.svg
Normal file
|
After Width: | Height: | Size: 6.1 KiB |
21
sites/vibes/public/images/logos/word.svg
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<svg width="64" height="62" viewBox="0 0 64 62" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="Group 1">
|
||||
<path id="Vector" d="M62.7291 0.767456H18.387C16.8567 0.767456 15.6162 2.00799 15.6162 3.53828V15.8837L41.3139 23.4419L65.4999 15.8837V3.53828C65.4999 2.00799 64.2594 0.767456 62.7291 0.767456Z" fill="#41A5EE"/>
|
||||
<path id="Vector_2" d="M65.4999 15.8838H15.6162V31.0001L41.3139 35.5349L65.4999 31.0001V15.8838Z" fill="#2B7CD3"/>
|
||||
<path id="Vector_3" d="M15.6162 31V46.1163L39.8023 49.1395L65.4999 46.1163V31H15.6162Z" fill="#185ABD"/>
|
||||
<path id="Vector_4" d="M18.387 61.2326H62.7291C64.2594 61.2326 65.5 59.9921 65.5 58.4618V46.1163H15.6162V58.4618C15.6162 59.9921 16.8567 61.2326 18.387 61.2326Z" fill="#103F91"/>
|
||||
<path id="Vector_5" opacity="0.1" d="M34.0082 12.8605H15.6162V50.6512H34.0082C35.5364 50.6462 36.774 49.4086 36.779 47.8803V15.6313C36.774 14.1031 35.5364 12.8654 34.0082 12.8605Z" fill="black"/>
|
||||
<path id="Vector_6" opacity="0.2" d="M32.4965 14.3721H15.6162V52.1628H32.4965C34.0248 52.1578 35.2624 50.9202 35.2674 49.3919V17.1429C35.2624 15.6147 34.0248 14.377 32.4965 14.3721Z" fill="black"/>
|
||||
<path id="Vector_7" opacity="0.2" d="M32.4965 14.3721H15.6162V49.1395H32.4965C34.0248 49.1345 35.2624 47.8969 35.2674 46.3687V17.1429C35.2624 15.6147 34.0248 14.377 32.4965 14.3721Z" fill="black"/>
|
||||
<path id="Vector_8" opacity="0.2" d="M30.9849 14.3721H15.6162V49.1395H30.9849C32.5131 49.1345 33.7508 47.8969 33.7557 46.3687V17.1429C33.7508 15.6147 32.5131 14.377 30.9849 14.3721Z" fill="black"/>
|
||||
<path id="Vector_9" d="M3.27082 14.3721H30.985C32.5153 14.3721 33.7558 15.6126 33.7558 17.1429V44.8571C33.7558 46.3874 32.5153 47.6279 30.985 47.6279H3.27082C1.74054 47.6279 0.5 46.3874 0.5 44.8571V17.1429C0.5 15.6126 1.74054 14.3721 3.27082 14.3721Z" fill="url(#paint0_linear_54_3776)"/>
|
||||
<path id="Vector_10" d="M11.8704 35.6226C11.9248 36.0503 11.9611 36.4222 11.9777 36.7412H12.0412C12.0654 36.4388 12.1157 36.0745 12.1923 35.6483C12.2689 35.222 12.338 34.8617 12.3994 34.5675L15.3139 21.9937H19.0823L22.1056 34.3785C22.2813 35.1486 22.407 35.9292 22.482 36.7154H22.5319C22.5883 35.9537 22.6933 35.1964 22.8463 34.448L25.2574 21.9877H28.6872L24.4517 40.0063H20.4443L17.5722 28.0735C17.4891 27.7303 17.3939 27.2814 17.2896 26.7296C17.1853 26.1779 17.1203 25.7743 17.0946 25.5203H17.0447C17.0114 25.8136 16.9464 26.2489 16.8497 26.8264C16.7529 27.4038 16.6753 27.8311 16.6169 28.1082L13.9171 40.0032H9.84173L5.5835 21.9937H9.07534L11.701 34.5931C11.76 34.8516 11.816 35.1963 11.8704 35.6226Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_54_3776" x1="6.27721" y1="12.207" x2="27.9786" y2="49.7929" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#2368C4"/>
|
||||
<stop offset="0.5" stop-color="#1A5DBE"/>
|
||||
<stop offset="1" stop-color="#1146AC"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.7 KiB |
BIN
sites/vibes/public/og-image.jpg
Normal file
|
After Width: | Height: | Size: 90 KiB |
75
sites/vibes/src/blog-sites.js
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
export const BLOG_SITES = [
|
||||
{
|
||||
key: "research",
|
||||
title: "Research",
|
||||
description:
|
||||
"Notes from papers I am reading, and mini-surveys of themes I'm learning about.",
|
||||
url: "https://research.neeldhara.blog",
|
||||
},
|
||||
{
|
||||
key: "vibes",
|
||||
title: "Vibes",
|
||||
description:
|
||||
"Conversations with LLMs with an intent to create something meaningful.",
|
||||
url: "https://vibes.neeldhara.blog",
|
||||
},
|
||||
{
|
||||
key: "puzzles",
|
||||
title: "Puzzles",
|
||||
description:
|
||||
"Problems, puzzles, contest editorials, and mathematical magic.",
|
||||
url: "https://puzzles.neeldhara.blog",
|
||||
},
|
||||
{
|
||||
key: "reflections",
|
||||
title: "Reflections",
|
||||
description: "Random musings, pseudo-advice, how-tos. Core dumped.",
|
||||
url: "https://reflections.neeldhara.blog",
|
||||
},
|
||||
{
|
||||
key: "poetry",
|
||||
title: "Poetry",
|
||||
description: "Failed attempts at organizing thoughts in verse.",
|
||||
url: "https://poetry.neeldhara.blog",
|
||||
},
|
||||
{
|
||||
key: "reviews",
|
||||
title: "Reviews",
|
||||
description:
|
||||
"Reviews of things I use or consume: software, books, hardware, etc.",
|
||||
url: "https://reviews.neeldhara.blog",
|
||||
},
|
||||
{
|
||||
key: "art",
|
||||
title: "Art",
|
||||
description:
|
||||
"This exists so I can stay accountable to my doodling practice :)",
|
||||
url: "https://art.neeldhara.blog",
|
||||
},
|
||||
];
|
||||
|
||||
export const DEFAULT_BLOG_SITE_KEY = "vibes";
|
||||
export const BLOG_SITE_KEYS = BLOG_SITES.map((site) => site.key);
|
||||
|
||||
function getEnvironmentBlogSiteKey() {
|
||||
if (typeof process === "undefined") {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return process.env?.BLOG_SITE;
|
||||
}
|
||||
|
||||
export function getBlogSite(key = getEnvironmentBlogSiteKey()) {
|
||||
const normalizedKey = key || DEFAULT_BLOG_SITE_KEY;
|
||||
const site = BLOG_SITES.find((candidate) => candidate.key === normalizedKey);
|
||||
|
||||
if (!site) {
|
||||
throw new Error(
|
||||
`Unknown BLOG_SITE "${normalizedKey}". Expected one of: ${BLOG_SITE_KEYS.join(", ")}`,
|
||||
);
|
||||
}
|
||||
|
||||
return site;
|
||||
}
|
||||
|
||||
export const ACTIVE_BLOG_SITE = getBlogSite();
|
||||
107
sites/vibes/src/components/BaseHead.astro
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
---
|
||||
// Import the global.css file here so that it is included on
|
||||
// all pages through the use of the <BaseHead /> component.
|
||||
import '../styles/global.css';
|
||||
import { SITE_TITLE, SITE_METADATA } from '../consts';
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
description: string;
|
||||
image?: string;
|
||||
}
|
||||
|
||||
const canonicalURL = new URL(Astro.url.pathname, Astro.site);
|
||||
|
||||
const { title, description, image } = Astro.props;
|
||||
const finalTitle = title || SITE_METADATA.title.default;
|
||||
const finalDescription = description || SITE_METADATA.description;
|
||||
const finalImage = image || SITE_METADATA.openGraph.images[0].url;
|
||||
const imageURL = new URL(finalImage, Astro.url);
|
||||
---
|
||||
|
||||
<!-- Global Metadata -->
|
||||
<meta charset="utf-8" />
|
||||
|
||||
<!-- Preconnect to Google Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
|
||||
<!-- Load Google Fonts -->
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500;600&family=Hubot+Sans:wght@400;500;600;700&family=Mona+Sans:wght@400;500;600;700&display=swap" rel="stylesheet" />
|
||||
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<meta name="robots" content={`${SITE_METADATA.robots.index ? 'index' : 'noindex'}, ${SITE_METADATA.robots.follow ? 'follow' : 'nofollow'}`} />
|
||||
<meta name="keywords" content={SITE_METADATA.keywords.join(', ')} />
|
||||
<meta name="author" content={SITE_METADATA.authors[0].name} />
|
||||
<meta name="creator" content={SITE_METADATA.creator} />
|
||||
<meta name="publisher" content={SITE_METADATA.publisher} />
|
||||
|
||||
<!-- Theme Script -->
|
||||
<script is:inline>
|
||||
// Get theme from localStorage or system preference
|
||||
const savedTheme = localStorage.getItem('theme');
|
||||
const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
||||
const initialTheme = savedTheme || systemTheme;
|
||||
|
||||
// Apply theme immediately to prevent flashing
|
||||
document.documentElement.classList.toggle('dark', initialTheme === 'dark');
|
||||
</script>
|
||||
|
||||
<!-- Favicon -->
|
||||
{SITE_METADATA.icons.icon.map((icon) => (
|
||||
<link
|
||||
rel="icon"
|
||||
type={icon.type}
|
||||
sizes={icon.sizes}
|
||||
href={icon.url}
|
||||
/>
|
||||
))}
|
||||
{SITE_METADATA.icons.apple.map((icon) => (
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes={icon.sizes}
|
||||
href={icon.url}
|
||||
/>
|
||||
))}
|
||||
{SITE_METADATA.icons.shortcut.map((icon) => (
|
||||
<link
|
||||
rel="shortcut icon"
|
||||
href={icon.url}
|
||||
/>
|
||||
))}
|
||||
|
||||
<link rel="sitemap" href="/sitemap-index.xml" />
|
||||
<link
|
||||
rel="alternate"
|
||||
type="application/rss+xml"
|
||||
title={SITE_TITLE}
|
||||
href={new URL('rss.xml', Astro.site)}
|
||||
/>
|
||||
<meta name="generator" content={Astro.generator} />
|
||||
|
||||
<!-- Canonical URL -->
|
||||
<link rel="canonical" href={canonicalURL} />
|
||||
|
||||
<!-- Primary Meta Tags -->
|
||||
<title>{finalTitle}</title>
|
||||
<meta name="title" content={finalTitle} />
|
||||
<meta name="description" content={finalDescription} />
|
||||
|
||||
<!-- Open Graph / Facebook -->
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:url" content={Astro.url} />
|
||||
<meta property="og:site_name" content={SITE_METADATA.openGraph.siteName} />
|
||||
<meta property="og:title" content={finalTitle} />
|
||||
<meta property="og:description" content={finalDescription} />
|
||||
<meta property="og:image" content={imageURL} />
|
||||
<meta property="og:image:width" content={SITE_METADATA.openGraph.images[0].width.toString()} />
|
||||
<meta property="og:image:height" content={SITE_METADATA.openGraph.images[0].height.toString()} />
|
||||
<meta property="og:image:alt" content={SITE_METADATA.openGraph.images[0].alt} />
|
||||
|
||||
<!-- Twitter -->
|
||||
<meta property="twitter:card" content={SITE_METADATA.twitter.card} />
|
||||
<meta property="twitter:url" content={Astro.url} />
|
||||
<meta property="twitter:title" content={finalTitle} />
|
||||
<meta property="twitter:description" content={finalDescription} />
|
||||
<meta property="twitter:image" content={imageURL} />
|
||||
<meta property="twitter:creator" content={SITE_METADATA.twitter.creator} />
|
||||
22
sites/vibes/src/components/elements/header-link.tsx
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import { type ComponentProps } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type HeaderLinkProps = ComponentProps<"a">;
|
||||
|
||||
export default function HeaderLink({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: HeaderLinkProps) {
|
||||
return (
|
||||
<a
|
||||
{...props}
|
||||
className={cn(
|
||||
"inline-block text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-200",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
44
sites/vibes/src/components/elements/theme-toggle.tsx
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import { Moon, Sun } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const ThemeToggle = ({ className }: { className?: string }) => {
|
||||
const [theme, setTheme] = useState<"light" | "dark">("light");
|
||||
|
||||
useEffect(() => {
|
||||
// Get initial theme from localStorage or system preference
|
||||
const savedTheme = localStorage.getItem("theme");
|
||||
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
|
||||
.matches
|
||||
? "dark"
|
||||
: "light";
|
||||
const initialTheme = savedTheme || systemTheme;
|
||||
|
||||
setTheme(initialTheme as "light" | "dark");
|
||||
document.documentElement.classList.toggle("dark", initialTheme === "dark");
|
||||
}, []);
|
||||
|
||||
const toggleTheme = () => {
|
||||
const newTheme = theme === "light" ? "dark" : "light";
|
||||
setTheme(newTheme);
|
||||
document.documentElement.classList.toggle("dark");
|
||||
localStorage.setItem("theme", newTheme);
|
||||
};
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className={cn("size-8", className)}
|
||||
onClick={toggleTheme}
|
||||
>
|
||||
<Sun className="size-4 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
|
||||
<Moon className="absolute size-4 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
|
||||
<span className="sr-only">Toggle theme</span>
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export { ThemeToggle };
|
||||
46
sites/vibes/src/components/icons/plus-signs.tsx
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
import { type SVGProps, useId } from "react";
|
||||
|
||||
interface PlusSignsProps extends SVGProps<SVGSVGElement> {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const PlusSigns = ({ className, ...props }: PlusSignsProps) => {
|
||||
const GAP = 16;
|
||||
const STROKE_WIDTH = 1;
|
||||
const PLUS_SIZE = 6;
|
||||
const id = useId();
|
||||
const patternId = `plus-pattern-${id}`;
|
||||
|
||||
return (
|
||||
<svg width={GAP * 2} height={GAP * 2} className={className} {...props}>
|
||||
<defs>
|
||||
<pattern
|
||||
id={patternId}
|
||||
x="0"
|
||||
y="0"
|
||||
width={GAP}
|
||||
height={GAP}
|
||||
patternUnits="userSpaceOnUse"
|
||||
>
|
||||
<line
|
||||
x1={GAP / 2}
|
||||
y1={(GAP - PLUS_SIZE) / 2}
|
||||
x2={GAP / 2}
|
||||
y2={(GAP + PLUS_SIZE) / 2}
|
||||
stroke="currentColor"
|
||||
strokeWidth={STROKE_WIDTH}
|
||||
/>
|
||||
<line
|
||||
x1={(GAP - PLUS_SIZE) / 2}
|
||||
y1={GAP / 2}
|
||||
x2={(GAP + PLUS_SIZE) / 2}
|
||||
y2={GAP / 2}
|
||||
stroke="currentColor"
|
||||
strokeWidth={STROKE_WIDTH}
|
||||
/>
|
||||
</pattern>
|
||||
</defs>
|
||||
<rect width="100%" height="100%" fill={`url(#${patternId})`} />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
229
sites/vibes/src/components/problems/PermutationExplorer.tsx
Normal file
|
|
@ -0,0 +1,229 @@
|
|||
import React, { useState, useMemo } from 'react';
|
||||
|
||||
interface Cycle {
|
||||
elements: number[];
|
||||
length: number;
|
||||
}
|
||||
|
||||
interface PermutationData {
|
||||
permutation: number[];
|
||||
cycles: Cycle[];
|
||||
hasLongCycle: boolean;
|
||||
id: string;
|
||||
}
|
||||
|
||||
// Generate all permutations of [1, 2, ..., n]
|
||||
function generatePermutations(n: number): number[][] {
|
||||
if (n === 1) return [[1]];
|
||||
|
||||
const result: number[][] = [];
|
||||
const smaller = generatePermutations(n - 1);
|
||||
|
||||
for (const perm of smaller) {
|
||||
for (let i = 0; i <= perm.length; i++) {
|
||||
const newPerm = [...perm.slice(0, i), n, ...perm.slice(i)];
|
||||
result.push(newPerm);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Find cycles in a permutation
|
||||
function findCycles(permutation: number[]): Cycle[] {
|
||||
const n = permutation.length;
|
||||
const visited = new Array(n).fill(false);
|
||||
const cycles: Cycle[] = [];
|
||||
|
||||
for (let i = 0; i < n; i++) {
|
||||
if (!visited[i]) {
|
||||
const cycle: number[] = [];
|
||||
let current = i;
|
||||
|
||||
while (!visited[current]) {
|
||||
visited[current] = true;
|
||||
cycle.push(current + 1); // Convert to 1-indexed
|
||||
current = permutation[current] - 1; // Convert back to 0-indexed
|
||||
}
|
||||
|
||||
cycles.push({
|
||||
elements: cycle,
|
||||
length: cycle.length,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return cycles.sort((a, b) => b.length - a.length);
|
||||
}
|
||||
|
||||
const PermutationExplorer: React.FC = () => {
|
||||
const [n, setN] = useState(3);
|
||||
const [filter, setFilter] = useState<'all' | 'long' | 'short'>('all');
|
||||
|
||||
const allPermutations = useMemo(() => {
|
||||
const perms = generatePermutations(n);
|
||||
return perms.map((perm, idx) => {
|
||||
const cycles = findCycles(perm);
|
||||
const hasLongCycle = cycles.some(c => c.length > n / 2);
|
||||
|
||||
return {
|
||||
permutation: perm,
|
||||
cycles,
|
||||
hasLongCycle,
|
||||
id: `perm-${n}-${idx}`,
|
||||
};
|
||||
});
|
||||
}, [n]);
|
||||
|
||||
const filteredPermutations = useMemo(() => {
|
||||
if (filter === 'all') return allPermutations;
|
||||
if (filter === 'long') return allPermutations.filter(p => p.hasLongCycle);
|
||||
return allPermutations.filter(p => !p.hasLongCycle);
|
||||
}, [allPermutations, filter]);
|
||||
|
||||
const stats = useMemo(() => {
|
||||
const withLong = allPermutations.filter(p => p.hasLongCycle).length;
|
||||
const withoutLong = allPermutations.length - withLong;
|
||||
return { total: allPermutations.length, withLong, withoutLong };
|
||||
}, [allPermutations]);
|
||||
|
||||
const formatCycleNotation = (cycles: Cycle[]) => {
|
||||
return cycles.map(cycle => `(${cycle.elements.join(' ')})`).join(' ');
|
||||
};
|
||||
|
||||
const formatStandardNotation = (perm: number[]) => {
|
||||
return `[${perm.join(', ')}]`;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="my-8 p-6 border-2 border-gray-300 rounded-lg bg-white">
|
||||
<h3 className="text-xl font-semibold mb-4">Permutation Cycle Explorer</h3>
|
||||
|
||||
<div className="mb-6 space-y-4">
|
||||
<div>
|
||||
<label className="block text-md font-semibold mb-2">
|
||||
Size (n): {n}
|
||||
</label>
|
||||
<div className="flex items-center space-x-4">
|
||||
{[3, 4, 5].map(size => (
|
||||
<button
|
||||
key={size}
|
||||
onClick={() => setN(size)}
|
||||
className={`px-4 py-2 rounded font-semibold transition-colors ${
|
||||
n === size
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
|
||||
}`}
|
||||
>
|
||||
{size}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-md font-semibold mb-2">
|
||||
Filter Permutations:
|
||||
</label>
|
||||
<div className="flex items-center space-x-4">
|
||||
<button
|
||||
onClick={() => setFilter('all')}
|
||||
className={`px-4 py-2 rounded font-semibold transition-colors ${
|
||||
filter === 'all'
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
|
||||
}`}
|
||||
>
|
||||
All ({stats.total})
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setFilter('long')}
|
||||
className={`px-4 py-2 rounded font-semibold transition-colors ${
|
||||
filter === 'long'
|
||||
? 'bg-red-600 text-white'
|
||||
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
|
||||
}`}
|
||||
>
|
||||
With Long Cycle ({stats.withLong})
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setFilter('short')}
|
||||
className={`px-4 py-2 rounded font-semibold transition-colors ${
|
||||
filter === 'short'
|
||||
? 'bg-green-600 text-white'
|
||||
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
|
||||
}`}
|
||||
>
|
||||
Without Long Cycle ({stats.withoutLong})
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 p-4 bg-blue-50 rounded">
|
||||
<p className="text-sm">
|
||||
<strong>Note:</strong> A "long cycle" is one with length > n/2. For n={n},
|
||||
that means cycles longer than {Math.floor(n / 2)}.
|
||||
There are <strong>{stats.withLong}</strong> permutations with at least one long cycle
|
||||
(about {((stats.withLong / stats.total) * 100).toFixed(1)}%).
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3 max-h-96 overflow-y-auto">
|
||||
{filteredPermutations.map((perm) => (
|
||||
<div
|
||||
key={perm.id}
|
||||
className={`p-3 rounded border-2 ${
|
||||
perm.hasLongCycle
|
||||
? 'bg-red-50 border-red-300'
|
||||
: 'bg-green-50 border-green-300'
|
||||
}`}
|
||||
>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-2 text-sm">
|
||||
<div>
|
||||
<span className="font-semibold">Standard:</span>{' '}
|
||||
<code className="bg-white px-2 py-1 rounded">
|
||||
{formatStandardNotation(perm.permutation)}
|
||||
</code>
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-semibold">Cycles:</span>{' '}
|
||||
<code className="bg-white px-2 py-1 rounded">
|
||||
{formatCycleNotation(perm.cycles)}
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2 text-xs text-gray-600">
|
||||
Cycle lengths: {perm.cycles.map(c => c.length).join(', ')}
|
||||
{perm.hasLongCycle && (
|
||||
<span className="ml-2 text-red-700 font-semibold">
|
||||
⚠ Has long cycle!
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-6 p-4 bg-gray-100 rounded">
|
||||
<h4 className="font-semibold mb-2">Understanding the Notation:</h4>
|
||||
<ul className="text-sm space-y-1 list-disc list-inside">
|
||||
<li>
|
||||
<strong>Standard notation [a, b, c, ...]:</strong> Position i contains value a[i].
|
||||
For example, [2,3,1] means box 1→2, box 2→3, box 3→1.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Cycle notation (a b c ...):</strong> Shows the path a→b→c→...→a.
|
||||
For example, (1 2 3) means 1→2→3→1.
|
||||
</li>
|
||||
<li>
|
||||
Permutations with long cycles (shown in red) would cause the loop strategy to fail
|
||||
for at least one prisoner.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PermutationExplorer;
|
||||
222
sites/vibes/src/components/problems/PrisonerGameSimulator.tsx
Normal file
|
|
@ -0,0 +1,222 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
interface GameState {
|
||||
n: number;
|
||||
boxes: number[];
|
||||
currentPrisoner: number;
|
||||
openedBoxes: number[];
|
||||
foundNumber: boolean;
|
||||
gameOver: boolean;
|
||||
won: boolean;
|
||||
prisonersCompleted: number;
|
||||
attemptsLeft: number;
|
||||
}
|
||||
|
||||
const PrisonerGameSimulator: React.FC = () => {
|
||||
const [n, setN] = useState(10);
|
||||
const [gameState, setGameState] = useState<GameState | null>(null);
|
||||
|
||||
const initializeGame = (numPrisoners: number) => {
|
||||
// Create a random permutation
|
||||
const boxes = Array.from({ length: numPrisoners }, (_, i) => i + 1);
|
||||
for (let i = boxes.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[boxes[i], boxes[j]] = [boxes[j], boxes[i]];
|
||||
}
|
||||
|
||||
setGameState({
|
||||
n: numPrisoners,
|
||||
boxes,
|
||||
currentPrisoner: 1,
|
||||
openedBoxes: [],
|
||||
foundNumber: false,
|
||||
gameOver: false,
|
||||
won: false,
|
||||
prisonersCompleted: 0,
|
||||
attemptsLeft: Math.floor(numPrisoners / 2),
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
initializeGame(n);
|
||||
}, []);
|
||||
|
||||
const handleBoxClick = (boxIndex: number) => {
|
||||
if (!gameState || gameState.gameOver || gameState.openedBoxes.includes(boxIndex)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newOpenedBoxes = [...gameState.openedBoxes, boxIndex];
|
||||
const numberInBox = gameState.boxes[boxIndex];
|
||||
const foundTarget = numberInBox === gameState.currentPrisoner;
|
||||
const newAttemptsLeft = gameState.attemptsLeft - 1;
|
||||
|
||||
if (foundTarget) {
|
||||
// Prisoner found their number!
|
||||
const newPrisonersCompleted = gameState.prisonersCompleted + 1;
|
||||
|
||||
if (newPrisonersCompleted === gameState.n) {
|
||||
// All prisoners succeeded!
|
||||
setGameState({
|
||||
...gameState,
|
||||
openedBoxes: newOpenedBoxes,
|
||||
foundNumber: true,
|
||||
gameOver: true,
|
||||
won: true,
|
||||
prisonersCompleted: newPrisonersCompleted,
|
||||
attemptsLeft: newAttemptsLeft,
|
||||
});
|
||||
} else {
|
||||
// Move to next prisoner
|
||||
setGameState({
|
||||
...gameState,
|
||||
currentPrisoner: gameState.currentPrisoner + 1,
|
||||
openedBoxes: [],
|
||||
foundNumber: false,
|
||||
prisonersCompleted: newPrisonersCompleted,
|
||||
attemptsLeft: Math.floor(gameState.n / 2),
|
||||
});
|
||||
}
|
||||
} else if (newAttemptsLeft === 0) {
|
||||
// Ran out of attempts - game over
|
||||
setGameState({
|
||||
...gameState,
|
||||
openedBoxes: newOpenedBoxes,
|
||||
foundNumber: false,
|
||||
gameOver: true,
|
||||
won: false,
|
||||
attemptsLeft: 0,
|
||||
});
|
||||
} else {
|
||||
// Continue opening boxes
|
||||
setGameState({
|
||||
...gameState,
|
||||
openedBoxes: newOpenedBoxes,
|
||||
attemptsLeft: newAttemptsLeft,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
initializeGame(n);
|
||||
};
|
||||
|
||||
const handleNChange = (newN: number) => {
|
||||
setN(newN);
|
||||
initializeGame(newN);
|
||||
};
|
||||
|
||||
if (!gameState) return null;
|
||||
|
||||
return (
|
||||
<div className="my-8 p-6 border-2 border-gray-300 rounded-lg bg-gray-50">
|
||||
<div className="mb-6">
|
||||
<label className="block text-lg font-semibold mb-2">
|
||||
Number of Prisoners: {n}
|
||||
</label>
|
||||
<input
|
||||
type="range"
|
||||
min="5"
|
||||
max="100"
|
||||
value={n}
|
||||
onChange={(e) => handleNChange(parseInt(e.target.value))}
|
||||
className="w-full h-2 bg-blue-200 rounded-lg appearance-none cursor-pointer"
|
||||
disabled={gameState.prisonersCompleted > 0 && !gameState.gameOver}
|
||||
/>
|
||||
<div className="flex justify-between text-sm text-gray-600 mt-1">
|
||||
<span>5</span>
|
||||
<span>100</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 p-4 bg-blue-100 rounded">
|
||||
{!gameState.gameOver && (
|
||||
<>
|
||||
<p className="text-lg font-semibold">
|
||||
Prisoner #{gameState.currentPrisoner} is searching for their number
|
||||
</p>
|
||||
<p className="text-md">
|
||||
Attempts remaining: {gameState.attemptsLeft} / {Math.floor(gameState.n / 2)}
|
||||
</p>
|
||||
<p className="text-md">
|
||||
Prisoners completed: {gameState.prisonersCompleted} / {gameState.n}
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
{gameState.gameOver && gameState.won && (
|
||||
<p className="text-2xl font-bold text-green-600">
|
||||
🎉 SUCCESS! All {gameState.n} prisoners found their numbers!
|
||||
</p>
|
||||
)}
|
||||
{gameState.gameOver && !gameState.won && (
|
||||
<p className="text-2xl font-bold text-red-600">
|
||||
❌ GAME OVER! Prisoner #{gameState.currentPrisoner} failed to find their number.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<h3 className="text-md font-semibold mb-2">Click on boxes to open them:</h3>
|
||||
<div
|
||||
className="grid gap-2"
|
||||
style={{
|
||||
gridTemplateColumns: `repeat(${Math.min(10, Math.ceil(Math.sqrt(gameState.n)))}, minmax(0, 1fr))`,
|
||||
}}
|
||||
>
|
||||
{gameState.boxes.map((number, index) => {
|
||||
const isOpened = gameState.openedBoxes.includes(index);
|
||||
const isTarget = number === gameState.currentPrisoner && isOpened;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={index}
|
||||
onClick={() => handleBoxClick(index)}
|
||||
disabled={gameState.gameOver || isOpened}
|
||||
className={`
|
||||
aspect-square p-2 rounded border-2 font-semibold text-sm
|
||||
transition-all duration-200
|
||||
${isOpened
|
||||
? isTarget
|
||||
? 'bg-green-400 border-green-600 text-white'
|
||||
: 'bg-red-200 border-red-400 text-gray-700'
|
||||
: 'bg-white border-gray-400 hover:bg-blue-100 hover:border-blue-500 cursor-pointer'
|
||||
}
|
||||
${gameState.gameOver || isOpened ? 'cursor-not-allowed' : ''}
|
||||
`}
|
||||
>
|
||||
{isOpened ? (
|
||||
<div className="flex flex-col items-center justify-center h-full">
|
||||
<div className="text-xs">Box {index + 1}</div>
|
||||
<div className="text-lg">{number}</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
{index + 1}
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleReset}
|
||||
className="px-6 py-3 bg-blue-600 text-white font-semibold rounded-lg hover:bg-blue-700 transition-colors"
|
||||
>
|
||||
Reset Game
|
||||
</button>
|
||||
|
||||
{gameState.openedBoxes.length > 0 && !gameState.gameOver && (
|
||||
<div className="mt-4 p-3 bg-yellow-100 rounded">
|
||||
<p className="text-sm">
|
||||
<strong>Last opened:</strong> Box {gameState.openedBoxes[gameState.openedBoxes.length - 1] + 1}
|
||||
{' → '}contains number {gameState.boxes[gameState.openedBoxes[gameState.openedBoxes.length - 1]]}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PrisonerGameSimulator;
|
||||
225
sites/vibes/src/components/problems/ProbabilityChart.tsx
Normal file
|
|
@ -0,0 +1,225 @@
|
|||
import React, { useMemo } from 'react';
|
||||
|
||||
interface ProbabilityChartProps {
|
||||
strategy: 'naive' | 'loop';
|
||||
}
|
||||
|
||||
const ProbabilityChart: React.FC<ProbabilityChartProps> = ({ strategy }) => {
|
||||
const data = useMemo(() => {
|
||||
const points = [];
|
||||
const maxN = 50;
|
||||
|
||||
for (let n = 2; n <= maxN; n += 2) {
|
||||
let probability;
|
||||
|
||||
if (strategy === 'naive') {
|
||||
// Naive strategy: (1/2)^n
|
||||
probability = Math.pow(0.5, n);
|
||||
} else {
|
||||
// Loop strategy: 1 - sum(1/k for k from n/2+1 to n)
|
||||
let sum = 0;
|
||||
for (let k = Math.floor(n / 2) + 1; k <= n; k++) {
|
||||
sum += 1 / k;
|
||||
}
|
||||
probability = 1 - sum;
|
||||
}
|
||||
|
||||
points.push({ n, probability });
|
||||
}
|
||||
|
||||
return points;
|
||||
}, [strategy]);
|
||||
|
||||
// Find min and max for scaling
|
||||
const maxProb = Math.max(...data.map(d => d.probability));
|
||||
const minProb = Math.min(...data.map(d => d.probability));
|
||||
|
||||
// Use log scale for naive strategy since values get extremely small
|
||||
const useLogScale = strategy === 'naive';
|
||||
|
||||
const getY = (prob: number, height: number) => {
|
||||
if (useLogScale) {
|
||||
const logProb = prob > 0 ? Math.log10(prob) : -100;
|
||||
const logMax = Math.log10(maxProb);
|
||||
const logMin = Math.log10(minProb);
|
||||
return height - ((logProb - logMin) / (logMax - logMin)) * height;
|
||||
} else {
|
||||
return height - (prob / maxProb) * height;
|
||||
}
|
||||
};
|
||||
|
||||
const chartWidth = 600;
|
||||
const chartHeight = 300;
|
||||
const padding = { top: 20, right: 20, bottom: 50, left: 70 };
|
||||
const width = chartWidth - padding.left - padding.right;
|
||||
const height = chartHeight - padding.top - padding.bottom;
|
||||
|
||||
// Create path for the line
|
||||
const pathData = data.map((point, i) => {
|
||||
const x = (point.n / 50) * width;
|
||||
const y = getY(point.probability, height);
|
||||
return `${i === 0 ? 'M' : 'L'} ${x} ${y}`;
|
||||
}).join(' ');
|
||||
|
||||
// Y-axis labels
|
||||
const yLabels = useLogScale
|
||||
? [-5, -10, -15, -20, -25, -30].map(exp => ({
|
||||
label: `10^${exp}`,
|
||||
value: Math.pow(10, exp),
|
||||
}))
|
||||
: [0, 0.1, 0.2, 0.3, 0.4, 0.5].map(val => ({
|
||||
label: val.toFixed(1),
|
||||
value: val,
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className="my-8 p-6 border-2 border-gray-300 rounded-lg bg-white">
|
||||
<h3 className="text-xl font-semibold mb-4">
|
||||
{strategy === 'naive' ? 'Naive Strategy: Random Box Selection' : 'Loop Strategy'}
|
||||
</h3>
|
||||
|
||||
<svg width={chartWidth} height={chartHeight} className="mx-auto">
|
||||
<g transform={`translate(${padding.left}, ${padding.top})`}>
|
||||
{/* Grid lines */}
|
||||
{yLabels.map(({ value }) => {
|
||||
const y = getY(value, height);
|
||||
return (
|
||||
<line
|
||||
key={value}
|
||||
x1={0}
|
||||
y1={y}
|
||||
x2={width}
|
||||
y2={y}
|
||||
stroke="#e5e7eb"
|
||||
strokeWidth={1}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* X-axis */}
|
||||
<line
|
||||
x1={0}
|
||||
y1={height}
|
||||
x2={width}
|
||||
y2={height}
|
||||
stroke="#374151"
|
||||
strokeWidth={2}
|
||||
/>
|
||||
|
||||
{/* Y-axis */}
|
||||
<line
|
||||
x1={0}
|
||||
y1={0}
|
||||
x2={0}
|
||||
y2={height}
|
||||
stroke="#374151"
|
||||
strokeWidth={2}
|
||||
/>
|
||||
|
||||
{/* Plot line */}
|
||||
<path
|
||||
d={pathData}
|
||||
fill="none"
|
||||
stroke="#3b82f6"
|
||||
strokeWidth={3}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
|
||||
{/* Data points */}
|
||||
{data.filter((_, i) => i % 2 === 0).map((point) => {
|
||||
const x = (point.n / 50) * width;
|
||||
const y = getY(point.probability, height);
|
||||
return (
|
||||
<circle
|
||||
key={point.n}
|
||||
cx={x}
|
||||
cy={y}
|
||||
r={4}
|
||||
fill="#3b82f6"
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* X-axis labels */}
|
||||
{[10, 20, 30, 40, 50].map((n) => {
|
||||
const x = (n / 50) * width;
|
||||
return (
|
||||
<text
|
||||
key={n}
|
||||
x={x}
|
||||
y={height + 25}
|
||||
textAnchor="middle"
|
||||
fontSize="14"
|
||||
fill="#374151"
|
||||
>
|
||||
{n}
|
||||
</text>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Y-axis labels */}
|
||||
{yLabels.map(({ label, value }) => {
|
||||
const y = getY(value, height);
|
||||
return (
|
||||
<text
|
||||
key={value}
|
||||
x={-10}
|
||||
y={y + 5}
|
||||
textAnchor="end"
|
||||
fontSize="12"
|
||||
fill="#374151"
|
||||
>
|
||||
{label}
|
||||
</text>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* X-axis title */}
|
||||
<text
|
||||
x={width / 2}
|
||||
y={height + 45}
|
||||
textAnchor="middle"
|
||||
fontSize="16"
|
||||
fontWeight="600"
|
||||
fill="#374151"
|
||||
>
|
||||
Number of Prisoners (n)
|
||||
</text>
|
||||
|
||||
{/* Y-axis title */}
|
||||
<text
|
||||
x={-height / 2}
|
||||
y={-50}
|
||||
textAnchor="middle"
|
||||
fontSize="16"
|
||||
fontWeight="600"
|
||||
fill="#374151"
|
||||
transform={`rotate(-90, ${-height / 2}, -50)`}
|
||||
>
|
||||
Probability of Success
|
||||
</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
<div className="mt-4 p-4 bg-gray-100 rounded">
|
||||
<p className="text-sm">
|
||||
{strategy === 'naive' ? (
|
||||
<>
|
||||
The naive strategy has probability <strong>(1/2)^n</strong> which decreases
|
||||
exponentially. Note the logarithmic scale on the y-axis!
|
||||
With 20 prisoners, the probability is already less than one in a million.
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
The loop strategy maintains a probability around <strong>31%</strong> regardless
|
||||
of the number of prisoners, approaching 1 - ln(2) ≈ 0.3069 as n → ∞.
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProbabilityChart;
|
||||
116
sites/vibes/src/components/problems/README.md
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
# 100 Prisoners Problem - Interactive Blog Post
|
||||
|
||||
This directory contains the interactive React components for the "100 Prisoners Problem" blog post, inspired by the interactive style of Nicky Case's explorable explanations.
|
||||
|
||||
## Files Created
|
||||
|
||||
### Blog Post
|
||||
|
||||
- **`src/content/puzzles/100-prisoners.mdx`** - The main blog post with embedded interactive components
|
||||
|
||||
### Interactive Components
|
||||
|
||||
1. **`PrisonerGameSimulator.tsx`** - Interactive game simulation
|
||||
- Allows users to play the prisoner game themselves
|
||||
- Adjustable number of prisoners (5-100)
|
||||
- Manual box clicking to experience the problem
|
||||
- Visual feedback for success/failure
|
||||
- Tracks current prisoner and attempts remaining
|
||||
|
||||
2. **`ProbabilityChart.tsx`** - Probability visualization
|
||||
- Shows probability curves for naive and loop strategies
|
||||
- Uses logarithmic scale for naive strategy (values get extremely small)
|
||||
- Interactive SVG chart with proper axes and labels
|
||||
- Includes explanatory text
|
||||
|
||||
3. **`PermutationExplorer.tsx`** - Cycle structure explorer
|
||||
- Generates all permutations for n ∈ {3, 4, 5}
|
||||
- Displays both standard and cycle notation
|
||||
- Filter by presence of long cycles (> n/2)
|
||||
- Color-coded display (red for long cycles, green for short)
|
||||
- Shows statistics about cycle length distribution
|
||||
|
||||
4. **`StrategyComparison.tsx`** - Side-by-side strategy comparison
|
||||
- Compares naive vs. loop strategy on same chart
|
||||
- Shows probabilities up to n=100
|
||||
- Includes detailed numerical breakdown
|
||||
- Highlights the key 31.18% result at n=100
|
||||
|
||||
## Features
|
||||
|
||||
All components are:
|
||||
|
||||
- ✅ Fully interactive with React hooks
|
||||
- ✅ Styled with Tailwind CSS classes
|
||||
- ✅ Responsive for mobile and desktop
|
||||
- ✅ Self-contained with no external dependencies beyond React
|
||||
- ✅ Client-side only (using `client:only="react"` in Astro)
|
||||
|
||||
## How It Works
|
||||
|
||||
### The Problem
|
||||
|
||||
100 prisoners must each find their own number among 100 boxes by opening at most 50 boxes. If all prisoners succeed, they go free. They can agree on a strategy beforehand but cannot communicate during the challenge.
|
||||
|
||||
### The Naive Strategy
|
||||
|
||||
Each prisoner opens 50 random boxes. Probability of success: (1/2)^100 ≈ 0%
|
||||
|
||||
### The Clever Strategy (Loop Following)
|
||||
|
||||
1. Open the box with your own number
|
||||
2. Look at the number inside
|
||||
3. Open the box with that number
|
||||
4. Repeat until you find your number
|
||||
|
||||
This exploits the cycle structure of permutations. Success probability: ~31.18%!
|
||||
|
||||
### Key Mathematical Insight
|
||||
|
||||
The prisoners succeed if and only if the permutation of numbers in boxes has no cycle longer than n/2. For n=100, the probability of this is:
|
||||
|
||||
P(success) = 1 - Σ(1/k) for k=51 to 100 ≈ 0.3118
|
||||
|
||||
As n→∞, this converges to 1 - ln(2) ≈ 0.3069.
|
||||
|
||||
## Usage in MDX
|
||||
|
||||
The components are imported and used like this:
|
||||
|
||||
\`\`\`mdx
|
||||
import PrisonerGameSimulator from '@/components/problems/PrisonerGameSimulator.tsx';
|
||||
import ProbabilityChart from '@/components/problems/ProbabilityChart.tsx';
|
||||
import PermutationExplorer from '@/components/problems/PermutationExplorer.tsx';
|
||||
import StrategyComparison from '@/components/problems/StrategyComparison.tsx';
|
||||
|
||||
<PrisonerGameSimulator client:only="react" />
|
||||
<ProbabilityChart strategy="naive" client:only="react" />
|
||||
<PermutationExplorer client:only="react" />
|
||||
<StrategyComparison client:only="react" />
|
||||
\`\`\`
|
||||
|
||||
## Design Philosophy
|
||||
|
||||
Inspired by Nicky Case's "Parable of the Polygons", these components aim to:
|
||||
|
||||
- Make abstract mathematical concepts tangible through interaction
|
||||
- Allow readers to explore and discover patterns themselves
|
||||
- Provide immediate visual feedback
|
||||
- Balance playfulness with mathematical rigor
|
||||
- Guide understanding through progressive disclosure
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
Potential additions:
|
||||
|
||||
- Auto-play mode for the game simulator using the loop strategy
|
||||
- Animation showing cycle formation in real-time
|
||||
- Monte Carlo simulation running multiple trials
|
||||
- Variant problems (different box-opening rules, malicious director, etc.)
|
||||
- 3D visualization of cycle structures for larger n
|
||||
|
||||
## References
|
||||
|
||||
- [Wikipedia: 100 Prisoners Problem](https://en.wikipedia.org/wiki/100_prisoners_problem)
|
||||
- [Nicky Case's Explorable Explanations](https://ncase.me/)
|
||||
- Original problem: Gál & Miltersen (2003)
|
||||
313
sites/vibes/src/components/problems/StrategyComparison.tsx
Normal file
|
|
@ -0,0 +1,313 @@
|
|||
import React, { useMemo } from 'react';
|
||||
|
||||
const StrategyComparison: React.FC = () => {
|
||||
const data = useMemo(() => {
|
||||
const points = [];
|
||||
const maxN = 100;
|
||||
|
||||
for (let n = 5; n <= maxN; n += 5) {
|
||||
// Naive strategy
|
||||
const naiveProb = Math.pow(0.5, n);
|
||||
|
||||
// Loop strategy: 1 - sum(1/k for k from n/2+1 to n)
|
||||
let sum = 0;
|
||||
for (let k = Math.floor(n / 2) + 1; k <= n; k++) {
|
||||
sum += 1 / k;
|
||||
}
|
||||
const loopProb = 1 - sum;
|
||||
|
||||
points.push({ n, naiveProb, loopProb });
|
||||
}
|
||||
|
||||
return points;
|
||||
}, []);
|
||||
|
||||
const chartWidth = 700;
|
||||
const chartHeight = 350;
|
||||
const padding = { top: 20, right: 100, bottom: 50, left: 70 };
|
||||
const width = chartWidth - padding.left - padding.right;
|
||||
const height = chartHeight - padding.top - padding.bottom;
|
||||
|
||||
// Only show loop strategy on linear scale (naive is too small to see)
|
||||
const maxProb = 0.5;
|
||||
|
||||
const getY = (prob: number) => {
|
||||
return height - (prob / maxProb) * height;
|
||||
};
|
||||
|
||||
const getX = (n: number) => {
|
||||
return (n / 100) * width;
|
||||
};
|
||||
|
||||
// Create paths for both strategies
|
||||
const loopPath = data.map((point, i) => {
|
||||
const x = getX(point.n);
|
||||
const y = getY(point.loopProb);
|
||||
return `${i === 0 ? 'M' : 'L'} ${x} ${y}`;
|
||||
}).join(' ');
|
||||
|
||||
// For naive, we'll just show it near zero
|
||||
const naivePath = data.map((point, i) => {
|
||||
const x = getX(point.n);
|
||||
const y = height - 2; // Near the bottom (representing ~0)
|
||||
return `${i === 0 ? 'M' : 'L'} ${x} ${y}`;
|
||||
}).join(' ');
|
||||
|
||||
return (
|
||||
<div className="my-8 p-6 border-2 border-gray-300 rounded-lg bg-white">
|
||||
<h3 className="text-xl font-semibold mb-4">Strategy Comparison</h3>
|
||||
|
||||
<svg width={chartWidth} height={chartHeight} className="mx-auto">
|
||||
<g transform={`translate(${padding.left}, ${padding.top})`}>
|
||||
{/* Grid lines */}
|
||||
{[0, 0.1, 0.2, 0.3, 0.4, 0.5].map((val) => {
|
||||
const y = getY(val);
|
||||
return (
|
||||
<g key={val}>
|
||||
<line
|
||||
x1={0}
|
||||
y1={y}
|
||||
x2={width}
|
||||
y2={y}
|
||||
stroke="#e5e7eb"
|
||||
strokeWidth={1}
|
||||
/>
|
||||
<text
|
||||
x={-10}
|
||||
y={y + 5}
|
||||
textAnchor="end"
|
||||
fontSize="12"
|
||||
fill="#374151"
|
||||
>
|
||||
{val.toFixed(1)}
|
||||
</text>
|
||||
</g>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* X-axis */}
|
||||
<line
|
||||
x1={0}
|
||||
y1={height}
|
||||
x2={width}
|
||||
y2={height}
|
||||
stroke="#374151"
|
||||
strokeWidth={2}
|
||||
/>
|
||||
|
||||
{/* Y-axis */}
|
||||
<line
|
||||
x1={0}
|
||||
y1={0}
|
||||
x2={0}
|
||||
y2={height}
|
||||
stroke="#374151"
|
||||
strokeWidth={2}
|
||||
/>
|
||||
|
||||
{/* Loop strategy line */}
|
||||
<path
|
||||
d={loopPath}
|
||||
fill="none"
|
||||
stroke="#10b981"
|
||||
strokeWidth={3}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
|
||||
{/* Naive strategy line (at the bottom) */}
|
||||
<path
|
||||
d={naivePath}
|
||||
fill="none"
|
||||
stroke="#ef4444"
|
||||
strokeWidth={3}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeDasharray="5,5"
|
||||
/>
|
||||
|
||||
{/* Data points for loop strategy */}
|
||||
{data.filter((_, i) => i % 2 === 0).map((point) => {
|
||||
const x = getX(point.n);
|
||||
const y = getY(point.loopProb);
|
||||
return (
|
||||
<circle
|
||||
key={point.n}
|
||||
cx={x}
|
||||
cy={y}
|
||||
r={4}
|
||||
fill="#10b981"
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* X-axis labels */}
|
||||
{[20, 40, 60, 80, 100].map((n) => {
|
||||
const x = getX(n);
|
||||
return (
|
||||
<text
|
||||
key={n}
|
||||
x={x}
|
||||
y={height + 25}
|
||||
textAnchor="middle"
|
||||
fontSize="14"
|
||||
fill="#374151"
|
||||
>
|
||||
{n}
|
||||
</text>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* X-axis title */}
|
||||
<text
|
||||
x={width / 2}
|
||||
y={height + 45}
|
||||
textAnchor="middle"
|
||||
fontSize="16"
|
||||
fontWeight="600"
|
||||
fill="#374151"
|
||||
>
|
||||
Number of Prisoners (n)
|
||||
</text>
|
||||
|
||||
{/* Y-axis title */}
|
||||
<text
|
||||
x={-height / 2}
|
||||
y={-50}
|
||||
textAnchor="middle"
|
||||
fontSize="16"
|
||||
fontWeight="600"
|
||||
fill="#374151"
|
||||
transform={`rotate(-90, ${-height / 2}, -50)`}
|
||||
>
|
||||
Probability of Success
|
||||
</text>
|
||||
|
||||
{/* Legend */}
|
||||
<g transform={`translate(${width + 20}, 20)`}>
|
||||
<rect x={0} y={0} width={15} height={15} fill="#10b981" />
|
||||
<text x={20} y={12} fontSize="14" fill="#374151">
|
||||
Loop Strategy
|
||||
</text>
|
||||
|
||||
<line
|
||||
x1={0}
|
||||
y1={35}
|
||||
x2={15}
|
||||
y2={35}
|
||||
stroke="#ef4444"
|
||||
strokeWidth={3}
|
||||
strokeDasharray="5,5"
|
||||
/>
|
||||
<text x={20} y={40} fontSize="14" fill="#374151">
|
||||
Naive Strategy
|
||||
</text>
|
||||
<text x={20} y={55} fontSize="11" fill="#6b7280">
|
||||
(≈ 0 for all n)
|
||||
</text>
|
||||
</g>
|
||||
|
||||
{/* Highlight key value */}
|
||||
<g>
|
||||
{(() => {
|
||||
const n100Point = data.find(p => p.n === 100);
|
||||
if (n100Point) {
|
||||
const x = getX(100);
|
||||
const y = getY(n100Point.loopProb);
|
||||
return (
|
||||
<>
|
||||
<circle
|
||||
cx={x}
|
||||
cy={y}
|
||||
r={6}
|
||||
fill="none"
|
||||
stroke="#10b981"
|
||||
strokeWidth={2}
|
||||
/>
|
||||
<line
|
||||
x1={x}
|
||||
y1={y - 10}
|
||||
x2={x}
|
||||
y2={y - 40}
|
||||
stroke="#374151"
|
||||
strokeWidth={1}
|
||||
/>
|
||||
<text
|
||||
x={x}
|
||||
y={y - 45}
|
||||
textAnchor="middle"
|
||||
fontSize="12"
|
||||
fontWeight="600"
|
||||
fill="#10b981"
|
||||
>
|
||||
31.18%
|
||||
</text>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})()}
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
<div className="mt-6 grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="p-4 bg-red-50 border-2 border-red-300 rounded">
|
||||
<h4 className="font-semibold text-red-900 mb-2">❌ Naive Strategy</h4>
|
||||
<p className="text-sm mb-2">
|
||||
Each prisoner randomly selects 50 boxes.
|
||||
</p>
|
||||
<div className="text-xs space-y-1">
|
||||
<p><strong>n=10:</strong> P ≈ {(Math.pow(0.5, 10) * 100).toExponential(2)}%</p>
|
||||
<p><strong>n=20:</strong> P ≈ {(Math.pow(0.5, 20) * 100).toExponential(2)}%</p>
|
||||
<p><strong>n=50:</strong> P ≈ {(Math.pow(0.5, 50) * 100).toExponential(2)}%</p>
|
||||
<p><strong>n=100:</strong> P ≈ {(Math.pow(0.5, 100) * 100).toExponential(2)}%</p>
|
||||
</div>
|
||||
<p className="text-xs mt-2 italic">
|
||||
Essentially impossible for any reasonable n!
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="p-4 bg-green-50 border-2 border-green-300 rounded">
|
||||
<h4 className="font-semibold text-green-900 mb-2">✅ Loop Strategy</h4>
|
||||
<p className="text-sm mb-2">
|
||||
Each prisoner follows the cycle starting at their number.
|
||||
</p>
|
||||
<div className="text-xs space-y-1">
|
||||
{(() => {
|
||||
const calcProb = (n: number) => {
|
||||
let sum = 0;
|
||||
for (let k = Math.floor(n / 2) + 1; k <= n; k++) {
|
||||
sum += 1 / k;
|
||||
}
|
||||
return ((1 - sum) * 100).toFixed(2);
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<p><strong>n=10:</strong> P ≈ {calcProb(10)}%</p>
|
||||
<p><strong>n=20:</strong> P ≈ {calcProb(20)}%</p>
|
||||
<p><strong>n=50:</strong> P ≈ {calcProb(50)}%</p>
|
||||
<p><strong>n=100:</strong> P ≈ {calcProb(100)}%</p>
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
<p className="text-xs mt-2 italic">
|
||||
Converges to 1 - ln(2) ≈ 30.69% as n → ∞
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 p-4 bg-blue-50 rounded">
|
||||
<p className="text-sm">
|
||||
<strong>Key Insight:</strong> The loop strategy transforms an impossible problem
|
||||
(probability ≈ 10<sup>-30</sup>) into a reasonable one (probability ≈ 31%).
|
||||
This astronomical improvement comes from exploiting the mathematical structure
|
||||
of permutations rather than relying on independent random trials.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default StrategyComparison;
|
||||
190
sites/vibes/src/components/sections/about-section.tsx
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
import { PlusSigns } from "@/components/icons/plus-signs";
|
||||
import JoinUs from "@/components/sections/join-us";
|
||||
import {
|
||||
Carousel,
|
||||
CarouselContent,
|
||||
CarouselItem,
|
||||
} from "@/components/ui/carousel";
|
||||
|
||||
export function AboutSection() {
|
||||
return (
|
||||
<>
|
||||
{/* Hero Section */}
|
||||
<section className="lg:py-15 container relative max-w-5xl py-10 md:py-12">
|
||||
<div className="">
|
||||
<h1 className="text-4xl font-semibold tracking-tighter md:text-5xl lg:text-6xl">
|
||||
A different
|
||||
<br />
|
||||
kind of bank.
|
||||
</h1>
|
||||
<p className="text-muted-foreground font-mona mt-4 max-w-xl text-2xl md:text-3xl">
|
||||
We're on a mission to transform financial services by
|
||||
harnessing vast amounts of untapped financial data.
|
||||
</p>
|
||||
</div>
|
||||
{/* Background decoration */}
|
||||
<>
|
||||
<div className="absolute inset-0 z-[-1] -translate-y-1/2 translate-x-1/4 blur-[100px] will-change-transform">
|
||||
<div className="bg-primary-gradient/25 -translate-x-1/5 absolute right-0 top-0 h-[250px] w-[800px] rounded-full md:h-[300px]" />
|
||||
<div className="bg-secondary-gradient/15 absolute right-0 top-0 size-[250px] -translate-x-1/3 -translate-y-1/3 rounded-full md:size-[400px]" />
|
||||
</div>
|
||||
<div className="absolute -inset-40 z-[-1] [mask-image:radial-gradient(circle_at_center,black_0%,black_20%,transparent_80%)]">
|
||||
<PlusSigns className="text-foreground/[0.05] h-full w-full" />
|
||||
</div>
|
||||
</>
|
||||
</section>
|
||||
|
||||
{/* Stats Section */}
|
||||
<section className="container max-w-5xl border-y py-5">
|
||||
<h2 className="mini-title">By the numbers</h2>
|
||||
<div className="mt-6 grid grid-cols-2 gap-8 md:grid-cols-4">
|
||||
<div>
|
||||
<h3 className="text-4xl font-medium tracking-tight md:text-5xl lg:text-6xl">
|
||||
$150M
|
||||
</h3>
|
||||
<p className="text-muted-foreground mt-1 font-medium">Raised</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-4xl font-medium tracking-tight md:text-5xl lg:text-6xl">
|
||||
20K
|
||||
</h3>
|
||||
<p className="text-muted-foreground mt-1 font-medium">Companies</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-4xl font-medium tracking-tight md:text-5xl lg:text-6xl">
|
||||
1.3B
|
||||
</h3>
|
||||
<p className="text-muted-foreground mt-1 font-medium">
|
||||
Monthly transactions
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-4xl font-medium tracking-tight md:text-5xl lg:text-6xl">
|
||||
1.5K
|
||||
</h3>
|
||||
<p className="text-muted-foreground mt-1 font-medium">
|
||||
Connections per minute
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Mission Section */}
|
||||
<section className="lg:py-15 container max-w-5xl py-10 md:py-12">
|
||||
<div className="max-w-xl space-y-5 md:space-y-8 lg:space-y-10">
|
||||
<p className="text-lg">
|
||||
Financial services have changed, are changing, and will continue to
|
||||
change — for the better. Now is the time for finance to be
|
||||
developer-first and API-driven. But in order to do this — it needs a
|
||||
new foundation.
|
||||
</p>
|
||||
|
||||
<h2 className="font-mona text-2xl font-medium tracking-tight md:text-3xl">
|
||||
We were always told that banks can't be platforms.
|
||||
</h2>
|
||||
<p className="text-lg">
|
||||
Everyone tried fixing the problem by layering APIs on legacy
|
||||
systems, creating abstractions and inefficiencies. We have spent
|
||||
years building and scaling companies like Plaid, Stripe, and Affirm,
|
||||
confronting these limitations firsthand. The current solutions
|
||||
aren't good enough. We believe that banking infrastructure must
|
||||
be reimagined as an API platform. But we had to start from ground
|
||||
zero.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Image Grid Section */}
|
||||
<section className="lg:pb-15 my-5 pb-10 md:my-8 md:pb-12 lg:my-12">
|
||||
<Carousel
|
||||
opts={{
|
||||
align: "start",
|
||||
}}
|
||||
>
|
||||
<CarouselContent className="-ml-4">
|
||||
<CarouselItem className="basis-[80%] lg:basis-1/3 xl:basis-[40%]">
|
||||
<div className="relative h-[330px] lg:h-[440px]">
|
||||
<img
|
||||
src="/images/about/4.webp"
|
||||
alt="Charter team member working"
|
||||
className="size-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
</CarouselItem>
|
||||
<CarouselItem className="basis-[80%] lg:basis-1/3 xl:basis-[40%]">
|
||||
<div className="relative h-[330px] lg:h-[440px]">
|
||||
<img
|
||||
src="/images/about/2.webp"
|
||||
alt="Modern workspace setup"
|
||||
className="size-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
</CarouselItem>
|
||||
<CarouselItem className="basis-[80%] lg:basis-1/3 xl:basis-[40%]">
|
||||
<div className="relative h-[330px] lg:h-[440px]">
|
||||
<img
|
||||
src="/images/about/3.webp"
|
||||
alt="Team collaboration"
|
||||
className="size-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
</CarouselItem>
|
||||
</CarouselContent>
|
||||
</Carousel>
|
||||
</section>
|
||||
|
||||
{/* CoreAPI Section */}
|
||||
<section className="container max-w-5xl pb-10 md:pb-20">
|
||||
<div className="ml-auto mr-0 max-w-xl space-y-5 md:space-y-8 lg:-translate-x-10 lg:space-y-10">
|
||||
<p className="text-lg">
|
||||
We started building CoreAPI in 2019 and launched in 2022. Every
|
||||
endpoint has been designed from the ground up — with no technical
|
||||
debt or legacy systems. We are purpose-built to power financial
|
||||
innovation for the next hundred years.
|
||||
</p>
|
||||
|
||||
<h2 className="font-mona text-2xl font-medium tracking-tight md:text-3xl">
|
||||
We are a bit of a unique company — not your standard tech or fintech
|
||||
company.
|
||||
</h2>
|
||||
|
||||
<p className="text-lg">
|
||||
We are 100% founder and team-owned, profitable, and we keep our team
|
||||
lean. Over time, this page will become more polished, but for now,
|
||||
we're focused on delivering for developers.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Founding Team Section */}
|
||||
<section className="lg:py-15 container max-w-5xl py-10 md:py-12">
|
||||
<div className="grid gap-5 md:grid-cols-2 md:gap-10 lg:gap-16">
|
||||
<div className="order-2 md:order-1">
|
||||
<h2 className="font-mona text-3xl font-semibold md:text-4xl">
|
||||
The founding team
|
||||
</h2>
|
||||
<p className="mt-5 text-lg md:mt-6">
|
||||
We started building CoreAPI in 2019 and launched in 2022. Every
|
||||
endpoint has been designed from the ground up — with no technical
|
||||
debt or legacy systems. We are purpose-built to power financial
|
||||
innovation for the next hundred years. We are 100% founder and
|
||||
team-owned, profitable, and we keep our team lean. Over time, this
|
||||
page will become more polished, but for now, we're focused on
|
||||
delivering for developers. If you're interested in building
|
||||
the future of financial APIs, check out our open roles below.
|
||||
</p>
|
||||
</div>
|
||||
<div className="relative order-1 h-[300px] w-full md:order-2 md:h-[400px]">
|
||||
<img
|
||||
src="/images/about/1.webp"
|
||||
alt="Founding team collaboration"
|
||||
className="size-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<JoinUs />
|
||||
</>
|
||||
);
|
||||
}
|
||||
80
sites/vibes/src/components/sections/ai-chatbot.tsx
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
import { ChevronRight, Timer, Wallet, Terminal, Calendar } from "lucide-react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
} from "@/components/ui/card";
|
||||
|
||||
const features = [
|
||||
{
|
||||
description: "Faster than Wire, and more affordable, too.",
|
||||
icon: Timer,
|
||||
href: "/pricing",
|
||||
},
|
||||
{
|
||||
description: "Send or request funds. FedNow does both.",
|
||||
icon: Wallet,
|
||||
href: "/pricing",
|
||||
},
|
||||
{
|
||||
description: "API request to creation in seven minutes.",
|
||||
icon: Terminal,
|
||||
href: "/pricing",
|
||||
},
|
||||
{
|
||||
description: "Settle any time, any day. What Bank holiday?",
|
||||
icon: Calendar,
|
||||
href: "/pricing",
|
||||
},
|
||||
];
|
||||
|
||||
export default function AIChatbot() {
|
||||
return (
|
||||
<section id="ai-chatbot" className="relative py-16 md:py-28 lg:py-32">
|
||||
<div className="container max-w-5xl">
|
||||
<div className="text-center">
|
||||
<h3 className="mini-title">MORE COMPUTER FUGAZI</h3>
|
||||
|
||||
<h2 className="mt-4 text-4xl font-semibold tracking-tight md:text-5xl lg:text-6xl">
|
||||
And we have an AI chatbot
|
||||
</h2>
|
||||
<p className="text-muted-foreground mt-3 text-xl font-medium md:text-2xl">
|
||||
{`We're betting on agents replacing our staff next year.`}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 grid grid-cols-2 gap-2.5 md:mt-12 lg:mt-20 lg:grid-cols-4 lg:gap-6">
|
||||
{features.map((feature, index) => (
|
||||
<Card key={index} className="flex flex-col">
|
||||
<CardHeader className="max-md:p-3">
|
||||
<feature.icon className="text-primary size-8" />
|
||||
<CardDescription className="text-foreground mt-4 font-medium">
|
||||
{feature.description}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="mt-auto max-md:p-3">
|
||||
<Button
|
||||
variant="outline"
|
||||
asChild
|
||||
className="border-border group w-[min(100%,300px)]"
|
||||
>
|
||||
<a href={feature.href}>
|
||||
Learn more
|
||||
<span className="sr-only">
|
||||
{" "}
|
||||
about {feature.description.toLowerCase()}
|
||||
</span>
|
||||
<ChevronRight className="ml-1 size-4 transition-transform group-hover:translate-x-0.5" />
|
||||
</a>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
57
sites/vibes/src/components/sections/all-blogs.tsx
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import { ChevronRight } from "lucide-react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
} from "@/components/ui/card";
|
||||
import { BLOG_SITES } from "@/blog-sites.js";
|
||||
|
||||
const features = BLOG_SITES.map((site) => ({
|
||||
title: site.title,
|
||||
description: site.description,
|
||||
href: site.url,
|
||||
}));
|
||||
|
||||
export default function AllBlogs() {
|
||||
return (
|
||||
<section id="ai-chatbot" className="relative py-16 md:py-28 lg:py-32">
|
||||
<div className="container max-w-5xl">
|
||||
<div className="text-center">
|
||||
<h3 className="mini-title">CURRENTLY ACTIVE BLOGS</h3>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 grid grid-cols-1 gap-2.5 sm:grid-cols-2 md:mt-12 lg:mt-20 lg:grid-cols-3 lg:gap-6">
|
||||
{features.map((feature, index) => (
|
||||
<Card key={index} className="flex flex-col">
|
||||
<CardHeader className="max-md:p-3">
|
||||
<h3 className="text-lg font-semibold">{feature.title}</h3>
|
||||
<CardDescription className="text-foreground mt-4 font-medium">
|
||||
{feature.description}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="mt-auto max-md:p-3">
|
||||
<Button
|
||||
variant="outline"
|
||||
asChild
|
||||
className="border-border group w-[min(100%,300px)]"
|
||||
>
|
||||
<a href={feature.href}>
|
||||
Read On
|
||||
<span className="sr-only">
|
||||
{" "}
|
||||
about {feature.description.toLowerCase()}
|
||||
</span>
|
||||
<ChevronRight className="ml-1 size-4 transition-transform group-hover:translate-x-0.5" />
|
||||
</a>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
88
sites/vibes/src/components/sections/blog-post.tsx
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { format } from "date-fns";
|
||||
import { Calendar, Clock, ArrowLeft, User } from "lucide-react";
|
||||
|
||||
const BlogPost = ({
|
||||
post,
|
||||
children,
|
||||
}: {
|
||||
post: any;
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
const { title, authorName, image, pubDate, description, authorImage } =
|
||||
post.data;
|
||||
return (
|
||||
<>
|
||||
{/* Hero section with gradient background and post info */}
|
||||
<section className="pb-8 pt-4">
|
||||
<div className="container max-w-4xl">
|
||||
<div className="space-y-2 text-center">
|
||||
<h1 className="text-2xl font-bold tracking-tight md:text-4xl lg:text-5xl">
|
||||
{title}
|
||||
</h1>
|
||||
<p className="text-muted-foreground mx-auto max-w-2xl text-lg">
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mx-auto mt-6 flex max-w-md flex-wrap items-center justify-center gap-6">
|
||||
{/* Author info */}
|
||||
{authorImage ? (
|
||||
<div className="flex items-center gap-3">
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="bg-primary/10 text-primary flex h-10 w-10 items-center justify-center rounded-full shadow-sm">
|
||||
<User className="h-5 w-5" />
|
||||
</div>
|
||||
<div className="flex flex-col text-sm">
|
||||
<span className="font-medium">
|
||||
{authorName || "Anonymous"}
|
||||
</span>
|
||||
<span className="text-muted-foreground">Author</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Date info */}
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="text-muted-foreground flex items-center gap-1 text-sm">
|
||||
<Calendar className="h-4 w-4" />
|
||||
<span>{format(pubDate, "MMMM d, yyyy")}</span>
|
||||
</div>
|
||||
|
||||
<div className="text-muted-foreground flex items-center gap-1 text-sm">
|
||||
<Clock className="h-4 w-4" />
|
||||
<span>5 min read</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Featured image */}
|
||||
{image && (
|
||||
<section className="container max-w-5xl py-4">
|
||||
<div className="aspect-video w-full overflow-hidden rounded-xl border">
|
||||
<img
|
||||
src={image}
|
||||
alt={title}
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Article content */}
|
||||
<section className="container my-10 max-w-3xl">
|
||||
<article className="prose prose-lg dark:prose-invert prose-headings:font-semibold prose-a:text-primary mx-auto">
|
||||
{children}
|
||||
</article>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export { BlogPost };
|
||||
135
sites/vibes/src/components/sections/blog-posts.tsx
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
import { Button } from "@/components/ui/button";
|
||||
import { ArrowRight } from "lucide-react";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Calendar, Clock, User } from "lucide-react";
|
||||
import { PlusSigns } from "../icons/plus-signs";
|
||||
|
||||
const BlogPosts = ({
|
||||
posts,
|
||||
collection = "",
|
||||
}: {
|
||||
posts: any[];
|
||||
collection?: string;
|
||||
}) => {
|
||||
// Get the first post as the featured post
|
||||
const featuredPost = posts[0];
|
||||
const remainingPosts = posts.slice(1);
|
||||
const hrefFor = (post: any) =>
|
||||
collection ? `/${collection}/${post.id}/` : `/${post.id}/`;
|
||||
|
||||
return (
|
||||
<div className="relative py-16 md:py-28 lg:py-32">
|
||||
<div className="absolute -inset-40 z-[-1] [mask-image:radial-gradient(circle_at_center,black_0%,black_20%,transparent_75%)]">
|
||||
<PlusSigns className="text-foreground/[0.05] h-full w-full" />
|
||||
</div>
|
||||
|
||||
{/* Featured Post */}
|
||||
<section className="mt-8">
|
||||
<div className="container max-w-6xl">
|
||||
<a
|
||||
href={hrefFor(featuredPost)}
|
||||
className="bg-card group relative mb-16 overflow-hidden rounded-xl border shadow-md"
|
||||
>
|
||||
<div className="flex flex-col gap-6 lg:flex-row">
|
||||
<div className="lg:w-1/2">
|
||||
<div className="p-2 lg:p-4">
|
||||
<img
|
||||
src={featuredPost.data.image}
|
||||
alt={featuredPost.data.title}
|
||||
className="aspect-video w-full rounded-lg object-cover transition-transform group-hover:scale-[1.02]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col justify-center p-4 pb-8 lg:w-1/2 lg:pr-8">
|
||||
<Badge variant="outline" className="mb-3 w-fit">
|
||||
Featured Post
|
||||
</Badge>
|
||||
<h2 className="mb-3 text-2xl font-bold group-hover:underline md:text-3xl">
|
||||
{featuredPost.data.title}
|
||||
</h2>
|
||||
<p className="text-muted-foreground mb-4 line-clamp-3 text-base">
|
||||
{featuredPost.data.description}
|
||||
</p>
|
||||
|
||||
<div className="mb-6 flex gap-4">
|
||||
<div className="text-muted-foreground flex items-center text-sm">
|
||||
<Calendar className="mr-1 h-4 w-4" />
|
||||
{new Date(featuredPost.data.pubDate).toLocaleDateString(
|
||||
"en-US",
|
||||
{
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
},
|
||||
)}
|
||||
</div>
|
||||
<div className="text-muted-foreground flex items-center text-sm">
|
||||
<Clock className="mr-1 h-4 w-4" />5 min read
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Regular Posts Grid */}
|
||||
<section className="mt-8">
|
||||
<div className="container max-w-6xl">
|
||||
<h2 className="mb-8 text-xl font-semibold md:text-2xl">
|
||||
Recent Articles
|
||||
</h2>
|
||||
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
{remainingPosts.map((post) => (
|
||||
<a
|
||||
key={post.id}
|
||||
className="bg-card group rounded-xl border shadow-sm transition-all hover:shadow-md"
|
||||
href={hrefFor(post)}
|
||||
>
|
||||
<div className="p-2">
|
||||
<img
|
||||
src={post.data.image}
|
||||
alt={post.data.title}
|
||||
className="aspect-video w-full rounded-lg object-cover transition-transform group-hover:scale-[1.01]"
|
||||
/>
|
||||
</div>
|
||||
<div className="px-4 pb-5 pt-2">
|
||||
<h2 className="mb-2 text-xl font-semibold group-hover:underline">
|
||||
{post.data.title}
|
||||
</h2>
|
||||
<p className="text-muted-foreground line-clamp-2 text-sm">
|
||||
{post.data.description}
|
||||
</p>
|
||||
|
||||
<div className="text-muted-foreground mt-3 flex items-center gap-3 text-xs">
|
||||
<div className="flex items-center gap-1">
|
||||
<Calendar className="h-3.5 w-3.5" />
|
||||
<span>
|
||||
{new Date(post.data.pubDate).toLocaleDateString(
|
||||
"en-US",
|
||||
{
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
},
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Clock className="h-3.5 w-3.5" />
|
||||
<span>5 min read</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { BlogPosts };
|
||||
100
sites/vibes/src/components/sections/code-security.tsx
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
import { useState } from "react";
|
||||
|
||||
import { PlusSigns } from "../icons/plus-signs";
|
||||
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "@/components/ui/accordion";
|
||||
|
||||
const FEATURES = [
|
||||
{
|
||||
id: "move-money",
|
||||
title: "Move and hold money",
|
||||
description:
|
||||
"Previously, emerging financial companies were forced to navigate costly middleware solutions that connected them to outdated sponsor bank systems.",
|
||||
image: "/images/homepage/code-snippet.webp",
|
||||
},
|
||||
{
|
||||
id: "card-program",
|
||||
title: "Build a modern card program",
|
||||
description:
|
||||
"Create and manage virtual and physical cards with complete control over spending limits, merchant categories, and real-time transaction monitoring.",
|
||||
image: "/images/homepage/code-snippet.webp",
|
||||
},
|
||||
{
|
||||
id: "lend-money",
|
||||
title: "Lend money",
|
||||
description:
|
||||
"Offer various lending products through our platform with automated underwriting, loan servicing, and compliance management.",
|
||||
image: "/images/homepage/code-snippet.webp",
|
||||
},
|
||||
];
|
||||
|
||||
export default function CodeSecurity() {
|
||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||
|
||||
return (
|
||||
<section
|
||||
id="code-security"
|
||||
className="container max-w-5xl py-16 md:py-28 lg:py-32"
|
||||
>
|
||||
<div className="from-primary-gradient/20 bg-linear-to-bl relative overflow-hidden rounded-3xl border to-transparent py-5 md:py-6 lg:py-8">
|
||||
<div className="absolute inset-0 z-[-1]">
|
||||
<PlusSigns className="text-foreground/[0.05] h-full w-full" />
|
||||
</div>
|
||||
<div className="md:px-6 lg:px-8">
|
||||
<div className="max-md:px-5">
|
||||
<h3 className="mini-title">WHY CHARTER?</h3>
|
||||
<h2 className="mt-3 text-3xl font-semibold tracking-tight md:text-4xl lg:text-5xl">
|
||||
Code security
|
||||
</h2>
|
||||
</div>
|
||||
<div className="mt-10 flex gap-12 overflow-hidden max-md:flex-col md:mt-16 lg:mt-20">
|
||||
<Accordion
|
||||
type="single"
|
||||
className="flex-1"
|
||||
defaultValue="0"
|
||||
onValueChange={(value) => setSelectedIndex(Number(value))}
|
||||
>
|
||||
{FEATURES.map((feature, index) => (
|
||||
<AccordionItem
|
||||
key={feature.id}
|
||||
value={index.toString()}
|
||||
className="border-black/20 last:border-none dark:border-white/20"
|
||||
>
|
||||
<AccordionTrigger className="hover:no-underline max-md:px-5">
|
||||
<h3 className="text-input font-inter text-xl font-bold">
|
||||
{feature.title}
|
||||
</h3>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="">
|
||||
<p className="text-muted-foreground font-medium leading-relaxed max-md:px-5">
|
||||
{feature.description}
|
||||
</p>
|
||||
<div className="relative mt-4 h-[280px] translate-x-5 md:hidden">
|
||||
<img
|
||||
src={feature.image}
|
||||
alt={feature.title}
|
||||
className="size-full rounded-2xl object-cover object-left-top shadow-xl"
|
||||
/>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
))}
|
||||
</Accordion>
|
||||
<div className="relative h-[240px] max-md:hidden max-md:translate-x-6 md:flex-1">
|
||||
<img
|
||||
src={FEATURES[selectedIndex].image}
|
||||
alt={FEATURES[selectedIndex].title}
|
||||
className="size-full rounded-2xl object-cover object-left-top shadow-xl"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
84
sites/vibes/src/components/sections/contact-page.tsx
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
import { PlusSigns } from "@/components/icons/plus-signs";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
|
||||
export function ContactPage() {
|
||||
return (
|
||||
<section className="relative py-16 md:py-28 lg:py-32">
|
||||
<div className="container max-w-2xl">
|
||||
<div className="text-center">
|
||||
<h1 className="text-4xl font-semibold tracking-tight md:text-5xl lg:text-6xl">
|
||||
Book a demo
|
||||
</h1>
|
||||
<p className="text-muted-foreground mt-4 text-2xl md:text-3xl">
|
||||
Learn how Charter can work for you
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form className="mt-8 space-y-5 md:mt-12 lg:mt-20">
|
||||
{/* First Name */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="firstName">First name</Label>
|
||||
<Input id="firstName" placeholder="Enter your first name" />
|
||||
</div>
|
||||
|
||||
{/* Last Name */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="lastName">Last name</Label>
|
||||
<Input id="lastName" placeholder="Enter your last name" />
|
||||
</div>
|
||||
|
||||
{/* Email */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="Enter your email address"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Company */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="company">
|
||||
Company name{" "}
|
||||
<span className="text-muted-foreground">(optional)</span>
|
||||
</Label>
|
||||
<Input id="company" placeholder="Enter your company name" />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="employees">Number of employees</Label>
|
||||
<Input
|
||||
id="employees"
|
||||
placeholder="Enter number of employees"
|
||||
type="number"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Message */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="message">Message</Label>
|
||||
<Textarea
|
||||
id="message"
|
||||
rows={5}
|
||||
placeholder="Enter your message"
|
||||
className="resize-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button type="submit" size="lg">
|
||||
Submit
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div className="absolute -inset-40 z-[-1] [mask-image:radial-gradient(circle_at_center,black_0%,black_20%,transparent_75%)]">
|
||||
<PlusSigns className="text-foreground/[0.05] h-full w-full" />
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
120
sites/vibes/src/components/sections/faq-page.tsx
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
import { PlusSigns } from "@/components/icons/plus-signs";
|
||||
import Testimonials from "@/components/sections/testimonials";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "@/components/ui/accordion";
|
||||
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
||||
|
||||
const FAQ_ITEMS = [
|
||||
{
|
||||
category: "SUPPORT",
|
||||
items: [
|
||||
{
|
||||
question: "Is there a free version?",
|
||||
answer:
|
||||
"Yes! We offer a generous free plan with just enough features except that one feature you really want! Our strategy is to get your credit card details on file then steadily double our prices against inflation rates.",
|
||||
},
|
||||
{
|
||||
question: "How do I update my account without breaking my laptop?",
|
||||
answer:
|
||||
"Our platform is designed with safety in mind. You can update your account settings through our intuitive dashboard without any risk to your hardware. We have multiple safeguards in place to prevent any system conflicts.",
|
||||
},
|
||||
{
|
||||
question: "Is support free, or do I need to Google everything?",
|
||||
answer:
|
||||
"We provide comprehensive support at no additional cost. Our dedicated support team is available 24/7 to help you with any questions or issues you might encounter. No need to rely on Google - we're here to help!",
|
||||
},
|
||||
{
|
||||
question: "Are you going to be subsumed by AI?",
|
||||
answer:
|
||||
"While we embrace AI technology to enhance our services, we maintain a strong human element in our operations. Our team works alongside AI to provide the best possible service while ensuring human oversight and decision-making remain central to our operations.",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
category: "YOUR ACCOUNT",
|
||||
items: [
|
||||
{
|
||||
question: "Is support free, or do I need to Google everything?",
|
||||
answer:
|
||||
"We provide comprehensive support at no additional cost. Our dedicated support team is available 24/7 to help you with any questions or issues you might encounter. No need to rely on Google - we're here to help!",
|
||||
},
|
||||
{
|
||||
question: "Are you going to be subsumed by AI?",
|
||||
answer:
|
||||
"While we embrace AI technology to enhance our services, we maintain a strong human element in our operations. Our team works alongside AI to provide the best possible service while ensuring human oversight and decision-making remain central to our operations.",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
category: "OTHER QUESTIONS",
|
||||
items: [
|
||||
{
|
||||
question: "Is support free, or do I need to Google everything?",
|
||||
answer:
|
||||
"We provide comprehensive support at no additional cost. Our dedicated support team is available 24/7 to help you with any questions or issues you might encounter. No need to rely on Google - we're here to help!",
|
||||
},
|
||||
{
|
||||
question: "Are you going to be subsumed by AI?",
|
||||
answer:
|
||||
"While we embrace AI technology to enhance our services, we maintain a strong human element in our operations. Our team works alongside AI to provide the best possible service while ensuring human oversight and decision-making remain central to our operations.",
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export default function FAQPage() {
|
||||
return (
|
||||
<>
|
||||
<section className="relative py-16 md:py-28 lg:py-32">
|
||||
<div className="container">
|
||||
<div className="text-center">
|
||||
<h1 className="text-4xl font-semibold tracking-tight md:text-5xl lg:text-6xl">
|
||||
Frequently Asked Questions
|
||||
</h1>
|
||||
<p className="text-muted-foreground mt-4 text-2xl md:text-3xl">
|
||||
Everything you need to know about Charter
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mx-auto mt-8 max-w-2xl space-y-12 md:mt-12 lg:mt-20">
|
||||
{FAQ_ITEMS.map((category) => (
|
||||
<Card key={category.category} className="border-hidden">
|
||||
<CardHeader className="pb-0">
|
||||
<h3 className="text-accent-foreground border-b pb-4 font-mono text-sm font-medium uppercase tracking-widest">
|
||||
{category.category}
|
||||
</h3>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Accordion type="single" collapsible className="w-full">
|
||||
{category.items.map((item, i) => (
|
||||
<AccordionItem
|
||||
key={i}
|
||||
value={`${category.category}-${i}`}
|
||||
className="border-muted border-b last:border-0"
|
||||
>
|
||||
<AccordionTrigger className="text-base font-medium hover:no-underline">
|
||||
{item.question}
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="text-muted-foreground text-base font-medium">
|
||||
{item.answer}
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
))}
|
||||
</Accordion>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute -inset-40 z-[-1] [mask-image:radial-gradient(circle_at_center,black_0%,black_20%,transparent_75%)]">
|
||||
<PlusSigns className="text-foreground/[0.05] h-full w-full" />
|
||||
</div>
|
||||
</section>
|
||||
<Testimonials />
|
||||
</>
|
||||
);
|
||||
}
|
||||
157
sites/vibes/src/components/sections/featured-posts.tsx
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
import { PlusSigns } from "../icons/plus-signs";
|
||||
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const items = [
|
||||
{
|
||||
title: (
|
||||
<>
|
||||
Unparalleled
|
||||
<br />
|
||||
VAR flexibility
|
||||
</>
|
||||
),
|
||||
description: [
|
||||
"VAR is often known for its lengthy processing times. At Charter, we focus on achieving the fastest VAR transfers—often completed within hours.",
|
||||
"Unlike traditional banks and middleware, we connect directly with the Federal Reserve to ensure the fastest, most transparent transfers possible.",
|
||||
],
|
||||
image: {
|
||||
src: "/images/homepage/why-charter/1.webp",
|
||||
alt: "Code snippet",
|
||||
type: "fill",
|
||||
},
|
||||
className:
|
||||
"flex flex-col pl-6 py-6 overflow-hidden md:col-span-3 md:flex-row gap-6 md:gap-12",
|
||||
headerClassName: "flex-2 p-0",
|
||||
contentClassName:
|
||||
"relative h-[320px] w-full p-0 self-center overflow-hidden rounded-l-xl border md:flex-1",
|
||||
},
|
||||
{
|
||||
title: "Unparalleled VAR flexibility",
|
||||
description: [
|
||||
"VAR has a reputation for taking too long. At Charter, we optimise for the fastest VAR transfers possible — often in a matter of hours.",
|
||||
"Unlike legacy banks and middleware providers, we have a direct connection to the Federal Reserve to facilitate the quickest transfers.",
|
||||
],
|
||||
image: {
|
||||
src: "/images/homepage/why-charter/2.svg",
|
||||
alt: "VAR Process Flow",
|
||||
width: 283,
|
||||
height: 45,
|
||||
},
|
||||
className: "md:col-span-2 flex flex-col justify-center",
|
||||
contentClassName:
|
||||
"flex items-center justify-center p-6 max-md:mt-4 max-md:mb-8",
|
||||
imagePosition: "content",
|
||||
},
|
||||
{
|
||||
title: "Unparalleled VAR flexibility",
|
||||
description: [
|
||||
"VAR has a reputation for taking too long. At Charter, we optimise for the fastest VAR transfers possible — often in a matter of hours.",
|
||||
],
|
||||
image: {
|
||||
src: "/images/homepage/why-charter/3.svg",
|
||||
alt: "VAR Process Diagram",
|
||||
width: 283,
|
||||
height: 45,
|
||||
},
|
||||
className: "md:col-span-2",
|
||||
headerClassName: "h-full",
|
||||
imagePosition: "header",
|
||||
},
|
||||
{
|
||||
title: "Unparalleled VAR flexibility",
|
||||
description: [
|
||||
"Unlike traditional banks and middleware, we connect directly with the Federal Reserve to ensure the fastest, most transparent transfers possible.",
|
||||
],
|
||||
image: {
|
||||
src: "/images/homepage/code-snippet.webp",
|
||||
alt: "Code snippet",
|
||||
type: "fill",
|
||||
},
|
||||
className: "overflow-hidden md:col-span-3 ",
|
||||
headerClassName: "",
|
||||
contentClassName:
|
||||
"relative h-[242px] mt-2 p-0 ml-8 w-full md:max-w-[400px] lg:max-w-[500px] overflow-hidden md:mx-auto shadow-xl rounded-t-2xl",
|
||||
},
|
||||
];
|
||||
|
||||
const FeaturedPosts = () => {
|
||||
return (
|
||||
<section
|
||||
id="featured-posts"
|
||||
className="container relative py-16 md:py-28 lg:py-32"
|
||||
>
|
||||
<h3 className="mini-title">Featured Posts</h3>
|
||||
|
||||
<div className="relative z-10 mt-8 grid grid-cols-1 gap-6 md:grid-cols-5">
|
||||
{items.map((item, index) => (
|
||||
<Card
|
||||
key={index}
|
||||
className={cn("col-span-1 shadow-xl", item.className)}
|
||||
>
|
||||
<CardHeader className={item.headerClassName}>
|
||||
{item.imagePosition === "header" && (
|
||||
<img
|
||||
src={item.image.src}
|
||||
alt={item.image.alt}
|
||||
width={item.image.width}
|
||||
height={item.image.height}
|
||||
className="flex-1 self-center max-md:mb-8 max-md:mt-4 dark:[filter:brightness(0)_saturate(100%)_invert(100%)]"
|
||||
/>
|
||||
)}
|
||||
<CardTitle className="text-3xl">{item.title}</CardTitle>
|
||||
{item.description.map((desc, i) => (
|
||||
<CardDescription
|
||||
key={i}
|
||||
className="mt-2 text-base font-medium leading-snug"
|
||||
>
|
||||
{desc}
|
||||
</CardDescription>
|
||||
))}
|
||||
</CardHeader>
|
||||
<CardContent className={item.contentClassName}>
|
||||
{item.image.type === "fill" ? (
|
||||
<img
|
||||
src={item.image.src}
|
||||
alt={item.image.alt}
|
||||
className="size-full object-cover object-left-top"
|
||||
/>
|
||||
) : (
|
||||
item.imagePosition === "content" && (
|
||||
<img
|
||||
src={item.image.src}
|
||||
alt={item.image.alt}
|
||||
width={item.image.width}
|
||||
height={item.image.height}
|
||||
className="self-center dark:[filter:brightness(0)_saturate(100%)_invert(100%)]"
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Background decoration */}
|
||||
<>
|
||||
<div className="absolute inset-0 isolate will-change-transform">
|
||||
<div className="bg-primary-gradient/28 absolute top-1/2 size-[700px] -translate-y-1/2 rounded-full blur-[300px]" />
|
||||
<div className="bg-secondary-gradient/16 absolute right-0 top-1/2 size-[700px] -translate-y-1/2 -rotate-12 rounded-full blur-[300px]" />
|
||||
<div className="bg-tertiary-gradient/6 absolute bottom-1/4 right-20 z-[1] h-[500px] w-[800px] -rotate-12 rounded-full blur-[100px] md:bottom-10" />
|
||||
</div>
|
||||
<div className="absolute -inset-x-20 top-0 [mask-image:radial-gradient(circle_at_center,black_0%,black_20%,transparent_85%)]">
|
||||
<PlusSigns className="text-foreground/[0.075] h-full w-full" />
|
||||
</div>
|
||||
</>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default FeaturedPosts;
|
||||
86
sites/vibes/src/components/sections/footer.tsx
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
import { FaXTwitter, FaLinkedin, FaFacebook } from "react-icons/fa6";
|
||||
import { Home } from "lucide-react";
|
||||
|
||||
const navigation = [
|
||||
{
|
||||
title: "Products",
|
||||
links: [
|
||||
{ name: "VAR", href: "/#code-security" },
|
||||
{ name: "Credit Transfers", href: "/#why-charter" },
|
||||
{ name: "Credit Accounts", href: "/#ai-chatbot" },
|
||||
{ name: "Loan Origination", href: "/#ai-chatbot" },
|
||||
{ name: "Loan Purchase", href: "/#ai-chatbot" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Support",
|
||||
links: [
|
||||
{ name: "Pricing", href: "/pricing" },
|
||||
{ name: "FAQ", href: "/faq" },
|
||||
{ name: "Demo", href: "/contact" },
|
||||
{ name: "Contact", href: "/contact" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Company",
|
||||
links: [
|
||||
{ name: "About", href: "/about" },
|
||||
{ name: "Terms of Service", href: "/terms" },
|
||||
{ name: "Privacy Policy", href: "/privacy" },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const socialLinks = [
|
||||
{ icon: FaXTwitter, href: "https://twitter.com", label: "Twitter" },
|
||||
{ icon: FaFacebook, href: "https://facebook.com", label: "Facebook" },
|
||||
{ icon: FaLinkedin, href: "https://linkedin.com", label: "LinkedIn" },
|
||||
];
|
||||
|
||||
export default function Footer() {
|
||||
return (
|
||||
<footer className="pt-16 md:pt-28 lg:pt-32">
|
||||
<div className="w-full px-4 md:px-6 lg:px-8">
|
||||
<div className="mx-auto max-w-5xl">
|
||||
{/* Navigation Section */}
|
||||
<nav className="flex flex-wrap justify-between gap-x-32 gap-y-20 border-b pb-14 lg:pb-20">
|
||||
|
||||
</nav>
|
||||
|
||||
{/* Bottom Section */}
|
||||
<div className="py-8">
|
||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-4">
|
||||
<Home className="h-6 w-6" />
|
||||
<p className="text-sm font-medium">
|
||||
© {new Date().getFullYear()} Charter -{" "}
|
||||
<a
|
||||
href="https://shadcnblocks.com"
|
||||
className="underline transition-opacity hover:opacity-80"
|
||||
target="_blank"
|
||||
>
|
||||
Shadcnblocks.com
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-6">
|
||||
{socialLinks.map((link) => (
|
||||
<a
|
||||
aria-label={link.label}
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
className="hover:text-muted-foreground"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<link.icon />
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
130
sites/vibes/src/components/sections/hero.tsx
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
import {
|
||||
ChevronRight,
|
||||
Wallet,
|
||||
Waypoints,
|
||||
Building2,
|
||||
ArrowLeftRight,
|
||||
} from "lucide-react";
|
||||
|
||||
import { PlusSigns } from "../icons/plus-signs";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
|
||||
const features = [
|
||||
{
|
||||
id: "var",
|
||||
title: "Virtual Account",
|
||||
description: "Sed do eiusmod tempor incididunt ut labore",
|
||||
icon: Wallet,
|
||||
image: "/images/homepage/hero/1.webp",
|
||||
},
|
||||
{
|
||||
id: "wire",
|
||||
title: "Wire",
|
||||
description: "Sed do eiusmod tempor incididunt ut labore",
|
||||
icon: Waypoints,
|
||||
image: "/images/homepage/hero/2.webp",
|
||||
},
|
||||
{
|
||||
id: "bank-accounts",
|
||||
title: "Bank Accounts",
|
||||
description: "Sed do eiusmod tempor incididunt ut labore",
|
||||
icon: Building2,
|
||||
image: "/images/homepage/hero/3.webp",
|
||||
},
|
||||
{
|
||||
id: "bank-transfers",
|
||||
title: "Bank Transfers",
|
||||
description: "Sed do eiusmod tempor incididunt ut labore",
|
||||
icon: ArrowLeftRight,
|
||||
image: "/images/homepage/hero/4.webp",
|
||||
},
|
||||
];
|
||||
|
||||
export default function Hero() {
|
||||
return (
|
||||
<section className="bg-background relative overflow-hidden pt-16 md:pt-28 lg:pt-32">
|
||||
<div className="relative z-10">
|
||||
<div className="container max-w-5xl text-center">
|
||||
<h1 className="text-4xl font-semibold tracking-tighter md:text-5xl lg:text-6xl">
|
||||
Secure. Composable. Bankable.
|
||||
</h1>
|
||||
<p className="text-muted-foreground font-mona mt-4 text-balance text-2xl md:text-3xl">
|
||||
Charter is the fit-for-purpose developer API for building robust,
|
||||
encrypted finance products.
|
||||
</p>
|
||||
<div className="mt-7">
|
||||
<Button asChild size="lg">
|
||||
<a href="/signup">
|
||||
Start building for free
|
||||
<ChevronRight className="size-4" />
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{features.map((feature) => (
|
||||
<img
|
||||
key={feature.id}
|
||||
src={feature.image}
|
||||
alt={feature.title}
|
||||
className="lg:translate-y-15 hidden size-full translate-y-8 object-contain"
|
||||
/>
|
||||
))}
|
||||
<Tabs defaultValue="var" className="mt-8 md:mt-12 lg:mt-20">
|
||||
{/* Tab Content */}
|
||||
<div className="container">
|
||||
{features.map((feature) => (
|
||||
<TabsContent
|
||||
key={feature.id}
|
||||
value={feature.id}
|
||||
className="relative aspect-[2.116/1] overflow-hidden"
|
||||
>
|
||||
<img
|
||||
src={feature.image}
|
||||
alt={feature.title}
|
||||
className="lg:translate-y-15 size-full translate-y-8 object-contain"
|
||||
/>
|
||||
</TabsContent>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Tab Triggers */}
|
||||
<div className="bg-background pb-16 pt-12 md:pb-28 lg:pb-32">
|
||||
<TabsList className="mx-auto flex h-auto max-w-4xl justify-start gap-4 overflow-x-auto bg-transparent max-lg:px-5">
|
||||
{features.map((feature) => (
|
||||
<TabsTrigger
|
||||
key={feature.id}
|
||||
value={feature.id}
|
||||
className="ring-secondary-foreground group min-w-[200px] flex-1 justify-start whitespace-normal rounded-lg px-4 py-3 text-start transition-colors duration-300 data-[state=active]:bg-transparent data-[state=active]:ring lg:px-6 lg:py-4"
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<div className="bg-muted-foreground/40 group-data-[state=active]:bg-secondary-foreground flex size-8 items-center justify-center rounded-md p-1.5">
|
||||
<feature.icon className="stroke-background" />
|
||||
</div>
|
||||
<h2 className="group-data-[state=active]:text-primary font-inter text-foreground mt-2 text-lg font-bold">
|
||||
{feature.title}
|
||||
</h2>
|
||||
<p className="text-muted-foreground mt-1 text-sm">
|
||||
{feature.description}
|
||||
</p>
|
||||
</div>
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
</div>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
{/* Background decoration */}
|
||||
<div className="absolute inset-0 aspect-square [mask-image:radial-gradient(circle_at_center,black_0%,black_20%,transparent_75%)]">
|
||||
<PlusSigns className="text-foreground/[0.05] h-full w-full" />
|
||||
</div>
|
||||
<div className="">
|
||||
{/* will-change-transform improves performance on scroll on safari because of the high blur */}
|
||||
<div className="bg-primary-gradient/11 absolute inset-x-[0%] bottom-0 left-0 h-[500px] rounded-full blur-[100px] will-change-transform md:h-[950px]" />
|
||||
<div className="bg-secondary-gradient/9 absolute inset-x-[30%] bottom-0 right-0 h-[500px] rounded-full blur-[100px] will-change-transform md:h-[950px]" />
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
61
sites/vibes/src/components/sections/join-us.tsx
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import { Button } from "../ui/button";
|
||||
|
||||
const JoinUs = () => {
|
||||
const jobCategories = [
|
||||
{
|
||||
name: "Engineering",
|
||||
jobs: [
|
||||
{ title: "iOS Developer", location: "Remote" },
|
||||
{ title: "Backend Engineer", location: "Remote" },
|
||||
{ title: "Frontend Engineer", location: "Remote" },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Design",
|
||||
jobs: [
|
||||
{ title: "Senior Designer", location: "Remote" },
|
||||
{ title: "Staff Designer", location: "Remote" },
|
||||
{ title: "Designer", location: "Remote" },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<section className="lg:py-15 container max-w-5xl py-10 md:py-12">
|
||||
<div className="border-t pt-5">
|
||||
<div className="max-w-2xl">
|
||||
<h2 className="text-4xl font-semibold tracking-tight md:text-4xl">
|
||||
Join us
|
||||
</h2>
|
||||
<p className="text-muted-foreground font-mona lg:pb-15 mt-4 max-w-2xl pb-10 text-2xl md:pb-12 md:text-3xl">
|
||||
We work together from all over the world.
|
||||
</p>
|
||||
|
||||
{jobCategories.map((category, categoryIndex) => (
|
||||
<div key={categoryIndex}>
|
||||
<h3 className="border-foreground border-b py-6 text-lg font-semibold">
|
||||
{category.name}
|
||||
</h3>
|
||||
<div className="">
|
||||
{category.jobs.map((job, jobIndex) => (
|
||||
<div
|
||||
key={`${categoryIndex}-${jobIndex}`}
|
||||
className="flex items-center justify-between gap-10 border-b py-3 md:gap-16 lg:gap-28"
|
||||
>
|
||||
<h4 className="flex-1 font-medium">{job.title}</h4>
|
||||
<p className="text-muted-foreground">{job.location}</p>
|
||||
<Button variant="outline" asChild>
|
||||
<a href="#">View listing</a>
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default JoinUs;
|
||||
110
sites/vibes/src/components/sections/latest-posts.tsx
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
import { PlusSigns } from "@/components/icons/plus-signs";
|
||||
import Testimonials from "@/components/sections/testimonials";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "@/components/ui/accordion";
|
||||
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
||||
|
||||
const FAQ_ITEMS = [
|
||||
{
|
||||
category: "SUPPORT",
|
||||
items: [
|
||||
{
|
||||
question: "Is there a free version?",
|
||||
answer:
|
||||
"Yes! We offer a generous free plan with just enough features except that one feature you really want! Our strategy is to get your credit card details on file then steadily double our prices against inflation rates.",
|
||||
},
|
||||
{
|
||||
question: "How do I update my account without breaking my laptop?",
|
||||
answer:
|
||||
"Our platform is designed with safety in mind. You can update your account settings through our intuitive dashboard without any risk to your hardware. We have multiple safeguards in place to prevent any system conflicts.",
|
||||
},
|
||||
{
|
||||
question: "Is support free, or do I need to Google everything?",
|
||||
answer:
|
||||
"We provide comprehensive support at no additional cost. Our dedicated support team is available 24/7 to help you with any questions or issues you might encounter. No need to rely on Google - we're here to help!",
|
||||
},
|
||||
{
|
||||
question: "Are you going to be subsumed by AI?",
|
||||
answer:
|
||||
"While we embrace AI technology to enhance our services, we maintain a strong human element in our operations. Our team works alongside AI to provide the best possible service while ensuring human oversight and decision-making remain central to our operations.",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
category: "YOUR ACCOUNT",
|
||||
items: [
|
||||
{
|
||||
question: "Is support free, or do I need to Google everything?",
|
||||
answer:
|
||||
"We provide comprehensive support at no additional cost. Our dedicated support team is available 24/7 to help you with any questions or issues you might encounter. No need to rely on Google - we're here to help!",
|
||||
},
|
||||
{
|
||||
question: "Are you going to be subsumed by AI?",
|
||||
answer:
|
||||
"While we embrace AI technology to enhance our services, we maintain a strong human element in our operations. Our team works alongside AI to provide the best possible service while ensuring human oversight and decision-making remain central to our operations.",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
category: "OTHER QUESTIONS",
|
||||
items: [
|
||||
{
|
||||
question: "Is support free, or do I need to Google everything?",
|
||||
answer:
|
||||
"We provide comprehensive support at no additional cost. Our dedicated support team is available 24/7 to help you with any questions or issues you might encounter. No need to rely on Google - we're here to help!",
|
||||
},
|
||||
{
|
||||
question: "Are you going to be subsumed by AI?",
|
||||
answer:
|
||||
"While we embrace AI technology to enhance our services, we maintain a strong human element in our operations. Our team works alongside AI to provide the best possible service while ensuring human oversight and decision-making remain central to our operations.",
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export default function LatestPosts() {
|
||||
return (
|
||||
<>
|
||||
<section className="relative py-16 md:py-28 lg:py-32">
|
||||
<div className="container">
|
||||
<div className="mx-auto mt-8 max-w-2xl space-y-12 md:mt-12 lg:mt-20">
|
||||
{FAQ_ITEMS.map((category) => (
|
||||
<Card key={category.category} className="border-hidden">
|
||||
<CardHeader className="pb-0">
|
||||
<h3 className="text-accent-foreground border-b pb-4 font-mono text-sm font-medium uppercase tracking-widest">
|
||||
{category.category}
|
||||
</h3>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Accordion type="single" collapsible className="w-full">
|
||||
{category.items.map((item, i) => (
|
||||
<AccordionItem
|
||||
key={i}
|
||||
value={`${category.category}-${i}`}
|
||||
className="border-muted border-b last:border-0"
|
||||
>
|
||||
<AccordionTrigger className="text-base font-medium hover:no-underline">
|
||||
{item.question}
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="text-muted-foreground text-base font-medium">
|
||||
{item.answer}
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
))}
|
||||
</Accordion>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute -inset-40 z-[-1] [mask-image:radial-gradient(circle_at_center,black_0%,black_20%,transparent_75%)]">
|
||||
<PlusSigns className="text-foreground/[0.05] h-full w-full" />
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
86
sites/vibes/src/components/sections/launch-pricing.tsx
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
import { ChevronRight, Check } from "lucide-react";
|
||||
|
||||
import { PlusSigns } from "../icons/plus-signs";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
export default function LaunchPricing() {
|
||||
return (
|
||||
<section
|
||||
id="launch-today"
|
||||
className="text-background py-16 md:container md:max-w-5xl md:py-28 lg:py-32"
|
||||
>
|
||||
<div className="from-primary-900 to-primary/90 divide-background/20 bg-linear-to-r container relative isolate grid items-center overflow-hidden py-8 max-lg:gap-10 max-md:gap-6 md:rounded-3xl lg:grid-cols-2 lg:divide-x lg:px-8">
|
||||
<div className="absolute inset-0 -z-10 [mask-image:linear-gradient(to_left,black_50%,transparent_100%)]">
|
||||
<PlusSigns className="text-background/[0.05] h-full w-full" />
|
||||
</div>
|
||||
<div className="lg:py-16 lg:pr-20">
|
||||
<h2 className="text-3xl font-semibold tracking-tight md:text-4xl lg:text-5xl">
|
||||
Launch today
|
||||
</h2>
|
||||
<p className="text-background/70 mt-3 text-sm font-medium">
|
||||
In the past, new financial companies had to rely on expensive
|
||||
middleware that linked them to outdated sponsor bank systems,
|
||||
restricting their potential. Our API solves this today.
|
||||
</p>
|
||||
<div className="mt-8 flex flex-wrap gap-4 max-md:hidden">
|
||||
<Button size="lg" variant="secondary" className="group" asChild>
|
||||
<a href="/signup">
|
||||
Start for free
|
||||
<ChevronRight className="ml-1 size-4 transition-transform group-hover:translate-x-0.5" />
|
||||
</a>
|
||||
</Button>
|
||||
<Button size="lg" className="bg-secondary-foreground group" asChild>
|
||||
<a href="/contact">
|
||||
Get a demo
|
||||
<ChevronRight className="ml-1 size-4 transition-transform group-hover:translate-x-0.5" />
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6 lg:py-10 lg:pl-20">
|
||||
<div>
|
||||
<h3 className="text-background text-3xl font-semibold md:text-4xl lg:text-5xl">
|
||||
$29.99
|
||||
</h3>
|
||||
<p className="text-background/70 mt-1 text-xl font-medium">
|
||||
per user per month
|
||||
</p>
|
||||
</div>
|
||||
<ul className="text-background/70 space-y-3 text-sm">
|
||||
<li className="flex items-center gap-2">
|
||||
<Check className="size-4" />
|
||||
All free plan features and...
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<Check className="size-4" />
|
||||
Mainline AI
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<Check className="size-4" />
|
||||
Unlimited teams
|
||||
</li>
|
||||
</ul>
|
||||
<div className="mt-10 flex flex-wrap gap-4 md:hidden">
|
||||
<Button size="lg" variant="secondary" className="group w-full">
|
||||
<a href="/signup" className="flex items-center gap-2">
|
||||
Start building for free
|
||||
<ChevronRight className="ml-1 size-4 transition-transform group-hover:translate-x-0.5" />
|
||||
</a>
|
||||
</Button>
|
||||
<Button
|
||||
size="lg"
|
||||
className="bg-secondary-foreground border-background/20 group w-full border"
|
||||
>
|
||||
<a href="/contact" className="flex items-center gap-2">
|
||||
Get a demo
|
||||
<ChevronRight className="ml-1 size-4 transition-transform group-hover:translate-x-0.5" />
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
75
sites/vibes/src/components/sections/login-section.tsx
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
import { Card, CardContent, CardHeader } from "../ui/card";
|
||||
import { Input } from "../ui/input";
|
||||
import { Button } from "../ui/button";
|
||||
import { FcGoogle } from "react-icons/fc";
|
||||
import { Checkbox } from "../ui/checkbox";
|
||||
|
||||
const LoginSection = () => {
|
||||
return (
|
||||
<section className="bg-sand-100 py-16 md:py-28 lg:py-32">
|
||||
<div className="container">
|
||||
<div className="flex flex-col gap-4">
|
||||
<Card className="mx-auto w-full max-w-sm">
|
||||
<CardHeader className="flex flex-col items-center space-y-0">
|
||||
<img
|
||||
src="/images/layout/logo.svg"
|
||||
alt="logo"
|
||||
width={94}
|
||||
height={18}
|
||||
className="mb-7 dark:invert"
|
||||
/>
|
||||
<p className="mb-2 text-2xl font-bold">Welcome back</p>
|
||||
<p className="text-muted-foreground">
|
||||
Please enter your details.
|
||||
</p>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid gap-4">
|
||||
<Input type="email" placeholder="Enter your email" required />
|
||||
<div>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="Enter your password"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="remember"
|
||||
className="border-muted-foreground"
|
||||
/>
|
||||
<label
|
||||
htmlFor="remember"
|
||||
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
>
|
||||
Remember me
|
||||
</label>
|
||||
</div>
|
||||
<a href="#" className="text-primary text-sm font-medium">
|
||||
Forgot password
|
||||
</a>
|
||||
</div>
|
||||
<Button type="submit" className="mt-2 w-full">
|
||||
Create an account
|
||||
</Button>
|
||||
<Button variant="outline" className="w-full">
|
||||
<FcGoogle className="mr-2 size-5" />
|
||||
Sign up with Google
|
||||
</Button>
|
||||
</div>
|
||||
<div className="text-muted-foreground mx-auto mt-8 flex justify-center gap-1 text-sm">
|
||||
<p>Don't have an account?</p>
|
||||
<a href="/signup" className="text-primary font-medium">
|
||||
Sign up
|
||||
</a>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoginSection;
|
||||
111
sites/vibes/src/components/sections/logos.tsx
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
"use client";
|
||||
|
||||
const ITEMS = [
|
||||
{
|
||||
name: "Mercury",
|
||||
src: "/images/logos/mercury.svg",
|
||||
width: 143,
|
||||
height: 26,
|
||||
href: "https://mercury.com",
|
||||
},
|
||||
{
|
||||
name: "Watershed",
|
||||
src: "/images/logos/watershed.svg",
|
||||
width: 154,
|
||||
height: 31,
|
||||
href: "https://watershed.com",
|
||||
},
|
||||
{
|
||||
name: "Retool",
|
||||
src: "/images/logos/retool.svg",
|
||||
width: 113,
|
||||
height: 22,
|
||||
href: "https://retool.com",
|
||||
},
|
||||
{
|
||||
name: "Descript",
|
||||
src: "/images/logos/descript.svg",
|
||||
width: 112,
|
||||
height: 27,
|
||||
href: "https://descript.com",
|
||||
},
|
||||
{
|
||||
name: "Perplexity",
|
||||
src: "/images/logos/perplexity.svg",
|
||||
width: 141,
|
||||
height: 32,
|
||||
href: "https://perplexity.ai",
|
||||
},
|
||||
{
|
||||
name: "Monzo",
|
||||
src: "/images/logos/monzo.svg",
|
||||
width: 104,
|
||||
height: 18,
|
||||
href: "https://monzo.com",
|
||||
},
|
||||
{
|
||||
name: "Ramp",
|
||||
src: "/images/logos/ramp.svg",
|
||||
width: 105,
|
||||
height: 28,
|
||||
href: "https://ramp.com",
|
||||
},
|
||||
{
|
||||
name: "Raycast",
|
||||
src: "/images/logos/raycast.svg",
|
||||
width: 128,
|
||||
height: 33,
|
||||
href: "https://raycast.com",
|
||||
},
|
||||
{
|
||||
name: "Arc",
|
||||
src: "/images/logos/arc.svg",
|
||||
width: 90,
|
||||
height: 28,
|
||||
href: "https://arc.com",
|
||||
},
|
||||
];
|
||||
|
||||
export default function Logos() {
|
||||
return (
|
||||
<section className="overflow-hidden">
|
||||
<h2 className="text-muted-foreground text-center text-2xl">
|
||||
From next-gen startups to established enterprises.
|
||||
</h2>
|
||||
|
||||
<div className="relative mt-10 flex w-full">
|
||||
<div className="from-background bg-linear-to-r absolute left-0 z-20 h-full w-10 to-transparent" />
|
||||
<div className="from-background bg-linear-to-l absolute right-0 z-20 h-full w-10 to-transparent" />
|
||||
|
||||
{/* First marquee group */}
|
||||
<div className="animate-marquee flex shrink-0 items-center gap-12">
|
||||
{ITEMS.map((logo, index) => (
|
||||
<a href={logo.href} target="_blank" key={index} className="p-6">
|
||||
<img
|
||||
src={logo.src}
|
||||
alt={logo.name}
|
||||
width={logo.width}
|
||||
height={logo.height}
|
||||
className="object-contain transition-opacity hover:opacity-70"
|
||||
/>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
{/* Second marquee group */}
|
||||
<div className="animate-marquee flex shrink-0 items-center gap-12">
|
||||
{ITEMS.map((logo, index) => (
|
||||
<a href={logo.href} target="_blank" key={index} className="p-6">
|
||||
<img
|
||||
src={logo.src}
|
||||
alt={logo.name}
|
||||
width={logo.width}
|
||||
height={logo.height}
|
||||
className="object-contain transition-opacity hover:opacity-70"
|
||||
/>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
133
sites/vibes/src/components/sections/navbar.tsx
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
import React, { useState, useEffect } from "react";
|
||||
|
||||
import { ChevronRight, Menu, X, Home } from "lucide-react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
NavigationMenu,
|
||||
NavigationMenuContent,
|
||||
NavigationMenuItem,
|
||||
NavigationMenuLink,
|
||||
NavigationMenuList,
|
||||
NavigationMenuTrigger,
|
||||
} from "@/components/ui/navigation-menu";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ThemeToggle } from "../elements/theme-toggle";
|
||||
|
||||
const Navbar = ({ currentPage }: { currentPage: string }) => {
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
const pathname = currentPage;
|
||||
|
||||
useEffect(() => {
|
||||
if (isMenuOpen) {
|
||||
document.body.classList.add("overflow-hidden");
|
||||
} else {
|
||||
document.body.classList.remove("overflow-hidden");
|
||||
}
|
||||
|
||||
// Cleanup on unmount
|
||||
return () => {
|
||||
document.body.classList.remove("overflow-hidden");
|
||||
};
|
||||
}, [isMenuOpen]);
|
||||
|
||||
const ITEMS = [
|
||||
{ label: "About", href: "/about" },
|
||||
{ label: "Latest", href: "/latest" },
|
||||
];
|
||||
|
||||
return (
|
||||
<header className={"relative z-50"}>
|
||||
<div className="container max-w-5xl lg:pt-10">
|
||||
<div className="flex items-center justify-between py-3">
|
||||
{/* Logo */}
|
||||
<a href="/" className="flex items-center gap-2">
|
||||
<Home className="h-8 w-8" />
|
||||
</a>
|
||||
|
||||
{/* Desktop Navigation */}
|
||||
<NavigationMenu className="hidden items-center gap-8 lg:flex">
|
||||
<NavigationMenuList>
|
||||
{ITEMS.map((link) =>
|
||||
<NavigationMenuItem key={link.label}>
|
||||
<a
|
||||
href={link.href}
|
||||
className={cn(
|
||||
"hover:text-accent-foreground p-2 lg:text-base",
|
||||
pathname === link.href && "text-accent-foreground",
|
||||
)}
|
||||
>
|
||||
{link.label}
|
||||
</a>
|
||||
</NavigationMenuItem>
|
||||
)}
|
||||
</NavigationMenuList>
|
||||
</NavigationMenu>
|
||||
|
||||
{/* Auth Buttons */}
|
||||
<div className="flex items-center gap-2.5">
|
||||
|
||||
<div
|
||||
className={`transition-opacity duration-300 ${isMenuOpen ? "max-lg:pointer-events-none max-lg:opacity-0" : "opacity-100"}`}
|
||||
>
|
||||
<ThemeToggle className="dark:bg-foreground dark:text-background" />
|
||||
</div>
|
||||
|
||||
{/* Hamburger Menu Button (Mobile Only) */}
|
||||
<button
|
||||
className="relative flex size-8 lg:hidden"
|
||||
onClick={() => setIsMenuOpen(!isMenuOpen)}
|
||||
>
|
||||
<span className="sr-only">Open main menu</span>
|
||||
<div className="absolute left-1/2 top-1/2 block w-[18px] -translate-x-1/2 -translate-y-1/2">
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={`absolute block h-0.5 w-full rounded-full bg-current transition duration-500 ease-in-out ${isMenuOpen ? "rotate-45" : "-translate-y-1.5"}`}
|
||||
></span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={`absolute block h-0.5 w-full rounded-full bg-current transition duration-500 ease-in-out ${isMenuOpen ? "opacity-0" : ""}`}
|
||||
></span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={`absolute block h-0.5 w-full rounded-full bg-current transition duration-500 ease-in-out ${isMenuOpen ? "-rotate-45" : "translate-y-1.5"}`}
|
||||
></span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile Menu Overlay */}
|
||||
<div
|
||||
className={cn(
|
||||
"bg-background container absolute inset-0 top-full flex h-[calc(100vh-64px)] flex-col transition-all duration-300 ease-in-out lg:hidden",
|
||||
isMenuOpen
|
||||
? "visible translate-x-0 opacity-100"
|
||||
: "invisible translate-x-full opacity-0",
|
||||
)}
|
||||
>
|
||||
|
||||
<nav className="mt-3 flex flex-1 flex-col gap-6">
|
||||
{ITEMS.map((link) =>
|
||||
<a
|
||||
key={link.label}
|
||||
href={link.href}
|
||||
className={cn(
|
||||
"text-lg tracking-[-0.36px]",
|
||||
pathname === link.href && "text-muted-foreground",
|
||||
)}
|
||||
onClick={() => setIsMenuOpen(false)}
|
||||
>
|
||||
{link.label}
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
|
||||
export default Navbar;
|
||||
64
sites/vibes/src/components/sections/newsletter-signup.tsx
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
import React from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Mail } from "lucide-react";
|
||||
|
||||
const NewsletterSignup = () => {
|
||||
const [email, setEmail] = React.useState("");
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
// This would be connected to your newsletter service
|
||||
console.log("Submitting email:", email);
|
||||
alert("Thanks for subscribing!");
|
||||
setEmail("");
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="bg-muted/50 border-t py-16 md:py-24">
|
||||
<div className="container max-w-5xl">
|
||||
<div className="bg-card flex flex-col items-center justify-between gap-8 rounded-2xl border p-8 shadow-sm md:p-12 lg:flex-row">
|
||||
<div className="max-w-md text-center lg:text-left">
|
||||
<div className="mb-3 flex justify-center lg:justify-start">
|
||||
<div className="bg-primary/10 text-primary flex h-10 w-10 items-center justify-center rounded-full">
|
||||
<Mail className="h-5 w-5" />
|
||||
</div>
|
||||
</div>
|
||||
<h2 className="mb-3 text-2xl font-bold md:text-3xl">
|
||||
Subscribe to our newsletter
|
||||
</h2>
|
||||
<p className="text-muted-foreground mb-0 text-base">
|
||||
Stay updated with the latest articles, tutorials, and insights
|
||||
from our team. We'll never spam your inbox.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="w-full max-w-md">
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
className="flex flex-col gap-2 sm:flex-row"
|
||||
>
|
||||
<Input
|
||||
type="email"
|
||||
placeholder="Enter your email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
required
|
||||
className="h-12"
|
||||
/>
|
||||
<Button type="submit" className="h-12">
|
||||
Subscribe
|
||||
</Button>
|
||||
</form>
|
||||
<p className="text-muted-foreground mt-2 text-xs">
|
||||
By subscribing, you agree to our Privacy Policy and consent to
|
||||
receive updates from our company.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default NewsletterSignup;
|
||||
187
sites/vibes/src/components/sections/pricing-cards.tsx
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
import { Check, ChevronRight } from "lucide-react";
|
||||
|
||||
import { PlusSigns } from "../icons/plus-signs";
|
||||
import { Button } from "../ui/button";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type PricingTier = {
|
||||
name: string;
|
||||
price: string;
|
||||
description: string;
|
||||
features: string[];
|
||||
cta: {
|
||||
text: string;
|
||||
href: string;
|
||||
};
|
||||
};
|
||||
|
||||
const ITEMS: PricingTier[] = [
|
||||
{
|
||||
name: "STARTER",
|
||||
price: "$0",
|
||||
description: "Free for everyone",
|
||||
features: ["Unlimited members", "250 transactions", "No support"],
|
||||
cta: {
|
||||
text: "Start for free",
|
||||
href: "/signup",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "BASIC",
|
||||
price: "$29.99",
|
||||
description: "per user per month",
|
||||
features: [
|
||||
"All free plan features and...",
|
||||
"Mainline AI",
|
||||
"Unlimited teams",
|
||||
],
|
||||
cta: {
|
||||
text: "7 days free",
|
||||
href: "/signup",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ENTERPRISE",
|
||||
price: "$ENT",
|
||||
description: "Custom pricing",
|
||||
features: [
|
||||
"All basic plan features and...",
|
||||
"Advanced security controls",
|
||||
"Migration support",
|
||||
],
|
||||
cta: {
|
||||
text: "Book a demo",
|
||||
href: "/contact",
|
||||
},
|
||||
},
|
||||
];
|
||||
const PricingCards = () => {
|
||||
return (
|
||||
<section className="relative py-16 md:py-28 lg:py-32">
|
||||
<div className="container">
|
||||
<h1 className="text-center text-4xl font-semibold tracking-tight md:text-5xl lg:text-6xl">
|
||||
Pricing
|
||||
</h1>
|
||||
<div className="mx-auto mt-4 max-w-[45rem] space-y-2 text-center">
|
||||
<p className="text-muted-foreground text-2xl md:text-3xl">
|
||||
Use Charter for free with your whole team. Upgrade to enable
|
||||
enhanced features.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="relative mt-8 md:mt-12 lg:mt-20">
|
||||
{/* Background and layout wrapper */}
|
||||
<div className="from-primary-900 to-primary/90 bg-linear-to-r absolute inset-0 isolate hidden rounded-3xl md:block">
|
||||
<div className="absolute inset-0 -z-10 [mask-image:linear-gradient(to_left,black_50%,transparent_100%)]">
|
||||
<PlusSigns className="h-full w-full text-white/[0.05]" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="md:divide-background/20 relative space-y-6 md:grid md:grid-cols-3 md:space-y-0 md:divide-x md:p-6 lg:p-8">
|
||||
{ITEMS.map((tier, index) => (
|
||||
<PricingCard
|
||||
key={tier.name}
|
||||
tier={tier}
|
||||
isHighlighted={index === 1}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute -inset-40 z-[-1] [mask-image:radial-gradient(circle_at_center,black_0%,black_20%,transparent_75%)]">
|
||||
<PlusSigns className="text-foreground/[0.05] h-full w-full" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default PricingCards;
|
||||
function PricingCard({
|
||||
tier,
|
||||
isHighlighted,
|
||||
}: {
|
||||
tier: PricingTier;
|
||||
isHighlighted: boolean;
|
||||
}) {
|
||||
const styles = {
|
||||
card: cn(
|
||||
"flex flex-col gap-6 rounded-xl p-6 sm:rounded-2xl md:rounded-none lg:p-8",
|
||||
// Mobile styles
|
||||
isHighlighted
|
||||
? "max-md:from-primary-900 max-md:to-primary/90 max-md:bg-linear-to-r"
|
||||
: "bg-background max-md:border",
|
||||
// Desktop styles
|
||||
"md:bg-transparent",
|
||||
),
|
||||
title: cn(
|
||||
"font-mono text-sm tracking-widest",
|
||||
// Mobile styles
|
||||
isHighlighted ? "text-background/70" : "text-foreground/70",
|
||||
// Desktop styles
|
||||
"md:text-background/70",
|
||||
),
|
||||
price: cn(
|
||||
"text-5xl font-semibold tracking-tight",
|
||||
// Mobile styles
|
||||
isHighlighted ? "text-background" : "text-foreground",
|
||||
// Desktop styles
|
||||
"md:text-background",
|
||||
),
|
||||
description: cn(
|
||||
"mt-2 text-xl font-medium",
|
||||
// Mobile styles
|
||||
isHighlighted ? "text-background/70" : "text-foreground/70",
|
||||
// Desktop styles
|
||||
"md:text-background/70",
|
||||
),
|
||||
features: cn(
|
||||
"space-y-3 text-sm",
|
||||
// Mobile styles
|
||||
isHighlighted ? "text-background/70" : "text-foreground/70",
|
||||
// Desktop styles
|
||||
"md:text-background/70",
|
||||
),
|
||||
button: cn(
|
||||
"group border-foreground/20 relative w-full",
|
||||
// inset shadow
|
||||
"after:from-border after:via-border after:absolute after:inset-0 after:bg-linear-to-t after:to-transparent after:content-[''] after:group-hover:opacity-100",
|
||||
// Desktop styles
|
||||
"md:border-background/40 md:text-background md:bg-transparent",
|
||||
isHighlighted &&
|
||||
"md:bg-background md:text-primary hover:md:bg-background/90",
|
||||
),
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.card}>
|
||||
<h3 className={styles.title}>{tier.name}</h3>
|
||||
<div>
|
||||
<p className={styles.price}>{tier.price}</p>
|
||||
<p className={styles.description}>{tier.description}</p>
|
||||
</div>
|
||||
<ul className={styles.features}>
|
||||
{tier.features.map((feature) => (
|
||||
<li key={feature} className="flex items-center gap-2">
|
||||
<Check className="size-4 shrink-0" />
|
||||
<span>{feature}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<div className="flex flex-1 items-end">
|
||||
<Button
|
||||
asChild
|
||||
variant={isHighlighted ? "secondary" : "outline"}
|
||||
size="lg"
|
||||
className={styles.button}
|
||||
>
|
||||
<a href={tier.cta.href}>
|
||||
{tier.cta.text}
|
||||
<ChevronRight className="ml-1 size-4 transition-transform group-hover:translate-x-0.5" />
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
304
sites/vibes/src/components/sections/pricing-table.tsx
Normal file
|
|
@ -0,0 +1,304 @@
|
|||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
import { Check, ChevronsUpDown } from "lucide-react";
|
||||
|
||||
import { Button } from "../ui/button";
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from "../ui/collapsible";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type PlanType = "free" | "startup" | "enterprise";
|
||||
|
||||
interface Plan {
|
||||
name: string;
|
||||
type: PlanType;
|
||||
button: {
|
||||
text: string;
|
||||
variant: "outline";
|
||||
href: string;
|
||||
};
|
||||
features: {
|
||||
[Category in "usage" | "features" | "support"]: {
|
||||
name: string;
|
||||
value: string | boolean;
|
||||
}[];
|
||||
};
|
||||
}
|
||||
|
||||
const plans: Plan[] = [
|
||||
{
|
||||
name: "Free",
|
||||
type: "free",
|
||||
button: {
|
||||
text: "Get started",
|
||||
variant: "outline",
|
||||
href: "/signup",
|
||||
},
|
||||
features: {
|
||||
usage: [
|
||||
{ name: "Members", value: "Unlimited" },
|
||||
{ name: "Transactions", value: "250" },
|
||||
{ name: "Teams", value: "2" },
|
||||
],
|
||||
features: [
|
||||
{ name: "Reporting", value: true },
|
||||
{ name: "Analytics", value: true },
|
||||
{ name: "Import and export", value: true },
|
||||
{ name: "Integrations", value: true },
|
||||
{ name: "Mainline AI", value: false },
|
||||
{ name: "Admin roles", value: false },
|
||||
{ name: "Audit log", value: false },
|
||||
],
|
||||
support: [
|
||||
{ name: "Priority Support", value: true },
|
||||
{ name: "Account Manager", value: false },
|
||||
{ name: "Uptime SLA", value: false },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Basic",
|
||||
type: "startup",
|
||||
button: {
|
||||
text: "Get started",
|
||||
variant: "outline",
|
||||
href: "/signup",
|
||||
},
|
||||
features: {
|
||||
usage: [
|
||||
{ name: "Members", value: "Unlimited" },
|
||||
{ name: "Transactions", value: "Unlimited" },
|
||||
{ name: "Teams", value: "Unlimited" },
|
||||
],
|
||||
features: [
|
||||
{ name: "Reporting", value: true },
|
||||
{ name: "Analytics", value: true },
|
||||
{ name: "Import and export", value: true },
|
||||
{ name: "Integrations", value: true },
|
||||
{ name: "Mainline AI", value: true },
|
||||
{ name: "Admin roles", value: false },
|
||||
{ name: "Audit log", value: false },
|
||||
],
|
||||
support: [
|
||||
{ name: "Priority Support", value: true },
|
||||
{ name: "Account Manager", value: false },
|
||||
{ name: "Uptime SLA", value: false },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Ent",
|
||||
type: "enterprise",
|
||||
button: {
|
||||
text: "Get a demo",
|
||||
variant: "outline",
|
||||
href: "/contact",
|
||||
},
|
||||
features: {
|
||||
usage: [
|
||||
{ name: "Members", value: "Unlimited" },
|
||||
{ name: "Transactions", value: "Unlimited" },
|
||||
{ name: "Teams", value: "Unlimited" },
|
||||
],
|
||||
features: [
|
||||
{ name: "Reporting", value: true },
|
||||
{ name: "Analytics", value: true },
|
||||
{ name: "Import and export", value: true },
|
||||
{ name: "Integrations", value: true },
|
||||
{ name: "Mainline AI", value: true },
|
||||
{ name: "Admin roles", value: false },
|
||||
{ name: "Audit log", value: false },
|
||||
],
|
||||
support: [
|
||||
{ name: "Priority Support", value: true },
|
||||
{ name: "Account Manager", value: true },
|
||||
{ name: "Uptime SLA", value: true },
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export default function PricingTable() {
|
||||
const [selectedPlan, setSelectedPlan] = useState(1);
|
||||
|
||||
return (
|
||||
<section className="py-28 lg:py-32">
|
||||
<div className="container font-medium">
|
||||
<MobilePricingTable
|
||||
selectedPlan={selectedPlan}
|
||||
onPlanChange={setSelectedPlan}
|
||||
/>
|
||||
<DesktopPricingTable />
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
const MobilePricingTable = ({
|
||||
selectedPlan,
|
||||
onPlanChange,
|
||||
}: {
|
||||
selectedPlan: number;
|
||||
onPlanChange: (index: number) => void;
|
||||
}) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const plan = plans[selectedPlan];
|
||||
|
||||
return (
|
||||
<div className="md:hidden">
|
||||
<Collapsible open={isOpen} onOpenChange={setIsOpen}>
|
||||
<div className="flex items-center justify-between border-b py-4">
|
||||
<CollapsibleTrigger className="flex items-center gap-2">
|
||||
<h3 className="text-2xl font-semibold">{plan.name}</h3>
|
||||
<ChevronsUpDown
|
||||
className={`size-5 transition-transform ${isOpen ? "rotate-180" : ""}`}
|
||||
/>
|
||||
</CollapsibleTrigger>
|
||||
<Button variant={plan.button.variant} className="w-fit">
|
||||
{plan.button.text}
|
||||
</Button>
|
||||
</div>
|
||||
<CollapsibleContent className="flex flex-col space-y-2 p-2">
|
||||
{plans.map(
|
||||
(p, index) =>
|
||||
index !== selectedPlan && (
|
||||
<Button
|
||||
size="lg"
|
||||
variant="secondary"
|
||||
key={index}
|
||||
onClick={() => {
|
||||
onPlanChange(index);
|
||||
setIsOpen(false);
|
||||
}}
|
||||
>
|
||||
{p.name}
|
||||
</Button>
|
||||
),
|
||||
)}
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
|
||||
{/* Features List */}
|
||||
<div className="mt-8">
|
||||
{Object.entries(plan.features).map(
|
||||
([category, features], sectionIndex) => (
|
||||
<div key={sectionIndex} className="mb-8 space-y-2">
|
||||
<h3 className="mb-4 text-lg font-semibold capitalize">
|
||||
{category}
|
||||
</h3>
|
||||
{features.map((feature, featureIndex) => (
|
||||
<div
|
||||
key={featureIndex}
|
||||
className="grid grid-cols-2 items-center gap-8"
|
||||
>
|
||||
<span className="border-b py-2">{feature.name}</span>
|
||||
<div className="flex items-center gap-1 border-b py-2">
|
||||
{typeof feature.value === "boolean" ? (
|
||||
feature.value ? (
|
||||
<Check className="size-5" />
|
||||
) : (
|
||||
<span className="size-5" />
|
||||
)
|
||||
) : (
|
||||
<div className="flex items-center gap-1">
|
||||
<Check className="size-5" />
|
||||
<span>{feature.value}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const DesktopPricingTable = () => {
|
||||
return (
|
||||
<div className="hidden md:grid md:grid-cols-4">
|
||||
<FeaturesColumn />
|
||||
{plans.map((plan, index) => (
|
||||
<PricingColumn
|
||||
key={plan.name}
|
||||
plan={plan}
|
||||
isHighlighted={index === 1}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const FeaturesColumn = () => (
|
||||
<div>
|
||||
<div className="h-[140px]" /> {/* Spacer for plan header alignment */}
|
||||
{Object.entries(plans[0].features).map(([category, features], index) => (
|
||||
<div key={index}>
|
||||
<h3 className="flex h-20 items-center text-lg font-semibold capitalize">
|
||||
{category}
|
||||
</h3>
|
||||
{features.map((feature, featureIndex) => (
|
||||
<div key={featureIndex} className="py-4">
|
||||
{feature.name}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
const PricingColumn = ({
|
||||
plan,
|
||||
isHighlighted,
|
||||
}: {
|
||||
plan: Plan;
|
||||
isHighlighted: boolean;
|
||||
}) => {
|
||||
const columnClass = cn("px-6", isHighlighted && "bg-card border rounded-xl");
|
||||
|
||||
return (
|
||||
<div className={columnClass}>
|
||||
{/* Plan Header */}
|
||||
<div className="py-8">
|
||||
<h3 className="mb-3 text-2xl font-semibold">{plan.name}</h3>
|
||||
<Button variant={plan.button.variant} asChild>
|
||||
<a href={plan.button.href}>{plan.button.text}</a>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Features */}
|
||||
{Object.entries(plan.features).map(([, features], sectionIndex) => (
|
||||
<div key={sectionIndex}>
|
||||
<div className="flex h-20 items-center"></div> {/* space */}
|
||||
{features.map((feature, featureIndex) => (
|
||||
<div
|
||||
key={featureIndex}
|
||||
className="flex items-center gap-1 border-b py-4"
|
||||
>
|
||||
{typeof feature.value === "boolean" ? (
|
||||
feature.value ? (
|
||||
<Check className="size-5" />
|
||||
) : (
|
||||
<span className="size-5" />
|
||||
)
|
||||
) : (
|
||||
<div className="flex items-center gap-1">
|
||||
<Check className="size-4" />
|
||||
<span>{feature.value}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
61
sites/vibes/src/components/sections/signup-section.tsx
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import { Card, CardContent, CardHeader } from "../ui/card";
|
||||
import { Input } from "../ui/input";
|
||||
import { Button } from "../ui/button";
|
||||
import { FcGoogle } from "react-icons/fc";
|
||||
|
||||
const SignupSection = () => {
|
||||
return (
|
||||
<section className="bg-sand-100 py-16 md:py-28 lg:py-32">
|
||||
<div className="container">
|
||||
<div className="flex flex-col gap-4">
|
||||
<Card className="mx-auto w-full max-w-sm">
|
||||
<CardHeader className="flex flex-col items-center space-y-0">
|
||||
<img
|
||||
src="/images/layout/logo.svg"
|
||||
alt="logo"
|
||||
width={94}
|
||||
height={18}
|
||||
className="mb-7 dark:invert"
|
||||
/>
|
||||
<p className="mb-2 text-2xl font-bold">Start your free trial</p>
|
||||
<p className="text-muted-foreground">
|
||||
Sign up in less than 2 minutes.
|
||||
</p>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid gap-4">
|
||||
<Input type="text" placeholder="Enter your name" required />
|
||||
<Input type="email" placeholder="Enter your email" required />
|
||||
<div>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="Enter your password"
|
||||
required
|
||||
/>
|
||||
<p className="text-muted-foreground mt-1 text-sm">
|
||||
Must be at least 8 characters.
|
||||
</p>
|
||||
</div>
|
||||
<Button type="submit" className="mt-2 w-full">
|
||||
Create an account
|
||||
</Button>
|
||||
<Button variant="outline" className="w-full">
|
||||
<FcGoogle className="mr-2 size-5" />
|
||||
Sign up with Google
|
||||
</Button>
|
||||
</div>
|
||||
<div className="text-muted-foreground mx-auto mt-8 flex justify-center gap-1 text-sm">
|
||||
<p>Already have an account?</p>
|
||||
<a href="/login" className="text-primary font-medium">
|
||||
Log in
|
||||
</a>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default SignupSection;
|
||||
148
sites/vibes/src/components/sections/testimonials.tsx
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
import { useEffect, useState } from "react";
|
||||
|
||||
import { PlusSigns } from "../icons/plus-signs";
|
||||
|
||||
import {
|
||||
Carousel,
|
||||
type CarouselApi,
|
||||
CarouselContent,
|
||||
CarouselItem,
|
||||
} from "@/components/ui/carousel";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const TESTIMONIALS = [
|
||||
{
|
||||
id: 1,
|
||||
quote:
|
||||
"Charter's platform delivers unmatched speed, a flexible account framework, and an API-first design. Their deep understanding of the ecosystem has been crucial for scaling our financial operations effectively.",
|
||||
author: "Henry Francis",
|
||||
role: "Founder, Ramp",
|
||||
logo: "/images/logos/ramp.svg",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
quote:
|
||||
"We have a deep understanding of the ecosystem, and Charter delivers unmatched speed, a flexible account framework, and an API-first design. These features are crucial for creating revenue that grows our office plants.",
|
||||
author: "David Chen",
|
||||
role: "Head of Engineering, Notion",
|
||||
logo: "/images/logos/notion.svg",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
quote:
|
||||
"Charter's platform has exceeded our expectations with its seamless integration process and exceptional support team. Their powerful tools and robust infrastructure have been instrumental in our scaling journey.",
|
||||
author: "Sarah Williams",
|
||||
role: "CTO, Mercury",
|
||||
logo: "/images/logos/mercury.svg",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
quote:
|
||||
"Charter has revolutionized how we manage our financial infrastructure effectively. Their API-first approach and robust platform capabilities have given us the flexibility to build custom solutions at scale seamlessly and efficiently.",
|
||||
author: "Michael Ross",
|
||||
role: "CEO, Raycast",
|
||||
logo: "/images/logos/raycast.svg",
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
quote:
|
||||
"The combination of Charter's flexible platform architecture and exceptional support team has been transformative for our operations. Their speed, reliability, and scalability are unmatched in the industry.",
|
||||
author: "Emily Chen",
|
||||
role: "Product Lead, Asana",
|
||||
logo: "/images/logos/asana.svg",
|
||||
},
|
||||
];
|
||||
|
||||
export default function Testimonials() {
|
||||
const [api, setApi] = useState<CarouselApi>();
|
||||
const [current, setCurrent] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (!api) {
|
||||
return;
|
||||
}
|
||||
|
||||
api.on("select", () => {
|
||||
setCurrent(api.selectedScrollSnap());
|
||||
});
|
||||
}, [api]);
|
||||
|
||||
return (
|
||||
<section
|
||||
className="relative py-16 md:py-28 lg:py-32"
|
||||
aria-label="Customer Testimonials"
|
||||
>
|
||||
<div className="container max-w-4xl">
|
||||
<Carousel
|
||||
opts={{
|
||||
align: "center",
|
||||
loop: true,
|
||||
}}
|
||||
setApi={setApi}
|
||||
>
|
||||
<CarouselContent>
|
||||
{TESTIMONIALS.map((testimonial) => (
|
||||
<CarouselItem
|
||||
key={testimonial.id}
|
||||
className="flex cursor-grab flex-col gap-6 lg:gap-8"
|
||||
>
|
||||
<blockquote className="pointer-events-none select-none text-balance font-sans text-2xl font-semibold leading-tight tracking-tight md:text-3xl lg:text-4xl">
|
||||
{testimonial.quote}
|
||||
</blockquote>
|
||||
<div className="flex items-center gap-4">
|
||||
{testimonial.logo && (
|
||||
<div className="relative h-8 w-24">
|
||||
<img
|
||||
src={testimonial.logo}
|
||||
alt={`${testimonial.author}'s company logo`}
|
||||
className="size-full object-contain dark:invert"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="bg-border h-8 w-[1px]" aria-hidden="true" />
|
||||
<div>
|
||||
<cite className="font-semibold not-italic">
|
||||
{testimonial.author}
|
||||
</cite>
|
||||
<p className="text-muted-foreground text-sm font-medium">
|
||||
{testimonial.role}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CarouselItem>
|
||||
))}
|
||||
</CarouselContent>
|
||||
</Carousel>
|
||||
|
||||
<div
|
||||
className="mt-10 flex gap-2 lg:mt-16"
|
||||
role="tablist"
|
||||
aria-label="Testimonials navigation"
|
||||
>
|
||||
{TESTIMONIALS.map((_, index) => (
|
||||
<button
|
||||
key={index}
|
||||
className={cn(
|
||||
"size-4 cursor-pointer rounded-full transition-colors duration-300",
|
||||
index === current
|
||||
? "bg-muted-foreground"
|
||||
: "bg-muted-foreground/20 hover:bg-muted-foreground/50",
|
||||
)}
|
||||
onClick={() => api?.scrollTo(index)}
|
||||
aria-label={`Go to testimonial ${index + 1}`}
|
||||
aria-selected={index === current}
|
||||
role="tab"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="absolute inset-x-0 top-16 isolate z-[-1] h-[300px] md:top-28 lg:top-32">
|
||||
<div className="from-background via-background/40 absolute inset-x-0 bottom-0 z-10 h-40 bg-gradient-to-t to-transparent" />
|
||||
<div className="from-background via-background/40 absolute inset-x-0 top-0 z-10 h-40 bg-gradient-to-b to-transparent" />
|
||||
<PlusSigns className="text-foreground/[0.05] size-full" />
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
157
sites/vibes/src/components/sections/why-charter.tsx
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
import { PlusSigns } from "../icons/plus-signs";
|
||||
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const items = [
|
||||
{
|
||||
title: (
|
||||
<>
|
||||
Unparalleled
|
||||
<br />
|
||||
VAR flexibility
|
||||
</>
|
||||
),
|
||||
description: [
|
||||
"VAR is often known for its lengthy processing times. At Charter, we focus on achieving the fastest VAR transfers—often completed within hours.",
|
||||
"Unlike traditional banks and middleware, we connect directly with the Federal Reserve to ensure the fastest, most transparent transfers possible.",
|
||||
],
|
||||
image: {
|
||||
src: "/images/homepage/why-charter/1.webp",
|
||||
alt: "Code snippet",
|
||||
type: "fill",
|
||||
},
|
||||
className:
|
||||
"flex flex-col pl-6 py-6 overflow-hidden md:col-span-3 md:flex-row gap-6 md:gap-12",
|
||||
headerClassName: "flex-2 p-0",
|
||||
contentClassName:
|
||||
"relative h-[320px] w-full p-0 self-center overflow-hidden rounded-l-xl border md:flex-1",
|
||||
},
|
||||
{
|
||||
title: "Unparalleled VAR flexibility",
|
||||
description: [
|
||||
"VAR has a reputation for taking too long. At Charter, we optimise for the fastest VAR transfers possible — often in a matter of hours.",
|
||||
"Unlike legacy banks and middleware providers, we have a direct connection to the Federal Reserve to facilitate the quickest transfers.",
|
||||
],
|
||||
image: {
|
||||
src: "/images/homepage/why-charter/2.svg",
|
||||
alt: "VAR Process Flow",
|
||||
width: 283,
|
||||
height: 45,
|
||||
},
|
||||
className: "md:col-span-2 flex flex-col justify-center",
|
||||
contentClassName:
|
||||
"flex items-center justify-center p-6 max-md:mt-4 max-md:mb-8",
|
||||
imagePosition: "content",
|
||||
},
|
||||
{
|
||||
title: "Unparalleled VAR flexibility",
|
||||
description: [
|
||||
"VAR has a reputation for taking too long. At Charter, we optimise for the fastest VAR transfers possible — often in a matter of hours.",
|
||||
],
|
||||
image: {
|
||||
src: "/images/homepage/why-charter/3.svg",
|
||||
alt: "VAR Process Diagram",
|
||||
width: 283,
|
||||
height: 45,
|
||||
},
|
||||
className: "md:col-span-2",
|
||||
headerClassName: "h-full",
|
||||
imagePosition: "header",
|
||||
},
|
||||
{
|
||||
title: "Unparalleled VAR flexibility",
|
||||
description: [
|
||||
"Unlike traditional banks and middleware, we connect directly with the Federal Reserve to ensure the fastest, most transparent transfers possible.",
|
||||
],
|
||||
image: {
|
||||
src: "/images/homepage/code-snippet.webp",
|
||||
alt: "Code snippet",
|
||||
type: "fill",
|
||||
},
|
||||
className: "overflow-hidden md:col-span-3 ",
|
||||
headerClassName: "",
|
||||
contentClassName:
|
||||
"relative h-[242px] mt-2 p-0 ml-8 w-full md:max-w-[400px] lg:max-w-[500px] overflow-hidden md:mx-auto shadow-xl rounded-t-2xl",
|
||||
},
|
||||
];
|
||||
|
||||
const WhyCharter = () => {
|
||||
return (
|
||||
<section
|
||||
id="why-charter"
|
||||
className="container relative py-16 md:py-28 lg:py-32"
|
||||
>
|
||||
<h3 className="mini-title">WHY CHARTER?</h3>
|
||||
|
||||
<div className="relative z-10 mt-8 grid grid-cols-1 gap-6 md:grid-cols-5">
|
||||
{items.map((item, index) => (
|
||||
<Card
|
||||
key={index}
|
||||
className={cn("col-span-1 shadow-xl", item.className)}
|
||||
>
|
||||
<CardHeader className={item.headerClassName}>
|
||||
{item.imagePosition === "header" && (
|
||||
<img
|
||||
src={item.image.src}
|
||||
alt={item.image.alt}
|
||||
width={item.image.width}
|
||||
height={item.image.height}
|
||||
className="flex-1 self-center max-md:mb-8 max-md:mt-4 dark:[filter:brightness(0)_saturate(100%)_invert(100%)]"
|
||||
/>
|
||||
)}
|
||||
<CardTitle className="text-3xl">{item.title}</CardTitle>
|
||||
{item.description.map((desc, i) => (
|
||||
<CardDescription
|
||||
key={i}
|
||||
className="mt-2 text-base font-medium leading-snug"
|
||||
>
|
||||
{desc}
|
||||
</CardDescription>
|
||||
))}
|
||||
</CardHeader>
|
||||
<CardContent className={item.contentClassName}>
|
||||
{item.image.type === "fill" ? (
|
||||
<img
|
||||
src={item.image.src}
|
||||
alt={item.image.alt}
|
||||
className="size-full object-cover object-left-top"
|
||||
/>
|
||||
) : (
|
||||
item.imagePosition === "content" && (
|
||||
<img
|
||||
src={item.image.src}
|
||||
alt={item.image.alt}
|
||||
width={item.image.width}
|
||||
height={item.image.height}
|
||||
className="self-center dark:[filter:brightness(0)_saturate(100%)_invert(100%)]"
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Background decoration */}
|
||||
<>
|
||||
<div className="absolute inset-0 isolate will-change-transform">
|
||||
<div className="bg-primary-gradient/28 absolute top-1/2 size-[700px] -translate-y-1/2 rounded-full blur-[300px]" />
|
||||
<div className="bg-secondary-gradient/16 absolute right-0 top-1/2 size-[700px] -translate-y-1/2 -rotate-12 rounded-full blur-[300px]" />
|
||||
<div className="bg-tertiary-gradient/6 absolute bottom-1/4 right-20 z-[1] h-[500px] w-[800px] -rotate-12 rounded-full blur-[100px] md:bottom-10" />
|
||||
</div>
|
||||
<div className="absolute -inset-x-20 top-0 [mask-image:radial-gradient(circle_at_center,black_0%,black_20%,transparent_85%)]">
|
||||
<PlusSigns className="text-foreground/[0.075] h-full w-full" />
|
||||
</div>
|
||||
</>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default WhyCharter;
|
||||
58
sites/vibes/src/components/ui/accordion.tsx
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
import * as AccordionPrimitive from '@radix-ui/react-accordion';
|
||||
import { ChevronDown } from 'lucide-react';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const Accordion = AccordionPrimitive.Root;
|
||||
|
||||
const AccordionItem = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AccordionPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn('border-b', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
AccordionItem.displayName = 'AccordionItem';
|
||||
|
||||
const AccordionTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<AccordionPrimitive.Header className="flex">
|
||||
<AccordionPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'flex flex-1 items-center justify-between py-4 text-left text-sm font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronDown className="text-muted-foreground size-4 shrink-0 transition-transform duration-200" />
|
||||
</AccordionPrimitive.Trigger>
|
||||
</AccordionPrimitive.Header>
|
||||
));
|
||||
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
|
||||
|
||||
const AccordionContent = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<AccordionPrimitive.Content
|
||||
ref={ref}
|
||||
className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
|
||||
{...props}
|
||||
>
|
||||
<div className={cn('pt-0 pb-4', className)}>{children}</div>
|
||||
</AccordionPrimitive.Content>
|
||||
));
|
||||
AccordionContent.displayName = AccordionPrimitive.Content.displayName;
|
||||
|
||||
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
|
||||
51
sites/vibes/src/components/ui/avatar.tsx
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import * as React from "react"
|
||||
import * as AvatarPrimitive from "@radix-ui/react-avatar"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Avatar({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AvatarPrimitive.Root>) {
|
||||
return (
|
||||
<AvatarPrimitive.Root
|
||||
data-slot="avatar"
|
||||
className={cn(
|
||||
"relative flex size-8 shrink-0 overflow-hidden rounded-full",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AvatarImage({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AvatarPrimitive.Image>) {
|
||||
return (
|
||||
<AvatarPrimitive.Image
|
||||
data-slot="avatar-image"
|
||||
className={cn("aspect-square size-full", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AvatarFallback({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
|
||||
return (
|
||||
<AvatarPrimitive.Fallback
|
||||
data-slot="avatar-fallback"
|
||||
className={cn(
|
||||
"bg-muted flex size-full items-center justify-center rounded-full",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Avatar, AvatarImage, AvatarFallback }
|
||||
46
sites/vibes/src/components/ui/badge.tsx
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const badgeVariants = cva(
|
||||
"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
|
||||
secondary:
|
||||
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
|
||||
destructive:
|
||||
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40",
|
||||
outline:
|
||||
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function Badge({
|
||||
className,
|
||||
variant,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"span"> &
|
||||
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
||||
const Comp = asChild ? Slot : "span"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="badge"
|
||||
className={cn(badgeVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants }
|
||||
58
sites/vibes/src/components/ui/button.tsx
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import * as React from 'react';
|
||||
|
||||
import { Slot } from '@radix-ui/react-slot';
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const buttonVariants = cva(
|
||||
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-semibold transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
|
||||
{
|
||||
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-border 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',
|
||||
},
|
||||
},
|
||||
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 };
|
||||
83
sites/vibes/src/components/ui/card.tsx
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
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(
|
||||
'bg-card text-card-foreground rounded-xl border shadow-sm',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
Card.displayName = 'Card';
|
||||
|
||||
const CardHeader = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn('flex flex-col space-y-1.5 p-6', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CardHeader.displayName = 'CardHeader';
|
||||
|
||||
const CardTitle = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn('leading-none font-semibold tracking-tight', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CardTitle.displayName = 'CardTitle';
|
||||
|
||||
const CardDescription = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn('text-muted-foreground text-sm', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CardDescription.displayName = 'CardDescription';
|
||||
|
||||
const CardContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn('p-6 pt-0', className)} {...props} />
|
||||
));
|
||||
CardContent.displayName = 'CardContent';
|
||||
|
||||
const CardFooter = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn('flex items-center p-6 pt-0', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CardFooter.displayName = 'CardFooter';
|
||||
|
||||
export {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardFooter,
|
||||
CardTitle,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
};
|
||||
262
sites/vibes/src/components/ui/carousel.tsx
Normal file
|
|
@ -0,0 +1,262 @@
|
|||
|
||||
import * as React from 'react';
|
||||
|
||||
import useEmblaCarousel, {
|
||||
type UseEmblaCarouselType,
|
||||
} from 'embla-carousel-react';
|
||||
import { ArrowLeft, ArrowRight } from 'lucide-react';
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
type CarouselApi = UseEmblaCarouselType[1];
|
||||
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;
|
||||
type CarouselOptions = UseCarouselParameters[0];
|
||||
type CarouselPlugin = UseCarouselParameters[1];
|
||||
|
||||
type CarouselProps = {
|
||||
opts?: CarouselOptions;
|
||||
plugins?: CarouselPlugin;
|
||||
orientation?: 'horizontal' | 'vertical';
|
||||
setApi?: (api: CarouselApi) => void;
|
||||
};
|
||||
|
||||
type CarouselContextProps = {
|
||||
carouselRef: ReturnType<typeof useEmblaCarousel>[0];
|
||||
api: ReturnType<typeof useEmblaCarousel>[1];
|
||||
scrollPrev: () => void;
|
||||
scrollNext: () => void;
|
||||
canScrollPrev: boolean;
|
||||
canScrollNext: boolean;
|
||||
} & CarouselProps;
|
||||
|
||||
const CarouselContext = React.createContext<CarouselContextProps | null>(null);
|
||||
|
||||
function useCarousel() {
|
||||
const context = React.useContext(CarouselContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error('useCarousel must be used within a <Carousel />');
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
const Carousel = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement> & CarouselProps
|
||||
>(
|
||||
(
|
||||
{
|
||||
orientation = 'horizontal',
|
||||
opts,
|
||||
setApi,
|
||||
plugins,
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const [carouselRef, api] = useEmblaCarousel(
|
||||
{
|
||||
...opts,
|
||||
axis: orientation === 'horizontal' ? 'x' : 'y',
|
||||
},
|
||||
plugins,
|
||||
);
|
||||
const [canScrollPrev, setCanScrollPrev] = React.useState(false);
|
||||
const [canScrollNext, setCanScrollNext] = React.useState(false);
|
||||
|
||||
const onSelect = React.useCallback((api: CarouselApi) => {
|
||||
if (!api) {
|
||||
return;
|
||||
}
|
||||
|
||||
setCanScrollPrev(api.canScrollPrev());
|
||||
setCanScrollNext(api.canScrollNext());
|
||||
}, []);
|
||||
|
||||
const scrollPrev = React.useCallback(() => {
|
||||
api?.scrollPrev();
|
||||
}, [api]);
|
||||
|
||||
const scrollNext = React.useCallback(() => {
|
||||
api?.scrollNext();
|
||||
}, [api]);
|
||||
|
||||
const handleKeyDown = React.useCallback(
|
||||
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
if (event.key === 'ArrowLeft') {
|
||||
event.preventDefault();
|
||||
scrollPrev();
|
||||
} else if (event.key === 'ArrowRight') {
|
||||
event.preventDefault();
|
||||
scrollNext();
|
||||
}
|
||||
},
|
||||
[scrollPrev, scrollNext],
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!api || !setApi) {
|
||||
return;
|
||||
}
|
||||
|
||||
setApi(api);
|
||||
}, [api, setApi]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!api) {
|
||||
return;
|
||||
}
|
||||
|
||||
onSelect(api);
|
||||
api.on('reInit', onSelect);
|
||||
api.on('select', onSelect);
|
||||
|
||||
return () => {
|
||||
api?.off('select', onSelect);
|
||||
};
|
||||
}, [api, onSelect]);
|
||||
|
||||
return (
|
||||
<CarouselContext.Provider
|
||||
value={{
|
||||
carouselRef,
|
||||
api: api,
|
||||
opts,
|
||||
orientation:
|
||||
orientation || (opts?.axis === 'y' ? 'vertical' : 'horizontal'),
|
||||
scrollPrev,
|
||||
scrollNext,
|
||||
canScrollPrev,
|
||||
canScrollNext,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
ref={ref}
|
||||
onKeyDownCapture={handleKeyDown}
|
||||
className={cn('relative', className)}
|
||||
role="region"
|
||||
aria-roledescription="carousel"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</CarouselContext.Provider>
|
||||
);
|
||||
},
|
||||
);
|
||||
Carousel.displayName = 'Carousel';
|
||||
|
||||
const CarouselContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const { carouselRef, orientation } = useCarousel();
|
||||
|
||||
return (
|
||||
<div ref={carouselRef} className="overflow-hidden">
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'flex',
|
||||
orientation === 'horizontal' ? '-ml-4' : '-mt-4 flex-col',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
CarouselContent.displayName = 'CarouselContent';
|
||||
|
||||
const CarouselItem = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const { orientation } = useCarousel();
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
role="group"
|
||||
aria-roledescription="slide"
|
||||
className={cn(
|
||||
'min-w-0 shrink-0 grow-0 basis-full',
|
||||
orientation === 'horizontal' ? 'pl-4' : 'pt-4',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
CarouselItem.displayName = 'CarouselItem';
|
||||
|
||||
const CarouselPrevious = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
React.ComponentProps<typeof Button>
|
||||
>(({ className, variant = 'outline', size = 'icon', ...props }, ref) => {
|
||||
const { orientation, scrollPrev, canScrollPrev } = useCarousel();
|
||||
|
||||
return (
|
||||
<Button
|
||||
ref={ref}
|
||||
variant={variant}
|
||||
size={size}
|
||||
className={cn(
|
||||
'absolute h-8 w-8 rounded-full',
|
||||
orientation === 'horizontal'
|
||||
? 'top-1/2 -left-12 -translate-y-1/2'
|
||||
: '-top-12 left-1/2 -translate-x-1/2 rotate-90',
|
||||
className,
|
||||
)}
|
||||
disabled={!canScrollPrev}
|
||||
onClick={scrollPrev}
|
||||
{...props}
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
<span className="sr-only">Previous slide</span>
|
||||
</Button>
|
||||
);
|
||||
});
|
||||
CarouselPrevious.displayName = 'CarouselPrevious';
|
||||
|
||||
const CarouselNext = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
React.ComponentProps<typeof Button>
|
||||
>(({ className, variant = 'outline', size = 'icon', ...props }, ref) => {
|
||||
const { orientation, scrollNext, canScrollNext } = useCarousel();
|
||||
|
||||
return (
|
||||
<Button
|
||||
ref={ref}
|
||||
variant={variant}
|
||||
size={size}
|
||||
className={cn(
|
||||
'absolute h-8 w-8 rounded-full',
|
||||
orientation === 'horizontal'
|
||||
? 'top-1/2 -right-12 -translate-y-1/2'
|
||||
: '-bottom-12 left-1/2 -translate-x-1/2 rotate-90',
|
||||
className,
|
||||
)}
|
||||
disabled={!canScrollNext}
|
||||
onClick={scrollNext}
|
||||
{...props}
|
||||
>
|
||||
<ArrowRight className="h-4 w-4" />
|
||||
<span className="sr-only">Next slide</span>
|
||||
</Button>
|
||||
);
|
||||
});
|
||||
CarouselNext.displayName = 'CarouselNext';
|
||||
|
||||
export {
|
||||
type CarouselApi,
|
||||
Carousel,
|
||||
CarouselContent,
|
||||
CarouselItem,
|
||||
CarouselPrevious,
|
||||
CarouselNext,
|
||||
};
|
||||
31
sites/vibes/src/components/ui/checkbox.tsx
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
|
||||
import { Check } from 'lucide-react';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const Checkbox = React.forwardRef<
|
||||
React.ElementRef<typeof CheckboxPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CheckboxPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'peer border-primary focus-visible:ring-ring data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground h-4 w-4 shrink-0 rounded-xs border shadow-sm focus-visible:ring-1 focus-visible:outline-hidden disabled:cursor-not-allowed disabled:opacity-50',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<CheckboxPrimitive.Indicator
|
||||
className={cn('flex items-center justify-center text-current')}
|
||||
>
|
||||
<Check className="h-4 w-4" />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
));
|
||||
Checkbox.displayName = CheckboxPrimitive.Root.displayName;
|
||||
|
||||
export { Checkbox };
|
||||
11
sites/vibes/src/components/ui/collapsible.tsx
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
'use client';
|
||||
|
||||
import * as CollapsiblePrimitive from '@radix-ui/react-collapsible';
|
||||
|
||||
const Collapsible = CollapsiblePrimitive.Root;
|
||||
|
||||
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger;
|
||||
|
||||
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent;
|
||||
|
||||
export { Collapsible, CollapsibleTrigger, CollapsibleContent };
|
||||
22
sites/vibes/src/components/ui/input.tsx
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import * as React from 'react';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<'input'>>(
|
||||
({ className, type, ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
'border-border file:text-foreground placeholder:text-muted-foreground focus-visible:ring-ring bg-card flex h-11 w-full rounded-md border px-3 py-1 text-base shadow-xs transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:ring-1 focus-visible:outline-hidden disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
Input.displayName = 'Input';
|
||||
|
||||
export { Input };
|
||||
27
sites/vibes/src/components/ui/label.tsx
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
import * as LabelPrimitive from '@radix-ui/react-label';
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const labelVariants = cva(
|
||||
'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
|
||||
);
|
||||
|
||||
const Label = React.forwardRef<
|
||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
|
||||
VariantProps<typeof labelVariants>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<LabelPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(labelVariants(), className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
Label.displayName = LabelPrimitive.Root.displayName;
|
||||
|
||||
export { Label };
|
||||
129
sites/vibes/src/components/ui/navigation-menu.tsx
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
import * as React from 'react';
|
||||
|
||||
import * as NavigationMenuPrimitive from '@radix-ui/react-navigation-menu';
|
||||
import { cva } from 'class-variance-authority';
|
||||
import { ChevronDown } from 'lucide-react';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const NavigationMenu = React.forwardRef<
|
||||
React.ElementRef<typeof NavigationMenuPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Root>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<NavigationMenuPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'relative z-10 flex max-w-max flex-1 items-center justify-center',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<NavigationMenuViewport />
|
||||
</NavigationMenuPrimitive.Root>
|
||||
));
|
||||
NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName;
|
||||
|
||||
const NavigationMenuList = React.forwardRef<
|
||||
React.ElementRef<typeof NavigationMenuPrimitive.List>,
|
||||
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.List>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<NavigationMenuPrimitive.List
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'group flex flex-1 list-none items-center justify-center space-x-1',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName;
|
||||
|
||||
const NavigationMenuItem = NavigationMenuPrimitive.Item;
|
||||
|
||||
const navigationMenuTriggerStyle = cva(
|
||||
'group inline-flex h-9 w-max items-center justify-center rounded-md bg-background p-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-hidden disabled:pointer-events-none disabled:opacity-50 data-active:bg-accent/50 data-[state=open]:bg-accent/50',
|
||||
);
|
||||
|
||||
const NavigationMenuTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof NavigationMenuPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Trigger>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<NavigationMenuPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(navigationMenuTriggerStyle(), 'group', className)}
|
||||
{...props}
|
||||
>
|
||||
{children}{' '}
|
||||
<ChevronDown
|
||||
className="relative top-px ml-1 size-3 transition duration-300 group-data-[state=open]:rotate-180"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</NavigationMenuPrimitive.Trigger>
|
||||
));
|
||||
NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName;
|
||||
|
||||
const NavigationMenuContent = React.forwardRef<
|
||||
React.ElementRef<typeof NavigationMenuPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Content>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<NavigationMenuPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 top-0 left-0 w-full md:absolute md:w-auto',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName;
|
||||
|
||||
const NavigationMenuLink = NavigationMenuPrimitive.Link;
|
||||
|
||||
const NavigationMenuViewport = React.forwardRef<
|
||||
React.ElementRef<typeof NavigationMenuPrimitive.Viewport>,
|
||||
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Viewport>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div className={cn('absolute top-full left-0 flex justify-center')}>
|
||||
<NavigationMenuPrimitive.Viewport
|
||||
className={cn(
|
||||
'origin-top-center bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border shadow-sm md:w-[var(--radix-navigation-menu-viewport-width)]',
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
));
|
||||
NavigationMenuViewport.displayName =
|
||||
NavigationMenuPrimitive.Viewport.displayName;
|
||||
|
||||
const NavigationMenuIndicator = React.forwardRef<
|
||||
React.ElementRef<typeof NavigationMenuPrimitive.Indicator>,
|
||||
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Indicator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<NavigationMenuPrimitive.Indicator
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in top-full z-1 flex h-1.5 items-end justify-center overflow-hidden',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className="bg-border relative top-[60%] size-2 rotate-45 rounded-tl-sm shadow-md" />
|
||||
</NavigationMenuPrimitive.Indicator>
|
||||
));
|
||||
NavigationMenuIndicator.displayName =
|
||||
NavigationMenuPrimitive.Indicator.displayName;
|
||||
|
||||
export {
|
||||
NavigationMenu,
|
||||
NavigationMenuContent,
|
||||
NavigationMenuIndicator,
|
||||
NavigationMenuItem,
|
||||
NavigationMenuLink,
|
||||
NavigationMenuList,
|
||||
NavigationMenuTrigger,
|
||||
navigationMenuTriggerStyle,
|
||||
NavigationMenuViewport,
|
||||
};
|
||||