Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
717 changes: 717 additions & 0 deletions .claude/skills/frontend-cyber/SKILL.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /app/api ./api
COPY --from=frontend /app/client/portal/dist ./static
COPY --from=frontend /app/client/portal/public/pwa-192x192.png ./client/portal/public/pwa-192x192.png
COPY --from=frontend /app/client/portal/src/assets/title-login.webp ./static/email-assets/zero-day-title.webp

EXPOSE 8080
CMD ["./api"]
11 changes: 6 additions & 5 deletions client/portal/branding/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ redesign completely.
| ------------------ | --------------------------------------------------------------------------- |
| `index.ts` | Product name, PWA manifest text, SuperTokens app name, browser theme colour |
| `theme.css` | The colour palette |
| `assets/logo.webp` | Sign-in page logo and browser favicon |
| `assets/logo.webp` | Sign-in page logo |

Change all three and you have rebranded the app's identity in about five
minutes. If that is as far as you get this year, it is a reasonable place to
Expand All @@ -32,8 +32,8 @@ the three do less than their names suggest.
tab, PWA install prompt, home-screen label, Android status bar, the fallback
title on a push notification, and the sender name on magic-link sign-in emails.

**`assets/logo.webp` — fully wired.** Favicon and sign-in page logo, from one
file.
**`assets/logo.webp` — the sign-in page logo.** The browser favicon comes from
the PWA icons in `public/`, so the two can be changed independently.

**`theme.css` — only partly.** It drives the admin and super-admin portal
completely; those pages are built from shadcn components that read the tokens.
Expand Down Expand Up @@ -98,8 +98,9 @@ upstream, since every school will want it.

`public/pwa-192x192.png` and `public/pwa-512x512.png` are **replace-in-place**:
swap the files, keep the names. They are referenced by absolute URL from the PWA
manifest and the service worker, and the Dockerfile copies the 192px one into
the runtime image for Apple Wallet passes. Renaming them breaks all three.
manifest, browser favicon, and service worker, and the Dockerfile copies the
192px one into the runtime image for Apple Wallet passes. Renaming them breaks
all four.

512×512 and 192×192 PNGs. The 512 also serves as the maskable icon, so keep
roughly 20% padding around the artwork or Android will crop into it.
Expand Down
15 changes: 8 additions & 7 deletions client/portal/branding/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,26 +20,27 @@ export const branding = {
* prompts, and as the fallback title on a push notification that arrives
* without one.
*/
appName: "Harp",
appName: "Zero Day Harp",

/** Full name in the PWA manifest — shown on the install prompt. */
fullName: "Harp Portal",
fullName: "Zero Day Harp",

/**
* Home-screen label once installed. Kept short deliberately: most launchers
* truncate past roughly 12 characters.
*/
shortName: "Harp",
shortName: "Zero Day Harp",

/** PWA manifest description, shown in some install UIs and app listings. */
description: "Hacker applications, reviews, and event operations.",
description:
"The official hacker portal for HackUTD 2026 Zero Day—applications, event access, schedules, and updates.",

/**
* Application name registered with SuperTokens. Appears in the subject and
* body of magic-link sign-in emails, so it should read as something a
* hacker recognises.
*/
authAppName: "Harp",
authAppName: "Zero Day Harp",

/**
* Browser UI colour — Android's status bar and the PWA splash screen.
Expand All @@ -50,8 +51,8 @@ export const branding = {
* the browser chrome rather than CSS, and support for modern colour
* functions here is inconsistent.
*/
themeColor: "#ffffff",
themeColor: "#020307",

/** Splash-screen background while the installed app starts up. */
backgroundColor: "#ffffff",
backgroundColor: "#020307",
} as const;
20 changes: 19 additions & 1 deletion client/portal/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,28 @@
<meta charset="UTF-8" />
<!-- Brand values below come from branding/index.ts, substituted at
build time by the harp-branding-html plugin in vite.config.ts. -->
<link rel="icon" type="image/webp" href="/branding/assets/logo.webp" />
<link rel="icon" type="image/png" href="/pwa-192x192.png" />
<link rel="apple-touch-icon" href="/pwa-192x192.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="%HARP_THEME_COLOR%" />
<meta name="color-scheme" content="dark" />
<meta name="application-name" content="%HARP_SHORT_NAME%" />
<meta name="description" content="%HARP_DESCRIPTION%" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta
name="apple-mobile-web-app-status-bar-style"
content="black-translucent"
/>
<meta name="apple-mobile-web-app-title" content="%HARP_SHORT_NAME%" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="%HARP_SHORT_NAME%" />
<meta property="og:title" content="%HARP_TITLE%" />
<meta property="og:description" content="%HARP_DESCRIPTION%" />
<meta property="og:image" content="/pwa-512x512.png" />
<meta name="twitter:card" content="summary" />
<meta name="twitter:title" content="%HARP_TITLE%" />
<meta name="twitter:description" content="%HARP_DESCRIPTION%" />
<meta name="twitter:image" content="/pwa-512x512.png" />
<title>%HARP_TITLE%</title>
</head>
<body>
Expand Down
Binary file modified client/portal/public/pwa-192x192.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified client/portal/public/pwa-512x512.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added client/portal/public/pwa.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added client/portal/src/assets/logo_glitch.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added client/portal/src/assets/mascots/dragonfly.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added client/portal/src/assets/mascots/jaguar.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added client/portal/src/assets/mascots/octopus.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed client/portal/src/assets/signin.webp
Binary file not shown.
Binary file added client/portal/src/assets/title-login.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added client/portal/src/assets/title.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
71 changes: 65 additions & 6 deletions client/portal/src/components/AuthFlowSkeleton.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,72 @@
import zeroDayTitle from "@/assets/title-login.webp";
import { Skeleton } from "@/components/ui/skeleton";

/**
* Loading state for the three public auth-flow pages (verify, callback, OAuth
* callback). It deliberately mirrors the Zero Day panel those pages resolve
* into — same canvas, HUD rail, title lockup, and panel geometry — so the
* handoff reads as the panel filling in rather than a new screen.
*/
export function AuthFlowSkeleton() {
return (
<div className="min-h-screen bg-linear-to-b from-gray-50 to-gray-100 flex items-center justify-center p-4">
<div className="w-full max-w-md space-y-4">
<Skeleton className="h-10 w-40 mx-auto" />
<Skeleton className="h-48 w-full rounded-xl" />
<Skeleton className="h-5 w-56 mx-auto" />
<main className="zero-login relative isolate min-h-svh overflow-hidden bg-black text-white">
<div
aria-hidden
className="zero-login-grid pointer-events-none absolute inset-0"
/>
<div
aria-hidden
className="zero-login-scanlines pointer-events-none absolute inset-0 z-20"
/>

<div className="pointer-events-none absolute inset-x-5 top-5 z-30 flex items-center justify-between font-mono text-[9px] tracking-[0.28em] text-white/45 uppercase sm:inset-x-8 sm:text-[10px]">
<span>HackUTD // Secure portal</span>
<span>MMXXVI</span>
</div>

<div className="relative z-30 flex min-h-svh items-center justify-center px-5 py-16">
<div
className="w-full max-w-[520px]"
role="status"
aria-label="Signing you in"
>
<img
src={zeroDayTitle}
alt="HackUTD Zero Day"
className="mx-auto mb-5 w-full max-w-[430px] object-contain"
/>

<section className="zero-login-panel relative p-px">
<div className="zero-login-panel-inner px-5 py-6 sm:px-8 sm:py-8">
<div className="flex items-center justify-between border-b border-white/10 pb-4">
<p className="font-mono text-[10px] tracking-[0.28em] text-[#21FFF0] uppercase">
Secure channel // Verifying
</p>
<span className="h-1.5 w-1.5 animate-pulse bg-[#21FFF0] shadow-[0_0_10px_#21FFF0]" />
</div>

<div className="pt-6">
<Skeleton className="size-11 rounded-none bg-white/[0.07]" />
<Skeleton className="mt-5 h-8 w-3/5 rounded-none bg-white/[0.07]" />
<Skeleton className="mt-3 h-2.5 w-2/5 rounded-none bg-white/[0.05]" />
</div>

<div className="zero-scan-track my-6 h-px w-full overflow-hidden bg-white/10">
<div className="zero-scan-line h-px w-1/4 bg-[#21FFF0] shadow-[0_0_10px_#21FFF0]" />
</div>

<div className="space-y-3">
<Skeleton className="zero-cut-button h-12 w-full bg-[#5900FF]/25" />
<Skeleton className="zero-cut-button h-12 w-full bg-white/[0.05]" />
</div>
</div>
</section>

<p className="mt-4 text-center font-mono text-[9px] tracking-[0.25em] text-white/25 uppercase">
Zero Day Harp // Establishing session
</p>
</div>
</div>
</div>
</main>
);
}
74 changes: 74 additions & 0 deletions client/portal/src/components/HackerPageLoader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import {
HackerSkeleton,
HackerSkeletonStatus,
} from "@/components/HackerSkeleton";
import { cn } from "@/shared/lib/utils";

interface HackerPageLoaderProps {
/**
* Paint the Zero Day canvas too. Needed when the loader renders outside
* `HackerLayout` — the auth guard and the lazy layout import — where the
* page would otherwise flash the portal's light background.
*/
fullscreen?: boolean;
}

/**
* Suspense fallback for hacker routes. Shaped like a dormant dashboard —
* status panel, date tiles, notification rows — so the most common landing
* fills in rather than swapping layouts.
*/
export function HackerPageLoader({
fullscreen = false,
}: HackerPageLoaderProps) {
return (
<div
role="status"
className={cn(
fullscreen &&
"hacker-zero-theme zero-hacker-surface min-h-svh text-white",
)}
>
<div className="mx-auto max-w-2xl px-5 pt-4 pb-6 md:max-w-5xl md:px-8 md:pt-6">
<HackerSkeletonStatus label="Portal" />

<div className="mt-4 rounded-xl border border-[#A857FF]/20 bg-[#0B0C15]/80 p-5 shadow-[inset_0_1px_0_rgba(255,255,255,0.05)]">
<HackerSkeleton className="h-6 w-28 rounded-full bg-[#5900FF]/20" />
<HackerSkeleton className="mt-4 h-6 w-3/5" />
<HackerSkeleton className="mt-3 h-3 w-2/5" />
<HackerSkeleton className="mt-4 h-1 w-full rounded-full" />
<HackerSkeleton className="mt-5 h-9 w-32 rounded-full bg-[#5900FF]/20" />
</div>

<div className="mt-6 grid grid-cols-3 gap-3">
{[...Array(3)].map((_, i) => (
<div
key={i}
className="rounded-lg border border-white/10 bg-[#0B0C15]/70 p-4"
>
<HackerSkeleton className="h-2.5 w-8" />
<HackerSkeleton className="mt-3 h-7 w-12" />
<HackerSkeleton className="mt-3 h-2.5 w-16" />
</div>
))}
</div>

<div className="mt-6 space-y-3">
<HackerSkeleton className="h-4 w-32" />
{[...Array(3)].map((_, i) => (
<div
key={i}
className="flex items-center gap-3 rounded-lg border border-white/10 bg-[#0B0C15]/70 px-4 py-3.5"
>
<HackerSkeleton className="size-2 shrink-0 rounded-full" />
<div className="min-w-0 flex-1">
<HackerSkeleton className="h-3.5 w-1/2" />
<HackerSkeleton className="mt-2 h-2.5 w-3/4" />
</div>
</div>
))}
</div>
</div>
</div>
);
}
92 changes: 92 additions & 0 deletions client/portal/src/components/HackerSkeleton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { cn } from "@/shared/lib/utils";

type SkeletonCut = "none" | "sm" | "md";

// Clipped corners follow the login panel geometry. Thin text bars stay sharp
// because an 8px cut would swallow most of their height.
const CUT_CLASS: Record<SkeletonCut, string> = {
none: "",
sm: "zero-cut-sm",
md: "zero-cut-button",
};

interface HackerSkeletonProps extends React.ComponentProps<"div"> {
cut?: SkeletonCut;
}

/**
* Zero Day placeholder block for hacker-side loading states: a dim panel
* with a slow cyan sweep. Pair with `HackerSkeletonStatus` so every loading
* screen opens with the same HUD line.
*/
export function HackerSkeleton({
className,
cut = "none",
...props
}: HackerSkeletonProps) {
return (
<div
data-slot="skeleton"
aria-hidden
className={cn("zero-skeleton", CUT_CLASS[cut], className)}
{...props}
/>
);
}

interface HackerSkeletonStatusProps {
/** What is loading, e.g. "Schedule". Rendered as `Loading // Schedule`. */
label: string;
className?: string;
}

/** Mono HUD status line that heads a hacker-side loading state. */
export function HackerSkeletonStatus({
label,
className,
}: HackerSkeletonStatusProps) {
return (
<p
className={cn(
"flex items-center gap-2.5 font-mono text-[10px] tracking-[0.28em] text-[#21FFF0]/80 uppercase",
className,
)}
>
<span
aria-hidden
className="h-1.5 w-1.5 shrink-0 animate-pulse bg-[#21FFF0] shadow-[0_0_10px_#21FFF0]"
/>
Loading // {label}
</p>
);
}

interface HackerSkeletonEmbedProps {
label: string;
className?: string;
}

/**
* Loading state for a large embedded surface (iframe, PDF, receipt image).
* A single block that big reads as a glitch, so this centres the status line
* and the auth flow's scan track instead.
*/
export function HackerSkeletonEmbed({
label,
className,
}: HackerSkeletonEmbedProps) {
return (
<div
role="status"
className={cn(
"flex size-full flex-col items-center justify-center gap-5 px-6",
className,
)}
>
<HackerSkeletonStatus label={label} />
<div className="zero-scan-track h-px w-full max-w-[220px] overflow-hidden bg-white/10">
<div className="zero-scan-line h-px w-1/4 bg-[#21FFF0] shadow-[0_0_10px_#21FFF0]" />
</div>
</div>
);
}
Loading
Loading