├── .github └── FUNDING.yml ├── public ├── robots.txt ├── og.png ├── favicon.ico ├── favicon-48x48.png ├── apple-touch-icon.png ├── ddr-logo-circle.png ├── web-app-manifest-192x192.png ├── web-app-manifest-512x512.png ├── site.webmanifest ├── logo.svg └── favicon.svg ├── .env.example ├── .vscode ├── extensions.json └── settings.json ├── .npmrc ├── fonts ├── GTWalsheimPro-Bold.woff2 ├── GTWalsheimPro-Medium.woff2 └── GTWalsheimPro-Regular.woff2 ├── renovate.json ├── lib ├── constants.ts └── utils.ts ├── app ├── roadmap │ └── page.tsx ├── template.tsx ├── sponsorship │ └── page.tsx ├── page.tsx ├── not-found.tsx ├── globals.css └── layout.tsx ├── postcss.config.mjs ├── components.json ├── next.config.mjs ├── components ├── ui │ ├── link.tsx │ ├── tooltip.tsx │ ├── text.tsx │ ├── avatar.tsx │ ├── rainbow-button.tsx │ ├── hyper-text.tsx │ ├── navbar.tsx │ ├── badge.tsx │ ├── layouts │ │ └── stacked-layout.tsx │ ├── sidebar.tsx │ ├── dropdown.tsx │ └── button.tsx ├── custom │ ├── roadmap-achievement.tsx │ ├── breakpoint-debug.tsx │ ├── animated-gradient-sponsor-text.tsx │ ├── active-goal.tsx │ ├── sponsor.tsx │ ├── logo.tsx │ └── event-sponsor.tsx └── section │ ├── sponsorships-tiers.tsx │ ├── support-cta.tsx │ ├── sponsorships-mine.tsx │ ├── sponsorships-header.tsx │ ├── hero.tsx │ ├── sponsorships-category.tsx │ ├── pain-point.tsx │ ├── all-features.tsx │ ├── supporters.tsx │ └── roadmap.tsx ├── .gitignore ├── tsconfig.json ├── biome.json ├── package.json ├── README.md ├── types └── global.d.ts └── server └── github.ts /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [needim] 2 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Allow: / -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | GITHUB_TOKEN=your_github_token_here -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["biomejs.biome"] 3 | } 4 | -------------------------------------------------------------------------------- /public/og.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/needim/gider.im-website/HEAD/public/og.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/needim/gider.im-website/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/favicon-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/needim/gider.im-website/HEAD/public/favicon-48x48.png -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | @hugeicons:registry=https://npm.hugeicons.com 2 | //npm.hugeicons.com/:_authToken=${HUGEICONS_NPM_TOKEN} -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/needim/gider.im-website/HEAD/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/ddr-logo-circle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/needim/gider.im-website/HEAD/public/ddr-logo-circle.png -------------------------------------------------------------------------------- /fonts/GTWalsheimPro-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/needim/gider.im-website/HEAD/fonts/GTWalsheimPro-Bold.woff2 -------------------------------------------------------------------------------- /fonts/GTWalsheimPro-Medium.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/needim/gider.im-website/HEAD/fonts/GTWalsheimPro-Medium.woff2 -------------------------------------------------------------------------------- /fonts/GTWalsheimPro-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/needim/gider.im-website/HEAD/fonts/GTWalsheimPro-Regular.woff2 -------------------------------------------------------------------------------- /public/web-app-manifest-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/needim/gider.im-website/HEAD/public/web-app-manifest-192x192.png -------------------------------------------------------------------------------- /public/web-app-manifest-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/needim/gider.im-website/HEAD/public/web-app-manifest-512x512.png -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["config:recommended"] 4 | } 5 | -------------------------------------------------------------------------------- /lib/constants.ts: -------------------------------------------------------------------------------- 1 | export const SPONSOR_LINK = "https://github.com/sponsors/needim"; 2 | export const PWA_LINK = "https://app.gider.im"; 3 | -------------------------------------------------------------------------------- /app/roadmap/page.tsx: -------------------------------------------------------------------------------- 1 | import Roadmap from "@/components/section/roadmap"; 2 | 3 | export default function Page() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | '@tailwindcss/postcss': {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[typescript]": { 3 | "editor.defaultFormatter": "biomejs.biome" 4 | }, 5 | "[typescriptreact]": { 6 | "editor.defaultFormatter": "biomejs.biome" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx"; 2 | import { twMerge } from "tailwind-merge"; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | images: { 4 | remotePatterns: [ 5 | { 6 | protocol: "https", 7 | hostname: "avatars.githubusercontent.com", 8 | port: "", 9 | }, 10 | { 11 | protocol: "https", 12 | hostname: "unavatar.io", 13 | port: "", 14 | }, 15 | ], 16 | }, 17 | }; 18 | 19 | export default nextConfig; 20 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /public/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gider.im", 3 | "short_name": "gider.im", 4 | "icons": [ 5 | { 6 | "src": "/web-app-manifest-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png", 9 | "purpose": "maskable" 10 | }, 11 | { 12 | "src": "/web-app-manifest-512x512.png", 13 | "sizes": "512x512", 14 | "type": "image/png", 15 | "purpose": "maskable" 16 | } 17 | ], 18 | "theme_color": "#ffffff", 19 | "background_color": "#ffffff", 20 | "display": "standalone" 21 | } -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": [ 4 | "dom", 5 | "dom.iterable", 6 | "esnext" 7 | ], 8 | "allowJs": true, 9 | "skipLibCheck": true, 10 | "strict": true, 11 | "noEmit": true, 12 | "esModuleInterop": true, 13 | "module": "esnext", 14 | "moduleResolution": "bundler", 15 | "resolveJsonModule": true, 16 | "isolatedModules": true, 17 | "jsx": "preserve", 18 | "incremental": true, 19 | "plugins": [ 20 | { 21 | "name": "next" 22 | } 23 | ], 24 | "paths": { 25 | "@/*": [ 26 | "./*" 27 | ] 28 | }, 29 | "target": "ES2017" 30 | }, 31 | "include": [ 32 | "next-env.d.ts", 33 | "**/*.ts", 34 | "**/*.tsx", 35 | ".next/types/**/*.ts", 36 | "types/global.d.ts" 37 | ], 38 | "exclude": [ 39 | "node_modules" 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /public/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | -------------------------------------------------------------------------------- /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/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/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/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/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/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 | -------------------------------------------------------------------------------- /public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /components/ui/tooltip.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as TooltipPrimitive from "@radix-ui/react-tooltip"; 4 | import * as React from "react"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | const TooltipProvider = TooltipPrimitive.Provider; 9 | 10 | const Tooltip = TooltipPrimitive.Root; 11 | 12 | const TooltipTrigger = TooltipPrimitive.Trigger; 13 | 14 | const TooltipContent = React.forwardRef< 15 | React.ElementRef, 16 | React.ComponentPropsWithoutRef 17 | >(({ className, sideOffset = 4, ...props }, ref) => ( 18 | 19 | 28 | 29 | )); 30 | TooltipContent.displayName = TooltipPrimitive.Content.displayName; 31 | 32 | export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger }; 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@giderim/website", 3 | "version": "0.5.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "check": "biome check --write .", 10 | "lint": "next lint", 11 | "knip": "knip" 12 | }, 13 | "dependencies": { 14 | "@headlessui/react": "^2.2.0", 15 | "@radix-ui/react-avatar": "^1.1.2", 16 | "@radix-ui/react-tooltip": "^1.1.7", 17 | "@tabler/icons-react": "^3.29.0", 18 | "clsx": "^2.1.1", 19 | "markdown-it": "^14.1.0", 20 | "motion": "^12.0.3", 21 | "next": "15.1.6", 22 | "next-plausible": "^3.12.4", 23 | "react": "19.0.0", 24 | "react-dom": "19.0.0", 25 | "tailwind-merge": "^2.6.0", 26 | "tailwindcss-animate": "^1.0.7" 27 | }, 28 | "devDependencies": { 29 | "@biomejs/biome": "1.9.4", 30 | "@tailwindcss/postcss": "^4.0.0", 31 | "@types/markdown-it": "^14.1.2", 32 | "@types/node": "^22.10.10", 33 | "@types/react": "19.0.8", 34 | "@types/react-dom": "19.0.3", 35 | "knip": "^5.43.1", 36 | "postcss": "^8", 37 | "tailwindcss": "^4.0.0", 38 | "typescript": "^5.7.3" 39 | }, 40 | "packageManager": "pnpm@9.15.4", 41 | "pnpm": { 42 | "overrides": { 43 | "@types/react": "19.0.8", 44 | "@types/react-dom": "19.0.3" 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /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/ui/text.tsx: -------------------------------------------------------------------------------- 1 | import clsx from "clsx"; 2 | import { Link } from "./link"; 3 | 4 | export function Text({ 5 | className, 6 | ...props 7 | }: React.ComponentPropsWithoutRef<"p">) { 8 | return ( 9 |

17 | ); 18 | } 19 | 20 | export function TextLink({ 21 | className, 22 | ...props 23 | }: React.ComponentPropsWithoutRef) { 24 | return ( 25 | 32 | ); 33 | } 34 | 35 | export function Strong({ 36 | className, 37 | ...props 38 | }: React.ComponentPropsWithoutRef<"strong">) { 39 | return ( 40 | 44 | ); 45 | } 46 | 47 | export function Code({ 48 | className, 49 | ...props 50 | }: React.ComponentPropsWithoutRef<"code">) { 51 | return ( 52 | 59 | ); 60 | } 61 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/ui/rainbow-button.tsx: -------------------------------------------------------------------------------- 1 | import type React from "react"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | interface RainbowButtonProps 5 | extends React.ButtonHTMLAttributes {} 6 | 7 | export function RainbowButton({ 8 | children, 9 | className, 10 | ...props 11 | }: RainbowButtonProps) { 12 | return ( 13 | 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /types/global.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace Externals { 2 | namespace Github { 3 | interface APIResponse { 4 | data: Data; 5 | } 6 | } 7 | } 8 | 9 | interface Data { 10 | viewer: Viewer; 11 | } 12 | 13 | interface Viewer { 14 | login: string; 15 | sponsorshipsAsMaintainer: SponsorshipsAsMaintainer; 16 | sponsoring: Sponsoring; 17 | sponsorsListing: SponsorsListing; 18 | } 19 | 20 | interface Sponsoring { 21 | nodes: SponsoringNode[]; 22 | } 23 | 24 | interface SponsoringNode { 25 | login: string; 26 | name: string; 27 | bio: string; 28 | avatarUrl: string; 29 | twitterUsername: string; 30 | sponsorshipForViewerAsSponsor: SponsorshipForViewerAsSponsor; 31 | } 32 | 33 | interface SponsorshipForViewerAsSponsor { 34 | isOneTimePayment: boolean; 35 | isActive: boolean; 36 | createdAt: string; 37 | tier: TierElement; 38 | } 39 | 40 | interface TierElement { 41 | id: string; 42 | isCustomAmount: boolean; 43 | monthlyPriceInDollars: number; 44 | isOneTime: boolean; 45 | name: string; 46 | description: string; 47 | } 48 | 49 | interface SponsorsListing { 50 | url: string; 51 | fullDescription: string; 52 | activeGoal: ActiveGoal; 53 | tiers: Tiers; 54 | } 55 | 56 | interface ActiveGoal { 57 | kind: string; 58 | description: string; 59 | percentComplete: number; 60 | targetValue: number; 61 | title: string; 62 | } 63 | 64 | interface Tiers { 65 | nodes: TierElement[]; 66 | } 67 | 68 | interface SponsorshipsAsMaintainer { 69 | totalCount: number; 70 | nodes: SponsorshipsAsMaintainerNode[]; 71 | } 72 | 73 | interface SponsorshipsAsMaintainerNode { 74 | createdAt: string; 75 | isActive: boolean; 76 | tier: Tier; 77 | sponsorEntity: SponsorEntity; 78 | } 79 | 80 | interface SponsorEntity { 81 | __typename: string; 82 | login: string; 83 | name: null | string; 84 | bio?: null | string; 85 | avatarUrl: string; 86 | twitterUsername: null | string; 87 | description?: string; 88 | } 89 | 90 | interface Tier { 91 | id: string; 92 | name: string; 93 | isOneTime: boolean; 94 | monthlyPriceInDollars: number; 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/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/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/ui/hyper-text.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { AnimatePresence, type Variants, motion } from "motion/react"; 4 | import { useEffect, useRef, useState } from "react"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | interface HyperTextProps { 9 | text: string; 10 | duration?: number; 11 | framerProps?: Variants; 12 | className?: string; 13 | animateOnLoad?: boolean; 14 | } 15 | 16 | const alphabets = "abcdefghijklmnopqrstuvwxyz".split(""); 17 | 18 | const getRandomInt = (max: number) => Math.floor(Math.random() * max); 19 | 20 | export default function HyperText({ 21 | text, 22 | duration = 800, 23 | framerProps = { 24 | initial: { opacity: 0, y: -10 }, 25 | animate: { opacity: 1, y: 0 }, 26 | exit: { opacity: 0, y: 3 }, 27 | }, 28 | className, 29 | animateOnLoad = true, 30 | }: HyperTextProps) { 31 | const [displayText, setDisplayText] = useState(text.split("")); 32 | const [trigger, setTrigger] = useState(false); 33 | const interations = useRef(0); 34 | const isFirstRender = useRef(true); 35 | 36 | const triggerAnimation = () => { 37 | interations.current = 0; 38 | setTrigger(true); 39 | }; 40 | 41 | // biome-ignore lint/correctness/useExhaustiveDependencies: 42 | useEffect(() => { 43 | const interval = setInterval( 44 | () => { 45 | if (!animateOnLoad && isFirstRender.current) { 46 | clearInterval(interval); 47 | isFirstRender.current = false; 48 | return; 49 | } 50 | if (interations.current < text.length) { 51 | setDisplayText((t) => 52 | t.map((l, i) => 53 | l === " " 54 | ? l 55 | : i <= interations.current 56 | ? text[i] 57 | : alphabets[getRandomInt(26)], 58 | ), 59 | ); 60 | interations.current = interations.current + 0.1; 61 | } else { 62 | setTrigger(false); 63 | clearInterval(interval); 64 | } 65 | }, 66 | duration / (text.length * 10), 67 | ); 68 | 69 | const loopInterval = setInterval(() => { 70 | triggerAnimation(); 71 | }, 5000); 72 | 73 | // Clean up interval on unmount 74 | return () => { 75 | clearInterval(loopInterval); 76 | clearInterval(interval); 77 | }; 78 | }, [text, duration, trigger, animateOnLoad]); 79 | 80 | return ( 81 | 85 | 86 | {displayText.map((letter, i) => ( 87 | 96 | {letter} 97 | 98 | ))} 99 | 100 | 101 | ); 102 | } 103 | -------------------------------------------------------------------------------- /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/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/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 |