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 |
47 |
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 |
27 |
28 |
29 | {icon} {label}
30 |
31 |
32 |
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 |
61 |
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 |
80 | {feature.name}{" "}
81 | {feature.soon && (
82 |
83 | Coming soon
84 |
85 | )}
86 |
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 |
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 |
20 | );
21 | }
22 |
23 | export function NavbarDivider({
24 | className,
25 | ...props
26 | }: React.ComponentPropsWithoutRef<"div">) {
27 | return (
28 |
33 | );
34 | }
35 |
36 | export function NavbarSection({
37 | className,
38 | ...props
39 | }: React.ComponentPropsWithoutRef<"div">) {
40 | const id = useId();
41 |
42 | return (
43 |
44 |
45 |
46 | );
47 | }
48 |
49 | export function NavbarSpacer({
50 | className,
51 | ...props
52 | }: React.ComponentPropsWithoutRef<"div">) {
53 | return (
54 |
59 | );
60 | }
61 |
62 | export const NavbarItem = forwardRef(function NavbarItem(
63 | {
64 | current,
65 | className,
66 | children,
67 | ...props
68 | }: { current?: boolean; className?: string; children: React.ReactNode } & (
69 | | Omit
70 | | Omit, "className">
71 | ),
72 | ref: React.ForwardedRef,
73 | ) {
74 | const classes = clsx(
75 | // Base
76 | "relative flex min-w-0 items-center gap-2 rounded-lg p-2 text-left text-base/6 font-medium text-zinc-950 sm:text-sm/5",
77 | // Leading icon/icon-only
78 | "*:data-[slot=icon]:size-6 *:data-[slot=icon]:shrink-0 *:data-[slot=icon]:fill-zinc-500 sm:*:data-[slot=icon]:size-5",
79 | // Trailing icon (down chevron or similar)
80 | "*:not-nth-2:last:data-[slot=icon]:ml-auto *:not-nth-2:last:data-[slot=icon]:size-5 sm:*:not-nth-2:last:data-[slot=icon]:size-4",
81 | // Avatar
82 | "*:data-[slot=avatar]:-m-0.5 *:data-[slot=avatar]:size-7 *:data-[slot=avatar]:[--avatar-radius:var(--radius)] *:data-[slot=avatar]:[--ring-opacity:10%] sm:*:data-[slot=avatar]:size-6",
83 | // Hover
84 | "data-hover:bg-zinc-950/5 data-hover:*:data-[slot=icon]:fill-zinc-950",
85 | // Active
86 | "data-active:bg-zinc-950/5 data-active:*:data-[slot=icon]:fill-zinc-950",
87 | // Dark mode
88 | "dark:text-white dark:*:data-[slot=icon]:fill-zinc-400",
89 | "dark:data-hover:bg-white/5 dark:data-hover:*:data-[slot=icon]:fill-white",
90 | "dark:data-active:bg-white/5 dark:data-active:*:data-[slot=icon]:fill-white",
91 | );
92 |
93 | return (
94 |
95 | {current && (
96 |
100 | )}
101 | {"href" in props ? (
102 | }
107 | >
108 | {children}
109 |
110 | ) : (
111 |
117 | {children}
118 |
119 | )}
120 |
121 | );
122 | });
123 |
124 | export function NavbarLabel({
125 | className,
126 | ...props
127 | }: React.ComponentPropsWithoutRef<"span">) {
128 | return ;
129 | }
130 |
--------------------------------------------------------------------------------
/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/layouts/stacked-layout.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import Logo from "@/components/custom/logo";
4 | import { Button } from "@/components/ui/button";
5 | import { NavbarItem } from "@/components/ui/navbar";
6 | import * as Headless from "@headlessui/react";
7 | import type React from "react";
8 | import { useState } from "react";
9 |
10 | function OpenMenuIcon() {
11 | return (
12 |
13 |
14 |
15 | );
16 | }
17 |
18 | function CloseMenuIcon() {
19 | return (
20 |
21 |
22 |
23 | );
24 | }
25 |
26 | function MobileSidebar({
27 | open,
28 | close,
29 | children,
30 | }: React.PropsWithChildren<{ open: boolean; close: () => void }>) {
31 | return (
32 |
33 |
37 |
41 |
42 |
43 |
44 |
45 |
46 |
47 | {children}
48 |
49 |
50 |
51 | );
52 | }
53 |
54 | export function StackedLayout({
55 | navbar,
56 | sidebar,
57 | children,
58 | }: React.PropsWithChildren<{
59 | navbar: React.ReactNode;
60 | sidebar: React.ReactNode;
61 | }>) {
62 | const [showSidebar, setShowSidebar] = useState(false);
63 |
64 | return (
65 |
66 | {/* Sidebar on mobile */}
67 |
setShowSidebar(false)}>
68 | {sidebar}
69 |
70 |
71 | {/* Navbar */}
72 |
83 |
84 | {/* Content */}
85 |
86 |
89 |
90 |
91 |
92 |
93 |
94 |
95 | {/*
96 | Free & Open Source
97 |
98 |
99 | Privacy & Local first
100 | */}
101 |
102 | Roadmap
103 |
104 |
105 | Sponsorship
106 |
107 |
112 | GitHub
113 |
114 |
115 |
116 |
117 |
118 | Copyright © 2024 Nedim Arabacı
119 |
120 |
121 | );
122 | }
123 |
--------------------------------------------------------------------------------
/components/ui/sidebar.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 { Fragment, forwardRef, useId } from "react";
8 | import { TouchTarget } from "./button";
9 | import { Link } from "./link";
10 |
11 | export function Sidebar({
12 | className,
13 | ...props
14 | }: React.ComponentPropsWithoutRef<"nav">) {
15 | return (
16 |
20 | );
21 | }
22 |
23 | export function SidebarHeader({
24 | className,
25 | ...props
26 | }: React.ComponentPropsWithoutRef<"div">) {
27 | return (
28 | [data-slot=section]+[data-slot=section]]:mt-2.5",
33 | )}
34 | />
35 | );
36 | }
37 |
38 | export function SidebarBody({
39 | className,
40 | ...props
41 | }: React.ComponentPropsWithoutRef<"div">) {
42 | return (
43 |
[data-slot=section]+[data-slot=section]]:mt-8",
48 | )}
49 | />
50 | );
51 | }
52 |
53 | export function SidebarFooter({
54 | className,
55 | ...props
56 | }: React.ComponentPropsWithoutRef<"div">) {
57 | return (
58 |
[data-slot=section]+[data-slot=section]]:mt-2.5",
63 | )}
64 | />
65 | );
66 | }
67 |
68 | export function SidebarSection({
69 | className,
70 | ...props
71 | }: React.ComponentPropsWithoutRef<"div">) {
72 | const id = useId();
73 |
74 | return (
75 |
76 |
81 |
82 | );
83 | }
84 |
85 | export function SidebarDivider({
86 | className,
87 | ...props
88 | }: React.ComponentPropsWithoutRef<"hr">) {
89 | return (
90 |
97 | );
98 | }
99 |
100 | export function SidebarSpacer({
101 | className,
102 | ...props
103 | }: React.ComponentPropsWithoutRef<"div">) {
104 | return (
105 |
110 | );
111 | }
112 |
113 | export function SidebarHeading({
114 | className,
115 | ...props
116 | }: React.ComponentPropsWithoutRef<"h3">) {
117 | return (
118 |
125 | );
126 | }
127 |
128 | export const SidebarItem = forwardRef(function SidebarItem(
129 | {
130 | current,
131 | className,
132 | children,
133 | ...props
134 | }: { current?: boolean; className?: string; children: React.ReactNode } & (
135 | | Omit
136 | | Omit, "className">
137 | ),
138 | ref: React.ForwardedRef,
139 | ) {
140 | const classes = clsx(
141 | // Base
142 | "flex w-full items-center gap-3 rounded-lg px-2 py-2.5 text-left text-base/6 font-medium text-zinc-950 sm:py-2 sm:text-sm/5",
143 | // Leading icon/icon-only
144 | "*:data-[slot=icon]:size-6 *:data-[slot=icon]:shrink-0 *:data-[slot=icon]:fill-zinc-500 sm:*:data-[slot=icon]:size-5",
145 | // Trailing icon (down chevron or similar)
146 | "*:last:data-[slot=icon]:ml-auto *:last:data-[slot=icon]:size-5 sm:*:last:data-[slot=icon]:size-4",
147 | // Avatar
148 | "*:data-[slot=avatar]:-m-0.5 *:data-[slot=avatar]:size-7 *:data-[slot=avatar]:[--ring-opacity:10%] sm:*:data-[slot=avatar]:size-6",
149 | // Hover
150 | "data-hover:bg-zinc-950/5 data-hover:*:data-[slot=icon]:fill-zinc-950",
151 | // Active
152 | "data-active:bg-zinc-950/5 data-active:*:data-[slot=icon]:fill-zinc-950",
153 | // Current
154 | "data-current:*:data-[slot=icon]:fill-zinc-950",
155 | // Dark mode
156 | "dark:text-white dark:*:data-[slot=icon]:fill-zinc-400",
157 | "dark:data-hover:bg-white/5 dark:data-hover:*:data-[slot=icon]:fill-white",
158 | "dark:data-active:bg-white/5 dark:data-active:*:data-[slot=icon]:fill-white",
159 | "dark:data-current:*:data-[slot=icon]:fill-white",
160 | );
161 |
162 | return (
163 |
164 | {current && (
165 |
169 | )}
170 | {"href" in props ? (
171 |
172 |
177 | {children}
178 |
179 |
180 | ) : (
181 |
187 | {children}
188 |
189 | )}
190 |
191 | );
192 | });
193 |
194 | export function SidebarLabel({
195 | className,
196 | ...props
197 | }: React.ComponentPropsWithoutRef<"span">) {
198 | return ;
199 | }
200 |
--------------------------------------------------------------------------------
/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 |
192 |
193 |
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 |
230 |
231 | gider.im
232 |
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 |
--------------------------------------------------------------------------------
/components/ui/dropdown.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as Headless from "@headlessui/react";
4 | import clsx from "clsx";
5 | import type React from "react";
6 | import { Button } from "./button";
7 | import { Link } from "./link";
8 |
9 | export function Dropdown(props: Headless.MenuProps) {
10 | return ;
11 | }
12 |
13 | export function DropdownButton({
14 | as = Button,
15 | ...props
16 | }: { className?: string } & Omit, "className">) {
17 | return ;
18 | }
19 |
20 | export function DropdownMenu({
21 | anchor = "bottom",
22 | className,
23 | ...props
24 | }: { className?: string } & Omit) {
25 | return (
26 |
50 | );
51 | }
52 |
53 | export function DropdownItem({
54 | className,
55 | ...props
56 | }: { className?: string } & (
57 | | Omit, "as" | "className">
58 | | Omit, "className">
59 | )) {
60 | const classes = clsx(
61 | className,
62 | // Base styles
63 | "group cursor-default rounded-lg px-3.5 py-2.5 focus:outline-hidden sm:px-3 sm:py-1.5",
64 | // Text styles
65 | "text-left text-base/6 text-zinc-950 sm:text-sm/6 dark:text-white forced-colors:text-[CanvasText]",
66 | // Focus
67 | "data-focus:bg-zinc-800 data-focus:text-white",
68 | // Disabled state
69 | "data-disabled:opacity-50",
70 | // Forced colors mode
71 | "forced-color-adjust-none forced-colors:data-focus:bg-[Highlight] forced-colors:data-focus:text-[HighlightText] forced-colors:data-focus:*:data-[slot=icon]:text-[HighlightText]",
72 | // Use subgrid when available but fallback to an explicit grid layout if not
73 | "col-span-full grid grid-cols-[auto_1fr_1.5rem_0.5rem_auto] items-center supports-[grid-template-columns:subgrid]:grid-cols-subgrid",
74 | // Icons
75 | "*:data-[slot=icon]:col-start-1 *:data-[slot=icon]:row-start-1 *:data-[slot=icon]:-ml-0.5 *:data-[slot=icon]:mr-2.5 *:data-[slot=icon]:size-5 sm:*:data-[slot=icon]:mr-2 sm:*:data-[slot=icon]:size-4",
76 | "*:data-[slot=icon]:text-zinc-500 data-focus:*:data-[slot=icon]:text-white dark:*:data-[slot=icon]:text-zinc-400 dark:data-focus:*:data-[slot=icon]:text-white",
77 | // Avatar
78 | "*:data-[slot=avatar]:-ml-1 *:data-[slot=avatar]:mr-2.5 *:data-[slot=avatar]:size-6 sm:*:data-[slot=avatar]:mr-2 sm:*:data-[slot=avatar]:size-5",
79 | );
80 |
81 | return (
82 |
83 | {"href" in props ? (
84 |
85 | ) : (
86 |
87 | )}
88 |
89 | );
90 | }
91 |
92 | export function DropdownHeader({
93 | className,
94 | ...props
95 | }: React.ComponentPropsWithoutRef<"div">) {
96 | return (
97 |
101 | );
102 | }
103 |
104 | export function DropdownSection({
105 | className,
106 | ...props
107 | }: { className?: string } & Omit<
108 | Headless.MenuSectionProps,
109 | "as" | "className"
110 | >) {
111 | return (
112 |
120 | );
121 | }
122 |
123 | export function DropdownHeading({
124 | className,
125 | ...props
126 | }: { className?: string } & Omit<
127 | Headless.MenuHeadingProps,
128 | "as" | "className"
129 | >) {
130 | return (
131 |
138 | );
139 | }
140 |
141 | export function DropdownDivider({
142 | className,
143 | ...props
144 | }: { className?: string } & Omit<
145 | Headless.MenuSeparatorProps,
146 | "as" | "className"
147 | >) {
148 | return (
149 |
156 | );
157 | }
158 |
159 | export function DropdownLabel({
160 | className,
161 | ...props
162 | }: { className?: string } & Omit) {
163 | return (
164 |
170 | );
171 | }
172 |
173 | export function DropdownDescription({
174 | className,
175 | ...props
176 | }: { className?: string } & Omit<
177 | Headless.DescriptionProps,
178 | "as" | "className"
179 | >) {
180 | return (
181 |
189 | );
190 | }
191 |
192 | export function DropdownShortcut({
193 | keys,
194 | className,
195 | ...props
196 | }: { keys: string | string[]; className?: string } & Omit<
197 | Headless.DescriptionProps<"kbd">,
198 | "as" | "className"
199 | >) {
200 | return (
201 |
209 | {(Array.isArray(keys) ? keys : keys.split("")).map((char, index) => (
210 | 0 && char.length > 1 && "pl-1",
216 | ])}
217 | >
218 | {char}
219 |
220 | ))}
221 |
222 | );
223 | }
224 |
--------------------------------------------------------------------------------
/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/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 |
215 | {children}
216 | >
217 | );
218 | }
219 |
--------------------------------------------------------------------------------
/server/github.ts:
--------------------------------------------------------------------------------
1 | "use server";
2 |
3 | import { SPONSOR_LINK } from "@/lib/constants";
4 | import { unstable_cache as cache } from "next/cache";
5 |
6 | const CACHE_DURATION = 1800; // 30 minutes
7 | const USE_MOCK_DATA_FOR_DEVELOPMENT = true;
8 |
9 | const DEFAULT_GITHUB_RESPONSE = {
10 | data: {
11 | viewer: {
12 | login: "needim",
13 | sponsorshipsAsMaintainer: {
14 | totalCount: 35,
15 | nodes: [
16 | {
17 | createdAt: "2024-06-29T20:40:34Z",
18 | isActive: true,
19 | tier: {
20 | id: "ST_kwDOAAQmKM4AAvQg",
21 | name: "$5 a month",
22 | isOneTime: false,
23 | monthlyPriceInDollars: 5,
24 | },
25 | sponsorEntity: {
26 | __typename: "User",
27 | login: "aliosmandev",
28 | name: "Ali Osman",
29 | bio: "Software Developer",
30 | avatarUrl:
31 | "https://avatars.githubusercontent.com/u/106361546?u=92f87b20abae991b7bf9ca4dc8734a52f6532fa3&v=4",
32 | twitterUsername: "aliosmandev",
33 | },
34 | },
35 | {
36 | createdAt: "2024-07-21T20:18:05Z",
37 | isActive: false,
38 | tier: {
39 | id: "ST_kwDOAAQmKM4ABksM",
40 | name: "$5 one time",
41 | isOneTime: true,
42 | monthlyPriceInDollars: 5,
43 | },
44 | sponsorEntity: {
45 | __typename: "User",
46 | login: "thisisroi",
47 | name: "Amil Alekberov",
48 | bio: "✨ amil, ui/ux designer, front-end developer",
49 | avatarUrl:
50 | "https://avatars.githubusercontent.com/u/48417958?u=65a1d1ee60b9171a69552f26b3d25c22b8a445c3&v=4",
51 | twitterUsername: "alekberovamil",
52 | },
53 | },
54 | {
55 | createdAt: "2024-07-25T22:11:50Z",
56 | isActive: false,
57 | tier: {
58 | id: "ST_kwDOAAQmKM4ABksM",
59 | name: "$5 one time",
60 | isOneTime: true,
61 | monthlyPriceInDollars: 5,
62 | },
63 | sponsorEntity: {
64 | __typename: "User",
65 | login: "alpererdogan8",
66 | name: "Alper Erdogan",
67 | bio: "Software Developer / \r\nComputer Programmer & Management Information Systems\r\n",
68 | avatarUrl:
69 | "https://avatars.githubusercontent.com/u/19363785?u=e1e8a91d7da2c15cf669a9ef8ba2c1f6d4789bf6&v=4",
70 | twitterUsername: "alpererdogandev",
71 | },
72 | },
73 | {
74 | createdAt: "2024-07-21T20:12:55Z",
75 | isActive: true,
76 | tier: {
77 | id: "ST_kwDOAAQmKM4ABl2T",
78 | name: "$10 a month",
79 | isOneTime: false,
80 | monthlyPriceInDollars: 10,
81 | },
82 | sponsorEntity: {
83 | __typename: "User",
84 | login: "kanadikirik",
85 | name: "Osman Kanadıkırık",
86 | bio: "Full Stack Developer, Team Lead. 🔋 ",
87 | avatarUrl:
88 | "https://avatars.githubusercontent.com/u/32852874?u=a4d70ec14a97e39caf83ee984a9cfa1de99ca81c&v=4",
89 | twitterUsername: "ozi_dev",
90 | },
91 | },
92 | {
93 | createdAt: "2024-07-25T10:04:02Z",
94 | isActive: false,
95 | tier: {
96 | id: "ST_kwDOAAQmKM4ABksM",
97 | name: "$5 one time",
98 | isOneTime: true,
99 | monthlyPriceInDollars: 5,
100 | },
101 | sponsorEntity: {
102 | __typename: "User",
103 | login: "apo-bozdag",
104 | name: "Abdullah Bozdağ",
105 | bio: "Backend Developer",
106 | avatarUrl:
107 | "https://avatars.githubusercontent.com/u/33822884?u=8e62d7baad0d259b63e00084e9446e7614452f35&v=4",
108 | twitterUsername: "apo_bozdag",
109 | },
110 | },
111 | {
112 | createdAt: "2024-07-21T17:54:47Z",
113 | isActive: true,
114 | tier: {
115 | id: "ST_kwDOAAQmKM4AAvQg",
116 | name: "$5 a month",
117 | isOneTime: false,
118 | monthlyPriceInDollars: 5,
119 | },
120 | sponsorEntity: {
121 | __typename: "User",
122 | login: "meminuygur",
123 | name: "Mehmet Emin Uygur",
124 | bio: "",
125 | avatarUrl:
126 | "https://avatars.githubusercontent.com/u/21062398?u=2177cce13b9d0180d5929bc1b64984d211fda6f6&v=4",
127 | twitterUsername: "meminuygur",
128 | },
129 | },
130 | {
131 | createdAt: "2024-07-21T13:54:36Z",
132 | isActive: true,
133 | tier: {
134 | id: "ST_kwDOAAQmKM4AAvQg",
135 | name: "$5 a month",
136 | isOneTime: false,
137 | monthlyPriceInDollars: 5,
138 | },
139 | sponsorEntity: {
140 | __typename: "User",
141 | login: "evrenvural",
142 | name: "Evren",
143 | bio: "Frontend Developer",
144 | avatarUrl:
145 | "https://avatars.githubusercontent.com/u/45872638?u=f7ae49125ff6caf7f7f78d4f8456e44709fd2539&v=4",
146 | twitterUsername: null,
147 | },
148 | },
149 | {
150 | createdAt: "2024-07-20T22:45:23Z",
151 | isActive: true,
152 | tier: {
153 | id: "ST_kwDOAAQmKM4AAvQg",
154 | name: "$5 a month",
155 | isOneTime: false,
156 | monthlyPriceInDollars: 5,
157 | },
158 | sponsorEntity: {
159 | __typename: "User",
160 | login: "giraybatiturk",
161 | name: "Giray BATITURK",
162 | bio: "UX Product Manager - simplifying web3 and AI with UX",
163 | avatarUrl:
164 | "https://avatars.githubusercontent.com/u/9551312?u=0c33650140022803ff21c05dce42615c80447038&v=4",
165 | twitterUsername: "giraybatiturk",
166 | },
167 | },
168 | {
169 | createdAt: "2024-07-22T13:44:27Z",
170 | isActive: false,
171 | tier: {
172 | id: "ST_kwDOAAQmKM4AAvQj",
173 | name: "$10 one time",
174 | isOneTime: true,
175 | monthlyPriceInDollars: 10,
176 | },
177 | sponsorEntity: {
178 | __typename: "User",
179 | login: "kemalersin",
180 | name: "Kemal Ersin YILMAZ",
181 | bio: null,
182 | avatarUrl:
183 | "https://avatars.githubusercontent.com/u/1591411?u=9a38cb9a23bbdaa1f9ddb1d619bbdab76467785b&v=4",
184 | twitterUsername: null,
185 | },
186 | },
187 | {
188 | createdAt: "2024-07-22T16:34:25Z",
189 | isActive: false,
190 | tier: {
191 | id: "ST_kwDOAAQmKM4AAvQj",
192 | name: "$10 one time",
193 | isOneTime: true,
194 | monthlyPriceInDollars: 10,
195 | },
196 | sponsorEntity: {
197 | __typename: "User",
198 | login: "isikmuhamm",
199 | name: "Muhammet Işık",
200 | bio: "",
201 | avatarUrl:
202 | "https://avatars.githubusercontent.com/u/120049215?u=c58b321944715192a387a992b0d73ff33805e392&v=4",
203 | twitterUsername: null,
204 | },
205 | },
206 | {
207 | createdAt: "2024-07-24T18:16:16Z",
208 | isActive: false,
209 | tier: {
210 | id: "ST_kwDOAAQmKM4ABmA2",
211 | name: "$100 one time",
212 | isOneTime: true,
213 | monthlyPriceInDollars: 100,
214 | },
215 | sponsorEntity: {
216 | __typename: "User",
217 | login: "9ssi7",
218 | name: "Sami Salih İbrahimbaş",
219 | bio: "product developer",
220 | avatarUrl:
221 | "https://avatars.githubusercontent.com/u/76786120?u=c85d67cb992280a75cd3a20fdec32e179fe3b795&v=4",
222 | twitterUsername: "9ssi7",
223 | },
224 | },
225 | {
226 | createdAt: "2024-07-22T20:04:32Z",
227 | isActive: false,
228 | tier: {
229 | id: "ST_kwDOAAQmKM4ABksM",
230 | name: "$5 one time",
231 | isOneTime: true,
232 | monthlyPriceInDollars: 5,
233 | },
234 | sponsorEntity: {
235 | __typename: "User",
236 | login: "uygar",
237 | name: "Uygar İşiçelik",
238 | bio: "📱mobile software developer • ⌨️ mechanical keyboard builder •👨💻 maker",
239 | avatarUrl:
240 | "https://avatars.githubusercontent.com/u/11055350?u=444bc38b5b55477c63eb52694097be0ee5e1d5cb&v=4",
241 | twitterUsername: "uuygaar",
242 | },
243 | },
244 | {
245 | createdAt: "2024-07-21T13:51:09Z",
246 | isActive: true,
247 | tier: {
248 | id: "ST_kwDOAAQmKM4AAvQg",
249 | name: "$5 a month",
250 | isOneTime: false,
251 | monthlyPriceInDollars: 5,
252 | },
253 | sponsorEntity: {
254 | __typename: "User",
255 | login: "eser",
256 | name: "Eser Ozvataf",
257 | bio: "Head of Engineering @Teknasyon • Founder @acikyazilim • Streaming @ eser.live • Open Source, DevRel, DevOps and Agile Evangelist • Generalist",
258 | avatarUrl:
259 | "https://avatars.githubusercontent.com/u/866558?u=060df27f13f5f9afa18bc8faed253956b55c8c13&v=4",
260 | twitterUsername: "eser",
261 | },
262 | },
263 | {
264 | createdAt: "2024-07-21T19:36:19Z",
265 | isActive: false,
266 | tier: {
267 | id: "ST_kwDOAAQmKM4ABksM",
268 | name: "$5 one time",
269 | isOneTime: true,
270 | monthlyPriceInDollars: 5,
271 | },
272 | sponsorEntity: {
273 | __typename: "User",
274 | login: "aardabayram",
275 | name: "Arda",
276 | bio: "I'm a passionate product designer who loves to solve daily life problems and creating human experiences ",
277 | avatarUrl:
278 | "https://avatars.githubusercontent.com/u/48804865?u=f873c097f92a872adbabc5102a7879c172317982&v=4",
279 | twitterUsername: "aardabayram",
280 | },
281 | },
282 | {
283 | createdAt: "2024-07-30T21:52:16Z",
284 | isActive: false,
285 | tier: {
286 | id: "ST_kwDOAAQmKM4AAvQj",
287 | name: "$10 one time",
288 | isOneTime: true,
289 | monthlyPriceInDollars: 10,
290 | },
291 | sponsorEntity: {
292 | __typename: "User",
293 | login: "mhasanince",
294 | name: "Muhammed Hasan Ince",
295 | bio: "",
296 | avatarUrl:
297 | "https://avatars.githubusercontent.com/u/49279517?u=74c220842ba768a5b1506bf87de7808b0f2f905b&v=4",
298 | twitterUsername: "mhasanince",
299 | },
300 | },
301 | {
302 | createdAt: "2024-07-21T15:35:17Z",
303 | isActive: true,
304 | tier: {
305 | id: "ST_kwDOAAQmKM4AAvQg",
306 | name: "$5 a month",
307 | isOneTime: false,
308 | monthlyPriceInDollars: 5,
309 | },
310 | sponsorEntity: {
311 | __typename: "User",
312 | login: "calganaygun",
313 | name: "Çalgan Aygün",
314 | bio: "Devloper, YAML Engineer, Button Clicker on AWS, OSINT Hackerman",
315 | avatarUrl:
316 | "https://avatars.githubusercontent.com/u/20268300?u=bd8c7e48add63b2b273e7a2f4dc49244cb1f4134&v=4",
317 | twitterUsername: "calganaygun",
318 | },
319 | },
320 | {
321 | createdAt: "2024-08-14T06:50:39Z",
322 | isActive: true,
323 | tier: {
324 | id: "ST_kwDOAAQmKM4ABksM",
325 | name: "$5 one time",
326 | isOneTime: true,
327 | monthlyPriceInDollars: 5,
328 | },
329 | sponsorEntity: {
330 | __typename: "User",
331 | login: "ufukcaneski",
332 | name: "Ufukcan Eski",
333 | bio: "",
334 | avatarUrl:
335 | "https://avatars.githubusercontent.com/u/24737685?u=ff274dd83de095d8bb89792d95eb5c571ac6a48a&v=4",
336 | twitterUsername: "UfukcanEski",
337 | },
338 | },
339 | {
340 | createdAt: "2024-08-13T09:19:15Z",
341 | isActive: true,
342 | tier: {
343 | id: "ST_kwDOAAQmKM4AAvQj",
344 | name: "$10 one time",
345 | isOneTime: true,
346 | monthlyPriceInDollars: 10,
347 | },
348 | sponsorEntity: {
349 | __typename: "User",
350 | login: "Edleron",
351 | name: "Ertuğrul",
352 | bio: "Life to Earth",
353 | avatarUrl:
354 | "https://avatars.githubusercontent.com/u/30155849?u=3ec3d562a8b96f84ae0f1ec826dafa34977f63bd&v=4",
355 | twitterUsername: "edleron",
356 | },
357 | },
358 | {
359 | createdAt: "2024-07-22T14:40:05Z",
360 | isActive: true,
361 | tier: {
362 | id: "ST_kwDOAAQmKM4AAvQg",
363 | name: "$5 a month",
364 | isOneTime: false,
365 | monthlyPriceInDollars: 5,
366 | },
367 | sponsorEntity: {
368 | __typename: "User",
369 | login: "islamsanliturk",
370 | name: "İslam Şanlıtürk | IZO",
371 | bio: "ikas.com\r\n",
372 | avatarUrl:
373 | "https://avatars.githubusercontent.com/u/31260749?u=c6e6e7a5ca20bb762337edbe4f2bf2068069dd25&v=4",
374 | twitterUsername: null,
375 | },
376 | },
377 | {
378 | createdAt: "2024-07-25T17:50:29Z",
379 | isActive: false,
380 | tier: {
381 | id: "ST_kwDOAAQmKM4ABksN",
382 | name: "$19 one time",
383 | isOneTime: true,
384 | monthlyPriceInDollars: 19,
385 | },
386 | sponsorEntity: {
387 | __typename: "User",
388 | login: "alperiskender",
389 | name: "Alper İskender",
390 | bio: "",
391 | avatarUrl: "https://avatars.githubusercontent.com/u/35170570?v=4",
392 | twitterUsername: "iskenderalper",
393 | },
394 | },
395 | {
396 | createdAt: "2024-07-22T09:56:37Z",
397 | isActive: false,
398 | tier: {
399 | id: "ST_kwDOAAQmKM4AAvQj",
400 | name: "$10 one time",
401 | isOneTime: true,
402 | monthlyPriceInDollars: 10,
403 | },
404 | sponsorEntity: {
405 | __typename: "Organization",
406 | login: "komunite",
407 | name: "Komünite",
408 | description:
409 | "Zamanı ölçeklemek mümkün değil. Katma değerli çalışmaya başlayın.",
410 | avatarUrl:
411 | "https://avatars.githubusercontent.com/u/105446227?v=4",
412 | twitterUsername: "komunitecomtr",
413 | },
414 | },
415 | {
416 | createdAt: "2024-08-07T11:23:54Z",
417 | isActive: true,
418 | tier: {
419 | id: "ST_kwDOAAQmKM4AAvQg",
420 | name: "$5 a month",
421 | isOneTime: false,
422 | monthlyPriceInDollars: 5,
423 | },
424 | sponsorEntity: {
425 | __typename: "Organization",
426 | login: "International-Labour-Association",
427 | name: "ILA",
428 | description:
429 | "International Labour Association (ILA) is an NGO that provides mechanisms to facilitate knowledge and experience transfer among sectors and countries.",
430 | avatarUrl:
431 | "https://avatars.githubusercontent.com/u/116907187?v=4",
432 | twitterUsername: "iladesk",
433 | },
434 | },
435 | {
436 | createdAt: "2024-07-25T20:35:20Z",
437 | isActive: false,
438 | tier: {
439 | id: "ST_kwDOAAQmKM4ABmEi",
440 | name: "$150 one time",
441 | isOneTime: true,
442 | monthlyPriceInDollars: 150,
443 | },
444 | sponsorEntity: {
445 | __typename: "User",
446 | login: "batuhankrskl",
447 | name: "Batuhan Karasakal",
448 | bio: "A senior multi-disciplinary designer with over 9 years of experience who has been collaborating with enterprises and startups on brandⒷ, websiteⓌ, productⓅ \r\n\r\n",
449 | avatarUrl:
450 | "https://avatars.githubusercontent.com/u/127359404?u=f8fa23be20d379332b245eb2c347e8e8249bfa08&v=4",
451 | twitterUsername: null,
452 | },
453 | },
454 | {
455 | createdAt: "2024-07-25T11:26:56Z",
456 | isActive: true,
457 | tier: {
458 | id: "ST_kwDOAAQmKM4AAvQg",
459 | name: "$5 a month",
460 | isOneTime: false,
461 | monthlyPriceInDollars: 5,
462 | },
463 | sponsorEntity: {
464 | __typename: "User",
465 | login: "serhatkildaci",
466 | name: "Serhat Kıldacı",
467 | bio: "Game Developer\r\n",
468 | avatarUrl:
469 | "https://avatars.githubusercontent.com/u/96617749?u=cd4be0fc757e65a3cc96964182fd5260f4980851&v=4",
470 | twitterUsername: "sreaht",
471 | },
472 | },
473 | {
474 | createdAt: "2024-07-26T06:19:27Z",
475 | isActive: true,
476 | tier: {
477 | id: "ST_kwDOAAQmKM4ABksQ",
478 | name: "$500 a month",
479 | isOneTime: false,
480 | monthlyPriceInDollars: 500,
481 | },
482 | sponsorEntity: {
483 | __typename: "User",
484 | login: "ugorur",
485 | name: "Umurcan Gorur",
486 | bio: "",
487 | avatarUrl: "https://avatars.githubusercontent.com/u/2701458?v=4",
488 | twitterUsername: "ugorur",
489 | },
490 | },
491 | {
492 | createdAt: "2024-07-21T15:35:53Z",
493 | isActive: false,
494 | tier: {
495 | id: "ST_kwDOAAQmKM4ABksM",
496 | name: "$5 one time",
497 | isOneTime: true,
498 | monthlyPriceInDollars: 5,
499 | },
500 | sponsorEntity: {
501 | __typename: "User",
502 | login: "mehmetaltugakgul",
503 | name: "Mehmet Altuğ Akgül",
504 | bio: "Electrical & Electronics Engineer - Cloud Solutions Architect - MBA",
505 | avatarUrl:
506 | "https://avatars.githubusercontent.com/u/10194009?u=8ae94b4f1cca8ee1871d208b08904e67c0440e0e&v=4",
507 | twitterUsername: null,
508 | },
509 | },
510 | {
511 | createdAt: "2024-07-30T10:13:55Z",
512 | isActive: false,
513 | tier: {
514 | id: "ST_kwDOAAQmKM4ABksM",
515 | name: "$5 one time",
516 | isOneTime: true,
517 | monthlyPriceInDollars: 5,
518 | },
519 | sponsorEntity: {
520 | __typename: "User",
521 | login: "aorhandev",
522 | name: "Ahmet Orhan",
523 | bio: "https://www.linkedin.com/in/ahmetorhan",
524 | avatarUrl: "https://avatars.githubusercontent.com/u/5780242?v=4",
525 | twitterUsername: "aorhan",
526 | },
527 | },
528 | {
529 | createdAt: "2024-07-25T19:41:12Z",
530 | isActive: false,
531 | tier: {
532 | id: "ST_kwDOAAQmKM4AAvQj",
533 | name: "$10 one time",
534 | isOneTime: true,
535 | monthlyPriceInDollars: 10,
536 | },
537 | sponsorEntity: {
538 | __typename: "User",
539 | login: "alimuratumutlu",
540 | name: "Murat Umutlu",
541 | bio: "Game & Software Developer\r\nFounder of Muum Dev. Software Company\r\nMarketing | Monetizing | Investment",
542 | avatarUrl:
543 | "https://avatars.githubusercontent.com/u/6642361?u=f45ccdc28bbe86dc740e5296fed068680f35d8df&v=4",
544 | twitterUsername: "alimuratumutlu",
545 | },
546 | },
547 | {
548 | createdAt: "2024-07-21T19:49:21Z",
549 | isActive: false,
550 | tier: {
551 | id: "ST_kwDOAAQmKM4ABksM",
552 | name: "$5 one time",
553 | isOneTime: true,
554 | monthlyPriceInDollars: 5,
555 | },
556 | sponsorEntity: {
557 | __typename: "User",
558 | login: "aykutkardas",
559 | name: "Aykut Kardaş",
560 | bio: "I'm Product Developer.",
561 | avatarUrl:
562 | "https://avatars.githubusercontent.com/u/7966133?u=594d1e7f93a2cc7f2b04391b414d464196c5266a&v=4",
563 | twitterUsername: "aykutkardas",
564 | },
565 | },
566 | {
567 | createdAt: "2024-08-14T17:00:24Z",
568 | isActive: true,
569 | tier: {
570 | id: "ST_kwDOAAQmKM4AAvQg",
571 | name: "$5 a month",
572 | isOneTime: false,
573 | monthlyPriceInDollars: 5,
574 | },
575 | sponsorEntity: {
576 | __typename: "User",
577 | login: "msuleymansaglam",
578 | name: "suleymansaglam",
579 | bio: "",
580 | avatarUrl:
581 | "https://avatars.githubusercontent.com/u/149151487?v=4",
582 | twitterUsername: null,
583 | },
584 | },
585 | {
586 | createdAt: "2024-08-05T10:21:57Z",
587 | isActive: true,
588 | tier: {
589 | id: "ST_kwDOAAQmKM4AAvQg",
590 | name: "$5 a month",
591 | isOneTime: false,
592 | monthlyPriceInDollars: 5,
593 | },
594 | sponsorEntity: {
595 | __typename: "Organization",
596 | login: "mvpstudioco",
597 | name: "mvpstudio.co",
598 | description: "",
599 | avatarUrl:
600 | "https://avatars.githubusercontent.com/u/144166938?v=4",
601 | twitterUsername: "mvpstudioco",
602 | },
603 | },
604 | {
605 | createdAt: "2024-07-21T13:29:57Z",
606 | isActive: true,
607 | tier: {
608 | id: "ST_kwDOAAQmKM4AAvQg",
609 | name: "$5 a month",
610 | isOneTime: false,
611 | monthlyPriceInDollars: 5,
612 | },
613 | sponsorEntity: {
614 | __typename: "User",
615 | login: "adiguzelburak",
616 | name: "Burak Adıgüzel",
617 | bio: "I'm Burak Adıgüzel from Istanbul,Turkey",
618 | avatarUrl:
619 | "https://avatars.githubusercontent.com/u/67753295?u=1f0103f29d444d8845338b4ad5c54d0b457e4df4&v=4",
620 | twitterUsername: "adgzelburak",
621 | },
622 | },
623 | {
624 | createdAt: "2024-07-22T06:00:05Z",
625 | isActive: false,
626 | tier: {
627 | id: "ST_kwDOAAQmKM4ABksM",
628 | name: "$5 one time",
629 | isOneTime: true,
630 | monthlyPriceInDollars: 5,
631 | },
632 | sponsorEntity: {
633 | __typename: "User",
634 | login: "MATTAM540",
635 | name: null,
636 | bio: "Spaghetti Code Derneği Türkiye şubesi",
637 | avatarUrl: "https://avatars.githubusercontent.com/u/47248007?v=4",
638 | twitterUsername: null,
639 | },
640 | },
641 | {
642 | createdAt: "2024-07-22T11:51:50Z",
643 | isActive: false,
644 | tier: {
645 | id: "ST_kwDOAAQmKM4ABksM",
646 | name: "$5 one time",
647 | isOneTime: true,
648 | monthlyPriceInDollars: 5,
649 | },
650 | sponsorEntity: {
651 | __typename: "User",
652 | login: "mahmutyildizx",
653 | name: "Mahmut ✨",
654 | bio: "frontend developer | 💻 | ",
655 | avatarUrl:
656 | "https://avatars.githubusercontent.com/u/40712532?u=30dd8b4432cd541b3cb5ac3e4cd11384131b1294&v=4",
657 | twitterUsername: "mahmutyildizx",
658 | },
659 | },
660 | {
661 | createdAt: "2024-07-22T05:22:46Z",
662 | isActive: false,
663 | tier: {
664 | id: "ST_kwDOAAQmKM4AAvQj",
665 | name: "$10 one time",
666 | isOneTime: true,
667 | monthlyPriceInDollars: 10,
668 | },
669 | sponsorEntity: {
670 | __typename: "User",
671 | login: "kacmazm",
672 | name: "Mustafa",
673 | bio: "",
674 | avatarUrl:
675 | "https://avatars.githubusercontent.com/u/11290170?u=02617a1ec0470c7df1ff43843ca2c461f139aec8&v=4",
676 | twitterUsername: "devmustafao",
677 | },
678 | },
679 | ],
680 | },
681 | sponsoring: {
682 | nodes: [
683 | {
684 | login: "burakndmr",
685 | name: "Burak Demir",
686 | bio: "Front-end Developer | Youtube Content Creator",
687 | avatarUrl:
688 | "https://avatars.githubusercontent.com/u/45737592?u=64e31e6a191246787f618a93d4d05ef211f8ce65&v=4",
689 | twitterUsername: "burakdmr09",
690 | sponsorshipForViewerAsSponsor: {
691 | isOneTimePayment: false,
692 | isActive: true,
693 | createdAt: "2024-08-14T06:54:55Z",
694 | tier: {
695 | id: "ST_kwDOArnmeM4ABm7r",
696 | isCustomAmount: true,
697 | monthlyPriceInDollars: 5,
698 | isOneTime: false,
699 | name: "$5 a month",
700 | description: "",
701 | },
702 | },
703 | },
704 | {
705 | login: "aliosmandev",
706 | name: "Ali Osman",
707 | bio: "Software Developer",
708 | avatarUrl:
709 | "https://avatars.githubusercontent.com/u/106361546?u=92f87b20abae991b7bf9ca4dc8734a52f6532fa3&v=4",
710 | twitterUsername: "aliosmandev",
711 | sponsorshipForViewerAsSponsor: {
712 | isOneTimePayment: false,
713 | isActive: true,
714 | createdAt: "2024-06-29T20:46:46Z",
715 | tier: {
716 | id: "ST_kwDOBlbyys4ABksU",
717 | isCustomAmount: true,
718 | monthlyPriceInDollars: 5,
719 | isOneTime: false,
720 | name: "$5 a month",
721 | description: "",
722 | },
723 | },
724 | },
725 | {
726 | login: "mertcanaltin",
727 | name: "Mert Can Altin",
728 | bio: "🌟 Open Source Enthusiast | @nodejs & @expressjs Team Member | CPC Member at @openjs-foundation 🚀",
729 | avatarUrl:
730 | "https://avatars.githubusercontent.com/u/37827216?u=0b85252cdbf8f5d74c4134569eef30b639b1525f&v=4",
731 | twitterUsername: "mecaltin",
732 | sponsorshipForViewerAsSponsor: {
733 | isOneTimePayment: false,
734 | isActive: true,
735 | createdAt: "2024-08-02T20:40:11Z",
736 | tier: {
737 | id: "ST_kwDOAkEykM4ABflk",
738 | isCustomAmount: false,
739 | monthlyPriceInDollars: 5,
740 | isOneTime: false,
741 | name: "$5 a month",
742 | description:
743 | "As a **Backer** you get:\r\n\r\ngood feeling knowing that you support open source software\r\nseriously I'm super grateful for every bit of support since it gives me the feedback that my work is appreciated",
744 | },
745 | },
746 | },
747 | {
748 | login: "steida",
749 | name: "Daniel Steigerwald",
750 | bio: "",
751 | avatarUrl: "https://avatars.githubusercontent.com/u/66249?v=4",
752 | twitterUsername: "steida",
753 | sponsorshipForViewerAsSponsor: {
754 | isOneTimePayment: false,
755 | isActive: true,
756 | createdAt: "2024-05-22T08:16:39Z",
757 | tier: {
758 | id: "ST_kwDOAAECyc4ABmEt",
759 | isCustomAmount: true,
760 | monthlyPriceInDollars: 10,
761 | isOneTime: false,
762 | name: "$10 a month",
763 | description: "",
764 | },
765 | },
766 | },
767 | {
768 | login: "anonrig",
769 | name: "Yagiz Nizipli",
770 | bio: "@cloudflare principal systems engineer, @nodejs technical steering committee member",
771 | avatarUrl:
772 | "https://avatars.githubusercontent.com/u/1935246?u=440932d4aed022e31a2140e1825c0167b7a12357&v=4",
773 | twitterUsername: "yagiznizipli",
774 | sponsorshipForViewerAsSponsor: {
775 | isOneTimePayment: false,
776 | isActive: true,
777 | createdAt: "2024-07-29T10:08:25Z",
778 | tier: {
779 | id: "ST_kwDOAB2Hjs4ABQQp",
780 | isCustomAmount: false,
781 | monthlyPriceInDollars: 5,
782 | isOneTime: false,
783 | name: "$5 a month",
784 | description:
785 | "You will receive a Sponsor badge on your profile.",
786 | },
787 | },
788 | },
789 | ],
790 | },
791 | sponsorsListing: {
792 | url: SPONSOR_LINK,
793 | fullDescription:
794 | "Hello, I'm Nedim 👋\n\nBy sponsoring me, you will help me spend more time maintaining my projects. Thanks!\n",
795 | activeGoal: {
796 | kind: "TOTAL_SPONSORS_COUNT",
797 | description:
798 | "Reaching this goal will allow me to improve the gider.im app, all thanks to your support. It's a crucial step in sustaining and enriching the work I do.",
799 | percentComplete: 14,
800 | targetValue: 100,
801 | title: "100 monthly sponsors",
802 | },
803 | tiers: {
804 | nodes: [
805 | {
806 | id: "ST_kwDOAAQmKM4AAvQg",
807 | name: "$5 a month",
808 | isOneTime: false,
809 | description:
810 | "- Get a Sponsor badge on your profile\r\n- Get a shoutout on X\r\n- Avatar and name on gider.im website",
811 | monthlyPriceInDollars: 5,
812 | isCustomAmount: false,
813 | },
814 | {
815 | id: "ST_kwDOAAQmKM4ABksM",
816 | name: "$5 one time",
817 | isOneTime: true,
818 | description:
819 | "- Get a shoutout on X\r\n- Get a Sponsor badge on your profile\r\n",
820 | monthlyPriceInDollars: 5,
821 | isCustomAmount: false,
822 | },
823 | {
824 | id: "ST_kwDOAAQmKM4ABksL",
825 | name: "$7 one time",
826 | isOneTime: true,
827 | description:
828 | "- Get a shoutout on X\r\n- Get a Sponsor badge on your profile \r\n",
829 | monthlyPriceInDollars: 7,
830 | isCustomAmount: false,
831 | },
832 | {
833 | id: "ST_kwDOAAQmKM4AAvQj",
834 | name: "$10 one time",
835 | isOneTime: true,
836 | description:
837 | "- Get a shoutout on X\r\n- Get a Sponsor badge on your profile",
838 | monthlyPriceInDollars: 10,
839 | isCustomAmount: false,
840 | },
841 | {
842 | id: "ST_kwDOAAQmKM4ABm_c",
843 | name: "$10 a month",
844 | isOneTime: false,
845 | description:
846 | "- Get a Sponsor badge on your profile\r\n- Get a shoutout on X\r\n- Avatar and name on gider.im website",
847 | monthlyPriceInDollars: 10,
848 | isCustomAmount: false,
849 | },
850 | {
851 | id: "ST_kwDOAAQmKM4ABksN",
852 | name: "$19 one time",
853 | isOneTime: true,
854 | description:
855 | "- Get a shoutout on X\r\n- Get a Sponsor badge on your profile\r\n",
856 | monthlyPriceInDollars: 19,
857 | isCustomAmount: false,
858 | },
859 | {
860 | id: "ST_kwDOAAQmKM4ABm_b",
861 | name: "$29 one time",
862 | isOneTime: true,
863 | description:
864 | "- Get a shoutout on X\r\n- Get a Sponsor badge on your profile",
865 | monthlyPriceInDollars: 29,
866 | isCustomAmount: false,
867 | },
868 | {
869 | id: "ST_kwDOAAQmKM4ABm_Z",
870 | name: "$49 one time",
871 | isOneTime: true,
872 | description:
873 | "- Get a shoutout on X\r\n- Get a Sponsor badge on your profile",
874 | monthlyPriceInDollars: 49,
875 | isCustomAmount: false,
876 | },
877 | {
878 | id: "ST_kwDOAAQmKM4ABksO",
879 | name: "$100 a month",
880 | isOneTime: false,
881 | description:
882 | "**Silver Spot - 4 spots left**\r\n\r\n- Get a Sponsor badge on your profile\r\n- Get a shoutout on X\r\n- **Silver** Spot / Logo and link on gider.im website",
883 | monthlyPriceInDollars: 100,
884 | isCustomAmount: false,
885 | },
886 | {
887 | id: "ST_kwDOAAQmKM4ABksP",
888 | name: "$250 a month",
889 | isOneTime: false,
890 | description:
891 | "**Gold Spot - 2 spots left**\r\n\r\n- Get a Sponsor badge on your profile\r\n- Get a shoutout on X\r\n- **Gold** Spot / Logo and link on gider.im website\r\n- Early Access to gider.im repository",
892 | monthlyPriceInDollars: 250,
893 | isCustomAmount: false,
894 | },
895 | {
896 | id: "ST_kwDOAAQmKM4ABksQ",
897 | name: "$500 a month",
898 | isOneTime: false,
899 | description:
900 | "**Platinum - 1 spot left**\r\n\r\n- Get a Sponsor badge on your profile\r\n- Get a shoutout on X\r\n- **Platinum** Spot / Logo and link on gider.im website\r\n- Early Access to gider.im repository",
901 | monthlyPriceInDollars: 500,
902 | isCustomAmount: false,
903 | },
904 | ],
905 | },
906 | },
907 | },
908 | },
909 | } satisfies Externals.Github.APIResponse;
910 |
911 | export const getGithubInfo = cache(
912 | async (): Promise => {
913 | try {
914 | // Avoid rate limiting in development
915 | // set USE_MOCK_DATA_FOR_DEVELOPMENT false to use real data
916 | if (
917 | process.env.NODE_ENV === "development" &&
918 | USE_MOCK_DATA_FOR_DEVELOPMENT
919 | ) {
920 | return DEFAULT_GITHUB_RESPONSE;
921 | }
922 |
923 | const query = `
924 | {
925 | viewer {
926 | login
927 | ... on Sponsorable {
928 | sponsorshipsAsMaintainer(activeOnly: false, first: 100) {
929 | totalCount
930 | nodes {
931 | createdAt
932 | isActive
933 | tier {
934 | id
935 | name
936 | isOneTime
937 | monthlyPriceInDollars
938 | }
939 | sponsorEntity {
940 | __typename
941 | ... on User {
942 | login
943 | name
944 | bio
945 | avatarUrl
946 | twitterUsername
947 | }
948 | ... on Organization {
949 | login
950 | name
951 | description
952 | avatarUrl
953 | twitterUsername
954 | }
955 | }
956 | }
957 | }
958 | }
959 | sponsoring(first: 100) {
960 | nodes {
961 | ... on User {
962 | login
963 | name
964 | bio
965 | avatarUrl
966 | twitterUsername
967 | sponsorshipForViewerAsSponsor {
968 | isOneTimePayment
969 | isActive
970 | createdAt
971 | tier {
972 | id
973 | isCustomAmount
974 | monthlyPriceInDollars
975 | isOneTime
976 | name
977 | description
978 | }
979 | }
980 | }
981 | }
982 | }
983 | sponsorsListing {
984 | url
985 | fullDescription
986 | activeGoal {
987 | kind
988 | description
989 | percentComplete
990 | targetValue
991 | title
992 | }
993 | tiers(first: 100) {
994 | nodes {
995 | id
996 | name
997 | isOneTime
998 | description
999 | monthlyPriceInDollars
1000 | isCustomAmount
1001 | }
1002 | }
1003 | }
1004 | }
1005 | }
1006 | `;
1007 |
1008 | console.log("API HIT: github");
1009 | const res = await fetch("https://api.github.com/graphql", {
1010 | method: "POST",
1011 | headers: {
1012 | Authorization: `Bearer ${process.env.GITHUB_TOKEN}`,
1013 | },
1014 | body: JSON.stringify({
1015 | query,
1016 | }),
1017 | next: { revalidate: CACHE_DURATION },
1018 | });
1019 |
1020 | return await res.json();
1021 | } catch (error) {
1022 | console.error("github api error", error);
1023 | return DEFAULT_GITHUB_RESPONSE;
1024 | }
1025 | },
1026 | ["ned-im-github-sponsorship-data"],
1027 | {
1028 | revalidate: CACHE_DURATION,
1029 | },
1030 | );
1031 |
--------------------------------------------------------------------------------