├── .env.example ├── .github └── FUNDING.yml ├── .gitignore ├── .npmrc ├── .vscode ├── extensions.json └── settings.json ├── README.md ├── app ├── globals.css ├── layout.tsx ├── not-found.tsx ├── page.tsx ├── roadmap │ └── page.tsx ├── sponsorship │ └── page.tsx └── template.tsx ├── biome.json ├── components.json ├── components ├── custom │ ├── active-goal.tsx │ ├── animated-gradient-sponsor-text.tsx │ ├── breakpoint-debug.tsx │ ├── event-sponsor.tsx │ ├── logo.tsx │ ├── roadmap-achievement.tsx │ └── sponsor.tsx ├── section │ ├── all-features.tsx │ ├── hero.tsx │ ├── pain-point.tsx │ ├── roadmap.tsx │ ├── sponsorships-category.tsx │ ├── sponsorships-header.tsx │ ├── sponsorships-mine.tsx │ ├── sponsorships-tiers.tsx │ ├── support-cta.tsx │ └── supporters.tsx └── ui │ ├── avatar.tsx │ ├── badge.tsx │ ├── button.tsx │ ├── dropdown.tsx │ ├── hyper-text.tsx │ ├── layouts │ └── stacked-layout.tsx │ ├── link.tsx │ ├── navbar.tsx │ ├── rainbow-button.tsx │ ├── sidebar.tsx │ ├── text.tsx │ └── tooltip.tsx ├── fonts ├── GTWalsheimPro-Bold.woff2 ├── GTWalsheimPro-Medium.woff2 └── GTWalsheimPro-Regular.woff2 ├── lib ├── constants.ts └── utils.ts ├── next.config.mjs ├── package.json ├── pnpm-lock.yaml ├── postcss.config.mjs ├── public ├── apple-touch-icon.png ├── ddr-logo-circle.png ├── favicon-48x48.png ├── favicon.ico ├── favicon.svg ├── hero-left.svg ├── hero-right.svg ├── logo.svg ├── og.png ├── robots.txt ├── site.webmanifest ├── web-app-manifest-192x192.png └── web-app-manifest-512x512.png ├── renovate.json ├── server └── github.ts ├── tsconfig.json └── types └── global.d.ts /.env.example: -------------------------------------------------------------------------------- 1 | GITHUB_TOKEN=your_github_token_here -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [needim] 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | .env 38 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | @hugeicons:registry=https://npm.hugeicons.com 2 | //npm.hugeicons.com/:_authToken=${HUGEICONS_NPM_TOKEN} -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["biomejs.biome"] 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[typescript]": { 3 | "editor.defaultFormatter": "biomejs.biome" 4 | }, 5 | "[typescriptreact]": { 6 | "editor.defaultFormatter": "biomejs.biome" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gider.im website 2 | 3 | **Privacy focused income & expense tracking app.** 4 | 5 | --- 6 | 7 | 8 | 9 | Promo 10 | 11 | 12 |
 
13 |

14 | Website 15 | · 16 | Issues 17 |

18 |
 
19 | 20 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 21 | 22 | ## Getting Started 23 | 24 | First, install dependencies. 25 | 26 | ```bash 27 | corepack up 28 | ``` 29 | 30 | Then, run the development server: 31 | 32 | ```bash 33 | node --run dev 34 | ``` 35 | 36 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 37 | 38 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 39 | 40 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load custom Google Fonts. 41 | 42 | ## Learn More 43 | 44 | To learn more about Next.js, take a look at the following resources: 45 | 46 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 47 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 48 | 49 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 50 | -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | @plugin "tailwindcss-animate"; 3 | 4 | @custom-variant dark (&:is(.dark *)); 5 | 6 | @theme { 7 | --font-sans: var(--font-display); 8 | --font-display: var(--font-display); 9 | --font-mono: var(--font-mono); 10 | 11 | --color-background: hsl(var(--background)); 12 | --color-foreground: hsl(var(--foreground)); 13 | 14 | --color-muted: hsl(var(--muted)); 15 | --color-muted-foreground: hsl(var(--muted-foreground)); 16 | 17 | --color-color-1: hsl(var(--color-1)); 18 | --color-color-2: hsl(var(--color-2)); 19 | --color-color-3: hsl(var(--color-3)); 20 | --color-color-4: hsl(var(--color-4)); 21 | --color-color-5: hsl(var(--color-5)); 22 | 23 | --animate-shine-infinite: shine-infinite 6s ease-in-out infinite; 24 | --animate-marquee: marquee var(--duration) linear infinite; 25 | --animate-marquee-vertical: marquee-vertical var(--duration) linear infinite; 26 | --animate-shimmer: shimmer 8s infinite; 27 | --animate-gradient: gradient 8s linear infinite; 28 | --animate-rainbow: rainbow var(--speed, 2s) infinite linear; 29 | 30 | @keyframes accordion-down { 31 | from { 32 | height: 0; 33 | } 34 | to { 35 | height: var(--radix-accordion-content-height); 36 | } 37 | } 38 | @keyframes accordion-up { 39 | from { 40 | height: var(--radix-accordion-content-height); 41 | } 42 | to { 43 | height: 0; 44 | } 45 | } 46 | @keyframes shine-infinite { 47 | 0% { 48 | transform: skew(-12deg) translateX(-400%); 49 | } 50 | 100% { 51 | transform: skew(-12deg) translateX(400%); 52 | } 53 | } 54 | @keyframes marquee { 55 | from { 56 | transform: translateX(0); 57 | } 58 | to { 59 | transform: translateX(calc(-100% - var(--gap))); 60 | } 61 | } 62 | @keyframes marquee-vertical { 63 | from { 64 | transform: translateY(0); 65 | } 66 | to { 67 | transform: translateY(calc(-100% - var(--gap))); 68 | } 69 | } 70 | @keyframes shimmer { 71 | 0%, 72 | 90%, 73 | 100% { 74 | background-position: calc(-100% - var(--shimmer-width)) 0; 75 | } 76 | 30%, 77 | 60% { 78 | background-position: calc(100% + var(--shimmer-width)) 0; 79 | } 80 | } 81 | @keyframes gradient { 82 | to { 83 | background-position: var(--bg-size) 0; 84 | } 85 | } 86 | @keyframes rainbow { 87 | 0% { 88 | background-position: 0%; 89 | } 90 | 100% { 91 | background-position: 200%; 92 | } 93 | } 94 | } 95 | 96 | /* 97 | The default border color has changed to `currentColor` in Tailwind CSS v4, 98 | so we've added these compatibility styles to make sure everything still 99 | looks the same as it did with Tailwind CSS v3. 100 | 101 | If we ever want to remove these styles, we need to add an explicit border 102 | color utility to any element that depends on these defaults. 103 | */ 104 | @layer base { 105 | *, 106 | ::after, 107 | ::before, 108 | ::backdrop, 109 | ::file-selector-button { 110 | border-color: var(--color-zinc-200, currentColor); 111 | } 112 | } 113 | 114 | @utility strikethrough { 115 | position: relative; 116 | &:before { 117 | position: absolute; 118 | content: ""; 119 | left: 0; 120 | top: 50%; 121 | right: 0; 122 | border-top: 2px solid #f05d48 !important; 123 | border-color: inherit; 124 | border-radius: 99px; 125 | 126 | -webkit-transform: rotate(-4deg); 127 | -moz-transform: rotate(-4deg); 128 | -ms-transform: rotate(-4deg); 129 | -o-transform: rotate(-4deg); 130 | transform: rotate(-4deg); 131 | } 132 | } 133 | 134 | @utility underline-doodle { 135 | background: url('data:image/svg+xml,') 136 | no-repeat; 137 | background-position: 0 100%; 138 | } 139 | 140 | @utility sponsors-goal-progress-bar { 141 | /* Taken from GitHub */ 142 | background: linear-gradient( 143 | 90deg, 144 | #ffd33d 0%, 145 | #ea4aaa 17%, 146 | #b34bff 34%, 147 | #01feff 51%, 148 | #ffd33d 68%, 149 | #ea4aaa 85%, 150 | #b34bff 100% 151 | ); 152 | background-size: 300% 100%; 153 | animation: sponsors-progress-animation 2s linear infinite; 154 | } 155 | 156 | @utility tiers { 157 | & p strong { 158 | @apply text-pink-500 dark:text-pink-300 mt-1 block; 159 | } 160 | & strong { 161 | @apply font-medium; 162 | } 163 | } 164 | 165 | @utility tier { 166 | @apply border px-4 py-3; 167 | 168 | & li { 169 | @apply text-sm -ml-4; 170 | } 171 | } 172 | 173 | @layer base { 174 | :root { 175 | --background: 240 5% 96%; 176 | --foreground: 240 10% 3.9%; 177 | 178 | --muted: 240 4.8% 95.9%; 179 | --muted-foreground: 240 3.8% 46.1%; 180 | 181 | --color-1: 0 100% 63%; 182 | --color-2: 270 100% 63%; 183 | --color-3: 210 100% 63%; 184 | --color-4: 195 100% 63%; 185 | --color-5: 90 100% 63%; 186 | } 187 | } 188 | 189 | @layer base { 190 | html { 191 | @apply antialiased min-h-screen bg-background text-foreground; 192 | font-size: 18px; 193 | } 194 | body { 195 | @apply min-h-screen font-display bg-background text-foreground; 196 | } 197 | } 198 | 199 | @layer components { 200 | @keyframes sponsors-progress-animation { 201 | 0% { 202 | background-position: 100%; 203 | } 204 | 100% { 205 | background-position: 0%; 206 | } 207 | } 208 | 209 | ul { 210 | @apply list-none pl-4 mt-2; 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { BreakpointDebug } from "@/components/custom/breakpoint-debug"; 2 | import Logo from "@/components/custom/logo"; 3 | import { Button } from "@/components/ui/button"; 4 | import { 5 | DropdownDescription, 6 | DropdownDivider, 7 | DropdownItem, 8 | DropdownLabel, 9 | DropdownMenu, 10 | } from "@/components/ui/dropdown"; 11 | import { Link } from "@/components/ui/link"; 12 | import { 13 | Navbar, 14 | NavbarDivider, 15 | NavbarItem, 16 | NavbarSection, 17 | NavbarSpacer, 18 | } from "@/components/ui/navbar"; 19 | import { RainbowButton } from "@/components/ui/rainbow-button"; 20 | import { 21 | Sidebar, 22 | SidebarBody, 23 | SidebarHeader, 24 | SidebarItem, 25 | SidebarSection, 26 | } from "@/components/ui/sidebar"; 27 | import { Strong } from "@/components/ui/text"; 28 | import { TooltipProvider } from "@/components/ui/tooltip"; 29 | import { 30 | IconArrowUpRight, 31 | IconBrandOpenSource, 32 | IconShieldLock, 33 | } from "@tabler/icons-react"; 34 | import type { Metadata } from "next"; 35 | import PlausibleProvider from "next-plausible"; 36 | import { Fira_Mono } from "next/font/google"; 37 | import localFont from "next/font/local"; 38 | 39 | const font = localFont({ 40 | src: [ 41 | { 42 | path: "../fonts/GTWalsheimPro-Regular.woff2", 43 | weight: "400", 44 | style: "normal", 45 | }, 46 | { 47 | path: "../fonts/GTWalsheimPro-Medium.woff2", 48 | weight: "500", 49 | style: "normal", 50 | }, 51 | { 52 | path: "../fonts/GTWalsheimPro-Bold.woff2", 53 | weight: "700", 54 | style: "normal", 55 | }, 56 | ], 57 | variable: "--font-display", 58 | }); 59 | 60 | import { StackedLayout } from "@/components/ui/layouts/stacked-layout"; 61 | import { PWA_LINK } from "@/lib/constants"; 62 | import "./globals.css"; 63 | 64 | const fontMono = Fira_Mono({ 65 | weight: ["400", "500"], 66 | subsets: ["latin"], 67 | variable: "--font-mono", 68 | }); 69 | 70 | const navItems = [ 71 | { label: "Roadmap", url: "/roadmap" }, 72 | // { label: "Design Contest", url: "/design" }, 73 | { 74 | label: "Sponsorship", 75 | url: "/sponsorship", 76 | }, 77 | { 78 | label: ( 79 | <> 80 | GitHub 81 | 82 | 83 | ), 84 | url: "https://github.com/needim/gider.im-pwa", 85 | external: true, 86 | }, 87 | ]; 88 | 89 | function WhyDropdownMenu() { 90 | return ( 91 | 92 | 93 | 94 | Free & Open Source 95 | 96 | You can read, fork, and contribute to the source code on GitHub. 97 | 98 | 99 | 100 | 101 | 102 | Local & Privacy First 103 | 104 | Only you have access to your data. No ads, no data collection. 105 | 106 | 107 | 108 | ); 109 | } 110 | 111 | export const metadata: Metadata = { 112 | title: "gider.im", 113 | description: 114 | "Free, privacy first, local first, no tracking, no ads, no data collection.", 115 | openGraph: { 116 | title: "gider.im", 117 | description: 118 | "Free, privacy first, local first, no tracking, no ads, no data collection.", 119 | url: "https://gider.im", 120 | siteName: "gider.im", 121 | locale: "en_US", 122 | type: "website", 123 | images: [ 124 | { 125 | url: "https://gider.im/og.png", 126 | width: 1200, 127 | height: 630, 128 | alt: "gider.im", 129 | }, 130 | ], 131 | }, 132 | twitter: { 133 | title: "gider.im", 134 | card: "summary_large_image", 135 | images: [ 136 | { 137 | url: "https://gider.im/og.png", 138 | width: 1200, 139 | height: 630, 140 | alt: "gider.im", 141 | }, 142 | ], 143 | }, 144 | robots: { 145 | index: true, 146 | follow: true, 147 | googleBot: { 148 | index: true, 149 | follow: true, 150 | "max-video-preview": -1, 151 | "max-image-preview": "large", 152 | "max-snippet": -1, 153 | }, 154 | }, 155 | }; 156 | 157 | export default function RootLayout({ 158 | children, 159 | }: Readonly<{ 160 | children: React.ReactNode; 161 | }>) { 162 | return ( 163 | 164 | 165 | 171 | 172 | 173 | 178 | 179 | 180 | 185 | 186 | 187 | 188 | 191 | 194 | 195 | 196 | {/* 197 | 198 | Why gider.im 199 | 200 | 201 | 202 | */} 203 | {navItems.map(({ label, url, external }) => ( 204 | 209 | {label} 210 | 211 | ))} 212 | 213 | 214 | 215 | 220 | Get it for free 221 | 222 | 223 | 224 | } 225 | sidebar={ 226 | 227 | 228 | 229 | 233 | 234 | 235 | 236 | 237 | {navItems.map(({ label, url }) => ( 238 | 239 | {label} 240 | 241 | ))} 242 | 243 | 244 | 245 | } 246 | > 247 | {children} 248 | 249 | 250 | 251 | 252 | 253 | ); 254 | } 255 | -------------------------------------------------------------------------------- /app/not-found.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@/components/ui/button"; 2 | 3 | export default function NotFound() { 4 | return ( 5 |
6 |
7 |

404

8 |

9 | Page not found 10 |

11 |

12 | Sorry, we couldn’t find the page you’re looking for. 13 |

14 |
15 | 16 |
17 |
18 |
19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | import { AllFeatures } from "@/components/section/all-features"; 2 | import { Hero } from "@/components/section/hero"; 3 | import { PainPoint } from "@/components/section/pain-point"; 4 | import { Supporters } from "@/components/section/supporters"; 5 | 6 | export default function Home() { 7 | return ( 8 | <> 9 | 10 | 11 |
12 |
13 | 14 |
15 |
16 | 17 |
18 | 19 | 20 |
21 | 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /app/roadmap/page.tsx: -------------------------------------------------------------------------------- 1 | import Roadmap from "@/components/section/roadmap"; 2 | 3 | export default function Page() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /app/sponsorship/page.tsx: -------------------------------------------------------------------------------- 1 | import { SponsorshipsHeader } from "@/components/section/sponsorships-header"; 2 | import { SponsorshipsMine } from "@/components/section/sponsorships-mine"; 3 | import { SponsorshipsTiers } from "@/components/section/sponsorships-tiers"; 4 | import { getGithubInfo } from "@/server/github"; 5 | 6 | export default async function Page() { 7 | const githubResponse = await getGithubInfo(); 8 | 9 | return ( 10 | <> 11 | 12 | 13 | 14 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /app/template.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { motion } from "motion/react"; 4 | 5 | export default function Template({ children }: { children: React.ReactNode }) { 6 | return ( 7 | 12 | {children} 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", 3 | "organizeImports": { 4 | "enabled": true 5 | }, 6 | "vcs": { 7 | "enabled": true, 8 | "clientKind": "git", 9 | "useIgnoreFile": true 10 | }, 11 | "files": { 12 | "include": ["**/*.ts", "**/*.tsx"], 13 | "ignoreUnknown": true 14 | }, 15 | "linter": { 16 | "enabled": true, 17 | "rules": { 18 | "recommended": true, 19 | "style": { 20 | "noNonNullAssertion": "off", 21 | "noParameterAssign": "off" 22 | }, 23 | "complexity": { 24 | "noForEach": "off", 25 | "noBannedTypes": "off" 26 | }, 27 | "suspicious": { 28 | "noPrototypeBuiltins": "off", 29 | "noArrayIndexKey": "off" 30 | }, 31 | "a11y": { 32 | "noSvgWithoutTitle": "off", 33 | "useButtonType": "off", 34 | "noInteractiveElementToNoninteractiveRole": "off" 35 | } 36 | } 37 | }, 38 | "formatter": { 39 | "enabled": true 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "app/globals.css", 9 | "baseColor": "zinc", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /components/custom/active-goal.tsx: -------------------------------------------------------------------------------- 1 | export function ActiveGoal({ 2 | goal, 3 | firstMonthlySponsor, 4 | monthlySponsorCount, 5 | }: { 6 | goal: ActiveGoal; 7 | firstMonthlySponsor?: SponsorshipsAsMaintainerNode; 8 | monthlySponsorCount: number; 9 | }) { 10 | return ( 11 |
12 |

Goal status

13 |

14 | {goal.percentComplete}% towards {goal.targetValue} monthly sponsors goal 15 |

16 | 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /components/custom/animated-gradient-sponsor-text.tsx: -------------------------------------------------------------------------------- 1 | import type { ReactNode } from "react"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | 5 | export default function AnimatedGradientSponsorText({ 6 | children, 7 | className, 8 | }: { 9 | children: ReactNode; 10 | className?: string; 11 | }) { 12 | return ( 13 |
19 |
24 | 25 | {children} 26 |
27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /components/custom/breakpoint-debug.tsx: -------------------------------------------------------------------------------- 1 | export function BreakpointDebug() { 2 | if (process.env.NODE_ENV !== "development") { 3 | return <>; 4 | } 5 | 6 | return ( 7 |
8 | 9 |
xs
10 |
11 | sm 12 |
13 |
14 | md 15 |
16 |
17 | lg 18 |
19 |
xl
20 |
2xl
21 |
3xl
22 |
23 |
24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /components/custom/event-sponsor.tsx: -------------------------------------------------------------------------------- 1 | import { Avatar, AvatarImage } from "@/components/ui/avatar"; 2 | import { 3 | Tooltip, 4 | TooltipContent, 5 | TooltipTrigger, 6 | } from "@/components/ui/tooltip"; 7 | import { cn } from "@/lib/utils"; 8 | 9 | export function EventSponsor({ 10 | username, 11 | tooltip, 12 | platform, 13 | className, 14 | }: { 15 | username: string; 16 | tooltip: string; 17 | avatar?: string; 18 | platform: string; 19 | className?: string; 20 | }) { 21 | return ( 22 | 23 | 24 |

{tooltip}

25 |

@{username}

26 |
27 | 28 | 40 | 46 | 53 | 54 | 55 | 56 |
57 | ); 58 | } 59 | -------------------------------------------------------------------------------- /components/custom/logo.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from "react"; 2 | 3 | interface Props extends SVGProps {} 4 | 5 | const Logo = (props: Props) => ( 6 | 14 | 20 | 21 | ); 22 | 23 | export default Logo; 24 | -------------------------------------------------------------------------------- /components/custom/roadmap-achievement.tsx: -------------------------------------------------------------------------------- 1 | import { IconCircleDashed, IconCircleDashedCheck } from "@tabler/icons-react"; 2 | 3 | export function RoadmapAchievement({ 4 | title, 5 | completed, 6 | }: { title: string; completed: boolean }) { 7 | return ( 8 |

9 | {completed ? ( 10 | 14 | ) : ( 15 | 19 | )}{" "} 20 | {title} 21 |

22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /components/custom/sponsor.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Avatar, AvatarImage } from "@/components/ui/avatar"; 4 | import { 5 | Tooltip, 6 | TooltipContent, 7 | TooltipTrigger, 8 | } from "@/components/ui/tooltip"; 9 | import { cn } from "@/lib/utils"; 10 | import { motion } from "motion/react"; 11 | 12 | export function Sponsor({ login, name }: SponsorEntity) { 13 | return ( 14 | 15 | 16 |

{name}

17 |

@{login}

18 |
19 | 20 | 30 | 31 | 34 | 35 | 36 | 37 |
38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /components/section/all-features.tsx: -------------------------------------------------------------------------------- 1 | import { Badge } from "@/components/ui/badge"; 2 | import { IconCheck } from "@tabler/icons-react"; 3 | 4 | const features = [ 5 | { 6 | name: "Track Transactions", 7 | description: "Mark transactions as paid or unpaid for easy tracking.", 8 | }, 9 | { 10 | name: "Offline Support", 11 | description: "Track your finances even when you’re offline.", 12 | }, 13 | { 14 | name: "Multiple Currencies", 15 | description: "Track your finances in multiple currencies.", 16 | }, 17 | { 18 | name: "Recurring Transactions", 19 | description: "Set up recurring transactions for easy tracking.", 20 | }, 21 | { 22 | name: "Groups & Tags", 23 | description: "Organize your transactions with groups and tags.", 24 | }, 25 | { 26 | name: "Filters", 27 | description: "Find transactions quickly with filter options.", 28 | }, 29 | { 30 | name: "Multiple-Device Sync", 31 | description: "Sync your data across multiple devices securely.", 32 | }, 33 | { 34 | name: "Insights", 35 | description: "Get insights into your finances with charts and graphs.", 36 | soon: true, 37 | }, 38 | { 39 | name: "Notifications", 40 | description: "Get notifications for upcoming transactions.", 41 | soon: true, 42 | }, 43 | { 44 | name: "Investment Tracking", 45 | description: "Track your investments and monitor your portfolio.", 46 | soon: true, 47 | }, 48 | ]; 49 | 50 | export function AllFeatures() { 51 | return ( 52 |
53 |
54 |
55 |
56 |

57 | Everything you need 58 |

59 |

60 | All-in-one application 61 |

62 |

63 | gider.im simplifies financial tracking with a user-friendly, 64 | ad-free experience that respects your privacy. Your data is 65 | encrypted and exclusively yours, with no registration required.{" "} 66 |
67 |
68 | It’s secure, syncs across devices, and offers insights to keep 69 | your financial life on track. 70 |

71 |
72 |
73 | {features.map((feature) => ( 74 |
75 |
76 |
87 |
{feature.description}
88 |
89 | ))} 90 |
91 |
92 |
93 |
94 | ); 95 | } 96 | -------------------------------------------------------------------------------- /components/section/hero.tsx: -------------------------------------------------------------------------------- 1 | import HyperText from "@/components/ui/hyper-text"; 2 | import { RainbowButton } from "@/components/ui/rainbow-button"; 3 | import { Strong } from "@/components/ui/text"; 4 | import { PWA_LINK } from "@/lib/constants"; 5 | import Image from "next/image"; 6 | import Link from "next/link"; 7 | 8 | export function Hero() { 9 | return ( 10 |
11 |
12 |
13 | Now you can sync your data across all your devices. 14 |
15 |
16 |

17 | Privacy focused, income,{" "} 18 | expense &{" "} 19 | asset tracking. 20 |

21 |

22 | Open source,{" "} 23 | 24 | 28 | , 29 | {" "} 30 | no ads &{" "} 31 | data collection 32 |

33 |
34 | 35 | 36 | Get it for free 37 | 38 | 39 |
40 | Hero left image 47 | Hero right image 54 |
55 | ); 56 | } 57 | -------------------------------------------------------------------------------- /components/section/pain-point.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | IconBrandGithubFilled, 3 | IconEyeFilled, 4 | IconLockFilled, 5 | IconUserFilled, 6 | } from "@tabler/icons-react"; 7 | 8 | const features = [ 9 | { 10 | name: "Free & Open Source", 11 | description: 12 | "gider.im is completely open source, giving you access to the code and allowing you to use the app without any cost.", 13 | icon: IconBrandGithubFilled, 14 | }, 15 | { 16 | name: "Secure & Encrypted", 17 | description: 18 | "Your financial data is secured with encryption, along with optional biometric authentication and passcode protection for extra security.", 19 | icon: IconLockFilled, 20 | }, 21 | { 22 | name: "Local & Privacy First", 23 | description: 24 | "Your data is exclusively yours, with no data collection or tracking. We prioritize your privacy and never sell your information.", 25 | icon: IconEyeFilled, 26 | }, 27 | { 28 | name: "No Ads & No Registration", 29 | description: 30 | "Unlike many finance apps, gider.im is ad-free with no registration required. You can start using it instantly with complete anonymity and freedom.", 31 | icon: IconUserFilled, 32 | }, 33 | ]; 34 | export function PainPoint() { 35 | return ( 36 |
37 |
38 |
39 |

40 | Problem 41 |

42 |

43 | The Pain Point 44 |

45 |

46 | Managing finances can feel overwhelming, especially when data 47 | privacy and security are concerns. Many apps lack transparency and 48 | invade privacy. 49 |

50 |
51 |
52 |
53 | {features.map((feature) => ( 54 |
55 |
56 |
57 |
62 | {feature.name} 63 |
64 |
65 | {feature.description} 66 |
67 |
68 | ))} 69 |
70 |
71 |
72 |
73 | ); 74 | } 75 | -------------------------------------------------------------------------------- /components/section/roadmap.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { RoadmapAchievement } from "@/components/custom/roadmap-achievement"; 4 | import { cn } from "@/lib/utils"; 5 | import { IconArrowRight } from "@tabler/icons-react"; 6 | import { type Variants, motion } from "motion/react"; 7 | import Link from "next/link"; 8 | 9 | // TODO: progreess visualisation calculation needs a fix 10 | 11 | const ROADMAP_ITEMS = [ 12 | { 13 | date: "26 May, 2024", 14 | progress: 100, 15 | icon: "🥳", 16 | title: "Open beta launch", 17 | description: "The open beta for the project is now live!", 18 | achievements: [ 19 | { title: "gider.im website is live.", completed: true }, 20 | { title: "app.gider.im is live for beta testers.", completed: true }, 21 | ], 22 | }, 23 | { 24 | date: "30 June, 2024", 25 | progress: 100, 26 | icon: "🔖", 27 | title: "Groups, Tags & Improvements", 28 | description: 29 | "Organize your entries with groups and tags. Improvements to the UI and UX.", 30 | achievements: [ 31 | { title: "Schema changes are ready.", completed: true }, 32 | { 33 | title: 34 | "Completed UX improvements and adding more customization options.", 35 | completed: true, 36 | }, 37 | { title: "Groups and tags are ready for testing.", completed: true }, 38 | ], 39 | }, 40 | { 41 | date: "23 July - 6 August 2024", 42 | progress: 100, 43 | icon: "🏆", 44 | title: "Design Contest", 45 | description: "Design contest for the gider.im app.", 46 | achievements: [ 47 | { title: "Design contest announcement.", completed: true }, 48 | { title: "Design contest entries.", completed: true }, 49 | { title: "Design contest winners.", completed: true }, 50 | ], 51 | footer: ( 52 | 53 | View the winners{" "} 54 | 55 | 56 | ), 57 | }, 58 | { 59 | date: "30 August, 2024", 60 | progress: 10, 61 | icon: "🔄", 62 | title: "Sync & Backup", 63 | description: "Sync your data across devices and backup your data.", 64 | achievements: [ 65 | { title: "Add new setting for decimal symbol.", completed: true }, 66 | { title: "Localization improvements.", completed: false }, 67 | { title: "Install as application tutorial.", completed: false }, 68 | { title: "Sync between another device.", completed: false }, 69 | ], 70 | }, 71 | { 72 | date: "30 September, 2024", 73 | progress: 0, 74 | icon: "✨", 75 | title: "Overview & Visual Stats", 76 | description: "Get an overview of your transactions and stats.", 77 | achievements: [ 78 | { title: "Overview page with stats and charts.", completed: false }, 79 | ], 80 | }, 81 | { 82 | date: "30 October, 2024", 83 | progress: 0, 84 | icon: "💎", 85 | title: "Assets & Budgets", 86 | description: 87 | "Add assets, such as gold, stocks, foreign currencies, cryptocurrencies, and more.", 88 | achievements: [ 89 | { title: "Add assets.", completed: false }, 90 | { title: "Add budgets.", completed: false }, 91 | ], 92 | }, 93 | { 94 | date: "30 November, 2024", 95 | progress: 0, 96 | icon: ( 97 | 106 | 107 | 108 | ), 109 | title: "Open Source", 110 | description: "The project will be open-sourced.", 111 | achievements: [ 112 | { title: "Write tests.", completed: false }, 113 | { title: "Refactor the codebase.", completed: false }, 114 | { title: "Write contributing guidelines.", completed: false }, 115 | { title: "Prepare workflows for CI/CD.", completed: false }, 116 | { title: "Publish to GitHub.", completed: false }, 117 | { title: "Accept contributions.", completed: false }, 118 | ], 119 | }, 120 | { 121 | progress: 0, 122 | date: "30 December, 2024", 123 | icon: "📱", 124 | title: "iOS Widgets", 125 | description: "Separate app", 126 | achievements: [ 127 | { title: "Separate app for iOS widgets.", completed: false }, 128 | ], 129 | }, 130 | ]; 131 | 132 | export default function Roadmap() { 133 | const item: Variants = { 134 | hidden: { opacity: 0, marginLeft: 20 }, 135 | show: { opacity: 1, marginLeft: 0 }, 136 | }; 137 | return ( 138 |
139 |
140 |
141 |

142 | Roadmap 143 |

144 |
145 |
146 | {ROADMAP_ITEMS.map((roadmap_item, index) => ( 147 | 154 |
164 |
165 | 166 | {roadmap_item.icon} 167 | 168 |
169 |
170 |
171 |
172 |

173 | {roadmap_item.date} 174 |

175 |
176 |

177 | {roadmap_item.title} 178 |

179 |

180 | {roadmap_item.description} 181 |

182 | {roadmap_item.achievements.map((achievement, index) => ( 183 | 184 | ))} 185 | 186 | {roadmap_item.footer && ( 187 |

{roadmap_item.footer}

188 | )} 189 |
190 |
191 | ))} 192 |
193 |
194 |
195 | ); 196 | } 197 | -------------------------------------------------------------------------------- /components/section/sponsorships-category.tsx: -------------------------------------------------------------------------------- 1 | import { Sponsor } from "@/components/custom/sponsor"; 2 | import markdownit from "markdown-it"; 3 | 4 | export function SponsorshipsCategory({ 5 | tiers, 6 | sponsors, 7 | icon, 8 | label, 9 | }: { 10 | tiers: TierElement[]; 11 | sponsors: SponsorshipsAsMaintainerNode[]; 12 | icon: React.ReactNode; 13 | label: string; 14 | }) { 15 | const md = markdownit(); 16 | function getSponsorsByTier(tierId: string) { 17 | return sponsors.filter((sponsor) => sponsor.tier.id === tierId); 18 | } 19 | const featuredTierId = "ST_kwDOAAQmKM4AAvQg"; 20 | 21 | return ( 22 |
23 |
24 | 33 | 34 |
35 | {tiers.map((tier) => { 36 | const tierSponsors = getSponsorsByTier(tier.id); 37 | return ( 38 |
42 |

43 | {tier.name} 44 | 45 | {tierSponsors.length > 0 && ( 46 | 47 | {tierSponsors.length} sponsor 48 | {tierSponsors.length > 1 ? "s" : ""} 49 | 50 | )} 51 |

52 | 53 |
54 | {tierSponsors.map((sponsor) => ( 55 | 59 | ))} 60 |
61 | 62 |
65 | dangerouslySetInnerHTML={{ 66 | __html: md.render(tier.description!), 67 | }} 68 | /> 69 |
70 | ); 71 | })} 72 |
73 |
74 | ); 75 | } 76 | -------------------------------------------------------------------------------- /components/section/sponsorships-header.tsx: -------------------------------------------------------------------------------- 1 | import { ActiveGoal } from "@/components/custom/active-goal"; 2 | import { SupportCta } from "@/components/section/support-cta"; 3 | import { cn } from "@/lib/utils"; 4 | import Image from "next/image"; 5 | 6 | export function SponsorshipsHeader({ 7 | githubResponse, 8 | }: { githubResponse: Externals.Github.APIResponse }) { 9 | const activeGoal = githubResponse.data.viewer.sponsorsListing.activeGoal; 10 | const firstMonthlySponsor = 11 | githubResponse.data.viewer.sponsorshipsAsMaintainer.nodes.find( 12 | (sponsor) => sponsor.tier.isOneTime === false, 13 | ); 14 | const monthlySponsors = 15 | githubResponse.data.viewer.sponsorshipsAsMaintainer.nodes.filter( 16 | (sponsor) => sponsor.isActive && sponsor.tier.isOneTime === false, 17 | ); 18 | const monthlySponsorCount = monthlySponsors.length; 19 | 20 | return ( 21 |
22 |
23 | sponsorship 30 |
31 | {githubResponse.data.viewer.sponsorsListing.fullDescription 32 | .split("\n") 33 | .map((line, index) => ( 34 |

38 | {line} 39 |

40 | ))} 41 |
42 |
43 |
44 | {activeGoal && ( 45 | 50 | )} 51 | 52 |
53 |
54 | ); 55 | } 56 | -------------------------------------------------------------------------------- /components/section/sponsorships-mine.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | import Link from "next/link"; 3 | 4 | export function SponsorshipsMine({ 5 | githubResponse, 6 | }: { githubResponse: Externals.Github.APIResponse }) { 7 | const mySponsorships = githubResponse.data.viewer.sponsoring.nodes.sort( 8 | (a, b) => 9 | new Date(a.sponsorshipForViewerAsSponsor.createdAt).getTime() > 10 | new Date(b.sponsorshipForViewerAsSponsor.createdAt).getTime() 11 | ? -1 12 | : 1, 13 | ); 14 | 15 | return ( 16 |
17 |

My Sponsorships

18 | 19 |
20 | {mySponsorships.map((sponsorship) => { 21 | return ( 22 | 28 |
29 | 36 |
37 |

{sponsorship.name}

38 |

39 | @{sponsorship.login}{" "} 40 | 41 | • {sponsorship.sponsorshipForViewerAsSponsor.tier.name} 42 | 43 |

44 |
45 |
46 | 47 | ); 48 | })} 49 |
50 |
51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /components/section/sponsorships-tiers.tsx: -------------------------------------------------------------------------------- 1 | import { SponsorshipsCategory } from "@/components/section/sponsorships-category"; 2 | import { IconCircleNumber1, IconRotateClockwise2 } from "@tabler/icons-react"; 3 | 4 | export function SponsorshipsTiers({ 5 | githubResponse, 6 | }: { githubResponse: Externals.Github.APIResponse }) { 7 | const tiers = githubResponse.data.viewer.sponsorsListing.tiers.nodes.sort( 8 | (a, b) => { 9 | if (a.isOneTime !== b.isOneTime) { 10 | return a.isOneTime ? -1 : 1; 11 | } 12 | return a.monthlyPriceInDollars - b.monthlyPriceInDollars; 13 | }, 14 | ); 15 | 16 | const tiersByMode = tiers.reduce( 17 | (acc, tier) => { 18 | if (tier.isOneTime) { 19 | acc.oneTime.push(tier); 20 | } else { 21 | acc.recurring.push(tier); 22 | } 23 | return acc; 24 | }, 25 | { oneTime: [] as TierElement[], recurring: [] as TierElement[] }, 26 | ); 27 | 28 | return ( 29 |
30 | } 34 | label="One-time" 35 | /> 36 | } 40 | label="Recurring" 41 | /> 42 |
43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /components/section/support-cta.tsx: -------------------------------------------------------------------------------- 1 | import AnimatedGradientSponsorText from "@/components/custom/animated-gradient-sponsor-text"; 2 | import { SPONSOR_LINK } from "@/lib/constants"; 3 | import { cn } from "@/lib/utils"; 4 | import { IconChevronRight, IconHeartFilled } from "@tabler/icons-react"; 5 | import Link from "next/link"; 6 | 7 | export function SupportCta({ 8 | className, 9 | subClassName, 10 | minimal = false, 11 | label = "Sponsor this project", 12 | }: { 13 | className?: string; 14 | subClassName?: string; 15 | label?: string; 16 | minimal?: boolean; 17 | }) { 18 | return ( 19 |
20 | 25 | 26 | {" "} 27 | 33 | {label} 34 | 35 | 36 | 37 | 38 |
39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /components/section/supporters.tsx: -------------------------------------------------------------------------------- 1 | import { EventSponsor } from "@/components/custom/event-sponsor"; 2 | import { Sponsor } from "@/components/custom/sponsor"; 3 | import { Strong } from "@/components/ui/text"; 4 | import { 5 | Tooltip, 6 | TooltipContent, 7 | TooltipTrigger, 8 | } from "@/components/ui/tooltip"; 9 | import { cn } from "@/lib/utils"; 10 | import { getGithubInfo } from "@/server/github"; 11 | import { IconArrowRight, IconBrandYoutubeFilled } from "@tabler/icons-react"; 12 | import Image from "next/image"; 13 | import Link from "next/link"; 14 | 15 | const EXTERNAL_SPONSORS = [ 16 | { 17 | username: "oguzyagizkara", 18 | tooltip: "Oğuz Yağız Kara", 19 | platform: "twitter", 20 | includeInTotal: true, 21 | }, 22 | ]; 23 | 24 | export async function Supporters() { 25 | const githubResponse = await getGithubInfo(); 26 | const allSupporters = 27 | githubResponse.data.viewer.sponsorshipsAsMaintainer.nodes.sort((a, b) => 28 | new Date(a.createdAt).getTime() > new Date(b.createdAt).getTime() 29 | ? 1 30 | : -1, 31 | ); 32 | 33 | return ( 34 | <> 35 |

36 | 37 | {githubResponse.data.viewer.sponsorshipsAsMaintainer.totalCount + 38 | EXTERNAL_SPONSORS.filter((s) => s.includeInTotal).length}{" "} 39 | people 40 | {" "} 41 | sponsoring gider.im 42 |

43 | 44 |
45 | 46 | 47 | Special Sponsor 48 |

Designer Daily Report

49 |

50 | Daily Curated, Interactive Newspaper 51 |
52 | for Designers. 53 |

54 |
55 | 56 | 64 | Designerdailyreport 71 | 72 | 73 |
74 | 75 | 76 | Special Sponsor 77 |

eser.live

78 |

79 | Eser Özvataf's
80 | YouTube channel 81 |

82 |
83 | 84 | 92 | 93 | 94 | 95 |
96 | {EXTERNAL_SPONSORS.map((sponsor) => ( 97 | 98 | ))} 99 | {allSupporters.map((sponsor) => ( 100 | 104 | ))} 105 |
106 | 107 |
108 | 109 | 110 | Become a sponsor{" "} 111 | 112 | 113 | 114 |
115 | 116 | ); 117 | } 118 | -------------------------------------------------------------------------------- /components/ui/avatar.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as AvatarPrimitive from "@radix-ui/react-avatar"; 4 | import * as React from "react"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | const Avatar = React.forwardRef< 9 | React.ComponentRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, ...props }, ref) => ( 12 | 20 | )); 21 | Avatar.displayName = AvatarPrimitive.Root.displayName; 22 | 23 | const AvatarImage = React.forwardRef< 24 | React.ComponentRef, 25 | React.ComponentPropsWithoutRef 26 | >(({ className, ...props }, ref) => ( 27 | 32 | )); 33 | AvatarImage.displayName = AvatarPrimitive.Image.displayName; 34 | 35 | const AvatarFallback = React.forwardRef< 36 | React.ComponentRef, 37 | React.ComponentPropsWithoutRef 38 | >(({ className, ...props }, ref) => ( 39 | 47 | )); 48 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName; 49 | 50 | export { Avatar, AvatarFallback, AvatarImage }; 51 | -------------------------------------------------------------------------------- /components/ui/badge.tsx: -------------------------------------------------------------------------------- 1 | import * as Headless from "@headlessui/react"; 2 | import clsx from "clsx"; 3 | import type React from "react"; 4 | import { forwardRef } from "react"; 5 | import { TouchTarget } from "./button"; 6 | import { Link } from "./link"; 7 | 8 | const colors = { 9 | red: "bg-red-500/15 text-red-700 group-data-hover:bg-red-500/25 dark:bg-red-500/10 dark:text-red-400 dark:group-data-hover:bg-red-500/20", 10 | orange: 11 | "bg-orange-500/15 text-orange-700 group-data-hover:bg-orange-500/25 dark:bg-orange-500/10 dark:text-orange-400 dark:group-data-hover:bg-orange-500/20", 12 | amber: 13 | "bg-amber-400/20 text-amber-700 group-data-hover:bg-amber-400/30 dark:bg-amber-400/10 dark:text-amber-400 dark:group-data-hover:bg-amber-400/15", 14 | yellow: 15 | "bg-yellow-400/20 text-yellow-700 group-data-hover:bg-yellow-400/30 dark:bg-yellow-400/10 dark:text-yellow-300 dark:group-data-hover:bg-yellow-400/15", 16 | lime: "bg-lime-400/20 text-lime-700 group-data-hover:bg-lime-400/30 dark:bg-lime-400/10 dark:text-lime-300 dark:group-data-hover:bg-lime-400/15", 17 | green: 18 | "bg-green-500/15 text-green-700 group-data-hover:bg-green-500/25 dark:bg-green-500/10 dark:text-green-400 dark:group-data-hover:bg-green-500/20", 19 | emerald: 20 | "bg-emerald-500/15 text-emerald-700 group-data-hover:bg-emerald-500/25 dark:bg-emerald-500/10 dark:text-emerald-400 dark:group-data-hover:bg-emerald-500/20", 21 | teal: "bg-teal-500/15 text-teal-700 group-data-hover:bg-teal-500/25 dark:bg-teal-500/10 dark:text-teal-300 dark:group-data-hover:bg-teal-500/20", 22 | cyan: "bg-cyan-400/20 text-cyan-700 group-data-hover:bg-cyan-400/30 dark:bg-cyan-400/10 dark:text-cyan-300 dark:group-data-hover:bg-cyan-400/15", 23 | sky: "bg-sky-500/15 text-sky-700 group-data-hover:bg-sky-500/25 dark:bg-sky-500/10 dark:text-sky-300 dark:group-data-hover:bg-sky-500/20", 24 | blue: "bg-blue-500/15 text-blue-700 group-data-hover:bg-blue-500/25 dark:text-blue-400 dark:group-data-hover:bg-blue-500/25", 25 | indigo: 26 | "bg-indigo-500/15 text-indigo-700 group-data-hover:bg-indigo-500/25 dark:text-indigo-400 dark:group-data-hover:bg-indigo-500/20", 27 | violet: 28 | "bg-violet-500/15 text-violet-700 group-data-hover:bg-violet-500/25 dark:text-violet-400 dark:group-data-hover:bg-violet-500/20", 29 | purple: 30 | "bg-purple-500/15 text-purple-700 group-data-hover:bg-purple-500/25 dark:text-purple-400 dark:group-data-hover:bg-purple-500/20", 31 | fuchsia: 32 | "bg-fuchsia-400/15 text-fuchsia-700 group-data-hover:bg-fuchsia-400/25 dark:bg-fuchsia-400/10 dark:text-fuchsia-400 dark:group-data-hover:bg-fuchsia-400/20", 33 | pink: "bg-pink-400/15 text-pink-700 group-data-hover:bg-pink-400/25 dark:bg-pink-400/10 dark:text-pink-400 dark:group-data-hover:bg-pink-400/20", 34 | rose: "bg-rose-400/15 text-rose-700 group-data-hover:bg-rose-400/25 dark:bg-rose-400/10 dark:text-rose-400 dark:group-data-hover:bg-rose-400/20", 35 | zinc: "bg-zinc-600/10 text-zinc-700 group-data-hover:bg-zinc-600/20 dark:bg-white/5 dark:text-zinc-400 dark:group-data-hover:bg-white/10", 36 | }; 37 | 38 | type BadgeProps = { color?: keyof typeof colors }; 39 | 40 | export function Badge({ 41 | color = "zinc", 42 | className, 43 | ...props 44 | }: BadgeProps & React.ComponentPropsWithoutRef<"span">) { 45 | return ( 46 | 54 | ); 55 | } 56 | 57 | export const BadgeButton = forwardRef(function BadgeButton( 58 | { 59 | color = "zinc", 60 | className, 61 | children, 62 | ...props 63 | }: BadgeProps & { className?: string; children: React.ReactNode } & ( 64 | | Omit 65 | | Omit, "className"> 66 | ), 67 | ref: React.ForwardedRef, 68 | ) { 69 | const classes = clsx( 70 | className, 71 | "group relative inline-flex rounded-md focus:outline-hidden data-focus:outline data-focus:outline-2 data-focus:outline-offset-2 data-focus:outline-blue-500", 72 | ); 73 | 74 | return "href" in props ? ( 75 | } 79 | > 80 | 81 | {children} 82 | 83 | 84 | ) : ( 85 | 86 | 87 | {children} 88 | 89 | 90 | ); 91 | }); 92 | -------------------------------------------------------------------------------- /components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as Headless from "@headlessui/react"; 2 | import clsx from "clsx"; 3 | import type React from "react"; 4 | import { forwardRef } from "react"; 5 | import { Link } from "./link"; 6 | 7 | const styles = { 8 | base: [ 9 | // Base 10 | "relative isolate inline-flex items-center justify-center gap-x-3 rounded-lg border text-base/6 font-medium", 11 | "cursor-pointer", 12 | // Sizing 13 | "px-[calc(--spacing(3.5)-1px)] py-[calc(--spacing(2.5)-1px)] sm:px-[calc(--spacing(3)-1px)] sm:py-[calc(--spacing(1.5)-1px)] sm:text-sm/6", 14 | // Focus 15 | "focus:outline-hidden data-focus:outline data-focus:outline-2 data-focus:outline-offset-2 data-focus:outline-blue-500", 16 | // Disabled 17 | "data-disabled:opacity-50", 18 | // Icon 19 | "*:data-[slot=icon]:-mx-0.5 *:data-[slot=icon]:my-0.5 *:data-[slot=icon]:size-5 *:data-[slot=icon]:shrink-0 *:data-[slot=icon]:text-(--btn-icon) sm:*:data-[slot=icon]:my-1 sm:*:data-[slot=icon]:size-4 forced-colors:[--btn-icon:ButtonText] forced-colors:data-hover:[--btn-icon:ButtonText]", 20 | ], 21 | solid: [ 22 | // Optical border, implemented as the button background to avoid corner artifacts 23 | "border-transparent bg-(--btn-border)", 24 | // Dark mode: border is rendered on `after` so background is set to button background 25 | "dark:bg-(--btn-bg)", 26 | // Button background, implemented as foreground layer to stack on top of pseudo-border layer 27 | "before:absolute before:inset-0 before:-z-10 before:rounded-[calc(var(--radius-lg)-1px)] before:bg-(--btn-bg)", 28 | // Drop shadow, applied to the inset `before` layer so it blends with the border 29 | "before:shadow-sm", 30 | // Background color is moved to control and shadow is removed in dark mode so hide `before` pseudo 31 | "dark:before:hidden", 32 | // Dark mode: Subtle white outline is applied using a border 33 | "dark:border-white/5", 34 | // Shim/overlay, inset to match button foreground and used for hover state + highlight shadow 35 | "after:absolute after:inset-0 after:-z-10 after:rounded-[calc(var(--radius-lg)-1px)]", 36 | // Inner highlight shadow 37 | "after:shadow-[shadow:inset_0_1px_--theme(--color-white/15%)]", 38 | // White overlay on hover 39 | "data-active:after:bg-(--btn-hover-overlay) data-hover:after:bg-(--btn-hover-overlay)", 40 | // Dark mode: `after` layer expands to cover entire button 41 | "dark:after:-inset-px dark:after:rounded-lg", 42 | // Disabled 43 | "data-disabled:before:shadow-none data-disabled:after:shadow-none", 44 | ], 45 | outline: [ 46 | // Base 47 | "border-zinc-950/10 text-zinc-950 data-active:bg-zinc-950/[2.5%] data-hover:bg-zinc-950/[2.5%]", 48 | // Dark mode 49 | "dark:border-white/15 dark:text-white dark:[--btn-bg:transparent] dark:data-active:bg-white/5 dark:data-hover:bg-white/5", 50 | // Icon 51 | "[--btn-icon:var(--color-zinc-500)] data-active:[--btn-icon:var(--color-zinc-700)] data-hover:[--btn-icon:var(--color-zinc-700)] dark:data-active:[--btn-icon:var(--color-zinc-400)] dark:data-hover:[--btn-icon:var(--color-zinc-400)]", 52 | ], 53 | plain: [ 54 | // Base 55 | "border-transparent text-zinc-950 data-active:bg-zinc-950/5 data-hover:bg-zinc-950/5", 56 | // Dark mode 57 | "dark:text-white dark:data-active:bg-white/10 dark:data-hover:bg-white/10", 58 | // Icon 59 | "[--btn-icon:var(--color-zinc-500)] data-active:[--btn-icon:var(--color-zinc-700)] data-hover:[--btn-icon:var(--color-zinc-700)] dark:[--btn-icon:var(--color-zinc-500)] dark:data-active:[--btn-icon:var(--color-zinc-400)] dark:data-hover:[--btn-icon:var(--color-zinc-400)]", 60 | ], 61 | colors: { 62 | "dark/zinc": [ 63 | "text-white [--btn-bg:var(--color-zinc-900)] [--btn-border:var(--color-zinc-950)]/90 [--btn-hover-overlay:var(--color-white)]/10", 64 | "dark:text-white dark:[--btn-bg:var(--color-zinc-600)] dark:[--btn-hover-overlay:var(--color-white)]/5", 65 | "[--btn-icon:var(--color-zinc-400)] data-active:[--btn-icon:var(--color-zinc-300)] data-hover:[--btn-icon:var(--color-zinc-300)]", 66 | ], 67 | light: [ 68 | "text-zinc-950 [--btn-bg:white] [--btn-border:var(--color-zinc-950)]/10 [--btn-hover-overlay:var(--color-zinc-950)]/[2.5%] data-active:[--btn-border:var(--color-zinc-950)]/15 data-hover:[--btn-border:var(--color-zinc-950)]/15", 69 | "dark:text-white dark:[--btn-hover-overlay:var(--color-white)]/5 dark:[--btn-bg:var(--color-zinc-800)]", 70 | "[--btn-icon:var(--color-zinc-500)] data-active:[--btn-icon:var(--color-zinc-700)] data-hover:[--btn-icon:var(--color-zinc-700)] dark:[--btn-icon:var(--color-zinc-500)] dark:data-active:[--btn-icon:var(--color-zinc-400)] dark:data-hover:[--btn-icon:var(--color-zinc-400)]", 71 | ], 72 | "dark/white": [ 73 | "text-white [--btn-bg:var(--color-zinc-900)] [--btn-border:var(--color-zinc-950)]/90 [--btn-hover-overlay:var(--color-white)]/10", 74 | "dark:text-zinc-950 dark:[--btn-bg:white] dark:[--btn-hover-overlay:var(--color-zinc-950)]/5", 75 | "[--btn-icon:var(--color-zinc-400)] data-active:[--btn-icon:var(--color-zinc-300)] data-hover:[--btn-icon:var(--color-zinc-300)] dark:[--btn-icon:var(--color-zinc-500)] dark:data-active:[--btn-icon:var(--color-zinc-400)] dark:data-hover:[--btn-icon:var(--color-zinc-400)]", 76 | ], 77 | dark: [ 78 | "text-white [--btn-bg:var(--color-zinc-900)] [--btn-border:var(--color-zinc-950)]/90 [--btn-hover-overlay:var(--color-white)]/10", 79 | "dark:[--btn-hover-overlay:var(--color-white)]/5 dark:[--btn-bg:var(--color-zinc-800)]", 80 | "[--btn-icon:var(--color-zinc-400)] data-active:[--btn-icon:var(--color-zinc-300)] data-hover:[--btn-icon:var(--color-zinc-300)]", 81 | ], 82 | white: [ 83 | "text-zinc-950 [--btn-bg:white] [--btn-border:var(--color-zinc-950)]/10 [--btn-hover-overlay:var(--color-zinc-950)]/[2.5%] data-active:[--btn-border:var(--color-zinc-950)]/15 data-hover:[--btn-border:var(--color-zinc-950)]/15", 84 | "dark:[--btn-hover-overlay:var(--color-zinc-950)]/5", 85 | "[--btn-icon:var(--color-zinc-400)] data-active:[--btn-icon:var(--color-zinc-500)] data-hover:[--btn-icon:var(--color-zinc-500)]", 86 | ], 87 | zinc: [ 88 | "text-white [--btn-hover-overlay:var(--color-white)]/10 [--btn-bg:var(--color-zinc-600)] [--btn-border:var(--color-zinc-700)]/90", 89 | "dark:[--btn-hover-overlay:var(--color-white)]/5", 90 | "[--btn-icon:var(--color-zinc-400)] data-active:[--btn-icon:var(--color-zinc-300)] data-hover:[--btn-icon:var(--color-zinc-300)]", 91 | ], 92 | indigo: [ 93 | "text-white [--btn-hover-overlay:var(--color-white)]/10 [--btn-bg:var(--color-indigo-500)] [--btn-border:var(--color-indigo-600)]/90", 94 | "[--btn-icon:var(--color-indigo-300)] data-active:[--btn-icon:var(--color-indigo-200)] data-hover:[--btn-icon:var(--color-indigo-200)]", 95 | ], 96 | cyan: [ 97 | "text-cyan-950 [--btn-bg:var(--color-cyan-300)] [--btn-border:var(--color-cyan-400)]/80 [--btn-hover-overlay:var(--color-white)]/25", 98 | "[--btn-icon:var(--color-cyan-500)]", 99 | ], 100 | red: [ 101 | "text-white [--btn-hover-overlay:var(--color-white)]/10 [--btn-bg:var(--color-red-600)] [--btn-border:var(--color-red-700)]/90", 102 | "[--btn-icon:var(--color-red-300)] data-active:[--btn-icon:var(--color-red-200)] data-hover:[--btn-icon:var(--color-red-200)]", 103 | ], 104 | orange: [ 105 | "text-white [--btn-hover-overlay:var(--color-white)]/10 [--btn-bg:var(--color-orange-500)] [--btn-border:var(--color-orange-600)]/90", 106 | "[--btn-icon:var(--color-orange-300)] data-active:[--btn-icon:var(--color-orange-200)] data-hover:[--btn-icon:var(--color-orange-200)]", 107 | ], 108 | amber: [ 109 | "text-amber-950 [--btn-hover-overlay:var(--color-white)]/25 [--btn-bg:var(--color-amber-400)] [--btn-border:var(--color-amber-500)]/80", 110 | "[--btn-icon:var(--color-amber-600)]", 111 | ], 112 | yellow: [ 113 | "text-yellow-950 [--btn-hover-overlay:var(--color-white)]/25 [--btn-bg:var(--color-yellow-300)] [--btn-border:var(--color-yellow-400)]/80", 114 | "[--btn-icon:var(--color-yellow-600)] data-active:[--btn-icon:var(--color-yellow-700)] data-hover:[--btn-icon:var(--color-yellow-700)]", 115 | ], 116 | lime: [ 117 | "text-lime-950 [--btn-hover-overlay:var(--color-white)]/25 [--btn-bg:var(--color-lime-300)] [--btn-border:var(--color-lime-400)]/80", 118 | "[--btn-icon:var(--color-lime-600)] data-active:[--btn-icon:var(--color-lime-700)] data-hover:[--btn-icon:var(--color-lime-700)]", 119 | ], 120 | green: [ 121 | "text-white [--btn-hover-overlay:var(--color-white)]/10 [--btn-bg:var(--color-green-600)] [--btn-border:var(--color-green-700)]/90", 122 | "[--btn-icon:var(--color-white)]/60 data-active:[--btn-icon:var(--color-white)]/80 data-hover:[--btn-icon:var(--color-white)]/80", 123 | ], 124 | emerald: [ 125 | "text-white [--btn-hover-overlay:var(--color-white)]/10 [--btn-bg:var(--color-emerald-600)] [--btn-border:var(--color-emerald-700)]/90", 126 | "[--btn-icon:var(--color-white)]/60 data-active:[--btn-icon:var(--color-white)]/80 data-hover:[--btn-icon:var(--color-white)]/80", 127 | ], 128 | teal: [ 129 | "text-white [--btn-hover-overlay:var(--color-white)]/10 [--btn-bg:var(--color-teal-600)] [--btn-border:var(--color-teal-700)]/90", 130 | "[--btn-icon:var(--color-white)]/60 data-active:[--btn-icon:var(--color-white)]/80 data-hover:[--btn-icon:var(--color-white)]/80", 131 | ], 132 | sky: [ 133 | "text-white [--btn-hover-overlay:var(--color-white)]/10 [--btn-bg:var(--color-sky-500)] [--btn-border:var(--color-sky-600)]/80", 134 | "[--btn-icon:var(--color-white)]/60 data-active:[--btn-icon:var(--color-white)]/80 data-hover:[--btn-icon:var(--color-white)]/80", 135 | ], 136 | blue: [ 137 | "text-white [--btn-hover-overlay:var(--color-white)]/10 [--btn-bg:var(--color-blue-600)] [--btn-border:var(--color-blue-700)]/90", 138 | "[--btn-icon:var(--color-blue-400)] data-active:[--btn-icon:var(--color-blue-300)] data-hover:[--btn-icon:var(--color-blue-300)]", 139 | ], 140 | violet: [ 141 | "text-white [--btn-hover-overlay:var(--color-white)]/10 [--btn-bg:var(--color-violet-500)] [--btn-border:var(--color-violet-600)]/90", 142 | "[--btn-icon:var(--color-violet-300)] data-active:[--btn-icon:var(--color-violet-200)] data-hover:[--btn-icon:var(--color-violet-200)]", 143 | ], 144 | purple: [ 145 | "text-white [--btn-hover-overlay:var(--color-white)]/10 [--btn-bg:var(--color-purple-500)] [--btn-border:var(--color-purple-600)]/90", 146 | "[--btn-icon:var(--color-purple-300)] data-active:[--btn-icon:var(--color-purple-200)] data-hover:[--btn-icon:var(--color-purple-200)]", 147 | ], 148 | fuchsia: [ 149 | "text-white [--btn-hover-overlay:var(--color-white)]/10 [--btn-bg:var(--color-fuchsia-500)] [--btn-border:var(--color-fuchsia-600)]/90", 150 | "[--btn-icon:var(--color-fuchsia-300)] data-active:[--btn-icon:var(--color-fuchsia-200)] data-hover:[--btn-icon:var(--color-fuchsia-200)]", 151 | ], 152 | pink: [ 153 | "text-white [--btn-hover-overlay:var(--color-white)]/10 [--btn-bg:var(--color-pink-500)] [--btn-border:var(--color-pink-600)]/90", 154 | "[--btn-icon:var(--color-pink-300)] data-active:[--btn-icon:var(--color-pink-200)] data-hover:[--btn-icon:var(--color-pink-200)]", 155 | ], 156 | rose: [ 157 | "text-white [--btn-hover-overlay:var(--color-white)]/10 [--btn-bg:var(--color-rose-500)] [--btn-border:var(--color-rose-600)]/90", 158 | "[--btn-icon:var(--color-rose-300)] data-active:[--btn-icon:var(--color-rose-200)] data-hover:[--btn-icon:var(--color-rose-200)]", 159 | ], 160 | }, 161 | }; 162 | 163 | type ButtonProps = ( 164 | | { color?: keyof typeof styles.colors; outline?: never; plain?: never } 165 | | { color?: never; outline: true; plain?: never } 166 | | { color?: never; outline?: never; plain: true } 167 | ) & { className?: string; children: React.ReactNode } & ( 168 | | Omit 169 | | Omit, "className"> 170 | ); 171 | 172 | export const Button = forwardRef(function Button( 173 | { color, outline, plain, className, children, ...props }: ButtonProps, 174 | ref: React.ForwardedRef, 175 | ) { 176 | const classes = clsx( 177 | className, 178 | styles.base, 179 | outline 180 | ? styles.outline 181 | : plain 182 | ? styles.plain 183 | : clsx(styles.solid, styles.colors[color ?? "dark/zinc"]), 184 | ); 185 | 186 | return "href" in props ? ( 187 | } 191 | > 192 | {children} 193 | 194 | ) : ( 195 | 200 | {children} 201 | 202 | ); 203 | }); 204 | 205 | /** 206 | * Expand the hit area to at least 44×44px on touch devices 207 | */ 208 | export function TouchTarget({ children }: { children: React.ReactNode }) { 209 | return ( 210 | <> 211 |
115 | 116 |
117 |

118 | Copyright © 2024 Nedim Arabacı 119 |

120 |
121 | ); 122 | } 123 | -------------------------------------------------------------------------------- /components/ui/link.tsx: -------------------------------------------------------------------------------- 1 | import * as Headless from "@headlessui/react"; 2 | import NextLink, { type LinkProps } from "next/link"; 3 | import type React from "react"; 4 | import { forwardRef } from "react"; 5 | 6 | export const Link = forwardRef(function Link( 7 | props: LinkProps & React.ComponentPropsWithoutRef<"a">, 8 | ref: React.ForwardedRef, 9 | ) { 10 | return ( 11 | 12 | 13 | 14 | ); 15 | }); 16 | -------------------------------------------------------------------------------- /components/ui/navbar.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as Headless from "@headlessui/react"; 4 | import clsx from "clsx"; 5 | import { LayoutGroup, motion } from "motion/react"; 6 | import type React from "react"; 7 | import { forwardRef, useId } from "react"; 8 | import { TouchTarget } from "./button"; 9 | import { Link } from "./link"; 10 | 11 | export function Navbar({ 12 | className, 13 | ...props 14 | }: React.ComponentPropsWithoutRef<"nav">) { 15 | return ( 16 |