`).
52 | 6. **Record changes to the repository:** Create a new commit containing the current contents of the index and the given log message describing the changes(`git commit -m 'Add: some feature'`).
53 | 7. **Submit your Contribution:** Upload your branch with the changes to forked repository on GitHub using (`git push origin feature/some-feature`).
54 | 8. **Generate a request:** To complete the process of creating your PR, simply hit [_pull request_](https://github.com/Afordin/aforshow-2024/pulls)
55 |
56 | ## Authors
57 |
58 |
59 |
60 |
61 |
62 | ### Contribution from Stackblitz
63 |
64 | If you want to contribute in a simpler way, you can start this project from _Stackblitz_ using your GitHub account:
65 |
66 | [](https://stackblitz.com/github/Afordin/aforshow-2024)
67 |
68 | **Thanks to all the contributors who have made this project possible!**
69 |
70 | [](https://github.com/Afordin/aforshow-2024/graphs/contributors)
71 |
72 | ## 🛠️ Stack
73 |
74 | [![Next][next-badge]][next-url]
75 | [![React][react-badge]][react-url]
76 | [![Tailwind][tailwind-badge]][tailwind-url]
77 |
78 | [contributors-shield]: https://img.shields.io/github/contributors/Afordin/aforshow-2024.svg?style=for-the-badge
79 | [contributors-url]: https://github.com/Afordin/aforshow-2024/graphs/contributors
80 | [forks-shield]: https://img.shields.io/github/forks/Afordin/aforshow-2024.svg?style=for-the-badge
81 | [forks-url]: https://github.com/Afordin/aforshow-2024/network/members
82 | [stars-shield]: https://img.shields.io/github/stars/Afordin/aforshow-2024.svg?style=for-the-badge
83 | [stars-url]: https://github.com/Afordin/aforshow-2024/stargazers
84 | [issues-shield]: https://img.shields.io/github/issues/Afordin/aforshow-2024.svg?style=for-the-badge
85 | [issues-url]: https://github.com/Afordin/aforshow-2024/issues
86 | [next-url]: https://nextjs.org/
87 | [react-url]: https://reactjs.org/
88 | [tailwind-url]: https://tailwindcss.com/
89 | [next-badge]: https://img.shields.io/badge/next.js-000000?style=for-the-badge&logo=nextdotjs&logoColor=333
90 | [react-badge]: https://img.shields.io/badge/React-61DAFB?style=for-the-badge&logo=react&logoColor=333
91 | [tailwind-badge]: https://img.shields.io/badge/-Tailwind%20CSS-%231a202c?style=for-the-badge&logo=tailwind-css
92 |
--------------------------------------------------------------------------------
/app/[userId]/opengraph-image.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable @next/next/no-img-element */
2 | import { createClient } from '@/utils/supabase/server'
3 | import { ImageResponse } from 'next/og'
4 |
5 |
6 | export const size = {
7 | width: 1200,
8 | height: 580,
9 | }
10 | export const contentType = 'image/png'
11 |
12 |
13 |
14 | export default async function Image({ params: { userId } }) {
15 | const apiClient = createClient()
16 | const { data: user, error } = await apiClient.from('profiles').select('*').eq('id', userId).single()
17 |
18 | if(!user) return new ImageResponse(
19 |
20 |

21 |
22 | , { ...size })
23 |
24 | const imageUrl = `https://uuljbqkwvruhxomkmxaj.supabase.co/storage/v1/object/public/aforshow/public/${user.id}.png`
25 |
26 | return new ImageResponse(
27 |
28 |

29 |
30 | ,
31 | { ...size }
32 | );
33 |
34 |
35 | }
36 |
--------------------------------------------------------------------------------
/app/[userId]/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import { Footer } from "@/components/Footer";
3 | import Hero from "@/components/Hero";
4 | import { Sponsors } from "@/components/Sponsors";
5 | import { Ticket } from "@/components/Ticket";
6 | import { WelcomeHero } from "@/components/WelcomeHero";
7 | import { User } from "@/store/useUserStore";
8 | import { apiClient } from "@/utils/supabase/client";
9 |
10 | export default async function Page({ params: { userId } }) {
11 | const { data, error } = await apiClient.from('profiles').select('*').eq('id', userId).single();
12 | const user = data as User
13 |
14 | return (
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | )
30 | }
--------------------------------------------------------------------------------
/app/components/FAQ.tsx:
--------------------------------------------------------------------------------
1 | import { Summary } from "./common/Summary";
2 |
3 | const questions = [
4 | {
5 | title: "¿Donde puedo ver el evento?",
6 | description: (
7 |
8 | El evento será totalmente gratuito y realizará en{" "}
9 |
10 | twitch.tv/afor_digital
11 |
12 |
13 | ),
14 | },
15 | {
16 | title: "¿Tengo que estar suscrito al canal para verlo?",
17 | description: (
18 |
19 | No, es totalmente gratuito. Pero si quieres dejar tu prime nadie te dirá
20 | nada.
21 |
22 | ),
23 | },
24 | {
25 | title: "¿Puedo presentar mi charla?",
26 | description: (
27 |
28 | Las inscripciones ya están cerradas, puedes ver las charlas
29 | seleccionadas en el apartado de horarios.
30 |
31 | ),
32 | },
33 | {
34 | title: "¿Se podrán ver las charlas más tarde?",
35 | description: (
36 |
37 | Sí, todas las charlas se podrán ver más tarde en{" "}
38 |
39 | afor lives
40 |
41 |
42 | ),
43 | },
44 | ];
45 |
46 | export const FAQ = () => {
47 | return (
48 |
49 |
50 | Preguntas frecuentes
51 |
52 |
53 | {questions.map((question, index) => (
54 |
55 | {question.description}
56 |
57 | ))}
58 |
59 |
60 | );
61 | };
62 |
--------------------------------------------------------------------------------
/app/components/Footer.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "./utils";
2 | import Image from "next/image";
3 | import { Github, Twitch, Instagram, X, Discord } from "@/components/logos";
4 |
5 | interface SocialIcon {
6 | icon: JSX.Element;
7 | url: string;
8 | }
9 |
10 | const socialIcons: SocialIcon[] = [
11 | {
12 | icon: ,
13 | url: "https://discord.com/invite/comuafor",
14 | },
15 | {
16 | icon: ,
17 | url: "https://www.twitch.tv/afor_digital",
18 | },
19 | {
20 | icon: ,
21 | url: "https://www.instagram.com/afor_digital",
22 | },
23 | {
24 | icon: ,
25 | url: "https://github.com/Afordin",
26 | },
27 | {
28 | icon: ,
29 | url: "https://twitter.com/afor_digital",
30 | },
31 | ];
32 |
33 | export const Footer = () => {
34 | const classes = {
35 | container: cn(
36 | "relative z-20 text-cWhite bg-gradient-to-r from-[#19101D] to-[#0D0D0E] py-5 w-full font-dmsans"
37 | ),
38 | innerContainer: cn(
39 | "max-w-7xl w-full mx-auto text-center mb-10 flex flex-col justify-between items-center"
40 | ),
41 | socialIcon: cn("inline-flex"),
42 | copyRight: cn("text-sm mt-5 inset-x-0 bottom-2 text-center px-2"),
43 | };
44 |
45 | const renderSocialIcons = () =>
46 | socialIcons.map((socialIcon, index) => (
47 |
55 | {socialIcon.icon}
56 |
57 | ));
58 |
59 | const handleScrollToTop = (e: React.MouseEvent) => {
60 | e.preventDefault();
61 | window.scrollTo({ top: 0, behavior: 'smooth' });
62 | };
63 |
64 | return (
65 |
122 | );
123 | };
--------------------------------------------------------------------------------
/app/components/Hero.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React from "react";
4 | import { Nav } from "./Nav";
5 | import { WelcomeHero } from "./WelcomeHero";
6 | import { Sponsors } from "./Sponsors";
7 |
8 | export default function Hero() {
9 | return (
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | );
21 | }
22 |
--------------------------------------------------------------------------------
/app/components/Nav.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useEffect, useRef, useState } from "react";
4 | import { useRouter } from "next/navigation";
5 | import Image from "next/image";
6 | import { Menu, X } from "lucide-react";
7 | import { useCursors } from "app/cursors-context";
8 | import { Button } from "./ui/button";
9 | import Logo from "app/components/icons/Logo";
10 | import { cn } from "./utils";
11 | import { useUserStore } from "@/store/useUserStore";
12 | import { useAuth } from "@/hooks/useAuth";
13 | import { useOnClickOutside } from "@/hooks/useOnClickOutside";
14 | import { Link } from "./common/Link";
15 |
16 | export const Nav = () => {
17 | const router = useRouter();
18 | const { cursors, disabled, setDisabled } = useCursors();
19 | const { signInWithDiscord, signOut } = useAuth();
20 | const user = useUserStore((state) => state.user);
21 |
22 | const [isOpen, setIsOpen] = useState(false);
23 | const [showNavbar, setShowNavbar] = useState(true);
24 | const [lastScrollY, setLastScrollY] = useState(0);
25 | const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
26 | const modalRef = useRef(null);
27 | const trigger = useOnClickOutside(
28 | modalRef,
29 | ({ isSameTrigger }) => setIsOpen(isSameTrigger)
30 | );
31 |
32 | const slice = 3;
33 | const cursorsSlice = cursors.slice(-slice);
34 |
35 | const imgCircleClass = cn(
36 | "relative rounded-full h-8 w-8 ring-2 ring-white overflow-hidden group-hover:ring-[3px]"
37 | );
38 |
39 | useEffect(() => {
40 | const handleScroll = () => {
41 | const currentScrollY = window.scrollY;
42 |
43 | if (currentScrollY > lastScrollY) {
44 | setShowNavbar(false);
45 | setMobileMenuOpen(false);
46 | } else {
47 | setShowNavbar(true);
48 | }
49 |
50 | setLastScrollY(currentScrollY);
51 | };
52 |
53 | window.addEventListener("scroll", handleScroll);
54 |
55 | return () => {
56 | window.removeEventListener("scroll", handleScroll);
57 | };
58 | }, [lastScrollY]);
59 |
60 | useEffect(() => {
61 | const handleResize = () => {
62 | setMobileMenuOpen(false);
63 | };
64 |
65 | window.addEventListener("resize", handleResize);
66 |
67 | return () => {
68 | window.removeEventListener("resize", handleResize);
69 | };
70 | }, []);
71 |
72 | return (
73 |
215 | );
216 | };
217 |
--------------------------------------------------------------------------------
/app/components/Schedule.tsx:
--------------------------------------------------------------------------------
1 | import { useTimezone } from "@/hooks/useTimezone";
2 | import { Talk } from "./common/Talk";
3 | import { Tag } from "./common/Tag";
4 | import { Table } from "./common/Table";
5 |
6 | export const Schedule = () => {
7 | const timezone = useTimezone();
8 | return (
9 |
10 |
11 | Horarios y charlas
12 |
13 |
14 |
15 | Zona horaria: {timezone}
16 |
17 |
18 |
19 |
26 |
33 |
38 |
45 |
52 |
57 |
64 |
65 |
66 | );
67 | };
68 |
--------------------------------------------------------------------------------
/app/components/Sponsors.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Image from 'next/image';
3 |
4 | interface SponsorImage {
5 | src: string;
6 | alt: string;
7 | }
8 |
9 | interface MarqueeProps {
10 | sponsors: SponsorImage[];
11 | speed?: number;
12 | direction?: 'left' | 'right';
13 | }
14 |
15 | const Marquee: React.FC = ({
16 | sponsors,
17 | speed = 50,
18 | direction = 'left'
19 | }) => {
20 | return (
21 |
22 |
26 | {[...sponsors, ...sponsors].map((sponsor, index) => (
27 |
28 |
34 |
35 | ))}
36 |
37 |
38 | );
39 | };
40 |
41 | export const Sponsors: React.FC = () => {
42 | const sponsors: SponsorImage[] = [
43 | { src: "/imgs/afordin-sponsor.png", alt: "afordin-logo-sponsor" },
44 | { src: "/imgs/afordin-sponsor.png", alt: "afordin-logo-sponsor" },
45 | { src: "/imgs/afordin-sponsor.png", alt: "afordin-logo-sponsor" },
46 | { src: "/imgs/afordin-sponsor.png", alt: "afordin-logo-sponsor" },
47 | { src: "/imgs/afordin-sponsor.png", alt: "afordin-logo-sponsor" },
48 | ];
49 |
50 | return (
51 |
52 |
53 | Evento sponsorizado gracias a
54 |
55 |
56 |
57 | );
58 | };
--------------------------------------------------------------------------------
/app/components/Ticket.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable @next/next/no-img-element */
2 | import "atropos/css";
3 | import { Atropos } from "atropos/react";
4 | import { forwardRef } from "react";
5 |
6 | interface TicketProps {
7 | avatar?: string;
8 | name?: string;
9 | number?: number;
10 | }
11 |
12 | export const sponsors = [
13 | {
14 | name: "Afordin",
15 | logo: "/imgs/ticket/sponsor_1.webp",
16 | },
17 | ];
18 | export const Ticket = forwardRef(function Ticket(
19 | { name = "tpicj aforcita", number = 1, avatar = "/imgs/ticket/avatar.png" },
20 | ref
21 | ) {
22 | return (
23 |
24 |
29 |
33 |
34 |
35 |

42 |
43 |
15 ? "text-2xl" : "text-4xl"
48 | }`}
49 | >
50 | {name}
51 |
52 |
56 |
57 |
58 | twitch.tv/afor_digital
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 | SEPT
67 | 20
68 |
69 |
70 |
71 | {sponsors.map((sponsor) => (
72 |

78 | ))}
79 |
80 |
81 |
82 |
86 | #{number.toString().padStart(5, "0")}
87 |
88 |
89 |
90 |
91 |
92 |

98 |
99 |
100 |
101 |
102 | );
103 | });
104 |
--------------------------------------------------------------------------------
/app/components/TicketDownload.tsx:
--------------------------------------------------------------------------------
1 | import { ArrowDown } from "lucide-react";
2 | import { Button } from "./ui/button";
3 | import { X } from "@/components/logos";
4 | import { useUserStore } from "@/store/useUserStore";
5 | import { Ticket } from "./Ticket";
6 | import { useRef } from "react";
7 | import { toPng } from "html-to-image";
8 | import { useAuth } from "@/hooks/useAuth";
9 | import { uploadTicket } from "./utils/uploadTicket";
10 |
11 | export const TicketDownload = () => {
12 | const { signInWithDiscord } = useAuth();
13 | const user = useUserStore((state) => state.user);
14 | const ticketRef = useRef(null);
15 |
16 | const downloadTicket = async () => {
17 | if (ticketRef.current) {
18 | try {
19 | const dataUrl = await toPng(ticketRef.current);
20 |
21 | if (!dataUrl) {
22 | console.error("Could not capture image");
23 | return;
24 | }
25 |
26 | const link = document.createElement("a");
27 | link.download = "hackafor-ticket.png";
28 | link.href = dataUrl;
29 | document.body.appendChild(link);
30 | link.click();
31 | document.body.removeChild(link);
32 | } catch (error) {
33 | console.error("Could not capture image:", error);
34 | }
35 | }
36 | };
37 |
38 | const shareTwitter = async () => {
39 | if (!user || !ticketRef.current) return;
40 |
41 | await uploadTicket(user.id, ticketRef.current);
42 |
43 | const urlstring =
44 | process.env.NEXT_PUBLIC_BASE_URL || "https://aforshow-2024.vercel.app";
45 | const url = `${urlstring}/${user.id}/`;
46 |
47 | const message =
48 | "Este es tu ticket exclusivo para el Aforshow, habrá charlas, premios y sorteos. ¡Te esperamos! 🚀🎉";
49 | const hashtags = ["aforshow"];
50 |
51 | const encodedText = encodeURIComponent(message);
52 | const encodedUrl = encodeURIComponent(url);
53 | const hashtagsEncoded = encodeURIComponent(hashtags.join(","));
54 |
55 | const twitterUrl = `https://twitter.com/intent/tweet?text=${encodedText}&url=${encodedUrl}&hashtags=${hashtagsEncoded}`;
56 |
57 | window.open(twitterUrl, "_blank");
58 | };
59 |
60 | return (
61 |
65 |
66 | Descarga tu ticket y compártelo en redes sociales
67 |
68 | {user && (
69 | <>
70 |
71 |
77 |
78 |
79 |
83 |
84 |
93 |
94 | >
95 | )}
96 |
97 | {user === undefined && (
98 |
99 |
100 |
101 | )}
102 |
103 | {user === null && (
104 | <>
105 |
106 |
107 |
108 |
109 |
118 |
119 | >
120 | )}
121 |
122 | );
123 | };
124 |
--------------------------------------------------------------------------------
/app/components/WelcomeHero.tsx:
--------------------------------------------------------------------------------
1 | import { ArrowUpRight, Ticket } from "lucide-react";
2 | import { Button } from "./ui/button";
3 | import { Countdown } from "./common/Countdown";
4 | import { FC, PropsWithChildren } from "react";
5 | import { cn } from "./utils";
6 |
7 | interface Props extends PropsWithChildren<{}> {
8 | variant: "home" | "ticket";
9 | }
10 |
11 | export const WelcomeHero: FC = ({ variant, children }) => {
12 | return (
13 |
19 |
20 |
21 |
22 | ¡Gracias por estar presente en el {' '}
23 |
27 | Aforshow!
28 |
29 |
30 | {children}
31 |
32 |
33 |
34 | Puedes ver la repetición del evento aquí:
35 |
36 |
37 |
38 |
39 |
40 |
41 |
59 |
60 |
61 | );
62 | };
63 |
--------------------------------------------------------------------------------
/app/components/common/Button.tsx:
--------------------------------------------------------------------------------
1 | import { ButtonHTMLAttributes, ReactNode } from "react";
2 | import { ButtonSize, HtmlType } from "../config/components/button";
3 | import { cn } from "../config/utils/cn";
4 | import { Variant } from "../config/constants/general";
5 |
6 | const Sizes: Record = {
7 | [ButtonSize.xs]: "py-1 px-3 text-xs font-semibold h-6",
8 | [ButtonSize.sm]: "py-1.5 px-4 text-sm font-semibold h-8",
9 | [ButtonSize.base]: "py-2 px-8 text-sm font-semibold h-10",
10 | [ButtonSize.lg]: "py-3 px-6 text-base font-semibold h-12",
11 | [ButtonSize.xl]: "py-3 px-6 text-lg font-semibold h-14",
12 | };
13 |
14 | const Variants: Record> = {
15 | [Variant.primary]: [
16 | "rounded-full",
17 | "bg-gradient-to-rb from-primary-600 via-secondary-500 to-white text-cWhite",
18 | "hover:text-cBlack hover:to-100%",
19 | "buttonBgTransition",
20 | ],
21 | [Variant.secondary]: [
22 | "text-cWhite",
23 | "bg-gradient-to-rb bg-gradient-to-rb from-black via-[#331e22] to-[#2c2130]",
24 | "from-100% hover:from-0%",
25 | "rounded-full",
26 | "buttonBgTransition",
27 | ],
28 | [Variant.ghost]: [
29 | "bg-black rounded-full relative ",
30 | "before:absolute before:inset-0 before:-z-1",
31 | 'before:content-[""]',
32 | "before:bg-gradient-to-rb before:from-primary-600/10 before:to-secondary-500/10",
33 | "before:opacity-0 before:hover:opacity-100",
34 | "before:rounded-full",
35 | "before:transition-opacity before:duration-300",
36 | ],
37 | [Variant.twitch]: [
38 | "rounded-full",
39 | "bg-gradient-to-rb from-[#4b2a88] via-[#7b4dda] to-[#2e195c] text-cWhite",
40 | "hover:to-100%",
41 | "buttonBgTransition",
42 | ],
43 | };
44 |
45 | interface ButtonProps extends ButtonHTMLAttributes {
46 | /**
47 | * Text inside the button.
48 | */
49 | children: ReactNode | Array | string;
50 |
51 | /**
52 | * Specify an optional className to be added to the component
53 | */
54 | className?: string;
55 |
56 | /**
57 | * Optional size (e.g., 'sm', 'md'), affects padding/font size.
58 | */
59 | size?: ButtonSize;
60 |
61 | /**
62 | * Style Variant (e.g., 'primary', 'secondary'), defines appearance.
63 | */
64 | variant?: Variant;
65 |
66 | /**
67 | * If true, disables user interaction.
68 | */
69 | isDisabled?: boolean;
70 |
71 | /**
72 | * If true, button width extends to 100%.
73 | */
74 | isFullWidth?: boolean;
75 |
76 | /**
77 | * HTML button type attribute ('button', 'submit', etc.).
78 | */
79 | htmlType?: HtmlType;
80 |
81 | /**
82 | * If true, adds a gradient border.
83 | */
84 | hasBorder?: boolean;
85 |
86 | /**
87 | * Optional className to be added to the inner button element.
88 | */
89 | innerClassName?: string;
90 |
91 | /**
92 | * Function to call on button click.
93 | */
94 | onClick?: (event: React.MouseEvent) => void;
95 | }
96 |
97 | export const Button = ({
98 | children,
99 | className,
100 | onClick = () => {},
101 | size = ButtonSize.base,
102 | variant = Variant.primary,
103 | isDisabled = false,
104 | hasBorder = false,
105 | innerClassName,
106 | htmlType = HtmlType.button,
107 | isFullWidth = false,
108 | ...restOfProps
109 | }: ButtonProps) => {
110 | const classes = {
111 | container: cn(
112 | "relative z-1",
113 | "disabled:opacity-30 disabled:pointer-events-none",
114 | "transition-all duration-300",
115 | Sizes[size],
116 | ...(!hasBorder ? Variants[variant] : []),
117 | {
118 | "w-full": isFullWidth,
119 | "h-fit w-fit rounded-full bg-gradient-to-rb from-primary-600 to-secondary-500 p-px buttonBgTransitionReset":
120 | hasBorder,
121 | },
122 | className
123 | ),
124 | innerContainer: cn(
125 | Variants[variant],
126 | Sizes[size],
127 | "inline-block transition-all duration-300 ease-in-out w-full h-full",
128 | innerClassName
129 | ),
130 | };
131 |
132 | return (
133 |
146 | );
147 | };
148 |
--------------------------------------------------------------------------------
/app/components/common/Contributors.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { cn } from '../config/utils/cn';
4 | import { type Contributor, useContributors } from './hooks';
5 |
6 | export const Contributors = () => {
7 | const { contributors, isLoading } = useContributors();
8 | const columns = contributors.length <= 10 ? contributors.length : 10
9 | const contributorsClasses = 'overflow-x-auto mt-5'
10 |
11 | const renderContributors = (contributorList: Contributor[]) => {
12 | return contributorList.map((contributor, index) => (
13 |
19 | {isLoading ? (
20 |
21 | ) : (
22 | // eslint-disable-next-line @next/next/no-img-element
23 |
24 | )}
25 |
26 | ));
27 | };
28 |
29 | return (
30 |
31 | Contribuidores del desarrollo
32 |
33 | {contributors.length > 0 && (
34 |
38 | {renderContributors(contributors)}
39 |
40 | )}
41 |
42 |
43 | );
44 | };
45 |
--------------------------------------------------------------------------------
/app/components/common/Countdown.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useLayoutEffect, useState } from "react";
2 | import { cn } from "../utils";
3 | import { useTime } from "@/hooks/useTimezone";
4 |
5 | interface CountDownProps {
6 | className?: string;
7 | }
8 |
9 | export const Countdown = ({ className }: CountDownProps) => {
10 | const time = [
11 | { key: "days", label: "Días" },
12 | { key: "hours", label: "Horas" },
13 | { key: "minutes", label: "Minutos" },
14 | { key: "seconds", label: "Segundos" },
15 | ];
16 |
17 | const startTime = useTime({ timestamp: 1726855200000});
18 | const [timeLeft, setTimeLeft] = useState({
19 | days: 0,
20 | hours: 0,
21 | minutes: 0,
22 | seconds: 0,
23 | });
24 |
25 | const updateCountdown = () => {
26 | // Establecer la fecha y hora de finalización (20 Septiembre 2024, 20:00 en UTC+2)
27 | const countdownDate = new Date("2024-09-20T18:00:00Z"); // UTC+2 es dos horas menos que UTC
28 |
29 | const now = new Date();
30 |
31 | const timeDifference = countdownDate.getTime() - now.getTime();
32 |
33 | const days = Math.floor(timeDifference / (1000 * 60 * 60 * 24));
34 | const hours = Math.floor((timeDifference / (1000 * 60 * 60)) % 24);
35 | const minutes = Math.floor((timeDifference / (1000 * 60)) % 60);
36 | const seconds = Math.floor((timeDifference / 1000) % 60);
37 |
38 | setTimeLeft({ days, hours, minutes, seconds });
39 | };
40 |
41 | useLayoutEffect(() => {
42 | updateCountdown();
43 | }, []);
44 |
45 | useEffect(() => {
46 | const intervalId = setInterval(() => {
47 | updateCountdown();
48 | }, 1000);
49 |
50 | return () => clearInterval(intervalId); // Limpiar el intervalo al desmontar el componente
51 | }, []);
52 |
53 | const formatNumber = (number: number) => {
54 | return number < 10 ? `0${number}` : number;
55 | };
56 |
57 | return (
58 |
59 |
60 | {time.map(({ key, label }, index) => (
61 |
62 |
63 | {formatNumber(timeLeft[key as keyof typeof timeLeft])}
64 |
65 | {label}
66 |
67 | ))}
68 |
69 |
20 de Septiembre de 2024 a las {startTime}
70 |
71 | );
72 | };
73 |
--------------------------------------------------------------------------------
/app/components/common/Divider.tsx:
--------------------------------------------------------------------------------
1 | export const Divider = () => {
2 | return (
3 |
4 | );
5 | };
6 |
--------------------------------------------------------------------------------
/app/components/common/Link.tsx:
--------------------------------------------------------------------------------
1 | import { ButtonHTMLAttributes, ReactNode } from "react";
2 | import { ButtonSize, HtmlType } from "../config/components/button";
3 | import { cn } from "../config/utils/cn";
4 | import { Variant } from "../config/constants/general";
5 |
6 | const Sizes: Record = {
7 | [ButtonSize.xs]: "py-1 px-3 text-xs font-semibold h-6",
8 | [ButtonSize.sm]: "py-1.5 px-4 text-sm font-semibold h-8",
9 | [ButtonSize.base]: "py-2 px-8 text-sm font-semibold h-10",
10 | [ButtonSize.lg]: "py-3 px-6 text-base font-semibold h-12",
11 | [ButtonSize.xl]: "py-3 px-6 text-lg font-semibold h-14",
12 | };
13 |
14 | const Variants: Record> = {
15 | [Variant.primary]: [
16 | "rounded-full",
17 | "bg-gradient-to-rb from-primary-600 via-secondary-500 to-white text-cWhite",
18 | "hover:text-cBlack hover:to-100%",
19 | "buttonBgTransition",
20 | ],
21 | [Variant.secondary]: [
22 | "text-cWhite",
23 | "bg-gradient-to-rb bg-gradient-to-rb from-black via-[#331e22] to-[#2c2130]",
24 | "from-100% hover:from-0%",
25 | "rounded-full",
26 | "buttonBgTransition",
27 | ],
28 | [Variant.ghost]: [
29 | "bg-black rounded-full relative ",
30 | "before:absolute before:inset-0 before:-z-1",
31 | 'before:content-[""]',
32 | "before:bg-gradient-to-rb before:from-primary-600/10 before:to-secondary-500/10",
33 | "before:opacity-0 before:hover:opacity-100",
34 | "before:rounded-full",
35 | "before:transition-opacity before:duration-300",
36 | ],
37 | [Variant.twitch]: [
38 | "rounded-full",
39 | "bg-gradient-to-rb from-[#4b2a88] via-[#7b4dda] to-[#2e195c] text-cWhite",
40 | "hover:to-100%",
41 | "buttonBgTransition",
42 | ],
43 | };
44 |
45 | interface LinkProps extends ButtonHTMLAttributes {
46 | /**
47 | * Text inside the button.
48 | */
49 | children: ReactNode | Array | string;
50 |
51 | /**
52 | * Specify an optional className to be added to the component
53 | */
54 | className?: string;
55 |
56 | /**
57 | * Optional size (e.g., 'sm', 'md'), affects padding/font size.
58 | */
59 | size?: ButtonSize;
60 |
61 | /**
62 | * Style Variant (e.g., 'primary', 'secondary'), defines appearance.
63 | */
64 | variant?: Variant;
65 |
66 | /**
67 | * If true, button width extends to 100%.
68 | */
69 | isFullWidth?: boolean;
70 |
71 | /**
72 | * HTML button type attribute ('button', 'submit', etc.).
73 | */
74 | htmlType?: HtmlType;
75 |
76 | /**
77 | * If true, adds a gradient border.
78 | */
79 | hasBorder?: boolean;
80 |
81 | /**
82 | * Optional className to be added to the inner button element.
83 | */
84 | innerClassName?: string;
85 |
86 | /**
87 | * Href
88 | */
89 | href: string;
90 | }
91 |
92 | export const Link = ({
93 | children,
94 | className,
95 | href,
96 | size = ButtonSize.base,
97 | variant = Variant.primary,
98 | hasBorder = false,
99 | innerClassName,
100 | htmlType = HtmlType.button,
101 | isFullWidth = false,
102 | ...restOfProps
103 | }: LinkProps) => {
104 | const classes = {
105 | container: cn(
106 | "relative z-1",
107 | "disabled:opacity-30 disabled:pointer-events-none",
108 | "transition-all duration-300",
109 | Sizes[size],
110 | ...(!hasBorder ? Variants[variant] : []),
111 | {
112 | "w-full": isFullWidth,
113 | "h-fit w-fit rounded-full bg-gradient-to-rb from-primary-600 to-secondary-500 p-px buttonBgTransitionReset":
114 | hasBorder,
115 | },
116 | className
117 | ),
118 | innerContainer: cn(
119 | Variants[variant],
120 | Sizes[size],
121 | "inline-block transition-all duration-300 ease-in-out w-full h-full",
122 | innerClassName
123 | ),
124 | };
125 |
126 | return (
127 |
133 | {hasBorder ? (
134 | {children}
135 | ) : (
136 | children
137 | )}
138 |
139 | );
140 | };
141 |
--------------------------------------------------------------------------------
/app/components/common/Summary.tsx:
--------------------------------------------------------------------------------
1 | import { useRef, useState } from "react";
2 | import { ChevronRight } from "lucide-react";
3 | import { cn } from "../utils";
4 |
5 | interface Props {
6 | className?: string;
7 | title: string;
8 | }
9 |
10 | export const Summary = ({
11 | title,
12 | className,
13 | children,
14 | }: React.PropsWithChildren) => {
15 | const [isOpen, setIsOpen] = useState(false);
16 | const contentRef = useRef(null);
17 |
18 | const toggleOpen = () => {
19 | setIsOpen(!isOpen);
20 | };
21 |
22 | return (
23 |
24 |
28 | {title}
29 |
30 |
31 |
41 | {children}
42 |
43 |
44 | );
45 | };
--------------------------------------------------------------------------------
/app/components/common/Table.tsx:
--------------------------------------------------------------------------------
1 | import { Tag } from "./Tag";
2 | import { useTime } from "@/hooks/useTimezone";
3 |
4 | type TableProps = {
5 | title: string;
6 | authors: string;
7 | timestamp: number;
8 | };
9 |
10 | export const Table = ({ title, authors, timestamp }: TableProps) => {
11 | const datetime = useTime({ timestamp });
12 | return (
13 |
14 |
23 |
24 | );
25 | };
26 |
--------------------------------------------------------------------------------
/app/components/common/Tag.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from "react";
2 | import { cn } from "../utils";
3 |
4 | type TagProps = {
5 | children: ReactNode;
6 | className?: string;
7 | };
8 |
9 | export const Tag = ({ children, className }: TagProps) => {
10 | return (
11 |
12 | {children}
13 |
14 | );
15 | };
16 |
--------------------------------------------------------------------------------
/app/components/common/Talk.tsx:
--------------------------------------------------------------------------------
1 | import Image from "next/image";
2 | import { Tag } from "./Tag";
3 | import { useTime } from "@/hooks/useTimezone";
4 |
5 | type TalkProps = {
6 | title: string;
7 | author: string;
8 | timestamp: number;
9 | img: string;
10 | alt: string;
11 | };
12 |
13 | export const Talk = ({ title, author, timestamp, img, alt }: TalkProps) => {
14 |
15 | const datetime = useTime({ timestamp });
16 | return (
17 |
18 |
25 |
33 | {datetime}h
34 |
35 | );
36 | };
37 |
--------------------------------------------------------------------------------
/app/components/common/hooks/index.ts:
--------------------------------------------------------------------------------
1 | export * from './useContributors'
2 |
--------------------------------------------------------------------------------
/app/components/common/hooks/useContributors.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 |
3 | export interface Contributor {
4 | username: string;
5 | avatarUrl: string;
6 | }
7 |
8 | export const useContributors = () => {
9 | const [error, setError] = useState(null);
10 | const [isLoading, setIsLoading] = useState(true);
11 | const [contributors, setContributors] = useState>([]);
12 |
13 | useEffect(() => {
14 | const fetchContributors = async () => {
15 | try {
16 | const response = await fetch('https://api.github.com/repos/Afordin/aforshow-2024/contributors');
17 | if (!response.ok) {
18 | throw new Error(`HTTP error! status: ${response.status}`);
19 | }
20 | const data = await response.json();
21 |
22 | if (!Array.isArray(data) || data.some((item) => typeof item.login !== 'string' || typeof item.avatar_url !== 'string')) {
23 | throw new Error('Invalid data format');
24 | }
25 |
26 | const contributorsData: Array = data
27 | .map(({ login, avatar_url, contributions }) => ({
28 | username: login,
29 | avatarUrl: avatar_url,
30 | contributions
31 | }))
32 | .sort((a, b) => b.contributions - a.contributions);
33 |
34 | setContributors(contributorsData);
35 | } catch (error) {
36 | if (error instanceof Error) {
37 | setError(`Error fetching contributors: ${error.message}`);
38 | } else {
39 | setError('An unexpected error occurred');
40 | }
41 | } finally {
42 | setIsLoading(false);
43 | }
44 | };
45 |
46 | fetchContributors();
47 | }, []);
48 |
49 | return { contributors, isLoading, error };
50 | };
51 |
--------------------------------------------------------------------------------
/app/components/config/components/button.ts:
--------------------------------------------------------------------------------
1 | export enum ButtonSize {
2 | xs = "xs",
3 | sm = "sm",
4 | base = "base",
5 | lg = "lg",
6 | xl = "xl",
7 | }
8 |
9 | export enum HtmlType {
10 | button = "button",
11 | reset = "reset",
12 | submit = "submit",
13 | }
14 |
--------------------------------------------------------------------------------
/app/components/config/constants/general.ts:
--------------------------------------------------------------------------------
1 | export enum Variant {
2 | primary = "primary",
3 | secondary = "secondary",
4 | ghost = "ghost",
5 | twitch = "twitch",
6 | }
7 |
--------------------------------------------------------------------------------
/app/components/config/utils/cn.ts:
--------------------------------------------------------------------------------
1 | import { ClassValue, clsx } from "clsx";
2 | import { twMerge } from "tailwind-merge";
3 |
4 | /**
5 | * Combines and merges Tailwind/UNO CSS classes using twMerge and clsx utility functions.
6 | * twMerge is used to handle conflicts between classes effectively.
7 | *
8 | * @param {...ClassValue} inputs - An array of class values to be combined and merged.
9 | * @returns {string} - The merged and combined class names as a string.
10 | */
11 | export const cn = (...inputs: ClassValue[]) => {
12 | return twMerge(clsx(inputs));
13 | };
14 |
15 | /**
16 | * Source:
17 | * Tailwind merge: https://github.com/dcastil/tailwind-merge/tree/v1.14.0
18 | */
19 |
--------------------------------------------------------------------------------
/app/components/icons/Logo.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import type { SVGProps } from "react";
3 |
4 | const Logo = (props: SVGProps) => (
5 |
31 | );
32 | export default Logo;
33 |
--------------------------------------------------------------------------------
/app/components/logos.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import type { SVGProps } from "react";
3 |
4 | export const Discord = (props: SVGProps) => (
5 |
16 | );
17 |
18 | export const Github = (props: SVGProps) => (
19 |
30 | );
31 |
32 | export const Instagram = (props: SVGProps) => (
33 |
47 | );
48 |
49 | export const X = (props: SVGProps) => (
50 |
63 | );
64 |
65 | export const Twitch = (props: SVGProps) => (
66 |
90 | );
91 |
--------------------------------------------------------------------------------
/app/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { cva, type VariantProps } from "class-variance-authority";
3 | import { cn } from "../utils";
4 |
5 | const buttonVariants = cva(
6 | "inline-flex items-center justify-center whitespace-nowrap rounded-full text-sm font-medium focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
7 | {
8 | variants: {
9 | variant: {
10 | default: [
11 | "bg-gradient-to-br from-caPrimary-500 via-caSecondary-500 to-white text-caWhite",
12 | "hover:text-caBlack",
13 | "buttonBgTransition",
14 | ],
15 | secondary: [
16 | "bg-gradient-to-br from-black via-[#331e22] to-[#2c2130] text-white",
17 | "from-100% hover:from-0%",
18 | "buttonBgTransition",
19 | ],
20 | twitch: [
21 | "bg-gradient-to-br from-[#4b2a88] via-[#7b4dda] to-[#2e195c] text-white",
22 | "buttonBgTransition",
23 | ]
24 | },
25 | size: {
26 | default: "py-2 px-8 text-sm h-10",
27 | xs: "py-1 px-3 text-xs h-6",
28 | sm: "py-1.5 px-4 text-sm h-8",
29 | lg: "py-3 px-6 text-base h-12",
30 | xl: "py-3 px-6 text-lg h-14",
31 | },
32 | },
33 | defaultVariants: {
34 | variant: "default",
35 | size: "default",
36 | },
37 | }
38 | );
39 |
40 | /* Extiende el button html y las variantes de cva (size, variant) */
41 | export interface ButtonProps
42 | extends React.ButtonHTMLAttributes,
43 | VariantProps {}
44 |
45 | const Button = ({ className, variant, size, ...props }: ButtonProps) => {
46 | return (
47 |
51 | );
52 | };
53 |
54 | export { Button, buttonVariants };
55 |
--------------------------------------------------------------------------------
/app/components/utils/index.ts:
--------------------------------------------------------------------------------
1 | import { type ClassValue, clsx } from "clsx"
2 | import { twMerge } from "tailwind-merge"
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs))
6 | }
7 |
--------------------------------------------------------------------------------
/app/components/utils/uploadTicket.ts:
--------------------------------------------------------------------------------
1 | import { apiClient } from "@/utils/supabase/client";
2 | import { toBlob } from 'html-to-image';
3 |
4 |
5 | export const uploadTicket = async (userId: string, element: HTMLDivElement): Promise => {
6 |
7 | try {
8 | const img = await toBlob(element);
9 | if (!img) {
10 | throw new Error('Could not create image');
11 | }
12 |
13 | const { data, error } = await apiClient.storage.from('aforshow').upload(`public/${userId}.png`, img, {
14 | cacheControl: '3600',
15 | upsert: true
16 | });
17 |
18 | if (error && error['statusCode'] != '403') {
19 | throw new Error(error.message);
20 | }
21 | if (!data) return;
22 |
23 | } catch (e) {
24 | console.error('Could not upload image:', e);
25 | }
26 | };
27 |
--------------------------------------------------------------------------------
/app/cursors-context.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useState, useEffect, useContext, createContext, useRef, Dispatch, SetStateAction } from "react";
4 | import usePartySocket from "partysocket/react";
5 | import twemoji from "twemoji";
6 |
7 | type Position = {
8 | x: number;
9 | y: number;
10 | pointer: "mouse";
11 | };
12 |
13 | export type Cursor = Position & {
14 | id: string;
15 | country: string | null;
16 | flag: string;
17 | flagUrl: string;
18 | lastUpdate: number;
19 | };
20 |
21 | type OtherCursorsMap = {
22 | [id: string]: Cursor;
23 | };
24 | interface CursorsContextType {
25 | cursors: Array
26 | disabled: boolean | null
27 | setDisabled: Dispatch> | null
28 | }
29 |
30 | export const CursorsContext = createContext({
31 | cursors: [],
32 | disabled: null,
33 | setDisabled: null,
34 | });
35 |
36 | export function useCursors() {
37 | return useContext(CursorsContext);
38 | }
39 |
40 | function getFlagEmoji(countryCode: string) {
41 | const codePoints = countryCode
42 | .toUpperCase()
43 | .split("")
44 | .map((char) => 127397 + char.charCodeAt(0));
45 | return String.fromCodePoint(...codePoints);
46 | }
47 |
48 | export default function CursorsContextProvider(props: {
49 | host: string;
50 | room: string;
51 | children: React.ReactNode;
52 | }) {
53 | const [disabled, setDisabled] = useState(false);
54 | const [dimensions, setDimensions] = useState<{
55 | width: number;
56 | height: number;
57 | }>({ width: 0, height: 0 });
58 |
59 | const socket = usePartySocket({
60 | host: props.host,
61 | room: props.room,
62 | });
63 | const [others, setOthers] = useState({});
64 |
65 | const cursors: Cursor[] = Object.entries(others).map(([id, cursor]): Cursor => {
66 | const flag = cursor.country ? getFlagEmoji(cursor.country) : "";
67 |
68 | const flagAsImage = twemoji.parse(flag,
69 | {
70 | base: 'https://cdn.jsdelivr.net/gh/twitter/twemoji@14.0.2/assets/'
71 | })
72 |
73 | const flagUrl = flagAsImage.match(/src="([^"]+)"/)?.[1] || "";
74 |
75 | return {
76 | ...cursor,
77 | id,
78 | flag: flagAsImage,
79 | flagUrl: flagUrl,
80 | }
81 | });
82 |
83 | useEffect(() => {
84 | if (socket) {
85 | const onMessage = (evt: WebSocketEventMap["message"]) => {
86 | const msg = JSON.parse(evt.data as string);
87 | switch (msg.type) {
88 | case "sync":
89 | const newOthers = { ...msg.cursors };
90 | setOthers(newOthers);
91 | break;
92 | case "update":
93 | const other = {
94 | x: msg.x,
95 | y: msg.y,
96 | country: msg.country,
97 | lastUpdate: msg.lastUpdate,
98 | pointer: msg.pointer,
99 | };
100 | setOthers((others) => ({ ...others, [msg.id]: other }));
101 | break;
102 | case "remove":
103 | setOthers((others) => {
104 | const newOthers = { ...others };
105 | delete newOthers[msg.id];
106 | return newOthers;
107 | });
108 | break;
109 | }
110 | };
111 | socket.addEventListener("message", onMessage);
112 |
113 | return () => {
114 | // @ts-ignore
115 | socket.removeEventListener("message", onMessage);
116 | };
117 | }
118 | }, [socket]);
119 |
120 | // Track window dimensions
121 | useEffect(() => {
122 | const onResize = () => {
123 | setDimensions({ width: window.innerWidth, height: window.innerHeight });
124 | };
125 | window.addEventListener("resize", onResize);
126 | onResize();
127 | return () => {
128 | window.removeEventListener("resize", onResize);
129 | };
130 | }, []);
131 |
132 | // Always track the mouse position
133 | useEffect(() => {
134 | const onMouseMove = (e: MouseEvent) => {
135 | if (!socket) return;
136 | if (!dimensions.width || !dimensions.height) return;
137 | const position = {
138 | x: e.clientX / dimensions.width,
139 | y: e.clientY / dimensions.height,
140 | pointer: "mouse",
141 | } as Position;
142 | socket.send(JSON.stringify(position));
143 | };
144 | window.addEventListener("mousemove", onMouseMove);
145 |
146 | return () => {
147 | window.removeEventListener("mousemove", onMouseMove);
148 | };
149 | }, [socket, dimensions]);
150 |
151 | return (
152 |
153 | {props.children}
154 |
155 | );
156 | }
157 |
--------------------------------------------------------------------------------
/app/cursors.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useEffect, useState } from "react";
4 | import { useCursors } from "./cursors-context";
5 | import OtherCursor from "./other-cursor";
6 |
7 | export default function Cursors() {
8 | const { cursors, disabled } = useCursors();
9 | const [windowDimensions, setWindowDimensions] = useState({
10 | width: 0,
11 | height: 0,
12 | });
13 |
14 | useEffect(() => {
15 | const onResize = () => {
16 | setWindowDimensions({
17 | width: window.innerWidth,
18 | height: window.innerHeight,
19 | });
20 | };
21 | window.addEventListener("resize", onResize);
22 | onResize();
23 | return () => {
24 | window.removeEventListener("resize", onResize);
25 | };
26 | }, []);
27 |
28 | const cursorsSliced = disabled ? [] : cursors.slice(-10);
29 |
30 | return (
31 |
32 | {cursorsSliced.map((cursor) => (
33 |
38 | ))}
39 |
40 | );
41 | }
42 |
--------------------------------------------------------------------------------
/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | html {
6 | background-color: #060606;
7 | scroll-behavior: smooth;
8 | }
9 |
10 | @layer components {
11 | .buttonBgTransition {
12 | @apply bg-pos-0 hover:bg-pos-100 bg-size-200 transition-all duration-300;
13 | }
14 |
15 | /* Contributors its a component, not an utility */
16 |
17 | .contributors {
18 | /* NOTE: The contributor-count should increase as the number of contributors increases */
19 | --contributor-count: 17;
20 | --contributor-size: 3.125rem;
21 | --column-size: calc(var(--contributor-size) / 1.5);
22 |
23 | display: grid;
24 | justify-content: center;
25 | grid-template-columns: repeat(var(--contributor-count), var(--column-size));
26 | transition: 500ms;
27 | transition-delay: 500ms;
28 |
29 | padding-inline: 2rem;
30 | padding-block: 2rem;
31 |
32 | /* Hide scrollbar */
33 | -ms-overflow-style: none;
34 | scrollbar-width: none;
35 | }
36 |
37 | .contributors:hover {
38 | --column-size: calc(var(--contributor-size) * 1);
39 | transition-delay: 0ms;
40 | }
41 |
42 | .contributors::-webkit-scrollbar {
43 | display: none;
44 | }
45 |
46 | .contributor {
47 | overflow: hidden;
48 | border-radius: 100svw;
49 | transition: scale 500ms;
50 | border: 0.125rem solid white;
51 | width: var(--contributor-size);
52 | box-shadow: 0.25rem 0.25rem 0.5rem hsl(0 0% 0% / 0.2);
53 | }
54 |
55 | .contributor:hover {
56 | scale: 1.8;
57 | z-index: 2;
58 | }
59 |
60 | @supports selector(:has(+ *)) {
61 | .contributor:hover + .contributor,
62 | .contributor:has(+ .contributor:hover) {
63 | scale: 1.1;
64 | }
65 | }
66 |
67 | @media (max-width: 480px) {
68 | .contributors {
69 | display: flex;
70 | flex-wrap: wrap;
71 | column-gap: 0.5rem;
72 | }
73 |
74 | @supports selector(:has(+ *)) {
75 | .contributor:hover,
76 | .contributor:hover + .contributor,
77 | .contributor:has(+ .contributor:hover) {
78 | scale: 1;
79 | }
80 | }
81 | }
82 | }
83 |
84 | .bg-pattern::before {
85 | content: "";
86 | position: absolute;
87 | width: 200%;
88 | height: 150%;
89 | top: -20%;
90 | zoom: 2;
91 | left: -50%;
92 | z-index: -1;
93 | transform: rotate(9deg);
94 | background-image: url("data:image/svg+xml,%3Csvg width='100' height='20' viewBox='0 0 100 20' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M21.184 20c.357-.13.72-.264 1.088-.402l1.768-.661C33.64 15.347 39.647 14 50 14c10.271 0 15.362 1.222 24.629 4.928.955.383 1.869.74 2.75 1.072h6.225c-2.51-.73-5.139-1.691-8.233-2.928C65.888 13.278 60.562 12 50 12c-10.626 0-16.855 1.397-26.66 5.063l-1.767.662c-2.475.923-4.66 1.674-6.724 2.275h6.335zm0-20C13.258 2.892 8.077 4 0 4V2c5.744 0 9.951-.574 14.85-2h6.334zM77.38 0C85.239 2.966 90.502 4 100 4V2c-6.842 0-11.386-.542-16.396-2h-6.225zM0 14c8.44 0 13.718-1.21 22.272-4.402l1.768-.661C33.64 5.347 39.647 4 50 4c10.271 0 15.362 1.222 24.629 4.928C84.112 12.722 89.438 14 100 14v-2c-10.271 0-15.362-1.222-24.629-4.928C65.888 3.278 60.562 2 50 2 39.374 2 33.145 3.397 23.34 7.063l-1.767.662C13.223 10.84 8.163 12 0 12v2z' fill='%234A4A4A' fill-opacity='0.2' fill-rule='evenodd'/%3E%3C/svg%3E");
95 | mask: linear-gradient(
96 | 172deg,
97 | rgba(0, 0, 0, 1),
98 | rgba(0, 0, 0, 1),
99 | rgba(0, 0, 0, 0),
100 | rgba(0, 0, 0, 0)
101 | );
102 | mask-composite: exclude;
103 | }
104 |
105 | .gradiant-tag::before {
106 | content: "";
107 | position: absolute;
108 | inset: 0;
109 | border-radius: 99999px;
110 | border: 2px solid transparent;
111 | background: linear-gradient(135deg, #fc1c37, #ad40e1) border-box;
112 | mask: linear-gradient(#fff 0 0) padding-box, linear-gradient(#fff 0 0);
113 | mask-composite: exclude;
114 | }
115 |
116 | /************************************************/
117 | /* LOGO ANIMATION */
118 | /************************************************/
119 |
120 | #logo {
121 | fill-opacity: 0;
122 | stroke-dasharray: 1300;
123 | stroke-dashoffset: 1300;
124 | animation: dash 2s ease-in forwards;
125 | }
126 |
127 | @keyframes dash {
128 | 70% {
129 | fill-opacity: 0;
130 | }
131 |
132 | 100% {
133 | fill-opacity: 100%;
134 | stroke-dashoffset: 0;
135 | stroke-width: 1;
136 | }
137 | }
138 |
139 | /************************************************/
140 | /* TICKET */
141 | /************************************************/
142 |
143 | .ticket-bg {
144 | background-repeat: repeat;
145 | background-size: cover;
146 | position: relative;
147 | background-color: #060606;
148 | }
149 |
150 | .ticket-bg:after {
151 | position: absolute;
152 | content: '';
153 | top: 0;
154 | z-index: 1;
155 | left: 0;
156 | height: 100%;
157 | width: 100%;
158 | background-image: url(../public/imgs/ticket/bg.png);
159 | opacity: 1%;
160 | }
161 |
162 | .atropos-shadow {
163 | background: #c138b830 !important ;
164 | }
165 |
166 | :root {
167 | --ease-in-quad: cubic-bezier(0.55, 0.085, 0.68, 0.53);
168 | --ease-in-cubic: cubic-bezier(0.55, 0.055, 0.675, 0.19);
169 | --ease-in-quart: cubic-bezier(0.895, 0.03, 0.685, 0.22);
170 | --ease-in-quint: cubic-bezier(0.755, 0.05, 0.855, 0.06);
171 | --ease-in-expo: cubic-bezier(0.95, 0.05, 0.795, 0.035);
172 | --ease-in-circ: cubic-bezier(0.6, 0.04, 0.98, 0.335);
173 |
174 | --ease-out-quad: cubic-bezier(0.25, 0.46, 0.45, 0.94);
175 | --ease-out-cubic: cubic-bezier(0.215, 0.61, 0.355, 1);
176 | --ease-out-quart: cubic-bezier(0.165, 0.84, 0.44, 1);
177 | --ease-out-quint: cubic-bezier(0.23, 1, 0.32, 1);
178 | --ease-out-expo: cubic-bezier(0.19, 1, 0.22, 1);
179 | --ease-out-circ: cubic-bezier(0.075, 0.82, 0.165, 1);
180 |
181 | --ease-in-out-quad: cubic-bezier(0.455, 0.03, 0.515, 0.955);
182 | --ease-in-out-cubic: cubic-bezier(0.645, 0.045, 0.355, 1);
183 | --ease-in-out-quart: cubic-bezier(0.77, 0, 0.175, 1);
184 | --ease-in-out-quint: cubic-bezier(0.86, 0, 0.07, 1);
185 | --ease-in-out-expo: cubic-bezier(1, 0, 0, 1);
186 | --ease-in-out-circ: cubic-bezier(0.785, 0.135, 0.15, 0.86);
187 | }
188 |
189 | /************************************************/
190 | /* Sponsors Marquee ANIMATION */
191 | /************************************************/
192 |
193 | .marquee-container {
194 | overflow: hidden;
195 | width: 100%;
196 | }
197 |
198 | .marquee {
199 | display: flex;
200 | width: max-content;
201 | animation: scroll var(--speed) linear infinite;
202 | }
203 |
204 | .marquee.left {
205 | animation-direction: normal;
206 | }
207 |
208 | .marquee.right {
209 | animation-direction: reverse;
210 | }
211 |
212 | .marquee-item {
213 | flex-shrink: 0;
214 | margin: 0 1rem;
215 | }
216 |
217 | @keyframes scroll {
218 | from {
219 | transform: translateX(0);
220 | }
221 | to {
222 | transform: translateX(-50%);
223 | }
224 | }
225 |
--------------------------------------------------------------------------------
/app/hooks/useAuth.ts:
--------------------------------------------------------------------------------
1 | import { apiClient } from "@/utils/supabase/client";
2 |
3 | export const useAuth = () => {
4 | function signInWithDiscord(redirectTo?: string) {
5 | apiClient.auth.signInWithOAuth({
6 | provider: "discord",
7 | options: {
8 | redirectTo: `${window.location.origin}${redirectTo ?? ''}`,
9 | }
10 | });
11 | }
12 |
13 | function signOut() {
14 | apiClient.auth
15 | .signOut()
16 | .then(() => {
17 | window.location.reload();
18 | })
19 | .catch((error) => {
20 | console.error("Error signing out:", error.message);
21 | });
22 | }
23 |
24 | return { signInWithDiscord, signOut };
25 | };
26 |
--------------------------------------------------------------------------------
/app/hooks/useOnClickOutside.ts:
--------------------------------------------------------------------------------
1 | import { RefObject, useEffect, useRef } from "react";
2 |
3 | type Event = MouseEvent | TouchEvent;
4 |
5 | type HandlerType = {
6 | event: Event;
7 | isSameTrigger: boolean;
8 | };
9 |
10 | export const useOnClickOutside = (
11 | ref: RefObject | HTMLElement | null,
12 | handler: (params: HandlerType) => void
13 | ) => {
14 | const trigger = useRef(null);
15 |
16 | useEffect(() => {
17 | if (!ref) return;
18 |
19 | const listener = (event: Event) => {
20 | const target = event.target as Node;
21 | const element = ref instanceof HTMLElement ? ref : ref.current;
22 |
23 | if (!element) return;
24 | if (!element || element.contains(target)) return;
25 |
26 | handler({
27 | event,
28 | isSameTrigger: Boolean(trigger.current?.contains(target)),
29 | });
30 | };
31 |
32 | document.addEventListener("mousedown", listener);
33 | document.addEventListener("touchstart", listener);
34 |
35 | return () => {
36 | document.removeEventListener("mousedown", listener);
37 | document.removeEventListener("touchstart", listener);
38 | };
39 | }, [trigger, ref, handler]);
40 |
41 | return trigger;
42 | };
43 |
--------------------------------------------------------------------------------
/app/hooks/useTimezone.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react"
2 |
3 | export const useTime = ({ timestamp }: { timestamp: number}): string | null => {
4 | const [time, setTime] = useState(null);
5 |
6 | useEffect(() => {
7 | if (!timestamp) return;
8 |
9 | const timeFormatConfig: Intl.DateTimeFormatOptions = {
10 | hour: '2-digit',
11 | minute: '2-digit',
12 | hour12: false,
13 | };
14 |
15 | const startAt = new Date(timestamp).toLocaleTimeString([], timeFormatConfig);
16 | setTime(startAt);
17 | }, [timestamp]);
18 |
19 | return time;
20 | };
21 |
22 | export const useTimezone = (): string | null => {
23 | const [timezone, setTimezone] = useState(null);
24 |
25 | useEffect(() => {
26 | const currentTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
27 | setTimezone(currentTimezone);
28 | }, []);
29 |
30 | return timezone;
31 | };
--------------------------------------------------------------------------------
/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import "./globals.css";
2 | import type { Metadata } from "next";
3 | import { DM_Sans } from "next/font/google";
4 | import { cn } from "./components/utils";
5 |
6 | const dmSans = DM_Sans({
7 | subsets: ["latin"],
8 | variable: "--font-inter",
9 | });
10 |
11 | export const metadata: Metadata = {
12 | title: "Aforshow",
13 | description: "A spanish programming event of the community making talks",
14 | };
15 |
16 | export default function RootLayout({
17 | children,
18 | }: {
19 | children: React.ReactNode;
20 | }) {
21 | return (
22 |
23 |
24 |
25 |
26 |
27 | {children}
28 |
29 |
30 | );
31 | }
32 |
--------------------------------------------------------------------------------
/app/middleware.ts:
--------------------------------------------------------------------------------
1 | import { type NextRequest } from 'next/server'
2 | import { updateSession } from '@/utils/supabase/middleware'
3 |
4 | export async function middleware(request: NextRequest) {
5 | return await updateSession(request)
6 | }
7 |
8 | export const config = {
9 | matcher: [
10 | /*
11 | * Match all request paths except for the ones starting with:
12 | * - _next/static (static files)
13 | * - _next/image (image optimization files)
14 | * - favicon.ico (favicon file)
15 | * Feel free to modify this pattern to include more paths.
16 | */
17 | '/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
18 | ],
19 | }
--------------------------------------------------------------------------------
/app/opengraph-image.tsx:
--------------------------------------------------------------------------------
1 | import { ImageResponse } from 'next/og'
2 | /* eslint-disable @next/next/no-img-element */
3 |
4 | export const size = {
5 | width: 1200,
6 | height: 580,
7 | }
8 | export const contentType = 'image/png'
9 |
10 |
11 | export default async function Image() {
12 |
13 | return new ImageResponse(
14 |
15 |

16 |
17 | , { ...size })
18 |
19 | }
20 |
--------------------------------------------------------------------------------
/app/other-cursor.tsx:
--------------------------------------------------------------------------------
1 | import { Cursor } from "./cursors-context";
2 |
3 | // NOTE
4 | // The pointer SVG is from https://github.com/daviddarnes/mac-cursors
5 | // The license is the Apple User Agreement
6 |
7 | export default function OtherCursor(props: {
8 | cursor: Cursor
9 | windowDimensions: { width: number; height: number };
10 | }) {
11 | const { cursor, windowDimensions } = props;
12 |
13 | const left = cursor.x * windowDimensions.width;
14 | const top = cursor.y * windowDimensions.height;
15 |
16 | return (
17 |
18 |
29 |
33 |
34 |
35 | );
36 | }
37 |
--------------------------------------------------------------------------------
/app/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import CursorsContextProvider from "./cursors-context";
3 | import { Contributors } from "./components/common/Contributors";
4 | import { Divider } from "./components/common/Divider";
5 | import { Schedule } from "./components/Schedule";
6 | import { FAQ } from "./components/FAQ";
7 | import { Footer } from "./components/Footer";
8 | import { TicketDownload } from "./components/TicketDownload";
9 | import Hero from "./components/Hero";
10 | import Cursors from "./cursors";
11 | import { useEffect } from "react";
12 | import { apiClient } from "./utils/supabase/client";
13 | import { useUserStore } from "./store/useUserStore";
14 |
15 | export default function Home({
16 | searchParams,
17 | }: {
18 | searchParams?: { [key: string]: string | string[] | undefined };
19 | }) {
20 | // when hosted in an iframe on the partykit website, don't render link to the site
21 | const room =
22 | typeof searchParams?.partyroom === "string"
23 | ? searchParams.partyroom
24 | : "aforshow-room";
25 |
26 | const host =
27 | typeof searchParams?.partyhost === "string"
28 | ? searchParams.partyhost
29 | : "aforshow-2024-party.jarrisondev.partykit.dev";
30 |
31 | const setUser = useUserStore((state) => state.setUser);
32 |
33 | useEffect(() => {
34 | apiClient.auth.onAuthStateChange((_event, session) => {
35 | if (!session?.user) {
36 | setUser(null);
37 | return;
38 | }
39 | const id = session.user.id;
40 | apiClient
41 | .from("profiles")
42 | .select("*")
43 | .eq("id", id)
44 | .then(({ data }) => {
45 | if (data?.[0]) setUser(data[0]);
46 | });
47 | });
48 | }, [setUser]);
49 |
50 | return (
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 | );
68 | }
69 |
--------------------------------------------------------------------------------
/app/store/useUserStore.ts:
--------------------------------------------------------------------------------
1 | import { create } from "zustand";
2 |
3 | export type User = {
4 | name: string;
5 | email: string;
6 | avatar_url: string;
7 | description: string;
8 | id: string;
9 | count: number;
10 | language: string;
11 |
12 | } | null | undefined;
13 | interface UserStore {
14 | user: User ;
15 | setUser: (user: User) => void;
16 | }
17 |
18 | export const useUserStore = create((set) => ({
19 | user: undefined,
20 | setUser: (user) => set({ user }),
21 | }));
22 |
--------------------------------------------------------------------------------
/app/utils/supabase/client.ts:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { createBrowserClient } from '@supabase/ssr'
4 |
5 | function createClient() {
6 | return createBrowserClient(
7 | process.env.NEXT_PUBLIC_PROJECT_URL!,
8 | process.env.NEXT_PUBLIC_API_KEY!
9 | )
10 | }
11 |
12 | export const apiClient = createClient()
--------------------------------------------------------------------------------
/app/utils/supabase/middleware.ts:
--------------------------------------------------------------------------------
1 | import { createServerClient } from '@supabase/ssr'
2 | import { NextResponse, type NextRequest } from 'next/server'
3 |
4 | export async function updateSession(request: NextRequest) {
5 | let supabaseResponse = NextResponse.next({
6 | request,
7 | })
8 |
9 | const supabase = createServerClient(
10 | process.env.NEXT_PUBLIC_PROJECT_URL!,
11 | process.env.NEXT_PUBLIC_API_KEY!,
12 | {
13 | cookies: {
14 | getAll() {
15 | return request.cookies.getAll()
16 | },
17 | setAll(cookiesToSet) {
18 | cookiesToSet.forEach(({ name, value, options }) => request.cookies.set(name, value))
19 | supabaseResponse = NextResponse.next({
20 | request,
21 | })
22 | cookiesToSet.forEach(({ name, value, options }) =>
23 | supabaseResponse.cookies.set(name, value, options)
24 | )
25 | },
26 | },
27 | }
28 | )
29 |
30 | // IMPORTANT: Avoid writing any logic between createServerClient and
31 | // supabase.auth.getUser(). A simple mistake could make it very hard to debug
32 | // issues with users being randomly logged out.
33 |
34 | const {
35 | data: { user },
36 | } = await supabase.auth.getUser()
37 |
38 | if (
39 | !user &&
40 | !request.nextUrl.pathname.startsWith('/login') &&
41 | !request.nextUrl.pathname.startsWith('/auth')
42 | ) {
43 | // no user, potentially respond by redirecting the user to the login page
44 | const url = request.nextUrl.clone()
45 | url.pathname = '/login'
46 | return NextResponse.redirect(url)
47 | }
48 |
49 | // IMPORTANT: You *must* return the supabaseResponse object as it is. If you're
50 | // creating a new response object with NextResponse.next() make sure to:
51 | // 1. Pass the request in it, like so:
52 | // const myNewResponse = NextResponse.next({ request })
53 | // 2. Copy over the cookies, like so:
54 | // myNewResponse.cookies.setAll(supabaseResponse.cookies.getAll())
55 | // 3. Change the myNewResponse object to fit your needs, but avoid changing
56 | // the cookies!
57 | // 4. Finally:
58 | // return myNewResponse
59 | // If this is not done, you may be causing the browser and server to go out
60 | // of sync and terminate the user's session prematurely!
61 |
62 | return supabaseResponse
63 | }
--------------------------------------------------------------------------------
/app/utils/supabase/server.ts:
--------------------------------------------------------------------------------
1 | import { createServerClient } from '@supabase/ssr'
2 | import { cookies } from 'next/headers'
3 |
4 | export function createClient() {
5 | const cookieStore = cookies()
6 |
7 | return createServerClient(
8 | process.env.NEXT_PUBLIC_PROJECT_URL!,
9 | process.env.NEXT_PUBLIC_API_KEY!,
10 | {
11 | cookies: {
12 | getAll() {
13 | return cookieStore.getAll()
14 | },
15 | setAll(cookiesToSet) {
16 | try {
17 | cookiesToSet.forEach(({ name, value, options }) =>
18 | cookieStore.set(name, value, options)
19 | )
20 | } catch {
21 | // The `setAll` method was called from a Server Component.
22 | // This can be ignored if you have middleware refreshing
23 | // user sessions.
24 | }
25 | },
26 | },
27 | }
28 | )
29 | }
--------------------------------------------------------------------------------
/next.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | images: {
4 | remotePatterns: [
5 | {
6 | protocol: "https",
7 | hostname: "cdn.discordapp.com",
8 | },
9 | {
10 | protocol: "https",
11 | hostname: "cdn.jsdelivr.net",
12 | },
13 | ],
14 | },
15 | };
16 |
17 | export default nextConfig;
18 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "aforshow-2024",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "@supabase/auth-ui-react": "^0.4.7",
13 | "@supabase/auth-ui-shared": "^0.1.8",
14 | "@supabase/ssr": "^0.4.0",
15 | "@supabase/supabase-js": "^2.45.1",
16 | "atropos": "^2.0.2",
17 | "class-variance-authority": "^0.7.0",
18 | "clsx": "^2.1.1",
19 | "html-to-image": "^1.11.11",
20 | "lucide-react": "^0.414.0",
21 | "next": "14.2.5",
22 | "partysocket": "1.0.1",
23 | "react": "^18",
24 | "react-dom": "^18",
25 | "tailwind-merge": "^2.4.0",
26 | "twemoji": "^14.0.2",
27 | "zustand": "^4.5.4"
28 | },
29 | "devDependencies": {
30 | "@types/node": "^20",
31 | "@types/react": "^18",
32 | "@types/react-dom": "^18",
33 | "eslint": "^8",
34 | "eslint-config-next": "14.2.5",
35 | "partykit": "0.0.107",
36 | "postcss": "^8",
37 | "tailwindcss": "^3.4.1",
38 | "typescript": "^5"
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/party/index.ts:
--------------------------------------------------------------------------------
1 | import type * as Party from "partykit/server";
2 |
3 | export default class Server implements Party.Server {
4 | constructor(readonly room: Party.Room) {}
5 |
6 | onConnect(conn: Party.Connection, ctx: Party.ConnectionContext) {
7 | // A websocket just connected!
8 | console.log(
9 | `Connected:
10 | id: ${conn.id}
11 | room: ${this.room.id}
12 | url: ${new URL(ctx.request.url).pathname}`
13 | );
14 |
15 | // let's send a message to the connection
16 | conn.send("hello from server");
17 | }
18 |
19 | onMessage(message: string, sender: Party.Connection) {
20 | // let's log the message
21 | console.log(`connection ${sender.id} sent message: ${message}`);
22 | // as well as broadcast it to all the other connections in the room...
23 | this.room.broadcast(
24 | `${sender.id}: ${message}`,
25 | // ...except for the connection it came from
26 | [sender.id]
27 | );
28 | }
29 | }
30 |
31 | Server satisfies Party.Worker;
32 |
--------------------------------------------------------------------------------
/party/server.ts:
--------------------------------------------------------------------------------
1 | import type * as Party from "partykit/server";
2 |
3 | type Cursor = {
4 | // replicating the default connection fields to avoid
5 | // having to do an extra deserializeAttachment
6 | id: string;
7 | uri: string;
8 |
9 | // country is set upon connection
10 | country: string | null;
11 |
12 | // cursor fields are only set on first message
13 | x?: number;
14 | y?: number;
15 | pointer?: "mouse" | "touch";
16 | lastUpdate?: number;
17 | };
18 |
19 | type UpdateMessage = {
20 | type: "update";
21 | id: string; // websocket.id
22 | } & Cursor;
23 |
24 | type SyncMessage = {
25 | type: "sync";
26 | cursors: { [id: string]: Cursor };
27 | };
28 |
29 | type RemoveMessage = {
30 | type: "remove";
31 | id: string; // websocket.id
32 | };
33 |
34 | type ConnectionWithCursor = Party.Connection & { cursor?: Cursor };
35 |
36 | // server.ts
37 | export default class CursorServer implements Party.Server {
38 | constructor(public party: Party.Party) {}
39 | options: Party.ServerOptions = {
40 | hibernate: true,
41 | };
42 |
43 | onConnect(
44 | websocket: Party.Connection,
45 | { request }: Party.ConnectionContext
46 | ): void | Promise {
47 | const country = request.cf?.country ?? null;
48 |
49 | // Stash the country in the websocket attachment
50 | websocket.serializeAttachment({
51 | ...websocket.deserializeAttachment(),
52 | country: country,
53 | });
54 |
55 | console.log("[connect]", this.party.id, websocket.id, country);
56 |
57 | // On connect, send a "sync" message to the new connection
58 | // Pull the cursor from all websocket attachments
59 | let cursors: { [id: string]: Cursor } = {};
60 | for (const ws of this.party.getConnections()) {
61 | const id = ws.id;
62 | let cursor =
63 | (ws as ConnectionWithCursor).cursor ?? ws.deserializeAttachment();
64 | if (
65 | id !== websocket.id &&
66 | cursor !== null &&
67 | cursor.x !== undefined &&
68 | cursor.y !== undefined
69 | ) {
70 | cursors[id] = cursor;
71 | }
72 | }
73 |
74 | const msg = {
75 | type: "sync",
76 | cursors: cursors,
77 | };
78 |
79 | websocket.send(JSON.stringify(msg));
80 | }
81 |
82 | onMessage(
83 | message: string,
84 | websocket: Party.Connection
85 | ): void | Promise {
86 | const position = JSON.parse(message as string);
87 | const prevCursor = this.getCursor(websocket);
88 | const cursor = {
89 | id: websocket.id,
90 | x: position.x,
91 | y: position.y,
92 | pointer: position.pointer,
93 | country: prevCursor?.country,
94 | lastUpdate: Date.now(),
95 | };
96 |
97 | this.setCursor(websocket, cursor);
98 |
99 | const msg =
100 | position.x && position.y
101 | ? {
102 | type: "update",
103 | ...cursor,
104 | id: websocket.id,
105 | }
106 | : {
107 | type: "remove",
108 | id: websocket.id,
109 | };
110 |
111 | // Broadcast, excluding self
112 | this.party.broadcast(JSON.stringify(msg), [websocket.id]);
113 | }
114 |
115 | getCursor(connection: ConnectionWithCursor) {
116 | if (!connection.cursor) {
117 | connection.cursor = connection.deserializeAttachment();
118 | }
119 |
120 | return connection.cursor;
121 | }
122 |
123 | setCursor(connection: ConnectionWithCursor, cursor: Cursor) {
124 | let prevCursor = connection.cursor;
125 | connection.cursor = cursor;
126 |
127 | // throttle writing to attachment to once every 100ms
128 | if (
129 | !prevCursor ||
130 | !prevCursor.lastUpdate ||
131 | (cursor.lastUpdate && cursor.lastUpdate - prevCursor.lastUpdate > 100)
132 | ) {
133 | // Stash the cursor in the websocket attachment
134 | connection.serializeAttachment({
135 | ...cursor,
136 | });
137 | }
138 | }
139 |
140 | onClose(websocket: Party.Connection) {
141 | // Broadcast a "remove" message to all connections
142 | const msg = {
143 | type: "remove",
144 | id: websocket.id,
145 | };
146 |
147 | console.log(
148 | "[disconnect]",
149 | this.party.id,
150 | websocket.id,
151 | websocket.readyState
152 | );
153 |
154 | this.party.broadcast(JSON.stringify(msg), []);
155 | }
156 | }
157 |
158 | CursorServer satisfies Party.Worker;
159 |
--------------------------------------------------------------------------------
/partykit.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://www.partykit.io/schema.json",
3 | "name": "aforshow-2024-party",
4 | "main": "party/server.ts",
5 | "compatibilityDate": "2024-07-30"
6 | }
--------------------------------------------------------------------------------
/postcss.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('postcss-load-config').Config} */
2 | const config = {
3 | plugins: {
4 | tailwindcss: {},
5 | },
6 | };
7 |
8 | export default config;
9 |
--------------------------------------------------------------------------------
/public/default-og.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Afordin/aforshow-2024/efa07284d0cae45753c451643d62c9bd87c64a87/public/default-og.png
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Afordin/aforshow-2024/efa07284d0cae45753c451643d62c9bd87c64a87/public/favicon.ico
--------------------------------------------------------------------------------
/public/imgs/afordin-sponsor.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Afordin/aforshow-2024/efa07284d0cae45753c451643d62c9bd87c64a87/public/imgs/afordin-sponsor.png
--------------------------------------------------------------------------------
/public/imgs/ikurotime.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Afordin/aforshow-2024/efa07284d0cae45753c451643d62c9bd87c64a87/public/imgs/ikurotime.png
--------------------------------------------------------------------------------
/public/imgs/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Afordin/aforshow-2024/efa07284d0cae45753c451643d62c9bd87c64a87/public/imgs/logo.png
--------------------------------------------------------------------------------
/public/imgs/readme-ticket.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Afordin/aforshow-2024/efa07284d0cae45753c451643d62c9bd87c64a87/public/imgs/readme-ticket.png
--------------------------------------------------------------------------------
/public/imgs/speakers/speaker-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Afordin/aforshow-2024/efa07284d0cae45753c451643d62c9bd87c64a87/public/imgs/speakers/speaker-1.png
--------------------------------------------------------------------------------
/public/imgs/speakers/speaker-2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Afordin/aforshow-2024/efa07284d0cae45753c451643d62c9bd87c64a87/public/imgs/speakers/speaker-2.jpg
--------------------------------------------------------------------------------
/public/imgs/speakers/speaker-3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Afordin/aforshow-2024/efa07284d0cae45753c451643d62c9bd87c64a87/public/imgs/speakers/speaker-3.jpg
--------------------------------------------------------------------------------
/public/imgs/speakers/speaker-4.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Afordin/aforshow-2024/efa07284d0cae45753c451643d62c9bd87c64a87/public/imgs/speakers/speaker-4.jpeg
--------------------------------------------------------------------------------
/public/imgs/speakers/speaker-5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Afordin/aforshow-2024/efa07284d0cae45753c451643d62c9bd87c64a87/public/imgs/speakers/speaker-5.png
--------------------------------------------------------------------------------
/public/imgs/ticket/aforshow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Afordin/aforshow-2024/efa07284d0cae45753c451643d62c9bd87c64a87/public/imgs/ticket/aforshow.png
--------------------------------------------------------------------------------
/public/imgs/ticket/avatar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Afordin/aforshow-2024/efa07284d0cae45753c451643d62c9bd87c64a87/public/imgs/ticket/avatar.png
--------------------------------------------------------------------------------
/public/imgs/ticket/bg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Afordin/aforshow-2024/efa07284d0cae45753c451643d62c9bd87c64a87/public/imgs/ticket/bg.png
--------------------------------------------------------------------------------
/public/imgs/ticket/sponsor_1.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Afordin/aforshow-2024/efa07284d0cae45753c451643d62c9bd87c64a87/public/imgs/ticket/sponsor_1.webp
--------------------------------------------------------------------------------
/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "tailwindcss";
2 |
3 | const config: Config = {
4 | content: [
5 | "./pages/**/*.{js,ts,jsx,tsx,mdx}",
6 | "./components/**/*.{js,ts,jsx,tsx,mdx}",
7 | "./app/**/*.{js,ts,jsx,tsx,mdx}",
8 | ],
9 | theme: {
10 | extend: {
11 | colors: {
12 | /* Same as unocss config */
13 | caTextSecondary: "#7A7A7A",
14 | caBackground: "#060606",
15 | caGray: "#737373",
16 | caDisabled: "#616161",
17 | caBorder: "#51546E",
18 | caBlurBoxes: "#E2276D",
19 | caWhite: "#FAFAFA",
20 | caBlack: "#0A0A0A",
21 | pBorder: "#262626",
22 | caPrimary: {
23 | 50: "#fff0f2",
24 | 100: "#ffdde1",
25 | 200: "#ffc1c8",
26 | 300: "#ff96a3",
27 | 400: "#ff596d",
28 | 500: "#ff2640",
29 | 600: "#fc1c37",
30 | 700: "#d4011a",
31 | 800: "#af0519",
32 | 900: "#900c1c",
33 | 950: "#4f000a",
34 | },
35 | caSecondary: {
36 | 50: "#fbf5fe",
37 | 100: "#f5eafd",
38 | 200: "#ebd3fb",
39 | 300: "#deb0f7",
40 | 400: "#cd82f0",
41 | 500: "#ad40e1",
42 | 600: "#9a32c7",
43 | 700: "#8226a5",
44 | 800: "#6b2187",
45 | 900: "#5c206f",
46 | 950: "#3a0949",
47 | },
48 | },
49 | backgroundSize: {
50 | "size-200": "200% 200%",
51 | },
52 | backgroundPosition: {
53 | "pos-0": "0% 0%",
54 | "pos-100": "100% 100%",
55 | },
56 | fontSize: {
57 | hero: ["clamp(4.5rem, 5.5vw, 3.75rem)", "1.1"],
58 | scheduleTitle: ["clamp(1rem, 5.5vw, 1.5rem)", "1.1"],
59 | scheduleAuthor: ["clamp(0.75rem, 5.5vw, 1.5rem)", "1.1"],
60 | },
61 | keyframes: {
62 | 'logo-cloud': {
63 | from: { transform: 'translateX(0)' },
64 | to: { transform: 'translateX(calc(-100% - 3rem))' },
65 | },
66 | },
67 | animation: {
68 | 'logo-cloud': 'logo-cloud 15s linear infinite',
69 | },
70 | backgroundImage: theme => ({
71 | 'gradient-to-rb': 'linear-gradient(to right bottom, var(--tw-gradient-stops))',
72 | }),
73 | },
74 | },
75 | variants: {
76 | extend: {
77 | backgroundPosition: ["hover"],
78 | },
79 | },
80 | plugins: [],
81 | };
82 |
83 | //TODO: Poner esto backgroundImage: { waves: "url('/tu/url/a/waves.png')" } }
84 |
85 | export default config;
86 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "forceConsistentCasingInFileNames": true,
9 | "noEmit": true,
10 | "esModuleInterop": true,
11 | "downlevelIteration": true,
12 | "noImplicitAny": false,
13 | "module": "esnext",
14 | "moduleResolution": "node",
15 | "resolveJsonModule": true,
16 | "isolatedModules": true,
17 | "jsx": "preserve",
18 | "incremental": true,
19 | "plugins": [
20 | {
21 | "name": "next"
22 | }
23 | ],
24 | "baseUrl": ".",
25 | "paths": {
26 | "@/*": ["./app/*"]
27 | }
28 | },
29 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
30 | "exclude": ["node_modules"]
31 | }
32 |
--------------------------------------------------------------------------------