From 880eb830020ba0a9c8ab7693d05c7e985bb0af47 Mon Sep 17 00:00:00 2001 From: bbb <132804471+beambeambeam@users.noreply.github.com> Date: Sat, 13 Sep 2025 17:12:49 +0700 Subject: [PATCH 1/6] Feat/close register (#186) * feat: update landing page and navbar for improved user flow * refactor: clean up imports and remove unused variables in member registration components * feat: add middleware for redirecting /register to /teams and configure request path matching * refactor: simplify TeamDone component layout and update button text for clarity * style: update button border radius in TeamDone component for improved aesthetics * feat: enhance TeamDone component with visual indicators for completed information sections * refactor: update Requirement component layout and styling for improved user experience --- .../(protected)/register/(member)/2/page.tsx | 2 - .../(protected)/register/(member)/3/page.tsx | 2 - .../register/(member)/_components/form.tsx | 11 -- .../(protected)/register/(member)/layout.tsx | 2 +- .../_components/document_uploader.tsx | 1 - .../app/(protected)/register/adviser/page.tsx | 2 +- apps/web/app/(protected)/teams/done.tsx | 123 +++++++++--------- .../web/app/(protected)/teams/requirement.tsx | 8 +- .../web/app/_components/application/index.tsx | 7 +- apps/web/app/_components/award/each.tsx | 28 ++-- apps/web/app/_components/footer.tsx | 1 + apps/web/app/_components/landing/index.tsx | 8 +- apps/web/app/_components/navbar/cta.tsx | 8 +- apps/web/app/_components/navbar/index.tsx | 4 +- .../app/_components/qualification/index.tsx | 1 - .../app/_components/scope/AccordionItem.tsx | 2 - apps/web/app/_components/story/index.tsx | 8 +- apps/web/app/page.tsx | 3 +- apps/web/app/sign-in/page.tsx | 9 +- apps/web/components/iconCircle.tsx | 4 +- apps/web/components/star.tsx | 6 +- apps/web/middleware.ts | 28 ++++ packages/eslint-config/next.js | 1 + 23 files changed, 143 insertions(+), 126 deletions(-) create mode 100644 apps/web/middleware.ts diff --git a/apps/web/app/(protected)/register/(member)/2/page.tsx b/apps/web/app/(protected)/register/(member)/2/page.tsx index 89db5ba1..1f495d3e 100644 --- a/apps/web/app/(protected)/register/(member)/2/page.tsx +++ b/apps/web/app/(protected)/register/(member)/2/page.tsx @@ -86,8 +86,6 @@ function MemberPage2() { ) const isReadyForSubmit = useIsReadyForFinalSubmit(2) - const showFinalSubmit = - teamQuery.data?.success && teamQuery.data.team?.memberCount === 2 && isReadyForSubmit // Only show register button for 2-member teams (final page) const shouldShowRegisterButton = teamQuery.data?.team?.memberCount === 2 diff --git a/apps/web/app/(protected)/register/(member)/3/page.tsx b/apps/web/app/(protected)/register/(member)/3/page.tsx index 7b9e93e1..705828ce 100644 --- a/apps/web/app/(protected)/register/(member)/3/page.tsx +++ b/apps/web/app/(protected)/register/(member)/3/page.tsx @@ -62,8 +62,6 @@ function MemberPage3() { ) const isReadyForSubmit = useIsReadyForFinalSubmit(3) - const showFinalSubmit = - teamQuery.data?.success && teamQuery.data.team?.memberCount === 3 && isReadyForSubmit // Only show register button for 3-member teams (final page) const shouldShowRegisterButton = teamQuery.data?.team?.memberCount === 3 diff --git a/apps/web/app/(protected)/register/(member)/_components/form.tsx b/apps/web/app/(protected)/register/(member)/_components/form.tsx index bc6b3b05..89dc1c83 100644 --- a/apps/web/app/(protected)/register/(member)/_components/form.tsx +++ b/apps/web/app/(protected)/register/(member)/_components/form.tsx @@ -5,17 +5,6 @@ import { useMember3Status } from "@/app/(protected)/_components/status/context" import DocumentUploader from "@/app/(protected)/register/_components/document_uploader" import { ExternalFormProps } from "@/types/form" import { zodResolver } from "@hookform/resolvers/zod" -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, - AlertDialogTrigger, -} from "@workspace/ui/components/alert-dialog" import { Button } from "@workspace/ui/components/button" import { Form, diff --git a/apps/web/app/(protected)/register/(member)/layout.tsx b/apps/web/app/(protected)/register/(member)/layout.tsx index 03e767b0..d95812c8 100644 --- a/apps/web/app/(protected)/register/(member)/layout.tsx +++ b/apps/web/app/(protected)/register/(member)/layout.tsx @@ -2,7 +2,7 @@ import { Navbar } from "@/app/_components/navbar" import { cn } from "@workspace/ui/lib/utils" import { ReactNode } from "react" -import { TeamNavMobileLinks, TeamNavMenu } from "../../_components/team-nav" +import { TeamNavMobileLinks } from "../../_components/team-nav" interface MemberLayoutProps { readonly children: ReactNode diff --git a/apps/web/app/(protected)/register/_components/document_uploader.tsx b/apps/web/app/(protected)/register/_components/document_uploader.tsx index e7e73024..8c957263 100644 --- a/apps/web/app/(protected)/register/_components/document_uploader.tsx +++ b/apps/web/app/(protected)/register/_components/document_uploader.tsx @@ -1,4 +1,3 @@ -import { Button } from "@workspace/ui/components/button" import { useFileUpload, formatBytes } from "@workspace/ui/hooks/use-file-upload" import type { FileMetadata } from "@workspace/ui/hooks/use-file-upload" import { cn } from "@workspace/ui/lib/utils" diff --git a/apps/web/app/(protected)/register/adviser/page.tsx b/apps/web/app/(protected)/register/adviser/page.tsx index 7025e807..765cc98c 100644 --- a/apps/web/app/(protected)/register/adviser/page.tsx +++ b/apps/web/app/(protected)/register/adviser/page.tsx @@ -7,7 +7,7 @@ import { orpc } from "@/utils/orpc" import { useQuery } from "@tanstack/react-query" import { cn } from "@workspace/ui/lib/utils" -import { TeamNavMobileLinks, TeamNavMenu } from "../../_components/team-nav" +import { TeamNavMobileLinks } from "../../_components/team-nav" function AdviserRegisterPage() { const query = useQuery(orpc.register.adviser.get.queryOptions()) diff --git a/apps/web/app/(protected)/teams/done.tsx b/apps/web/app/(protected)/teams/done.tsx index 5ec56b71..072d5f52 100644 --- a/apps/web/app/(protected)/teams/done.tsx +++ b/apps/web/app/(protected)/teams/done.tsx @@ -1,5 +1,4 @@ import { useSubmitRegister } from "@/app/(protected)/_components/status/context" -import ArrowIcon from "@/components/ArrowIcon" import CheckIcon from "@/components/CheckIcon" import DocumentIcon from "@/components/DocumentIcon" import EducationIcon from "@/components/EducationIcon" @@ -214,14 +213,6 @@ function TeamDone() { return (
- {!isSubmit && ( - - ลงทะเบียนต่อ - - - )}
-
-
- {teamData.teamImage ? ( - {teamData.teamName} - ) : ( - Team Image - )} -
- {teamData.teamCode} -
-
-
-
-
-
- {teamData.teamName} -
-
- {teamData.teamCode} -
-
-
-
- -
- {teamData.school} -
-
- {teamData.message.length > 1 && ( -
- -
- {teamData.message} -
-
- )} +
+ {/* Top row: Team image and button */} +
+
+ {teamData.teamImage ? ( + {teamData.teamName} + ) : ( + Team Image + )} +
+ {teamData.teamCode}
{!isSubmit && ( + className="flex h-fit w-fit items-center justify-between gap-4 rounded-[32px] bg-[radial-gradient(105.85%_133.12%_at_50%_100%,#DFDFDF_0%,rgba(223,223,223,0)_100%)] px-6 py-3 md:w-auto md:px-8 2xl:px-10 2xl:py-4"> - ลงทะเบียนต่อ + เลยกำหนดรับสมัคร - )}
+ + {/* Bottom row: Team information */} +
+
+
+ {teamData.teamName} +
+
+ {teamData.teamCode} +
+
+
+
+ +
+ {teamData.school} +
+
+ {teamData.message.length > 1 && ( +
+ +
+ {teamData.message} +
+
+ )} +
+
@@ -377,8 +370,14 @@ function TeamDone() { {/* General info */}
-
+
1. ข้อมูลทั่วไป + {member.info.general.nameTH[0] && + member.info.general.nameTH[1] && + member.info.general.nameEN[0] && + member.info.general.nameEN[1] && ( + + )}
@@ -439,8 +438,11 @@ function TeamDone() {
-
+
2. ข้อมูลติดต่อ + {member.info.contact.email && member.info.contact.phone && ( + + )}
@@ -471,8 +473,11 @@ function TeamDone() {
-
+
3. เอกสาร + {member.info.documents.length > 0 && ( + + )}
{member.info.documents.map((doc, i) => (
ลงทะเบียนเข้าแข่งขัน - เริ่มลงทะเบียน - + className="flex h-fit w-full items-center justify-between gap-4 rounded-[32px] bg-[radial-gradient(105.85%_133.12%_at_50%_100%,#DFDFDF_0%,rgba(223,223,223,0)_100%)] px-6 py-3 md:w-auto md:w-fit md:px-8 2xl:px-10 2xl:py-4"> + + เลยกำหนดรับสมัคร +
diff --git a/apps/web/app/_components/application/index.tsx b/apps/web/app/_components/application/index.tsx index 73605e6f..809b81e8 100644 --- a/apps/web/app/_components/application/index.tsx +++ b/apps/web/app/_components/application/index.tsx @@ -10,7 +10,6 @@ interface RequirementProps { function Requirement({ title, items, imgSrc }: RequirementProps) { return (
- {/* eslint-disable-next-line @next/next/no-img-element */} {items.map((item, i) => ( -
  • +
  • {item}
  • ))} @@ -48,7 +47,7 @@ const req: RequirementProps[] = [ { title: "สำหรับนักเรียนผู้เข้าแข่งขัน", items: [ - + สำเนาบัตรประชาชน หรือบัตรประจำตัวสำหรับบุคคลที่ไม่ใช่สัญชาติไทย
    (เฉพาะด้านหน้า)
    , @@ -60,7 +59,7 @@ const req: RequirementProps[] = [ { title: "สำหรับอาจารย์ที่ปรึกษา", items: [ - + สำเนาบัตรประชาชน หรือบัตรประจำตัวสำหรับบุคคลที่ไม่ใช่สัญชาติไทย
    (เฉพาะด้านหน้า)
    , diff --git a/apps/web/app/_components/award/each.tsx b/apps/web/app/_components/award/each.tsx index 772dcdf2..65ccac5d 100644 --- a/apps/web/app/_components/award/each.tsx +++ b/apps/web/app/_components/award/each.tsx @@ -15,11 +15,13 @@ export function EachAwardForTopRow({ data }: TopRowAwardProps) { return (
    + style={ + { + // radial glow with supplied color + ["--glow-color"]: glowColor, + background: "transparent", + } as React.CSSProperties & { "--glow-color": string } + }> + style={ + { + ["--glow-color"]: glowColor, + } as React.CSSProperties & { "--glow-color": string } + }>
    + style={ + { + ["--glow-color"]: glowColor, + } as React.CSSProperties & { "--glow-color": string } + }> {SPONSOR_LIST.map((s) => ( diff --git a/apps/web/app/_components/landing/index.tsx b/apps/web/app/_components/landing/index.tsx index fa83325e..5ffc58c6 100644 --- a/apps/web/app/_components/landing/index.tsx +++ b/apps/web/app/_components/landing/index.tsx @@ -1,5 +1,3 @@ -import Link from "next/link" - function Stat({ title, description }: { title: string; description: string | React.ReactElement }) { return (
    @@ -191,7 +189,6 @@ function LandingSection() {
    - {/* eslint-disable-next-line @next/next/no-img-element */} logo{descriptions.xl2}

    - + {/* - + */} +

    หมดเขตรับสมัครแล้ว

    diff --git a/apps/web/app/_components/navbar/cta.tsx b/apps/web/app/_components/navbar/cta.tsx index 00d0d1c8..8e51c3b2 100644 --- a/apps/web/app/_components/navbar/cta.tsx +++ b/apps/web/app/_components/navbar/cta.tsx @@ -6,16 +6,16 @@ const Landing = ({ isMobile }: { isMobile?: boolean }) => { if (isMobile) { return ( - ) } return ( - ) diff --git a/apps/web/app/_components/navbar/index.tsx b/apps/web/app/_components/navbar/index.tsx index 97042fe5..4b85b7d0 100644 --- a/apps/web/app/_components/navbar/index.tsx +++ b/apps/web/app/_components/navbar/index.tsx @@ -41,7 +41,7 @@ interface NavbarProps { } type Actions = { - [key: string]: (...args: any[]) => any + [key: string]: (...args: unknown[]) => void } function signOutWithBA() { @@ -74,7 +74,7 @@ export function Navbar({ links, CTAId, sections }: NavbarProps) { }) return () => observer.disconnect() - }, []) + }, [sections]) const isActive = (href: string) => active === href return ( diff --git a/apps/web/app/_components/qualification/index.tsx b/apps/web/app/_components/qualification/index.tsx index 23ddf0e4..8758f36c 100644 --- a/apps/web/app/_components/qualification/index.tsx +++ b/apps/web/app/_components/qualification/index.tsx @@ -29,7 +29,6 @@ function QualificationSector() { className="flex w-[345px] flex-col items-center justify-center gap-4 lg:w-[323.33px] lg:gap-6 2xl:w-[415px]">
    - {/* eslint-disable-next-line @next/next/no-img-element */} { + const startInterval = useCallback(() => { if (intervalRef.current) clearInterval(intervalRef.current) intervalRef.current = setInterval(() => { @@ -25,14 +25,14 @@ function Story() { setFade(true) // fade-in after change }, 200) // match fade-out duration }, 60000) // auto switch every 1 minute - } + }, [stories.length]) useEffect(() => { startInterval() return () => { if (intervalRef.current) clearInterval(intervalRef.current) } - }, [stories.length]) + }, [stories.length, startInterval]) useEffect(() => { stories.forEach((story) => { diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx index 4d843796..9ff63b7c 100644 --- a/apps/web/app/page.tsx +++ b/apps/web/app/page.tsx @@ -4,6 +4,7 @@ import Application from "@/app/_components/application" import Award from "@/app/_components/award" import Contact from "@/app/_components/contact" import DateAndContest from "@/app/_components/dateandcontest" +import Footer from "@/app/_components/footer" import LandingSection from "@/app/_components/landing" import { Navbar } from "@/app/_components/navbar" import QualificationSector from "@/app/_components/qualification" @@ -11,8 +12,6 @@ import Scope from "@/app/_components/scope" import Story from "@/app/_components/story" import Head from "next/head" -import Footer from "./_components/footer" - export default function Page() { return ( <> diff --git a/apps/web/app/sign-in/page.tsx b/apps/web/app/sign-in/page.tsx index d9412d4e..9c9859ba 100644 --- a/apps/web/app/sign-in/page.tsx +++ b/apps/web/app/sign-in/page.tsx @@ -1,7 +1,6 @@ "use client" import GlassCard from "@/components/glassCard" -import { showToast } from "@/components/toast" import { authClient } from "@/lib/auth-client" import { useRouter } from "next/navigation" import { useEffect, useRef, useState } from "react" @@ -168,7 +167,9 @@ function SignInPage() {
    {/* keyframes */} - + `, + }} + />
    ) } diff --git a/apps/web/components/iconCircle.tsx b/apps/web/components/iconCircle.tsx index 454d495c..7c253385 100644 --- a/apps/web/components/iconCircle.tsx +++ b/apps/web/components/iconCircle.tsx @@ -8,7 +8,7 @@ type IconCircleProps = { } const IconCircle: React.FC = ({ style, children, className, onClick }) => { - const baseStyle: React.CSSProperties & { [key: string]: any } = { + const baseStyle: React.CSSProperties & Record = { userSelect: "none", WebkitUserDrag: "none", ...style, @@ -16,7 +16,7 @@ const IconCircle: React.FC = ({ style, children, className, onC const renderChildren = React.Children.map(children, (child) => { if (React.isValidElement(child)) { - const element = child as React.ReactElement + const element = child as React.ReactElement return React.cloneElement(element, { draggable: false, style: { ...element.props.style, userSelect: "none", WebkitUserDrag: "none" }, diff --git a/apps/web/components/star.tsx b/apps/web/components/star.tsx index 13d9d407..aac21f6b 100644 --- a/apps/web/components/star.tsx +++ b/apps/web/components/star.tsx @@ -1,15 +1,12 @@ import React from "react" -const useUniqueId = () => React.useId().replace(/[^a-zA-Z0-9]/g, "") - -const baseStyle: React.CSSProperties & { [key: string]: any } = { +const baseStyle: React.CSSProperties & Record = { userSelect: "none", pointerEvents: "auto", WebkitUserDrag: "none", } export const StarLarge: React.FC> = (props) => { - const id = useUniqueId() return ( > = (p } export const Star: React.FC> = (props) => { - const id = useUniqueId() return ( Date: Sun, 14 Sep 2025 16:30:32 +0700 Subject: [PATCH 2/6] fix: wrong env in auth-client for kirin (#187) --- apps/staff/src/lib/auth-client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/staff/src/lib/auth-client.ts b/apps/staff/src/lib/auth-client.ts index 7eb9e05e..517973f4 100644 --- a/apps/staff/src/lib/auth-client.ts +++ b/apps/staff/src/lib/auth-client.ts @@ -2,6 +2,6 @@ import { adminClient, usernameClient } from "better-auth/client/plugins" import { createAuthClient } from "better-auth/react" export const authClient = createAuthClient({ - baseURL: process.env.NEXT_PUBLIC_STAFF_URL, + baseURL: process.env.NEXT_PUBLIC_STAFFAPP_URL, plugins: [adminClient(), usernameClient()], }) From a4702d77902e5328f1b6b519c71ca21a22ceb11c Mon Sep 17 00:00:00 2001 From: Jakkaphat Chalermphanaphan Date: Sun, 14 Sep 2025 19:52:21 +0700 Subject: [PATCH 3/6] Feat/1st round info table (#188) * feat: add Round 1 Competition page and team table components * feat: add CSV export functionality to team table * refactor: verify dialog to team dialog * feat: add VerifyForm component for Round 1 verification process * refactor: remove verify dialog * refactor: restructure TeamDialog layout for improved readability and organization * refactor: simplify TeamDialog usage in team table by integrating VerifyFormParent for enhanced verification interaction * refactor: adjust TeamDialog layout for better responsiveness and improved child component positioning * feat: add Team Award page and table components for displaying team awards * chore: fix all lint error * refactor: update CSV export functionality in team table to include index and adjust field names --------- Co-authored-by: beambeambeam --- .../app/(protected)/_components/navbar.tsx | 2 + .../team-dialog}/adviser.tsx | 10 +- .../_components/team-dialog/context.tsx | 15 ++ .../_components/team-dialog/index.tsx | 92 +++++++++++ .../team-dialog}/member-layout.tsx | 0 .../team-dialog}/member1.tsx | 10 +- .../team-dialog}/member2.tsx | 10 +- .../team-dialog}/member3.tsx | 10 +- .../team-dialog}/queries.ts | 0 .../team-dialog}/team.tsx | 8 +- .../_components/team-table/columns.tsx | 151 ++++++++++++++++++ .../_components/team-table/index.tsx | 69 ++++++++ .../_components/team-table/queries.ts | 87 ++++++++++ .../_components/team-table/validations.ts | 16 ++ .../src/app/(protected)/round-1-comp/page.tsx | 46 ++++++ .../_components/team-table/columns.tsx | 9 +- .../action.ts | 0 .../{verifly-dialog => verification}/form.tsx | 2 +- .../_components/verifly-dialog/context.tsx | 15 -- .../_components/verifly-dialog/index.tsx | 99 ------------ .../_components/team-award-table/columns.tsx | 98 ++++++++++++ .../_components/team-award-table/index.tsx | 36 +++++ .../_components/team-award-table/queries.ts | 67 ++++++++ .../team-award-table/validations.ts | 14 ++ .../src/app/(protected)/team-award/page.tsx | 46 ++++++ apps/staff/src/lib/csv-export.ts | 91 +++++++++++ 26 files changed, 858 insertions(+), 145 deletions(-) rename apps/staff/src/app/(protected)/{round-1/_components/verifly-dialog => _components/team-dialog}/adviser.tsx (70%) create mode 100644 apps/staff/src/app/(protected)/_components/team-dialog/context.tsx create mode 100644 apps/staff/src/app/(protected)/_components/team-dialog/index.tsx rename apps/staff/src/app/(protected)/{round-1/_components/verifly-dialog => _components/team-dialog}/member-layout.tsx (100%) rename apps/staff/src/app/(protected)/{round-1/_components/verifly-dialog => _components/team-dialog}/member1.tsx (70%) rename apps/staff/src/app/(protected)/{round-1/_components/verifly-dialog => _components/team-dialog}/member2.tsx (70%) rename apps/staff/src/app/(protected)/{round-1/_components/verifly-dialog => _components/team-dialog}/member3.tsx (70%) rename apps/staff/src/app/(protected)/{round-1/_components/verifly-dialog => _components/team-dialog}/queries.ts (100%) rename apps/staff/src/app/(protected)/{round-1/_components/verifly-dialog => _components/team-dialog}/team.tsx (91%) create mode 100644 apps/staff/src/app/(protected)/round-1-comp/_components/team-table/columns.tsx create mode 100644 apps/staff/src/app/(protected)/round-1-comp/_components/team-table/index.tsx create mode 100644 apps/staff/src/app/(protected)/round-1-comp/_components/team-table/queries.ts create mode 100644 apps/staff/src/app/(protected)/round-1-comp/_components/team-table/validations.ts create mode 100644 apps/staff/src/app/(protected)/round-1-comp/page.tsx rename apps/staff/src/app/(protected)/round-1/_components/{verifly-dialog => verification}/action.ts (100%) rename apps/staff/src/app/(protected)/round-1/_components/{verifly-dialog => verification}/form.tsx (99%) delete mode 100644 apps/staff/src/app/(protected)/round-1/_components/verifly-dialog/context.tsx delete mode 100644 apps/staff/src/app/(protected)/round-1/_components/verifly-dialog/index.tsx create mode 100644 apps/staff/src/app/(protected)/team-award/_components/team-award-table/columns.tsx create mode 100644 apps/staff/src/app/(protected)/team-award/_components/team-award-table/index.tsx create mode 100644 apps/staff/src/app/(protected)/team-award/_components/team-award-table/queries.ts create mode 100644 apps/staff/src/app/(protected)/team-award/_components/team-award-table/validations.ts create mode 100644 apps/staff/src/app/(protected)/team-award/page.tsx create mode 100644 apps/staff/src/lib/csv-export.ts diff --git a/apps/staff/src/app/(protected)/_components/navbar.tsx b/apps/staff/src/app/(protected)/_components/navbar.tsx index 746bbf72..37464219 100644 --- a/apps/staff/src/app/(protected)/_components/navbar.tsx +++ b/apps/staff/src/app/(protected)/_components/navbar.tsx @@ -36,6 +36,8 @@ function Navbar() { const navigationLinks = [ { href: "/dashboard", label: "Overview" }, { href: "/round-1", label: "Round 1 Verification" }, + { href: "/round-1-comp", label: "Round 1 Competition" }, + { href: "/team-award", label: "Team Award" }, ...(data?.user && typeof data.user.role === "string" && ["super_admin", "admin"].includes(data.user.role) ? [{ href: "/admin", label: "Admin" }] : []), diff --git a/apps/staff/src/app/(protected)/round-1/_components/verifly-dialog/adviser.tsx b/apps/staff/src/app/(protected)/_components/team-dialog/adviser.tsx similarity index 70% rename from apps/staff/src/app/(protected)/round-1/_components/verifly-dialog/adviser.tsx rename to apps/staff/src/app/(protected)/_components/team-dialog/adviser.tsx index f007fec6..3fd8a7d2 100644 --- a/apps/staff/src/app/(protected)/round-1/_components/verifly-dialog/adviser.tsx +++ b/apps/staff/src/app/(protected)/_components/team-dialog/adviser.tsx @@ -1,15 +1,13 @@ "use client" -import { useVerifyDialogContext } from "@/app/(protected)/round-1/_components/verifly-dialog/context" -import MemberLayout, { - MemberSkeleton, -} from "@/app/(protected)/round-1/_components/verifly-dialog/member-layout" -import { getAdviser } from "@/app/(protected)/round-1/_components/verifly-dialog/queries" +import { useTeamDialogContext } from "@/app/(protected)/_components/team-dialog/context" +import MemberLayout, { MemberSkeleton } from "@/app/(protected)/_components/team-dialog/member-layout" +import { getAdviser } from "@/app/(protected)/_components/team-dialog/queries" import { useQuery } from "@tanstack/react-query" import { UserIcon } from "lucide-react" function AdviserDisplay() { - const { id } = useVerifyDialogContext() + const { id } = useTeamDialogContext() const { data, isPending } = useQuery({ queryKey: [id, "adviser"], queryFn: async () => { diff --git a/apps/staff/src/app/(protected)/_components/team-dialog/context.tsx b/apps/staff/src/app/(protected)/_components/team-dialog/context.tsx new file mode 100644 index 00000000..f918e7f9 --- /dev/null +++ b/apps/staff/src/app/(protected)/_components/team-dialog/context.tsx @@ -0,0 +1,15 @@ +import { createContext, useContext } from "react" + +export interface TeamDialogContextValue { + id: string +} + +export const TeamDialogContext = createContext(undefined) + +export function useTeamDialogContext(): TeamDialogContextValue { + const context = useContext(TeamDialogContext) + if (!context) { + throw new Error("useTeamDialogContext must be used within a TeamDialogContext.Provider") + } + return context +} diff --git a/apps/staff/src/app/(protected)/_components/team-dialog/index.tsx b/apps/staff/src/app/(protected)/_components/team-dialog/index.tsx new file mode 100644 index 00000000..5d619734 --- /dev/null +++ b/apps/staff/src/app/(protected)/_components/team-dialog/index.tsx @@ -0,0 +1,92 @@ +import AdviserDisplay from "@/app/(protected)/_components/team-dialog/adviser" +import { TeamDialogContext, TeamDialogContextValue } from "@/app/(protected)/_components/team-dialog/context" +import Member1Display from "@/app/(protected)/_components/team-dialog/member1" +import Member2Display from "@/app/(protected)/_components/team-dialog/member2" +import Member3Display from "@/app/(protected)/_components/team-dialog/member3" +import TeamDisplay from "@/app/(protected)/_components/team-dialog/team" +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog" +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" +import { SearchCheckIcon, UserIcon } from "lucide-react" +import { parseAsString, useQueryState } from "nuqs" +import { ReactNode } from "react" + +interface TeamDialogProps extends TeamDialogContextValue { + children?: ReactNode +} + +function TeamDialog(props: TeamDialogProps) { + const [verify, setVerify] = useQueryState("verify", parseAsString.withDefault("")) + + const [tab, setTab] = useQueryState("verify-tab", parseAsString.withDefault("")) + const onTabChange = (value: string) => setTab(value) + + return ( + + { + setVerify(isOpen ? props.id : "") + setTab(null) + }}> + + + + + +
    + + + Team + Adviser + + 1 + + + 2 + + + 3 + + + + + + + + + + + + + + + + + + + {props.children && ( +
    {props.children}
    + )} +
    +
    +
    +
    + ) +} +export default TeamDialog diff --git a/apps/staff/src/app/(protected)/round-1/_components/verifly-dialog/member-layout.tsx b/apps/staff/src/app/(protected)/_components/team-dialog/member-layout.tsx similarity index 100% rename from apps/staff/src/app/(protected)/round-1/_components/verifly-dialog/member-layout.tsx rename to apps/staff/src/app/(protected)/_components/team-dialog/member-layout.tsx diff --git a/apps/staff/src/app/(protected)/round-1/_components/verifly-dialog/member1.tsx b/apps/staff/src/app/(protected)/_components/team-dialog/member1.tsx similarity index 70% rename from apps/staff/src/app/(protected)/round-1/_components/verifly-dialog/member1.tsx rename to apps/staff/src/app/(protected)/_components/team-dialog/member1.tsx index e467aec6..f9cc31f1 100644 --- a/apps/staff/src/app/(protected)/round-1/_components/verifly-dialog/member1.tsx +++ b/apps/staff/src/app/(protected)/_components/team-dialog/member1.tsx @@ -1,15 +1,13 @@ "use client" -import { useVerifyDialogContext } from "@/app/(protected)/round-1/_components/verifly-dialog/context" -import MemberLayout, { - MemberSkeleton, -} from "@/app/(protected)/round-1/_components/verifly-dialog/member-layout" -import { getMember } from "@/app/(protected)/round-1/_components/verifly-dialog/queries" +import { useTeamDialogContext } from "@/app/(protected)/_components/team-dialog/context" +import MemberLayout, { MemberSkeleton } from "@/app/(protected)/_components/team-dialog/member-layout" +import { getMember } from "@/app/(protected)/_components/team-dialog/queries" import { useQuery } from "@tanstack/react-query" import { UserIcon } from "lucide-react" function Member1Display() { - const { id } = useVerifyDialogContext() + const { id } = useTeamDialogContext() const { data, isPending } = useQuery({ queryKey: [id, "member1"], queryFn: async () => { diff --git a/apps/staff/src/app/(protected)/round-1/_components/verifly-dialog/member2.tsx b/apps/staff/src/app/(protected)/_components/team-dialog/member2.tsx similarity index 70% rename from apps/staff/src/app/(protected)/round-1/_components/verifly-dialog/member2.tsx rename to apps/staff/src/app/(protected)/_components/team-dialog/member2.tsx index 2d8ae7cb..8e8880e0 100644 --- a/apps/staff/src/app/(protected)/round-1/_components/verifly-dialog/member2.tsx +++ b/apps/staff/src/app/(protected)/_components/team-dialog/member2.tsx @@ -1,15 +1,13 @@ "use client" -import { useVerifyDialogContext } from "@/app/(protected)/round-1/_components/verifly-dialog/context" -import MemberLayout, { - MemberSkeleton, -} from "@/app/(protected)/round-1/_components/verifly-dialog/member-layout" -import { getMember } from "@/app/(protected)/round-1/_components/verifly-dialog/queries" +import { useTeamDialogContext } from "@/app/(protected)/_components/team-dialog/context" +import MemberLayout, { MemberSkeleton } from "@/app/(protected)/_components/team-dialog/member-layout" +import { getMember } from "@/app/(protected)/_components/team-dialog/queries" import { useQuery } from "@tanstack/react-query" import { UserIcon } from "lucide-react" function Member2Display() { - const { id } = useVerifyDialogContext() + const { id } = useTeamDialogContext() const { data, isPending } = useQuery({ queryKey: [id, "member2"], queryFn: async () => { diff --git a/apps/staff/src/app/(protected)/round-1/_components/verifly-dialog/member3.tsx b/apps/staff/src/app/(protected)/_components/team-dialog/member3.tsx similarity index 70% rename from apps/staff/src/app/(protected)/round-1/_components/verifly-dialog/member3.tsx rename to apps/staff/src/app/(protected)/_components/team-dialog/member3.tsx index d24fee89..a53e1b0d 100644 --- a/apps/staff/src/app/(protected)/round-1/_components/verifly-dialog/member3.tsx +++ b/apps/staff/src/app/(protected)/_components/team-dialog/member3.tsx @@ -1,15 +1,13 @@ "use client" -import { useVerifyDialogContext } from "@/app/(protected)/round-1/_components/verifly-dialog/context" -import MemberLayout, { - MemberSkeleton, -} from "@/app/(protected)/round-1/_components/verifly-dialog/member-layout" -import { getMember } from "@/app/(protected)/round-1/_components/verifly-dialog/queries" +import { useTeamDialogContext } from "@/app/(protected)/_components/team-dialog/context" +import MemberLayout, { MemberSkeleton } from "@/app/(protected)/_components/team-dialog/member-layout" +import { getMember } from "@/app/(protected)/_components/team-dialog/queries" import { useQuery } from "@tanstack/react-query" import { UserIcon } from "lucide-react" function Member3Display() { - const { id } = useVerifyDialogContext() + const { id } = useTeamDialogContext() const { data, isPending } = useQuery({ queryKey: [id, "member3"], queryFn: async () => { diff --git a/apps/staff/src/app/(protected)/round-1/_components/verifly-dialog/queries.ts b/apps/staff/src/app/(protected)/_components/team-dialog/queries.ts similarity index 100% rename from apps/staff/src/app/(protected)/round-1/_components/verifly-dialog/queries.ts rename to apps/staff/src/app/(protected)/_components/team-dialog/queries.ts diff --git a/apps/staff/src/app/(protected)/round-1/_components/verifly-dialog/team.tsx b/apps/staff/src/app/(protected)/_components/team-dialog/team.tsx similarity index 91% rename from apps/staff/src/app/(protected)/round-1/_components/verifly-dialog/team.tsx rename to apps/staff/src/app/(protected)/_components/team-dialog/team.tsx index 2acb3027..25a3bd45 100644 --- a/apps/staff/src/app/(protected)/round-1/_components/verifly-dialog/team.tsx +++ b/apps/staff/src/app/(protected)/_components/team-dialog/team.tsx @@ -1,15 +1,15 @@ "use client" +import { useTeamDialogContext } from "@/app/(protected)/_components/team-dialog/context" +import { MemberSkeleton } from "@/app/(protected)/_components/team-dialog/member-layout" +import { getTeam } from "@/app/(protected)/_components/team-dialog/queries" import { formatCodeName } from "@/app/(protected)/round-1/_components/team-table/format" -import { useVerifyDialogContext } from "@/app/(protected)/round-1/_components/verifly-dialog/context" -import { MemberSkeleton } from "@/app/(protected)/round-1/_components/verifly-dialog/member-layout" -import { getTeam } from "@/app/(protected)/round-1/_components/verifly-dialog/queries" import { Label } from "@/components/ui/label" import { useQuery } from "@tanstack/react-query" import { UsersIcon } from "lucide-react" function TeamDisplay() { - const { id } = useVerifyDialogContext() + const { id } = useTeamDialogContext() const { data, isPending } = useQuery({ queryKey: [id, "team"], queryFn: async () => { diff --git a/apps/staff/src/app/(protected)/round-1-comp/_components/team-table/columns.tsx b/apps/staff/src/app/(protected)/round-1-comp/_components/team-table/columns.tsx new file mode 100644 index 00000000..e8ff907a --- /dev/null +++ b/apps/staff/src/app/(protected)/round-1-comp/_components/team-table/columns.tsx @@ -0,0 +1,151 @@ +import TeamDialog from "@/app/(protected)/_components/team-dialog" +import { formatCodeName } from "@/app/(protected)/round-1/_components/team-table/format" +import { cn } from "@/lib/utils" +import { createColumnHelper } from "@tanstack/react-table" +import { teams } from "@workspace/db/schema" +import { Building2, Users, School, Mail, FileText } from "lucide-react" +import { Text } from "lucide-react" + +export type Team = Pick< + typeof teams.$inferSelect, + "id" | "name" | "school" | "memberCount" | "createdAt" | "index" +> & { + notes: string | null + firstMemberEmail: string | null + rowShouldBeRed: boolean +} + +const columnHelper = createColumnHelper() + +export const columns = [ + columnHelper.accessor("index", { + id: "codeName", + header: "Code Name", + cell: (info) => { + const code = formatCodeName(info.row.original.index) + const prefix = "BMHK" + const suffix = code.replace(prefix, "") + return ( +
    + {prefix} + {suffix} +
    + ) + }, + enableSorting: false, + enableColumnFilter: true, + meta: { + label: "Code Name", + placeholder: "Search code names...", + variant: "text", + icon: Text, + }, + }), + columnHelper.accessor("name", { + id: "name", + header: "Team Name", + cell: (info) =>
    {info.getValue()}
    , + enableSorting: false, + enableColumnFilter: true, + meta: { + label: "Team Name", + placeholder: "Search team names...", + variant: "text", + icon: Text, + }, + }), + columnHelper.accessor("school", { + id: "school", + header: "School", + cell: (info) => { + const schoolName = info.getValue() + const shouldBeRed = info.row.original.rowShouldBeRed + + return ( +
    + + {schoolName} +
    + ) + }, + enableSorting: false, + enableColumnFilter: true, + meta: { + label: "School", + placeholder: "Search schools...", + variant: "text", + icon: Building2, + }, + }), + columnHelper.accessor("memberCount", { + id: "memberCount", + header: "Members", + cell: (info) => ( +
    + + {info.getValue()} +
    + ), + enableSorting: false, + enableColumnFilter: true, + meta: { + label: "Member Count", + placeholder: "Filter by member count...", + variant: "select", + icon: Users, + options: [ + { label: "2 Members", value: "2" }, + { label: "3 Members", value: "3" }, + ], + }, + }), + columnHelper.accessor("firstMemberEmail", { + id: "firstMemberEmail", + header: "First Member Email", + cell: (info) => { + const email = info.getValue() + if (!email) { + return No email + } + return ( +
    + + {email} +
    + ) + }, + enableSorting: false, + enableColumnFilter: true, + meta: { + label: "First Member Email", + placeholder: "Search emails...", + variant: "text", + icon: Mail, + }, + }), + columnHelper.accessor("notes", { + id: "notes", + header: "Notes", + cell: (info) => { + const notes = info.getValue() + if (!notes) { + return No notes + } + return ( +
    + + + {notes} + +
    + ) + }, + enableSorting: false, + enableColumnFilter: false, + }), + columnHelper.display({ + id: "action", + header: "Action", + cell: ({ row }) => , + }), +] diff --git a/apps/staff/src/app/(protected)/round-1-comp/_components/team-table/index.tsx b/apps/staff/src/app/(protected)/round-1-comp/_components/team-table/index.tsx new file mode 100644 index 00000000..103dd39a --- /dev/null +++ b/apps/staff/src/app/(protected)/round-1-comp/_components/team-table/index.tsx @@ -0,0 +1,69 @@ +"use client" + +import { columns, Team } from "@/app/(protected)/round-1-comp/_components/team-table/columns" +import { getRound1CompTeams } from "@/app/(protected)/round-1-comp/_components/team-table/queries" +import { DataTable } from "@/components/data-table/data-table" +import { DataTableToolbar } from "@/components/data-table/data-table-toolbar" +import { Button } from "@/components/ui/button" +import { useDataTable } from "@/hooks/use-data-table" +import { exportTableToCSV } from "@/lib/csv-export" +import { Row } from "@tanstack/react-table" +import { Download } from "lucide-react" +import { CSSProperties, use, useCallback } from "react" + +interface Round1CompTeamTableProps { + promises: Promise<[Awaited>]> +} + +function Round1CompTeamTable({ promises }: Round1CompTeamTableProps) { + const [{ data, pageCount }] = use(promises) + + const { table } = useDataTable({ + data, + columns, + getRowId: (row) => row.id, + pageCount, + initialState: { + columnPinning: { right: [] }, + columnVisibility: {}, + }, + shallow: false, + clearOnDefault: true, + meta: { + getRowStyles: (row: Row): CSSProperties => ({ + background: row.original.rowShouldBeRed ? "rgba(239, 68, 68, 0.1)" : "transparent", + }), + }, + }) + + const handleExportCSV = useCallback(() => { + const filteredData = table.getFilteredRowModel().rows.map((row, index) => { + const team = row.original + return { + Index: `${index + 1}`, + CodeName: `BMHK${team.index.toString().padStart(3, "0")}`, + TeamName: team.name, + School: team.school, + FirstMemberEmail: team.firstMemberEmail || "", + Notes: team.notes || "", + } + }) + + exportTableToCSV(filteredData, { + filename: `round-1-competition-teams-${new Date().toISOString().split("T")[0]}.csv`, + }) + }, [table]) + + return ( + + + + + + ) +} + +export default Round1CompTeamTable diff --git a/apps/staff/src/app/(protected)/round-1-comp/_components/team-table/queries.ts b/apps/staff/src/app/(protected)/round-1-comp/_components/team-table/queries.ts new file mode 100644 index 00000000..7f15dc9b --- /dev/null +++ b/apps/staff/src/app/(protected)/round-1-comp/_components/team-table/queries.ts @@ -0,0 +1,87 @@ +"use server" + +import { GetRound1CompTeamsSchema } from "@/app/(protected)/round-1-comp/_components/team-table/validations" +import { shouldColorSchoolRed } from "@/lib/school-utils" +import { unstable_cache } from "@/lib/unstable-cache" +import { db, teams, round1Verification, member } from "@workspace/db" +import { and, asc, ilike, eq, or } from "@workspace/db/orm" + +export async function getRound1CompTeams(input: GetRound1CompTeamsSchema) { + return await unstable_cache( + async () => { + try { + const offset = (input.page - 1) * input.perPage + + const orderBy = [asc(teams.index)] + + const baseWhere = and( + // Only teams with DONE verification status + eq(round1Verification.status, "DONE"), + input.name ? ilike(teams.name, `%${input.name}%`) : undefined, + (() => { + const q = input.codeName?.trim() + if (!q) return undefined + const digits = q.replace(/\D/g, "") + if (!digits) return undefined + return eq(teams.index, parseInt(digits, 10)) + })(), + input.school ? ilike(teams.school, `%${input.school}%`) : undefined, + input.memberCount.length > 0 + ? or(...input.memberCount.map((count) => eq(teams.memberCount, parseInt(count)))) + : undefined, + input.email ? ilike(member.email, `%${input.email}%`) : undefined + ) + + const { data, total } = await db.transaction(async (tx) => { + const allTeams = await tx + .select({ + id: teams.id, + name: teams.name, + school: teams.school, + memberCount: teams.memberCount, + index: teams.index, + createdAt: teams.createdAt, + notes: round1Verification.notes, + firstMemberEmail: member.email, + }) + .from(teams) + .innerJoin(round1Verification, eq(round1Verification.teamId, teams.id)) + .leftJoin( + member, + and( + eq(member.teamId, teams.id), + eq(member.index, 1) // First member + ) + ) + .where(baseWhere) + .orderBy(...orderBy) + + const allSchoolNames = allTeams.map((team) => team.school) + + const teamsWithRedFlag = allTeams.map((team) => ({ + ...team, + rowShouldBeRed: shouldColorSchoolRed(team.school, allSchoolNames), + })) + + const data = teamsWithRedFlag.slice(offset, offset + input.perPage) + const total = teamsWithRedFlag.length + + return { + data, + total, + } + }) + + const pageCount = Math.ceil(total / input.perPage) + return { data, pageCount } + } catch { + return { data: [], pageCount: 0 } + } + }, + [JSON.stringify(input)], + { + revalidate: 1, + tags: ["round1-comp-teams"], + } + )() +} diff --git a/apps/staff/src/app/(protected)/round-1-comp/_components/team-table/validations.ts b/apps/staff/src/app/(protected)/round-1-comp/_components/team-table/validations.ts new file mode 100644 index 00000000..ae86ea3a --- /dev/null +++ b/apps/staff/src/app/(protected)/round-1-comp/_components/team-table/validations.ts @@ -0,0 +1,16 @@ +import { getFiltersStateParser } from "@/lib/parsers" +import { createSearchParamsCache, parseAsArrayOf, parseAsInteger, parseAsString } from "nuqs/server" +import * as z from "zod" + +export const searchParamsCache = createSearchParamsCache({ + page: parseAsInteger.withDefault(1), + perPage: parseAsInteger.withDefault(10), + name: parseAsString.withDefault(""), + codeName: parseAsString.withDefault(""), + school: parseAsString.withDefault(""), + memberCount: parseAsArrayOf(z.enum(["2", "3"])).withDefault([]), + email: parseAsString.withDefault(""), + filters: getFiltersStateParser().withDefault([]), +}) + +export type GetRound1CompTeamsSchema = Awaited> diff --git a/apps/staff/src/app/(protected)/round-1-comp/page.tsx b/apps/staff/src/app/(protected)/round-1-comp/page.tsx new file mode 100644 index 00000000..a51ad110 --- /dev/null +++ b/apps/staff/src/app/(protected)/round-1-comp/page.tsx @@ -0,0 +1,46 @@ +import Round1CompTeamTable from "@/app/(protected)/round-1-comp/_components/team-table" +import { getRound1CompTeams } from "@/app/(protected)/round-1-comp/_components/team-table/queries" +import { searchParamsCache } from "@/app/(protected)/round-1-comp/_components/team-table/validations" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { getValidFilters } from "@/lib/data-table" +import { SearchParams } from "@/types" +import { Suspense } from "react" + +interface Round1CompPageProps { + searchParams: Promise +} + +async function Round1CompPage(props: Round1CompPageProps) { + const searchParams = await props.searchParams + const search = searchParamsCache.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getRound1CompTeams({ + ...search, + filters: validFilters, + }), + ]) + + return ( +
    +
    +

    Round 1 Competition

    + + }> + + +
    +
    + ) +} + +export default Round1CompPage diff --git a/apps/staff/src/app/(protected)/round-1/_components/team-table/columns.tsx b/apps/staff/src/app/(protected)/round-1/_components/team-table/columns.tsx index ff83018b..04e49dc9 100644 --- a/apps/staff/src/app/(protected)/round-1/_components/team-table/columns.tsx +++ b/apps/staff/src/app/(protected)/round-1/_components/team-table/columns.tsx @@ -1,9 +1,10 @@ +import TeamDialog from "@/app/(protected)/_components/team-dialog" import { formatCodeName, RegsiterStatusToIcon, RegisterStatusToColorClass, } from "@/app/(protected)/round-1/_components/team-table/format" -import VerifyDialog from "@/app/(protected)/round-1/_components/verifly-dialog" +import { VerifyFormParent } from "@/app/(protected)/round-1/_components/verification/form" import { Button } from "@/components/ui/button" import { RelativeTimeCard } from "@/components/ui/relative-time-card" import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip" @@ -231,7 +232,11 @@ export const columns = [ columnHelper.display({ id: "verifyAction", header: "Action", - cell: ({ row }) => , + cell: ({ row }) => ( + + + + ), }), ] diff --git a/apps/staff/src/app/(protected)/round-1/_components/verifly-dialog/action.ts b/apps/staff/src/app/(protected)/round-1/_components/verification/action.ts similarity index 100% rename from apps/staff/src/app/(protected)/round-1/_components/verifly-dialog/action.ts rename to apps/staff/src/app/(protected)/round-1/_components/verification/action.ts diff --git a/apps/staff/src/app/(protected)/round-1/_components/verifly-dialog/form.tsx b/apps/staff/src/app/(protected)/round-1/_components/verification/form.tsx similarity index 99% rename from apps/staff/src/app/(protected)/round-1/_components/verifly-dialog/form.tsx rename to apps/staff/src/app/(protected)/round-1/_components/verification/form.tsx index 01b13583..9649f839 100644 --- a/apps/staff/src/app/(protected)/round-1/_components/verifly-dialog/form.tsx +++ b/apps/staff/src/app/(protected)/round-1/_components/verification/form.tsx @@ -3,7 +3,7 @@ import { submitRound1Verification, getRound1Verification, -} from "@/app/(protected)/round-1/_components/verifly-dialog/action" +} from "@/app/(protected)/round-1/_components/verification/action" import { Button } from "@/components/ui/button" import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form" import MultipleSelector from "@/components/ui/multiselect" diff --git a/apps/staff/src/app/(protected)/round-1/_components/verifly-dialog/context.tsx b/apps/staff/src/app/(protected)/round-1/_components/verifly-dialog/context.tsx deleted file mode 100644 index 0e9d2ac2..00000000 --- a/apps/staff/src/app/(protected)/round-1/_components/verifly-dialog/context.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { createContext, useContext } from "react" - -export interface VerifyDialogContextValue { - id: string -} - -export const VerifyDialogContext = createContext(undefined) - -export function useVerifyDialogContext(): VerifyDialogContextValue { - const context = useContext(VerifyDialogContext) - if (!context) { - throw new Error("useVerifyDialogContext must be used within a VerifyDialogContext.Provider") - } - return context -} diff --git a/apps/staff/src/app/(protected)/round-1/_components/verifly-dialog/index.tsx b/apps/staff/src/app/(protected)/round-1/_components/verifly-dialog/index.tsx deleted file mode 100644 index 2be520e5..00000000 --- a/apps/staff/src/app/(protected)/round-1/_components/verifly-dialog/index.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import AdviserDisplay from "@/app/(protected)/round-1/_components/verifly-dialog/adviser" -import { - VerifyDialogContext, - VerifyDialogContextValue, -} from "@/app/(protected)/round-1/_components/verifly-dialog/context" -import { VerifyFormParent } from "@/app/(protected)/round-1/_components/verifly-dialog/form" -import Member1Display from "@/app/(protected)/round-1/_components/verifly-dialog/member1" -import Member2Display from "@/app/(protected)/round-1/_components/verifly-dialog/member2" -import Member3Display from "@/app/(protected)/round-1/_components/verifly-dialog/member3" -import TeamDisplay from "@/app/(protected)/round-1/_components/verifly-dialog/team" -import { Button } from "@/components/ui/button" -import { - Dialog, - DialogContent, - DialogDescription, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "@/components/ui/dialog" -// import { Separator } from "@/components/ui/separator" -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" -import { SearchCheckIcon, UserIcon } from "lucide-react" -import { parseAsString, useQueryState } from "nuqs" -import { ReactNode } from "react" - -interface VerifyDialogProps extends VerifyDialogContextValue { - children?: ReactNode -} - -function VerifyDialog(props: VerifyDialogProps) { - const [verify, setVerify] = useQueryState("verify", parseAsString.withDefault("")) - - const [tab, setTab] = useQueryState("verify-tab", parseAsString.withDefault("")) - const onTabChange = (value: string) => setTab(value) - - return ( - - { - setVerify(isOpen ? props.id : "") - setTab(null) - }}> - - {props.children ?? ( - - )} - - - - - -
    - - - Team - Adviser - - 1 - - - 2 - - - 3 - - - - - - - - - - - - - - - - - - -
    - setVerify("")} /> -
    -
    -
    -
    -
    -
    - ) -} -export default VerifyDialog diff --git a/apps/staff/src/app/(protected)/team-award/_components/team-award-table/columns.tsx b/apps/staff/src/app/(protected)/team-award/_components/team-award-table/columns.tsx new file mode 100644 index 00000000..b143f847 --- /dev/null +++ b/apps/staff/src/app/(protected)/team-award/_components/team-award-table/columns.tsx @@ -0,0 +1,98 @@ +import TeamDialog from "@/app/(protected)/_components/team-dialog" +import { formatCodeName } from "@/app/(protected)/round-1/_components/team-table/format" +import { createColumnHelper } from "@tanstack/react-table" +import { teams } from "@workspace/db/schema" +import { Building2, School, Trophy, Text } from "lucide-react" + +export type TeamAward = Pick< + typeof teams.$inferSelect, + "id" | "name" | "school" | "award" | "createdAt" | "index" +> + +const columnHelper = createColumnHelper() + +export const columns = [ + columnHelper.accessor("index", { + id: "codeName", + header: "Code Name", + cell: (info) => { + const code = formatCodeName(info.row.original.index) + const prefix = "BMHK" + const suffix = code.replace(prefix, "") + return ( +
    + {prefix} + {suffix} +
    + ) + }, + enableSorting: false, + enableColumnFilter: true, + meta: { + label: "Code Name", + placeholder: "Search code names...", + variant: "text", + icon: Text, + }, + }), + columnHelper.accessor("name", { + id: "name", + header: "Team Name", + cell: (info) =>
    {info.getValue()}
    , + enableSorting: false, + enableColumnFilter: true, + meta: { + label: "Team Name", + placeholder: "Search team names...", + variant: "text", + icon: Text, + }, + }), + columnHelper.accessor("school", { + id: "school", + header: "School", + cell: (info) => { + const schoolName = info.getValue() + return ( +
    + + {schoolName} +
    + ) + }, + enableSorting: false, + enableColumnFilter: true, + meta: { + label: "School", + placeholder: "Search schools...", + variant: "text", + icon: Building2, + }, + }), + columnHelper.accessor("award", { + id: "award", + header: "Award", + cell: (info) => { + const award = info.getValue() + return ( +
    + + {award} +
    + ) + }, + enableSorting: false, + enableColumnFilter: true, + meta: { + label: "Award", + placeholder: "Search awards...", + variant: "text", + icon: Trophy, + }, + }), + columnHelper.display({ + id: "action", + header: "Action", + cell: ({ row }) => , + }), +] diff --git a/apps/staff/src/app/(protected)/team-award/_components/team-award-table/index.tsx b/apps/staff/src/app/(protected)/team-award/_components/team-award-table/index.tsx new file mode 100644 index 00000000..6be75aa4 --- /dev/null +++ b/apps/staff/src/app/(protected)/team-award/_components/team-award-table/index.tsx @@ -0,0 +1,36 @@ +"use client" + +import { columns } from "@/app/(protected)/team-award/_components/team-award-table/columns" +import { getTeamAwards } from "@/app/(protected)/team-award/_components/team-award-table/queries" +import { DataTable } from "@/components/data-table/data-table" +import { DataTableToolbar } from "@/components/data-table/data-table-toolbar" +import { useDataTable } from "@/hooks/use-data-table" +import { use } from "react" + +interface TeamAwardTableProps { + promises: Promise<[Awaited>]> +} + +function TeamAwardTable({ promises }: TeamAwardTableProps) { + const [{ data, pageCount }] = use(promises) + + const { table } = useDataTable({ + data, + columns, + getRowId: (row) => row.id, + pageCount, + initialState: { + columnVisibility: {}, + }, + shallow: false, + clearOnDefault: true, + }) + + return ( + + + + ) +} + +export default TeamAwardTable diff --git a/apps/staff/src/app/(protected)/team-award/_components/team-award-table/queries.ts b/apps/staff/src/app/(protected)/team-award/_components/team-award-table/queries.ts new file mode 100644 index 00000000..e1839e6f --- /dev/null +++ b/apps/staff/src/app/(protected)/team-award/_components/team-award-table/queries.ts @@ -0,0 +1,67 @@ +"use server" + +import { GetTeamAwardsSchema } from "@/app/(protected)/team-award/_components/team-award-table/validations" +import { unstable_cache } from "@/lib/unstable-cache" +import { db, teams, round1Verification } from "@workspace/db" +import { and, asc, ilike, eq } from "@workspace/db/orm" + +export async function getTeamAwards(input: GetTeamAwardsSchema) { + return await unstable_cache( + async () => { + try { + const offset = (input.page - 1) * input.perPage + + const orderBy = [asc(teams.index)] + + const baseWhere = and( + // Only show teams where round-1-verification status is DONE + eq(round1Verification.status, "DONE"), + input.name ? ilike(teams.name, `%${input.name}%`) : undefined, + (() => { + const q = input.codeName?.trim() + if (!q) return undefined + const digits = q.replace(/\D/g, "") + if (!digits) return undefined + return eq(teams.index, parseInt(digits, 10)) + })(), + input.school ? ilike(teams.school, `%${input.school}%`) : undefined, + input.award ? ilike(teams.award, `%${input.award}%`) : undefined + ) + + const { data, total } = await db.transaction(async (tx) => { + const allTeams = await tx + .select({ + id: teams.id, + name: teams.name, + school: teams.school, + award: teams.award, + index: teams.index, + createdAt: teams.createdAt, + }) + .from(teams) + .innerJoin(round1Verification, eq(round1Verification.teamId, teams.id)) + .where(baseWhere) + .orderBy(...orderBy) + + const data = allTeams.slice(offset, offset + input.perPage) + const total = allTeams.length + + return { + data, + total, + } + }) + + const pageCount = Math.ceil(total / input.perPage) + return { data, pageCount } + } catch { + return { data: [], pageCount: 0 } + } + }, + [JSON.stringify(input)], + { + revalidate: 1, + tags: ["team-awards"], + } + )() +} diff --git a/apps/staff/src/app/(protected)/team-award/_components/team-award-table/validations.ts b/apps/staff/src/app/(protected)/team-award/_components/team-award-table/validations.ts new file mode 100644 index 00000000..cba042a9 --- /dev/null +++ b/apps/staff/src/app/(protected)/team-award/_components/team-award-table/validations.ts @@ -0,0 +1,14 @@ +import { getFiltersStateParser } from "@/lib/parsers" +import { createSearchParamsCache, parseAsInteger, parseAsString } from "nuqs/server" + +export const searchParamsCache = createSearchParamsCache({ + page: parseAsInteger.withDefault(1), + perPage: parseAsInteger.withDefault(10), + name: parseAsString.withDefault(""), + codeName: parseAsString.withDefault(""), + school: parseAsString.withDefault(""), + award: parseAsString.withDefault(""), + filters: getFiltersStateParser().withDefault([]), +}) + +export type GetTeamAwardsSchema = Awaited> diff --git a/apps/staff/src/app/(protected)/team-award/page.tsx b/apps/staff/src/app/(protected)/team-award/page.tsx new file mode 100644 index 00000000..9cef37a3 --- /dev/null +++ b/apps/staff/src/app/(protected)/team-award/page.tsx @@ -0,0 +1,46 @@ +import TeamAwardTable from "@/app/(protected)/team-award/_components/team-award-table" +import { getTeamAwards } from "@/app/(protected)/team-award/_components/team-award-table/queries" +import { searchParamsCache } from "@/app/(protected)/team-award/_components/team-award-table/validations" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { getValidFilters } from "@/lib/data-table" +import { SearchParams } from "@/types" +import { Suspense } from "react" + +interface TeamAwardPageProps { + searchParams: Promise +} + +async function TeamAwardPage(props: TeamAwardPageProps) { + const searchParams = await props.searchParams + const search = searchParamsCache.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + const promises = Promise.all([ + getTeamAwards({ + ...search, + filters: validFilters, + }), + ]) + + return ( +
    +
    +

    Team Award

    + + }> + + +
    +
    + ) +} + +export default TeamAwardPage diff --git a/apps/staff/src/lib/csv-export.ts b/apps/staff/src/lib/csv-export.ts new file mode 100644 index 00000000..8dcb43a0 --- /dev/null +++ b/apps/staff/src/lib/csv-export.ts @@ -0,0 +1,91 @@ +/** + * Utility functions for CSV export functionality + */ + +export interface CSVExportOptions { + filename?: string + includeHeaders?: boolean +} + +/** + * Converts an array of objects to CSV format + */ +export function arrayToCSV>( + data: T[], + options: CSVExportOptions = {} +): string { + if (data.length === 0) return "" + + const { includeHeaders = true } = options + + // Get all unique keys from all objects + const allKeys = Array.from(new Set(data.flatMap((item) => Object.keys(item)))) + + // Create CSV content + const csvRows: string[] = [] + + // Add headers if requested + if (includeHeaders) { + csvRows.push(allKeys.map(escapeCSVField).join(",")) + } + + // Add data rows + for (const item of data) { + const row = allKeys.map((key) => { + const value = item[key] + return escapeCSVField(value) + }) + csvRows.push(row.join(",")) + } + + return csvRows.join("\n") +} + +/** + * Escapes a field value for CSV format + */ +function escapeCSVField(value: unknown): string { + if (value === null || value === undefined) { + return "" + } + + const stringValue = String(value) + + // If the value contains comma, newline, or double quote, wrap in quotes and escape quotes + if (stringValue.includes(",") || stringValue.includes("\n") || stringValue.includes('"')) { + return `"${stringValue.replace(/"/g, '""')}"` + } + + return stringValue +} + +/** + * Downloads a CSV string as a file + */ +export function downloadCSV(csvContent: string, filename: string = "export.csv"): void { + const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" }) + const link = document.createElement("a") + + if (link.download !== undefined) { + const url = URL.createObjectURL(blob) + link.setAttribute("href", url) + link.setAttribute("download", filename) + link.style.visibility = "hidden" + document.body.appendChild(link) + link.click() + document.body.removeChild(link) + URL.revokeObjectURL(url) + } +} + +/** + * Exports table data to CSV and downloads it + */ +export function exportTableToCSV>( + data: T[], + options: CSVExportOptions = {} +): void { + const { filename = "export.csv" } = options + const csvContent = arrayToCSV(data, options) + downloadCSV(csvContent, filename) +} From 467a3fa52f46abe6724ec230c6adb61f3139724a Mon Sep 17 00:00:00 2001 From: beambeambeam Date: Sun, 14 Sep 2025 20:09:51 +0700 Subject: [PATCH 4/6] refactor: update code name prefix from "BMHK" to "BH" across team table components and related formatting functions --- .../_components/team-table/columns.tsx | 9 ++++++- .../_components/team-table/index.tsx | 16 +++++++++++- .../_components/team-table/queries.ts | 26 +++++++++++++++++-- .../_components/team-table/columns.tsx | 2 +- .../round-1/_components/team-table/format.ts | 2 +- .../_components/team-award-table/columns.tsx | 2 +- 6 files changed, 50 insertions(+), 7 deletions(-) diff --git a/apps/staff/src/app/(protected)/round-1-comp/_components/team-table/columns.tsx b/apps/staff/src/app/(protected)/round-1-comp/_components/team-table/columns.tsx index e8ff907a..b6372c5f 100644 --- a/apps/staff/src/app/(protected)/round-1-comp/_components/team-table/columns.tsx +++ b/apps/staff/src/app/(protected)/round-1-comp/_components/team-table/columns.tsx @@ -13,6 +13,13 @@ export type Team = Pick< notes: string | null firstMemberEmail: string | null rowShouldBeRed: boolean + members: { + index: number + prefix: string + thaiFirstname: string + thaiMiddlename: string | null + thaiLastname: string + }[] } const columnHelper = createColumnHelper() @@ -23,7 +30,7 @@ export const columns = [ header: "Code Name", cell: (info) => { const code = formatCodeName(info.row.original.index) - const prefix = "BMHK" + const prefix = "BH" const suffix = code.replace(prefix, "") return (
    diff --git a/apps/staff/src/app/(protected)/round-1-comp/_components/team-table/index.tsx b/apps/staff/src/app/(protected)/round-1-comp/_components/team-table/index.tsx index 103dd39a..e5036250 100644 --- a/apps/staff/src/app/(protected)/round-1-comp/_components/team-table/index.tsx +++ b/apps/staff/src/app/(protected)/round-1-comp/_components/team-table/index.tsx @@ -7,6 +7,7 @@ import { DataTableToolbar } from "@/components/data-table/data-table-toolbar" import { Button } from "@/components/ui/button" import { useDataTable } from "@/hooks/use-data-table" import { exportTableToCSV } from "@/lib/csv-export" +import { mapPrefixToThai } from "@/lib/format" import { Row } from "@tanstack/react-table" import { Download } from "lucide-react" import { CSSProperties, use, useCallback } from "react" @@ -39,11 +40,24 @@ function Round1CompTeamTable({ promises }: Round1CompTeamTableProps) { const handleExportCSV = useCallback(() => { const filteredData = table.getFilteredRowModel().rows.map((row, index) => { const team = row.original + + const formatThaiName = (member: (typeof team.members)[0]) => { + const middleName = member.thaiMiddlename ? ` ${member.thaiMiddlename}` : "" + return `${mapPrefixToThai(member.prefix)}${member.thaiFirstname}${middleName} ${member.thaiLastname}` + } + + const member1 = team.members.find((m) => m.index === 1) + const member2 = team.members.find((m) => m.index === 2) + const member3 = team.members.find((m) => m.index === 3) + return { Index: `${index + 1}`, - CodeName: `BMHK${team.index.toString().padStart(3, "0")}`, + CodeName: `BH${team.index.toString().padStart(3, "0")}`, TeamName: team.name, School: team.school, + "สมาชิกคนที่ 1": member1 ? formatThaiName(member1) : "", + "สมาชิกคนที่ 2": member2 ? formatThaiName(member2) : "", + "สมาชิกคนที่ 3": member3 ? formatThaiName(member3) : "", FirstMemberEmail: team.firstMemberEmail || "", Notes: team.notes || "", } diff --git a/apps/staff/src/app/(protected)/round-1-comp/_components/team-table/queries.ts b/apps/staff/src/app/(protected)/round-1-comp/_components/team-table/queries.ts index 7f15dc9b..0a91df4c 100644 --- a/apps/staff/src/app/(protected)/round-1-comp/_components/team-table/queries.ts +++ b/apps/staff/src/app/(protected)/round-1-comp/_components/team-table/queries.ts @@ -56,9 +56,31 @@ export async function getRound1CompTeams(input: GetRound1CompTeamsSchema) { .where(baseWhere) .orderBy(...orderBy) - const allSchoolNames = allTeams.map((team) => team.school) + // Fetch all members for each team + const teamsWithMembers = await Promise.all( + allTeams.map(async (team) => { + const members = await tx + .select({ + index: member.index, + prefix: member.prefix, + thaiFirstname: member.thaiFirstname, + thaiMiddlename: member.thaiMiddlename, + thaiLastname: member.thaiLastname, + }) + .from(member) + .where(eq(member.teamId, team.id)) + .orderBy(asc(member.index)) - const teamsWithRedFlag = allTeams.map((team) => ({ + return { + ...team, + members, + } + }) + ) + + const allSchoolNames = teamsWithMembers.map((team) => team.school) + + const teamsWithRedFlag = teamsWithMembers.map((team) => ({ ...team, rowShouldBeRed: shouldColorSchoolRed(team.school, allSchoolNames), })) diff --git a/apps/staff/src/app/(protected)/round-1/_components/team-table/columns.tsx b/apps/staff/src/app/(protected)/round-1/_components/team-table/columns.tsx index 04e49dc9..1020ea8f 100644 --- a/apps/staff/src/app/(protected)/round-1/_components/team-table/columns.tsx +++ b/apps/staff/src/app/(protected)/round-1/_components/team-table/columns.tsx @@ -55,7 +55,7 @@ export const columns = [ header: "Code Name", cell: (info) => { const code = formatCodeName(info.row.original.index) - const prefix = "BMHK" + const prefix = "BH" const suffix = code.replace(prefix, "") return (
    diff --git a/apps/staff/src/app/(protected)/round-1/_components/team-table/format.ts b/apps/staff/src/app/(protected)/round-1/_components/team-table/format.ts index 282e40af..2ed82d8c 100644 --- a/apps/staff/src/app/(protected)/round-1/_components/team-table/format.ts +++ b/apps/staff/src/app/(protected)/round-1/_components/team-table/format.ts @@ -29,5 +29,5 @@ export function RegisterStatusToColorClass(status: "DONE" | "NOT_DONE" | "NOT_HA export function formatCodeName(index: number) { const number = String(index).padStart(3, "0") - return `BMHK${number}` + return `BH${number}` } diff --git a/apps/staff/src/app/(protected)/team-award/_components/team-award-table/columns.tsx b/apps/staff/src/app/(protected)/team-award/_components/team-award-table/columns.tsx index b143f847..824b2c3e 100644 --- a/apps/staff/src/app/(protected)/team-award/_components/team-award-table/columns.tsx +++ b/apps/staff/src/app/(protected)/team-award/_components/team-award-table/columns.tsx @@ -17,7 +17,7 @@ export const columns = [ header: "Code Name", cell: (info) => { const code = formatCodeName(info.row.original.index) - const prefix = "BMHK" + const prefix = "BH" const suffix = code.replace(prefix, "") return (
    From e24ae9cb604bdd26ad8dcbc82dd28b923f8c49c4 Mon Sep 17 00:00:00 2001 From: beambeambeam Date: Mon, 15 Sep 2025 20:20:46 +0700 Subject: [PATCH 5/6] feat: add missing array type for members in Team type definition --- .../(protected)/round-1-comp/_components/team-table/columns.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/staff/src/app/(protected)/round-1-comp/_components/team-table/columns.tsx b/apps/staff/src/app/(protected)/round-1-comp/_components/team-table/columns.tsx index 775898db..ec2a2003 100644 --- a/apps/staff/src/app/(protected)/round-1-comp/_components/team-table/columns.tsx +++ b/apps/staff/src/app/(protected)/round-1-comp/_components/team-table/columns.tsx @@ -19,6 +19,7 @@ export type Team = Pick< thaiFirstname: string thaiMiddlename: string | null thaiLastname: string + }[] advisor: { prefix: string thaiFirstname: string From 9f0d3c38f01fbe56356867a6b700d1ef9cc2b9e5 Mon Sep 17 00:00:00 2001 From: Jakkaphat Chalermphanaphan Date: Thu, 25 Sep 2025 02:20:17 +0700 Subject: [PATCH 6/6] feat: sponsors section (#198) --- apps/web/app/_components/footer.tsx | 19 +++------- apps/web/app/_components/sponsors/index.tsx | 26 +++++++++++++ .../_components/sponsors/logo-renderer.tsx | 31 ++++++++++++++++ apps/web/app/_components/sponsors/subgrid.tsx | 35 ++++++++++++++++++ apps/web/app/page.tsx | 5 +++ apps/web/config/sponsors.ts | 21 +++++++++++ .../static/logo/kmutt65-cpe-white-160px.webp | Bin 0 -> 5832 bytes .../web/public/static/sponsors/ire-white.webp | Bin 0 -> 3170 bytes apps/web/public/static/sponsors/ire.webp | Bin 0 -> 8152 bytes packages/ui/package.json | 1 + packages/ui/src/components/separator.tsx | 27 ++++++++++++++ pnpm-lock.yaml | 22 ++++++++--- 12 files changed, 169 insertions(+), 18 deletions(-) create mode 100644 apps/web/app/_components/sponsors/index.tsx create mode 100644 apps/web/app/_components/sponsors/logo-renderer.tsx create mode 100644 apps/web/app/_components/sponsors/subgrid.tsx create mode 100644 apps/web/config/sponsors.ts create mode 100644 apps/web/public/static/logo/kmutt65-cpe-white-160px.webp create mode 100644 apps/web/public/static/sponsors/ire-white.webp create mode 100644 apps/web/public/static/sponsors/ire.webp create mode 100644 packages/ui/src/components/separator.tsx diff --git a/apps/web/app/_components/footer.tsx b/apps/web/app/_components/footer.tsx index 8909b2c8..7ccbf09c 100644 --- a/apps/web/app/_components/footer.tsx +++ b/apps/web/app/_components/footer.tsx @@ -3,36 +3,29 @@ import GlassCard from "@/components/glassCard" import IconCircle from "@/components/iconCircle" import { siteConfig } from "@/config/site" +import { SponsorList, SponsorTiers } from "@/config/sponsors" import Link from "next/link" -interface Sponsor { - name: string - image_path: string -} - function Sponsor() { - const SPONSOR_LIST: Sponsor[] = [ - /* TBA */ - ] - if (SPONSOR_LIST.length < 1) return <> + if (SponsorList.length < 1) return <> return (
    สนับสนุนโดย
    - {SPONSOR_LIST.map((s) => ( + {SponsorList.map((s) => ( - + ))}
    - เราขอขอบคุณ {SPONSOR_LIST.map((s) => s.name).join(", ")} + เราขอขอบคุณ {SponsorList.map((s) => s.name).join(", ")}
    diff --git a/apps/web/app/_components/sponsors/index.tsx b/apps/web/app/_components/sponsors/index.tsx new file mode 100644 index 00000000..7130d3d7 --- /dev/null +++ b/apps/web/app/_components/sponsors/index.tsx @@ -0,0 +1,26 @@ +import { Heading } from "@/components/heading" +import { SponsorList, SponsorTiers } from "@/config/sponsors" + +import SponsorSubgrid from "./subgrid" + +export default function Sponsors() { + return ( +
    + +
    + kmutt cpe logo +
    + +
    + + + +
    +
    + ) +} diff --git a/apps/web/app/_components/sponsors/logo-renderer.tsx b/apps/web/app/_components/sponsors/logo-renderer.tsx new file mode 100644 index 00000000..7e4403e2 --- /dev/null +++ b/apps/web/app/_components/sponsors/logo-renderer.tsx @@ -0,0 +1,31 @@ +import GlassCard from "@/components/glassCard" +import { Sponsor } from "@/config/sponsors" +import { SponsorTiers } from "@/config/sponsors" + +interface SponsorLogoRendererProps { + data: Sponsor +} + +const SponsorLogoRenderer = ({ data: s }: SponsorLogoRendererProps) => { + let h = 60 + + if (s.tier === SponsorTiers.diamond) { + h = 160 + } else if (s.tier === SponsorTiers.platinum) { + h = 100 + } + + return ( +
    + {/* eslint-disable-next-line @next/next/no-img-element */} + {`${s.name} +
    + ) +} + +export default SponsorLogoRenderer diff --git a/apps/web/app/_components/sponsors/subgrid.tsx b/apps/web/app/_components/sponsors/subgrid.tsx new file mode 100644 index 00000000..321e4150 --- /dev/null +++ b/apps/web/app/_components/sponsors/subgrid.tsx @@ -0,0 +1,35 @@ +import { Sponsor } from "@/config/sponsors" +import { SponsorTiers } from "@/config/sponsors" + +import SponsorLogoRenderer from "./logo-renderer" + +interface SponsorSubgridProps { + data: Sponsor[] + tier: SponsorTiers +} + +function getTierName(t: SponsorTiers): string { + return t === SponsorTiers.diamond ? "Diamond" : t === SponsorTiers.platinum ? "Platinum" : "Gold" +} + +export default function SponsorSubgrid({ data, tier }: SponsorSubgridProps) { + const amt = data.filter((sp) => sp.tier === tier).length + const gridClass = amt === 1 ? "md:grid-cols-1" : amt === 2 ? "md:grid-cols-2" : "md:grid-cols-3" + + if (amt === 0) return <> + + return ( +
    + + {getTierName(tier)} Sponsor{amt > 1 ? "s" : ""} + +
    + {data + .filter((sp) => sp.tier === tier) + .map((sp) => ( + + ))} +
    +
    + ) +} diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx index 9ff63b7c..863bcd60 100644 --- a/apps/web/app/page.tsx +++ b/apps/web/app/page.tsx @@ -12,6 +12,8 @@ import Scope from "@/app/_components/scope" import Story from "@/app/_components/story" import Head from "next/head" +import Sponsors from "./_components/sponsors" + export default function Page() { return ( <> @@ -101,6 +103,9 @@ export default function Page() {
    +
    + +
    diff --git a/apps/web/config/sponsors.ts b/apps/web/config/sponsors.ts new file mode 100644 index 00000000..cba00295 --- /dev/null +++ b/apps/web/config/sponsors.ts @@ -0,0 +1,21 @@ +export enum SponsorTiers { + "diamond" = 3, + "platinum" = 2, + "gold" = 1, +} + +export interface Sponsor { + name: string + tier: SponsorTiers + image_path: string + link?: string +} + +export const SponsorList: Sponsor[] = [ + { + name: "IRE Learning", + tier: SponsorTiers.gold, + image_path: "/static/sponsors/ire-white.webp", + link: "https://www.facebook.com/iretutor/", + }, +] diff --git a/apps/web/public/static/logo/kmutt65-cpe-white-160px.webp b/apps/web/public/static/logo/kmutt65-cpe-white-160px.webp new file mode 100644 index 0000000000000000000000000000000000000000..bbf8ef00af9bb3e9dba02212ec4fffb43ad0471a GIT binary patch literal 5832 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 SVlLy#4}^JK9}B>2m=ATH`FLyf{x{1 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 I-)v9-0B%F_?f?J) literal 0 HcmV?d00001 diff --git a/apps/web/public/static/sponsors/ire.webp b/apps/web/public/static/sponsors/ire.webp new file mode 100644 index 0000000000000000000000000000000000000000..93f1bdf83e418b7f964a55ede2d137722ed46dd7 GIT binary patch literal 8152 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 literal 0 HcmV?d00001 diff --git a/packages/ui/package.json b/packages/ui/package.json index 460b6b50..748ee977 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -13,6 +13,7 @@ "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-label": "^2.1.7", "@radix-ui/react-select": "^2.2.6", + "@radix-ui/react-separator": "^1.1.7", "@radix-ui/react-slot": "^1.2.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", diff --git a/packages/ui/src/components/separator.tsx b/packages/ui/src/components/separator.tsx new file mode 100644 index 00000000..a6485017 --- /dev/null +++ b/packages/ui/src/components/separator.tsx @@ -0,0 +1,27 @@ +"use client" + +import * as SeparatorPrimitive from "@radix-ui/react-separator" +import { cn } from "@workspace/ui/lib/utils" +import * as React from "react" + +function Separator({ + className, + orientation = "horizontal", + decorative = true, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { Separator } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ae18b14e..fef2c91c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -480,6 +480,9 @@ importers: '@radix-ui/react-select': specifier: ^2.2.6 version: 2.2.6(@types/react-dom@19.1.7(@types/react@19.1.10))(@types/react@19.1.10)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-separator': + specifier: ^1.1.7 + version: 1.1.7(@types/react-dom@19.1.7(@types/react@19.1.10))(@types/react@19.1.10)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) '@radix-ui/react-slot': specifier: ^1.2.3 version: 1.2.3(@types/react@19.1.10)(react@19.1.1) @@ -8341,6 +8344,15 @@ snapshots: '@types/react': 19.1.10 '@types/react-dom': 19.1.7(@types/react@19.1.10) + '@radix-ui/react-separator@1.1.7(@types/react-dom@19.1.7(@types/react@19.1.10))(@types/react@19.1.10)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.7(@types/react@19.1.10))(@types/react@19.1.10)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) + optionalDependencies: + '@types/react': 19.1.10 + '@types/react-dom': 19.1.7(@types/react@19.1.10) + '@radix-ui/react-slider@1.3.6(@types/react-dom@19.1.7(@types/react@19.1.10))(@types/react@19.1.10)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: '@radix-ui/number': 1.1.1 @@ -10318,7 +10330,7 @@ snapshots: '@typescript-eslint/parser': 8.39.1(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2) eslint: 9.33.0(jiti@2.5.1) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.33.0(jiti@2.5.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.39.1(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.33.0(jiti@2.5.1)))(eslint@9.33.0(jiti@2.5.1)) eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.39.1(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1)(eslint@9.33.0(jiti@2.5.1)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.33.0(jiti@2.5.1)) eslint-plugin-react: 7.37.5(eslint@9.33.0(jiti@2.5.1)) @@ -10342,7 +10354,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.33.0(jiti@2.5.1)): + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.39.1(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.33.0(jiti@2.5.1)))(eslint@9.33.0(jiti@2.5.1)): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.1 @@ -10357,14 +10369,14 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.39.1(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.33.0(jiti@2.5.1)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.39.1(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.39.1(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.33.0(jiti@2.5.1)))(eslint@9.33.0(jiti@2.5.1)))(eslint@9.33.0(jiti@2.5.1)): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 8.39.1(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2) eslint: 9.33.0(jiti@2.5.1) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.33.0(jiti@2.5.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.39.1(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.33.0(jiti@2.5.1)))(eslint@9.33.0(jiti@2.5.1)) transitivePeerDependencies: - supports-color @@ -10379,7 +10391,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.33.0(jiti@2.5.1) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.39.1(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.33.0(jiti@2.5.1)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.39.1(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.39.1(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.33.0(jiti@2.5.1)))(eslint@9.33.0(jiti@2.5.1)))(eslint@9.33.0(jiti@2.5.1)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3