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/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 |
30 |
31 | {githubResponse.data.viewer.sponsorsListing.fullDescription
32 | .split("\n")
33 | .map((line, index) => (
34 |
38 | {line}
39 |
40 | ))}
41 |
42 |
43 |
44 | {activeGoal && (
45 |
50 | )}
51 |
52 |
53 |
54 | );
55 | }
56 |
--------------------------------------------------------------------------------
/components/section/sponsorships-mine.tsx:
--------------------------------------------------------------------------------
1 | import Image from "next/image";
2 | import Link from "next/link";
3 |
4 | export function SponsorshipsMine({
5 | githubResponse,
6 | }: { githubResponse: Externals.Github.APIResponse }) {
7 | const mySponsorships = githubResponse.data.viewer.sponsoring.nodes.sort(
8 | (a, b) =>
9 | new Date(a.sponsorshipForViewerAsSponsor.createdAt).getTime() >
10 | new Date(b.sponsorshipForViewerAsSponsor.createdAt).getTime()
11 | ? -1
12 | : 1,
13 | );
14 |
15 | return (
16 |
17 |
My Sponsorships
18 |
19 |
20 | {mySponsorships.map((sponsorship) => {
21 | return (
22 |
28 |
29 |
36 |
37 |
{sponsorship.name}
38 |
39 | @{sponsorship.login}{" "}
40 |
41 | • {sponsorship.sponsorshipForViewerAsSponsor.tier.name}
42 |
43 |
44 |
45 |
46 |
47 | );
48 | })}
49 |
50 |
51 | );
52 | }
53 |
--------------------------------------------------------------------------------
/components/section/sponsorships-tiers.tsx:
--------------------------------------------------------------------------------
1 | import { SponsorshipsCategory } from "@/components/section/sponsorships-category";
2 | import { IconCircleNumber1, IconRotateClockwise2 } from "@tabler/icons-react";
3 |
4 | export function SponsorshipsTiers({
5 | githubResponse,
6 | }: { githubResponse: Externals.Github.APIResponse }) {
7 | const tiers = githubResponse.data.viewer.sponsorsListing.tiers.nodes.sort(
8 | (a, b) => {
9 | if (a.isOneTime !== b.isOneTime) {
10 | return a.isOneTime ? -1 : 1;
11 | }
12 | return a.monthlyPriceInDollars - b.monthlyPriceInDollars;
13 | },
14 | );
15 |
16 | const tiersByMode = tiers.reduce(
17 | (acc, tier) => {
18 | if (tier.isOneTime) {
19 | acc.oneTime.push(tier);
20 | } else {
21 | acc.recurring.push(tier);
22 | }
23 | return acc;
24 | },
25 | { oneTime: [] as TierElement[], recurring: [] as TierElement[] },
26 | );
27 |
28 | return (
29 |
30 | }
34 | label="One-time"
35 | />
36 | }
40 | label="Recurring"
41 | />
42 |
43 | );
44 | }
45 |
--------------------------------------------------------------------------------
/components/section/support-cta.tsx:
--------------------------------------------------------------------------------
1 | import AnimatedGradientSponsorText from "@/components/custom/animated-gradient-sponsor-text";
2 | import { SPONSOR_LINK } from "@/lib/constants";
3 | import { cn } from "@/lib/utils";
4 | import { IconChevronRight, IconHeartFilled } from "@tabler/icons-react";
5 | import Link from "next/link";
6 |
7 | export function SupportCta({
8 | className,
9 | subClassName,
10 | minimal = false,
11 | label = "Sponsor this project",
12 | }: {
13 | className?: string;
14 | subClassName?: string;
15 | label?: string;
16 | minimal?: boolean;
17 | }) {
18 | return (
19 |
20 |
25 |
26 | {" "}
27 |
33 | {label}
34 |
35 |
36 |
37 |
38 |
39 | );
40 | }
41 |
--------------------------------------------------------------------------------
/components/section/supporters.tsx:
--------------------------------------------------------------------------------
1 | import { EventSponsor } from "@/components/custom/event-sponsor";
2 | import { Sponsor } from "@/components/custom/sponsor";
3 | import { Strong } from "@/components/ui/text";
4 | import {
5 | Tooltip,
6 | TooltipContent,
7 | TooltipTrigger,
8 | } from "@/components/ui/tooltip";
9 | import { cn } from "@/lib/utils";
10 | import { getGithubInfo } from "@/server/github";
11 | import { IconArrowRight, IconBrandYoutubeFilled } from "@tabler/icons-react";
12 | import Image from "next/image";
13 | import Link from "next/link";
14 |
15 | const EXTERNAL_SPONSORS = [
16 | {
17 | username: "oguzyagizkara",
18 | tooltip: "Oğuz Yağız Kara",
19 | platform: "twitter",
20 | includeInTotal: true,
21 | },
22 | ];
23 |
24 | export async function Supporters() {
25 | const githubResponse = await getGithubInfo();
26 | const allSupporters =
27 | githubResponse.data.viewer.sponsorshipsAsMaintainer.nodes.sort((a, b) =>
28 | new Date(a.createdAt).getTime() > new Date(b.createdAt).getTime()
29 | ? 1
30 | : -1,
31 | );
32 |
33 | return (
34 | <>
35 |
36 |
37 | {githubResponse.data.viewer.sponsorshipsAsMaintainer.totalCount +
38 | EXTERNAL_SPONSORS.filter((s) => s.includeInTotal).length}{" "}
39 | people
40 | {" "}
41 | sponsoring gider.im
42 |
43 |
44 |
45 |
46 |
47 | Special Sponsor
48 | Designer Daily Report
49 |
50 | Daily Curated, Interactive Newspaper
51 |
52 | for Designers.
53 |
54 |
55 |
56 |
64 |
71 |
72 |
73 |
74 |
75 |
76 | Special Sponsor
77 | eser.live
78 |
79 | Eser Özvataf's
80 | YouTube channel
81 |
82 |
83 |
84 |
92 |
93 |
94 |
95 |
96 | {EXTERNAL_SPONSORS.map((sponsor) => (
97 |
98 | ))}
99 | {allSupporters.map((sponsor) => (
100 |
104 | ))}
105 |
106 |
107 |
108 |
109 |
110 | Become a sponsor{" "}
111 |
112 |
113 |
114 |
115 | >
116 | );
117 | }
118 |
--------------------------------------------------------------------------------
/components/ui/avatar.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as AvatarPrimitive from "@radix-ui/react-avatar";
4 | import * as React from "react";
5 |
6 | import { cn } from "@/lib/utils";
7 |
8 | const Avatar = React.forwardRef<
9 | React.ComponentRef
,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 | ));
21 | Avatar.displayName = AvatarPrimitive.Root.displayName;
22 |
23 | const AvatarImage = React.forwardRef<
24 | React.ComponentRef,
25 | React.ComponentPropsWithoutRef
26 | >(({ className, ...props }, ref) => (
27 |
32 | ));
33 | AvatarImage.displayName = AvatarPrimitive.Image.displayName;
34 |
35 | const AvatarFallback = React.forwardRef<
36 | React.ComponentRef,
37 | React.ComponentPropsWithoutRef
38 | >(({ className, ...props }, ref) => (
39 |
47 | ));
48 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
49 |
50 | export { Avatar, AvatarFallback, AvatarImage };
51 |
--------------------------------------------------------------------------------
/components/ui/badge.tsx:
--------------------------------------------------------------------------------
1 | import * as Headless from "@headlessui/react";
2 | import clsx from "clsx";
3 | import type React from "react";
4 | import { forwardRef } from "react";
5 | import { TouchTarget } from "./button";
6 | import { Link } from "./link";
7 |
8 | const colors = {
9 | red: "bg-red-500/15 text-red-700 group-data-hover:bg-red-500/25 dark:bg-red-500/10 dark:text-red-400 dark:group-data-hover:bg-red-500/20",
10 | orange:
11 | "bg-orange-500/15 text-orange-700 group-data-hover:bg-orange-500/25 dark:bg-orange-500/10 dark:text-orange-400 dark:group-data-hover:bg-orange-500/20",
12 | amber:
13 | "bg-amber-400/20 text-amber-700 group-data-hover:bg-amber-400/30 dark:bg-amber-400/10 dark:text-amber-400 dark:group-data-hover:bg-amber-400/15",
14 | yellow:
15 | "bg-yellow-400/20 text-yellow-700 group-data-hover:bg-yellow-400/30 dark:bg-yellow-400/10 dark:text-yellow-300 dark:group-data-hover:bg-yellow-400/15",
16 | lime: "bg-lime-400/20 text-lime-700 group-data-hover:bg-lime-400/30 dark:bg-lime-400/10 dark:text-lime-300 dark:group-data-hover:bg-lime-400/15",
17 | green:
18 | "bg-green-500/15 text-green-700 group-data-hover:bg-green-500/25 dark:bg-green-500/10 dark:text-green-400 dark:group-data-hover:bg-green-500/20",
19 | emerald:
20 | "bg-emerald-500/15 text-emerald-700 group-data-hover:bg-emerald-500/25 dark:bg-emerald-500/10 dark:text-emerald-400 dark:group-data-hover:bg-emerald-500/20",
21 | teal: "bg-teal-500/15 text-teal-700 group-data-hover:bg-teal-500/25 dark:bg-teal-500/10 dark:text-teal-300 dark:group-data-hover:bg-teal-500/20",
22 | cyan: "bg-cyan-400/20 text-cyan-700 group-data-hover:bg-cyan-400/30 dark:bg-cyan-400/10 dark:text-cyan-300 dark:group-data-hover:bg-cyan-400/15",
23 | sky: "bg-sky-500/15 text-sky-700 group-data-hover:bg-sky-500/25 dark:bg-sky-500/10 dark:text-sky-300 dark:group-data-hover:bg-sky-500/20",
24 | blue: "bg-blue-500/15 text-blue-700 group-data-hover:bg-blue-500/25 dark:text-blue-400 dark:group-data-hover:bg-blue-500/25",
25 | indigo:
26 | "bg-indigo-500/15 text-indigo-700 group-data-hover:bg-indigo-500/25 dark:text-indigo-400 dark:group-data-hover:bg-indigo-500/20",
27 | violet:
28 | "bg-violet-500/15 text-violet-700 group-data-hover:bg-violet-500/25 dark:text-violet-400 dark:group-data-hover:bg-violet-500/20",
29 | purple:
30 | "bg-purple-500/15 text-purple-700 group-data-hover:bg-purple-500/25 dark:text-purple-400 dark:group-data-hover:bg-purple-500/20",
31 | fuchsia:
32 | "bg-fuchsia-400/15 text-fuchsia-700 group-data-hover:bg-fuchsia-400/25 dark:bg-fuchsia-400/10 dark:text-fuchsia-400 dark:group-data-hover:bg-fuchsia-400/20",
33 | pink: "bg-pink-400/15 text-pink-700 group-data-hover:bg-pink-400/25 dark:bg-pink-400/10 dark:text-pink-400 dark:group-data-hover:bg-pink-400/20",
34 | rose: "bg-rose-400/15 text-rose-700 group-data-hover:bg-rose-400/25 dark:bg-rose-400/10 dark:text-rose-400 dark:group-data-hover:bg-rose-400/20",
35 | zinc: "bg-zinc-600/10 text-zinc-700 group-data-hover:bg-zinc-600/20 dark:bg-white/5 dark:text-zinc-400 dark:group-data-hover:bg-white/10",
36 | };
37 |
38 | type BadgeProps = { color?: keyof typeof colors };
39 |
40 | export function Badge({
41 | color = "zinc",
42 | className,
43 | ...props
44 | }: BadgeProps & React.ComponentPropsWithoutRef<"span">) {
45 | return (
46 |
54 | );
55 | }
56 |
57 | export const BadgeButton = forwardRef(function BadgeButton(
58 | {
59 | color = "zinc",
60 | className,
61 | children,
62 | ...props
63 | }: BadgeProps & { className?: string; children: React.ReactNode } & (
64 | | Omit
65 | | Omit, "className">
66 | ),
67 | ref: React.ForwardedRef,
68 | ) {
69 | const classes = clsx(
70 | className,
71 | "group relative inline-flex rounded-md focus:outline-hidden data-focus:outline data-focus:outline-2 data-focus:outline-offset-2 data-focus:outline-blue-500",
72 | );
73 |
74 | return "href" in props ? (
75 | }
79 | >
80 |
81 | {children}
82 |
83 |
84 | ) : (
85 |
86 |
87 | {children}
88 |
89 |
90 | );
91 | });
92 |
--------------------------------------------------------------------------------
/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as Headless from "@headlessui/react";
2 | import clsx from "clsx";
3 | import type React from "react";
4 | import { forwardRef } from "react";
5 | import { Link } from "./link";
6 |
7 | const styles = {
8 | base: [
9 | // Base
10 | "relative isolate inline-flex items-center justify-center gap-x-3 rounded-lg border text-base/6 font-medium",
11 | "cursor-pointer",
12 | // Sizing
13 | "px-[calc(--spacing(3.5)-1px)] py-[calc(--spacing(2.5)-1px)] sm:px-[calc(--spacing(3)-1px)] sm:py-[calc(--spacing(1.5)-1px)] sm:text-sm/6",
14 | // Focus
15 | "focus:outline-hidden data-focus:outline data-focus:outline-2 data-focus:outline-offset-2 data-focus:outline-blue-500",
16 | // Disabled
17 | "data-disabled:opacity-50",
18 | // Icon
19 | "*:data-[slot=icon]:-mx-0.5 *:data-[slot=icon]:my-0.5 *:data-[slot=icon]:size-5 *:data-[slot=icon]:shrink-0 *:data-[slot=icon]:text-(--btn-icon) sm:*:data-[slot=icon]:my-1 sm:*:data-[slot=icon]:size-4 forced-colors:[--btn-icon:ButtonText] forced-colors:data-hover:[--btn-icon:ButtonText]",
20 | ],
21 | solid: [
22 | // Optical border, implemented as the button background to avoid corner artifacts
23 | "border-transparent bg-(--btn-border)",
24 | // Dark mode: border is rendered on `after` so background is set to button background
25 | "dark:bg-(--btn-bg)",
26 | // Button background, implemented as foreground layer to stack on top of pseudo-border layer
27 | "before:absolute before:inset-0 before:-z-10 before:rounded-[calc(var(--radius-lg)-1px)] before:bg-(--btn-bg)",
28 | // Drop shadow, applied to the inset `before` layer so it blends with the border
29 | "before:shadow-sm",
30 | // Background color is moved to control and shadow is removed in dark mode so hide `before` pseudo
31 | "dark:before:hidden",
32 | // Dark mode: Subtle white outline is applied using a border
33 | "dark:border-white/5",
34 | // Shim/overlay, inset to match button foreground and used for hover state + highlight shadow
35 | "after:absolute after:inset-0 after:-z-10 after:rounded-[calc(var(--radius-lg)-1px)]",
36 | // Inner highlight shadow
37 | "after:shadow-[shadow:inset_0_1px_--theme(--color-white/15%)]",
38 | // White overlay on hover
39 | "data-active:after:bg-(--btn-hover-overlay) data-hover:after:bg-(--btn-hover-overlay)",
40 | // Dark mode: `after` layer expands to cover entire button
41 | "dark:after:-inset-px dark:after:rounded-lg",
42 | // Disabled
43 | "data-disabled:before:shadow-none data-disabled:after:shadow-none",
44 | ],
45 | outline: [
46 | // Base
47 | "border-zinc-950/10 text-zinc-950 data-active:bg-zinc-950/[2.5%] data-hover:bg-zinc-950/[2.5%]",
48 | // Dark mode
49 | "dark:border-white/15 dark:text-white dark:[--btn-bg:transparent] dark:data-active:bg-white/5 dark:data-hover:bg-white/5",
50 | // Icon
51 | "[--btn-icon:var(--color-zinc-500)] data-active:[--btn-icon:var(--color-zinc-700)] data-hover:[--btn-icon:var(--color-zinc-700)] dark:data-active:[--btn-icon:var(--color-zinc-400)] dark:data-hover:[--btn-icon:var(--color-zinc-400)]",
52 | ],
53 | plain: [
54 | // Base
55 | "border-transparent text-zinc-950 data-active:bg-zinc-950/5 data-hover:bg-zinc-950/5",
56 | // Dark mode
57 | "dark:text-white dark:data-active:bg-white/10 dark:data-hover:bg-white/10",
58 | // Icon
59 | "[--btn-icon:var(--color-zinc-500)] data-active:[--btn-icon:var(--color-zinc-700)] data-hover:[--btn-icon:var(--color-zinc-700)] dark:[--btn-icon:var(--color-zinc-500)] dark:data-active:[--btn-icon:var(--color-zinc-400)] dark:data-hover:[--btn-icon:var(--color-zinc-400)]",
60 | ],
61 | colors: {
62 | "dark/zinc": [
63 | "text-white [--btn-bg:var(--color-zinc-900)] [--btn-border:var(--color-zinc-950)]/90 [--btn-hover-overlay:var(--color-white)]/10",
64 | "dark:text-white dark:[--btn-bg:var(--color-zinc-600)] dark:[--btn-hover-overlay:var(--color-white)]/5",
65 | "[--btn-icon:var(--color-zinc-400)] data-active:[--btn-icon:var(--color-zinc-300)] data-hover:[--btn-icon:var(--color-zinc-300)]",
66 | ],
67 | light: [
68 | "text-zinc-950 [--btn-bg:white] [--btn-border:var(--color-zinc-950)]/10 [--btn-hover-overlay:var(--color-zinc-950)]/[2.5%] data-active:[--btn-border:var(--color-zinc-950)]/15 data-hover:[--btn-border:var(--color-zinc-950)]/15",
69 | "dark:text-white dark:[--btn-hover-overlay:var(--color-white)]/5 dark:[--btn-bg:var(--color-zinc-800)]",
70 | "[--btn-icon:var(--color-zinc-500)] data-active:[--btn-icon:var(--color-zinc-700)] data-hover:[--btn-icon:var(--color-zinc-700)] dark:[--btn-icon:var(--color-zinc-500)] dark:data-active:[--btn-icon:var(--color-zinc-400)] dark:data-hover:[--btn-icon:var(--color-zinc-400)]",
71 | ],
72 | "dark/white": [
73 | "text-white [--btn-bg:var(--color-zinc-900)] [--btn-border:var(--color-zinc-950)]/90 [--btn-hover-overlay:var(--color-white)]/10",
74 | "dark:text-zinc-950 dark:[--btn-bg:white] dark:[--btn-hover-overlay:var(--color-zinc-950)]/5",
75 | "[--btn-icon:var(--color-zinc-400)] data-active:[--btn-icon:var(--color-zinc-300)] data-hover:[--btn-icon:var(--color-zinc-300)] dark:[--btn-icon:var(--color-zinc-500)] dark:data-active:[--btn-icon:var(--color-zinc-400)] dark:data-hover:[--btn-icon:var(--color-zinc-400)]",
76 | ],
77 | dark: [
78 | "text-white [--btn-bg:var(--color-zinc-900)] [--btn-border:var(--color-zinc-950)]/90 [--btn-hover-overlay:var(--color-white)]/10",
79 | "dark:[--btn-hover-overlay:var(--color-white)]/5 dark:[--btn-bg:var(--color-zinc-800)]",
80 | "[--btn-icon:var(--color-zinc-400)] data-active:[--btn-icon:var(--color-zinc-300)] data-hover:[--btn-icon:var(--color-zinc-300)]",
81 | ],
82 | white: [
83 | "text-zinc-950 [--btn-bg:white] [--btn-border:var(--color-zinc-950)]/10 [--btn-hover-overlay:var(--color-zinc-950)]/[2.5%] data-active:[--btn-border:var(--color-zinc-950)]/15 data-hover:[--btn-border:var(--color-zinc-950)]/15",
84 | "dark:[--btn-hover-overlay:var(--color-zinc-950)]/5",
85 | "[--btn-icon:var(--color-zinc-400)] data-active:[--btn-icon:var(--color-zinc-500)] data-hover:[--btn-icon:var(--color-zinc-500)]",
86 | ],
87 | zinc: [
88 | "text-white [--btn-hover-overlay:var(--color-white)]/10 [--btn-bg:var(--color-zinc-600)] [--btn-border:var(--color-zinc-700)]/90",
89 | "dark:[--btn-hover-overlay:var(--color-white)]/5",
90 | "[--btn-icon:var(--color-zinc-400)] data-active:[--btn-icon:var(--color-zinc-300)] data-hover:[--btn-icon:var(--color-zinc-300)]",
91 | ],
92 | indigo: [
93 | "text-white [--btn-hover-overlay:var(--color-white)]/10 [--btn-bg:var(--color-indigo-500)] [--btn-border:var(--color-indigo-600)]/90",
94 | "[--btn-icon:var(--color-indigo-300)] data-active:[--btn-icon:var(--color-indigo-200)] data-hover:[--btn-icon:var(--color-indigo-200)]",
95 | ],
96 | cyan: [
97 | "text-cyan-950 [--btn-bg:var(--color-cyan-300)] [--btn-border:var(--color-cyan-400)]/80 [--btn-hover-overlay:var(--color-white)]/25",
98 | "[--btn-icon:var(--color-cyan-500)]",
99 | ],
100 | red: [
101 | "text-white [--btn-hover-overlay:var(--color-white)]/10 [--btn-bg:var(--color-red-600)] [--btn-border:var(--color-red-700)]/90",
102 | "[--btn-icon:var(--color-red-300)] data-active:[--btn-icon:var(--color-red-200)] data-hover:[--btn-icon:var(--color-red-200)]",
103 | ],
104 | orange: [
105 | "text-white [--btn-hover-overlay:var(--color-white)]/10 [--btn-bg:var(--color-orange-500)] [--btn-border:var(--color-orange-600)]/90",
106 | "[--btn-icon:var(--color-orange-300)] data-active:[--btn-icon:var(--color-orange-200)] data-hover:[--btn-icon:var(--color-orange-200)]",
107 | ],
108 | amber: [
109 | "text-amber-950 [--btn-hover-overlay:var(--color-white)]/25 [--btn-bg:var(--color-amber-400)] [--btn-border:var(--color-amber-500)]/80",
110 | "[--btn-icon:var(--color-amber-600)]",
111 | ],
112 | yellow: [
113 | "text-yellow-950 [--btn-hover-overlay:var(--color-white)]/25 [--btn-bg:var(--color-yellow-300)] [--btn-border:var(--color-yellow-400)]/80",
114 | "[--btn-icon:var(--color-yellow-600)] data-active:[--btn-icon:var(--color-yellow-700)] data-hover:[--btn-icon:var(--color-yellow-700)]",
115 | ],
116 | lime: [
117 | "text-lime-950 [--btn-hover-overlay:var(--color-white)]/25 [--btn-bg:var(--color-lime-300)] [--btn-border:var(--color-lime-400)]/80",
118 | "[--btn-icon:var(--color-lime-600)] data-active:[--btn-icon:var(--color-lime-700)] data-hover:[--btn-icon:var(--color-lime-700)]",
119 | ],
120 | green: [
121 | "text-white [--btn-hover-overlay:var(--color-white)]/10 [--btn-bg:var(--color-green-600)] [--btn-border:var(--color-green-700)]/90",
122 | "[--btn-icon:var(--color-white)]/60 data-active:[--btn-icon:var(--color-white)]/80 data-hover:[--btn-icon:var(--color-white)]/80",
123 | ],
124 | emerald: [
125 | "text-white [--btn-hover-overlay:var(--color-white)]/10 [--btn-bg:var(--color-emerald-600)] [--btn-border:var(--color-emerald-700)]/90",
126 | "[--btn-icon:var(--color-white)]/60 data-active:[--btn-icon:var(--color-white)]/80 data-hover:[--btn-icon:var(--color-white)]/80",
127 | ],
128 | teal: [
129 | "text-white [--btn-hover-overlay:var(--color-white)]/10 [--btn-bg:var(--color-teal-600)] [--btn-border:var(--color-teal-700)]/90",
130 | "[--btn-icon:var(--color-white)]/60 data-active:[--btn-icon:var(--color-white)]/80 data-hover:[--btn-icon:var(--color-white)]/80",
131 | ],
132 | sky: [
133 | "text-white [--btn-hover-overlay:var(--color-white)]/10 [--btn-bg:var(--color-sky-500)] [--btn-border:var(--color-sky-600)]/80",
134 | "[--btn-icon:var(--color-white)]/60 data-active:[--btn-icon:var(--color-white)]/80 data-hover:[--btn-icon:var(--color-white)]/80",
135 | ],
136 | blue: [
137 | "text-white [--btn-hover-overlay:var(--color-white)]/10 [--btn-bg:var(--color-blue-600)] [--btn-border:var(--color-blue-700)]/90",
138 | "[--btn-icon:var(--color-blue-400)] data-active:[--btn-icon:var(--color-blue-300)] data-hover:[--btn-icon:var(--color-blue-300)]",
139 | ],
140 | violet: [
141 | "text-white [--btn-hover-overlay:var(--color-white)]/10 [--btn-bg:var(--color-violet-500)] [--btn-border:var(--color-violet-600)]/90",
142 | "[--btn-icon:var(--color-violet-300)] data-active:[--btn-icon:var(--color-violet-200)] data-hover:[--btn-icon:var(--color-violet-200)]",
143 | ],
144 | purple: [
145 | "text-white [--btn-hover-overlay:var(--color-white)]/10 [--btn-bg:var(--color-purple-500)] [--btn-border:var(--color-purple-600)]/90",
146 | "[--btn-icon:var(--color-purple-300)] data-active:[--btn-icon:var(--color-purple-200)] data-hover:[--btn-icon:var(--color-purple-200)]",
147 | ],
148 | fuchsia: [
149 | "text-white [--btn-hover-overlay:var(--color-white)]/10 [--btn-bg:var(--color-fuchsia-500)] [--btn-border:var(--color-fuchsia-600)]/90",
150 | "[--btn-icon:var(--color-fuchsia-300)] data-active:[--btn-icon:var(--color-fuchsia-200)] data-hover:[--btn-icon:var(--color-fuchsia-200)]",
151 | ],
152 | pink: [
153 | "text-white [--btn-hover-overlay:var(--color-white)]/10 [--btn-bg:var(--color-pink-500)] [--btn-border:var(--color-pink-600)]/90",
154 | "[--btn-icon:var(--color-pink-300)] data-active:[--btn-icon:var(--color-pink-200)] data-hover:[--btn-icon:var(--color-pink-200)]",
155 | ],
156 | rose: [
157 | "text-white [--btn-hover-overlay:var(--color-white)]/10 [--btn-bg:var(--color-rose-500)] [--btn-border:var(--color-rose-600)]/90",
158 | "[--btn-icon:var(--color-rose-300)] data-active:[--btn-icon:var(--color-rose-200)] data-hover:[--btn-icon:var(--color-rose-200)]",
159 | ],
160 | },
161 | };
162 |
163 | type ButtonProps = (
164 | | { color?: keyof typeof styles.colors; outline?: never; plain?: never }
165 | | { color?: never; outline: true; plain?: never }
166 | | { color?: never; outline?: never; plain: true }
167 | ) & { className?: string; children: React.ReactNode } & (
168 | | Omit
169 | | Omit, "className">
170 | );
171 |
172 | export const Button = forwardRef(function Button(
173 | { color, outline, plain, className, children, ...props }: ButtonProps,
174 | ref: React.ForwardedRef,
175 | ) {
176 | const classes = clsx(
177 | className,
178 | styles.base,
179 | outline
180 | ? styles.outline
181 | : plain
182 | ? styles.plain
183 | : clsx(styles.solid, styles.colors[color ?? "dark/zinc"]),
184 | );
185 |
186 | return "href" in props ? (
187 | }
191 | >
192 | {children}
193 |
194 | ) : (
195 |
200 | {children}
201 |
202 | );
203 | });
204 |
205 | /**
206 | * Expand the hit area to at least 44×44px on touch devices
207 | */
208 | export function TouchTarget({ children }: { children: React.ReactNode }) {
209 | return (
210 | <>
211 |
215 | {children}
216 | >
217 | );
218 | }
219 |
--------------------------------------------------------------------------------
/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/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/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/link.tsx:
--------------------------------------------------------------------------------
1 | import * as Headless from "@headlessui/react";
2 | import NextLink, { type LinkProps } from "next/link";
3 | import type React from "react";
4 | import { forwardRef } from "react";
5 |
6 | export const Link = forwardRef(function Link(
7 | props: LinkProps & React.ComponentPropsWithoutRef<"a">,
8 | ref: React.ForwardedRef,
9 | ) {
10 | return (
11 |
12 |
13 |
14 | );
15 | });
16 |
--------------------------------------------------------------------------------
/components/ui/navbar.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as Headless from "@headlessui/react";
4 | import clsx from "clsx";
5 | import { LayoutGroup, motion } from "motion/react";
6 | import type React from "react";
7 | import { forwardRef, useId } from "react";
8 | import { TouchTarget } from "./button";
9 | import { Link } from "./link";
10 |
11 | export function Navbar({
12 | className,
13 | ...props
14 | }: React.ComponentPropsWithoutRef<"nav">) {
15 | return (
16 |
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/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 |
32 | {children}
33 |
34 | );
35 | }
36 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/fonts/GTWalsheimPro-Bold.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/needim/gider.im-website/f460002166c8e07e7a9fa6b3a733760bbf9a80c4/fonts/GTWalsheimPro-Bold.woff2
--------------------------------------------------------------------------------
/fonts/GTWalsheimPro-Medium.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/needim/gider.im-website/f460002166c8e07e7a9fa6b3a733760bbf9a80c4/fonts/GTWalsheimPro-Medium.woff2
--------------------------------------------------------------------------------
/fonts/GTWalsheimPro-Regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/needim/gider.im-website/f460002166c8e07e7a9fa6b3a733760bbf9a80c4/fonts/GTWalsheimPro-Regular.woff2
--------------------------------------------------------------------------------
/lib/constants.ts:
--------------------------------------------------------------------------------
1 | export const SPONSOR_LINK = "https://github.com/sponsors/needim";
2 | export const PWA_LINK = "https://app.gider.im";
3 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/public/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/needim/gider.im-website/f460002166c8e07e7a9fa6b3a733760bbf9a80c4/public/apple-touch-icon.png
--------------------------------------------------------------------------------
/public/ddr-logo-circle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/needim/gider.im-website/f460002166c8e07e7a9fa6b3a733760bbf9a80c4/public/ddr-logo-circle.png
--------------------------------------------------------------------------------
/public/favicon-48x48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/needim/gider.im-website/f460002166c8e07e7a9fa6b3a733760bbf9a80c4/public/favicon-48x48.png
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/needim/gider.im-website/f460002166c8e07e7a9fa6b3a733760bbf9a80c4/public/favicon.ico
--------------------------------------------------------------------------------
/public/favicon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
5 |
--------------------------------------------------------------------------------
/public/og.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/needim/gider.im-website/f460002166c8e07e7a9fa6b3a733760bbf9a80c4/public/og.png
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | User-agent: *
2 | Allow: /
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/public/web-app-manifest-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/needim/gider.im-website/f460002166c8e07e7a9fa6b3a733760bbf9a80c4/public/web-app-manifest-192x192.png
--------------------------------------------------------------------------------
/public/web-app-manifest-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/needim/gider.im-website/f460002166c8e07e7a9fa6b3a733760bbf9a80c4/public/web-app-manifest-512x512.png
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3 | "extends": ["config:recommended"]
4 | }
5 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------