diff --git a/app/globals.css b/app/globals.css
index 3a201945..04a3e253 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -17,6 +17,31 @@
}
}
+@keyframes shiny {
+ 0% {
+ background-position: -1000px 0;
+ }
+ 100% {
+ background-position: 1000px 0;
+ }
+}
+
+.shiny-text {
+ background: linear-gradient(
+ 90deg,
+ #ffd700 0%,
+ #fff44f 25%,
+ #ffeb3b 50%,
+ #fff44f 75%,
+ #ffd700 100%
+ );
+ background-size: 200% auto;
+ background-clip: text;
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ animation: shiny 3s linear infinite;
+}
+
@layer base {
:root {
--background: 0 0% 100%;
diff --git a/app/hire/dashboard/page.tsx b/app/hire/dashboard/page.tsx
index 38e32a2c..3b3e8371 100644
--- a/app/hire/dashboard/page.tsx
+++ b/app/hire/dashboard/page.tsx
@@ -10,7 +10,7 @@ import { Loader } from "@/components/ui/loader";
import { useEmployerApplications, useOwnedJobs, useProfile } from "@/hooks/use-employer-api";
import { useMobile } from "@/hooks/use-mobile";
import { cn } from "@/lib/utils";
-import { Bell, Plus } from "lucide-react";
+import { Bell, Briefcase, Plus } from "lucide-react";
import Link from "next/link";
import { useState, useRef, useEffect } from "react";
import { useAuthContext } from "../authctx";
@@ -18,6 +18,7 @@ import { Job } from "@/lib/db/db.types";
import { FadeIn } from "@/components/animata/fade";
import { useModal } from "@/hooks/use-modal";
import { getNotificationPermission, requestNotificationPermission, checkNotificationSupport, shouldShowNotification, sendNotification } from "@/lib/notification-service";
+import { HeaderIcon, HeaderText } from "@/components/ui/text";
function DashboardContent() {
const { isMobile } = useMobile();
@@ -86,31 +87,34 @@ function DashboardContent() {
-
Welcome back, {profile.data?.name}
+
+
+ Job listings
+
-
-
- {activeJobs.length} active listing{activeJobs.length !== 1 ? "s" : ""}
- {inactiveJobs.length} inactive listing{inactiveJobs.length !== 1 ? "s" : ""}
-
-
- {isMobile && (
-
-
-
-
-
- )}
-
+
+
+ {activeJobs.length} active listing{activeJobs.length !== 1 ? "s" : ""}
+ {inactiveJobs.length} inactive listing{inactiveJobs.length !== 1 ? "s" : ""}
+
+
+ {isMobile && (
+
+
+
+
+
+ )}
+
diff --git a/app/hire/forgot-password/page.tsx b/app/hire/forgot-password/page.tsx
index a789931d..84e8dc71 100644
--- a/app/hire/forgot-password/page.tsx
+++ b/app/hire/forgot-password/page.tsx
@@ -9,6 +9,8 @@ import { EmployerUserService } from "@/lib/api/services";
import { cn } from "@/lib/utils";
import { useAppContext } from "@/lib/ctx-app";
import { AnimatePresence, motion } from "framer-motion";
+import { HeaderIcon, HeaderText } from "@/components/ui/text";
+import { HelpCircle } from "lucide-react";
/**
* Display the layout for the forgot password page.
@@ -68,9 +70,10 @@ const ForgotPasswordForm = ({}) => {
className="w-full"
>
-
- Forgot password
-
+
+
+ Reset password
+
{error && (
{error}
@@ -86,7 +89,10 @@ const ForgotPasswordForm = ({}) => {
onChange={(e) => setEmail(e.target.value)}
value={email}
/>
-
+
+
+ Remember your password? Log in here.
+
{
{isLoading ? "Sending request..." : "Request password reset"}
-
- Remember your password? Log in here.
-
Need help? Contact us at 0927 660 4999 or on Viber .
diff --git a/app/hire/help/page.tsx b/app/hire/help/page.tsx
index 32fa37aa..6a66f59a 100644
--- a/app/hire/help/page.tsx
+++ b/app/hire/help/page.tsx
@@ -3,9 +3,10 @@ import ContentLayout from "@/components/features/hire/content-layout";
import { Card } from "@/components/ui/card";
import { useAppContext } from "@/lib/ctx-app";
import { cn } from "@/lib/utils";
-import { BadgeInfo, Bug, Calendar, Facebook, LucideMessageCircleMore, Mail, Phone } from "lucide-react";
+import { BadgeInfo, Bug, Calendar, Facebook, HelpCircle, LucideMessageCircleMore, Mail, Phone } from "lucide-react";
import { AnimatePresence, motion } from "framer-motion";
import Link from "next/link";
+import { HeaderIcon, HeaderText } from "@/components/ui/text";
export default function HelpPage() {
const { isMobile } = useAppContext();
@@ -25,7 +26,10 @@ export default function HelpPage() {
transition={{ duration: 0.3, ease: "easeOut" }}
>
-
Help center
+
+
+ Help
+
) => {
+ setJobData(prev => prev ? { ...prev, ...updates } : null);
+ };
+
if (loading || !jobData) {
return (
@@ -55,11 +60,17 @@ function JobDetailsPageRouteContent() {
}
return (
-
+
)
diff --git a/app/hire/login/page.tsx b/app/hire/login/page.tsx
index 134cce9f..4d392434 100644
--- a/app/hire/login/page.tsx
+++ b/app/hire/login/page.tsx
@@ -12,9 +12,10 @@ import {
} from "@/components/EditForm";
import { Card } from "@/components/ui/card";
-import { MailCheck, TriangleAlert } from "lucide-react";
+import { MailCheck, TriangleAlert, User } from "lucide-react";
import { Loader } from "@/components/ui/loader";
import { AnimatePresence, motion } from "framer-motion";
+import { HeaderIcon, HeaderText } from "@/components/ui/text";
export default function LoginPage() {
return (
@@ -108,13 +109,14 @@ function LoginContent() {
{/* Welcome Message */}
-
- Employer Login
+
+
+ Log in
{/* Error Message */}
{error && (
diff --git a/app/hire/register/page.tsx b/app/hire/register/page.tsx
index 6e4c203b..9b57a3b6 100644
--- a/app/hire/register/page.tsx
+++ b/app/hire/register/page.tsx
@@ -20,8 +20,9 @@ import { Loader } from "@/components/ui/loader";
import { cn } from "@/lib/utils";
import { useAppContext } from "@/lib/ctx-app";
import Link from "next/link";
-import { TriangleAlert } from "lucide-react";
+import { TriangleAlert, User } from "lucide-react";
import { AnimatePresence, motion } from "framer-motion";
+import { HeaderIcon, HeaderText } from "@/components/ui/text";
const [EmployerRegisterForm, useEmployerRegisterForm] =
createEditForm
();
@@ -189,14 +190,13 @@ const EmployerEditor = ({
className="w-full"
>
-
-
- Employer Registration
-
+
+
+ Register
{missingFields.length > 0 && (
@@ -231,7 +231,6 @@ const EmployerEditor = ({
)}
/>
-
field === "Phone number") ? "border-destructive" : ""
)}
/>
+
-
field === "Contact email") ? "border-destructive" : ""
)}
/>
+
-
-
-
-
field === "Legal entity name") ? "border-destructive" : ""
)}
/>
+
+
+
+
{
- if (!isAuthenticated()) return; // Exit if not authenticated
+ if (!isAuthenticated()) {
+ return; // Exit if not authenticated
+ }
if (!profile.data?.department && !profile.isPending)
- router.push("/profile");
+ router.push("/profile/complete-profile");
}, [isAuthenticated, profile.data?.department, profile.isPending, router]);
// Query 1: Check for updates (cheap query - just a timestamp)
diff --git a/app/student/miro/page.tsx b/app/student/miro/page.tsx
index cb98db7b..b7539d80 100644
--- a/app/student/miro/page.tsx
+++ b/app/student/miro/page.tsx
@@ -11,6 +11,194 @@ import { InteractiveGridPattern } from "@/components/landingStudent/sections/1st
import { ArrowRight, Circle } from "lucide-react";
import { cn } from "@/lib/utils";
import confetti from "canvas-confetti";
+import { AnimatedShinyText } from "@/components/ui/animated-shiny-text";
+
+function EventEndAnimation({
+ show,
+ onDone,
+}: {
+ show: boolean;
+ onDone?: () => void;
+}) {
+ const [step, setStep] = useState<5 | 4 | 3 | 2 | 1 | 0>(5);
+ const isAnimatingRef = useRef(false);
+
+ useEffect(() => {
+ if (!show || isAnimatingRef.current) return;
+
+ isAnimatingRef.current = true;
+ setStep(5);
+
+ const timeline = [
+ { t: 0, value: 5 },
+ { t: 1000, value: 4 },
+ { t: 2000, value: 3 },
+ { t: 3000, value: 2 },
+ { t: 4000, value: 1 },
+ { t: 5000, value: 0 },
+ ];
+
+ const timeouts: NodeJS.Timeout[] = [];
+
+ timeline.forEach((item) => {
+ timeouts.push(setTimeout(() => setStep(item.value as any), item.t));
+ });
+
+ // confetti at TIME'S UP
+ timeouts.push(
+ setTimeout(() => {
+ confetti({
+ particleCount: 300,
+ spread: 90,
+ startVelocity: 60,
+ scalar: 1.2,
+ origin: { y: 0.6 },
+ });
+
+ confetti({
+ particleCount: 180,
+ spread: 120,
+ startVelocity: 50,
+ scalar: 1,
+ origin: { y: 0.7 },
+ });
+ }, 5000),
+ );
+
+ timeouts.push(
+ setTimeout(() => {
+ isAnimatingRef.current = false;
+ onDone?.();
+ }, 6700),
+ );
+
+ return () => {
+ timeouts.forEach(clearTimeout);
+ };
+ }, [show]);
+
+ const text = step === 0 ? "TIME'S UP!" : step;
+
+ return (
+
+ {show && (
+
+ {/* Dark overlay */}
+
+
+ {/* Red flash pulse */}
+
+
+ {/* Big countdown/text */}
+
+ {/* Glow ring */}
+
+
+ {/* Rotating red ring */}
+
+
+ {/* Text */}
+
+ {text}
+
+
+
+ {/* Bottom caption */}
+
+ {step === 0 ? "Event has concluded." : "Time left..."}
+
+
+ )}
+
+ );
+}
function CountdownDisplayAnimation({
show,
@@ -556,7 +744,7 @@ export default function MiroThonLandingPage() {
const discordLink = "https://discord.gg/QZ9mXJQm";
const eventStart = useMemo(() => new Date("2026-02-13T18:00:00+08:00"), []); // REAL DATE
- const eventEnd = useMemo(() => new Date("2026-02-14T23:59:00+08:00"), []);
+ const eventEnd = useMemo(() => new Date("2026-02-15T00:00:00+08:00"), []);
const [countdown, setCountdown] = useState(
getCountdown(eventStart),
@@ -567,18 +755,29 @@ export default function MiroThonLandingPage() {
return now >= eventStart.getTime() && now < eventEnd.getTime();
});
+ const [isEventPast, setIsEventPast] = useState(() => {
+ const now = new Date().getTime();
+ return now > eventEnd.getTime();
+ });
+
const [showLaunchAnimation, setShowLaunchAnimation] = useState(false);
+ const [showEndAnimation, setShowEndAnimation] = useState(false);
const wasLiveRef = useRef(false);
const animationTriggeredRef = useRef(false);
+ const endAnimationTriggeredRef = useRef(false);
useEffect(() => {
const interval = setInterval(() => {
const now = new Date().getTime();
const isLive = now >= eventStart.getTime() && now < eventEnd.getTime();
+ const isPast = now > eventEnd.getTime();
setIsEventLive(isLive);
+ setIsEventPast(isPast);
// Calculate countdown, switching between start and end times
- const newCountdown = getCountdown(isLive ? eventEnd : eventStart);
+ const newCountdown = getCountdown(
+ isPast ? eventStart : isLive ? eventEnd : eventStart,
+ );
setCountdown(newCountdown);
// Calculate total remaining seconds until eventStart
@@ -598,6 +797,22 @@ export default function MiroThonLandingPage() {
animationTriggeredRef.current = true;
}
+ // Trigger animation when 5 seconds remain before eventEnd
+ const secondsUntilEnd = eventEnd.getTime() - now;
+ const secondsUntilEndValue = Math.max(
+ 0,
+ Math.floor(secondsUntilEnd / 1000),
+ );
+
+ if (
+ secondsUntilEndValue === 5 &&
+ !endAnimationTriggeredRef.current &&
+ isEventLive
+ ) {
+ setShowEndAnimation(true);
+ endAnimationTriggeredRef.current = true;
+ }
+
// Reset animation trigger only when event goes live
if (isLive && !wasLiveRef.current) {
animationTriggeredRef.current = false;
@@ -621,6 +836,12 @@ export default function MiroThonLandingPage() {
onDone={() => setShowLaunchAnimation(false)}
/>
+ {/* Event End Animation */}
+ setShowEndAnimation(false)}
+ />
+
{/* texture */}
- {isEventLive ? (
+ {isEventPast ? (
+
+
+
+ You've crossed the finish line
+
+
+
+ ) : isEventLive ? (
{/* Sticky Note - Desktop: positioned bottom-right, Mobile: below */}
-
- {!isEventLive && (
-
- (yes, that{" "}
-
+ {!isEventLive && (
+
- Miro
-
- )
-
- )}
-
+ (yes, that{" "}
+
+ Miro
+
+ )
+
+ )}
+
+ )}
- {/* COUNTDOWN/COUNTDOWN TIMER */}
-
- {(() => {
- const totalSeconds =
- countdown.days * 86400 +
- countdown.hours * 3600 +
- countdown.minutes * 60 +
- countdown.seconds;
- const isUrgent = totalSeconds <= 60 && !isEventLive;
-
- return (
- <>
-
-
- :
-
-
-
- :
-
-
-
- :
-
-
- >
- );
- })()}
-
+ {/* COUNTDOWN/COUNTDOWN TIMER - Only show when event is live or before it starts */}
+ {!isEventPast && (
+
+ {(() => {
+ const totalSeconds =
+ countdown.days * 86400 +
+ countdown.hours * 3600 +
+ countdown.minutes * 60 +
+ countdown.seconds;
+ const isUrgent = totalSeconds <= 60;
+
+ return (
+ <>
+
+
+ :
+
+
+
+ :
+
+
+
+ :
+
+
+ >
+ );
+ })()}
+
+ )}
{/* DESCRIPTION */}
{isEventLive
? "Submit your work on Discord. Good luck!"
- : "Can you build something in 30 hours that will impress Miro?"}
+ : isEventPast
+ ? "Thank you for participating in the Miro-thon"
+ : "Can you build something in 30 hours that will impress Miro?"}
{/* CTA BUTTONS */}
@@ -880,7 +1122,7 @@ export default function MiroThonLandingPage() {
className={`w-full h-14 text-lg font-bold bg-blue-600 text-white hover:bg-blue-500`}
onClick={openDiscord}
>
- {isEventLive ? "Join Discord" : "Join the Miro-thon!"}
+ Join Discord
@@ -1015,7 +1257,7 @@ export default function MiroThonLandingPage() {
- {/* TIME IS TICKING / EVENT LIVE SECTION */}
+ {/* TIME IS TICKING / EVENT LIVE / MIRO-THON CONCLUDED SECTION */}
{/* Gradient background effect */}
-
- {/* Left: Title and Timer */}
-
-
- {isEventLive ? (
-
- The event is{" "}
- LIVE NOW
-
- ) : (
-
- Time is ticking
-
- )}
-
-
- {/* Countdown Timer */}
-
-
-
- {String(countdown.days).padStart(2, "0")}
-
-
- D
-
-
-
- :
-
-
-
- {String(countdown.hours).padStart(2, "0")}
-
-
- H
-
+ {isEventPast ? (
+ // Miro-thon Concluded State
+
+
+ Miro-thon Concluded
+
+
+ Thank you for participating in the Miro-thon!
+
+
+ ) : (
+ // Live or Before Event State
+
+ {/* Left: Title and Timer */}
+
+
+ {isEventLive ? (
+
+ The event is{" "}
+ LIVE NOW
+
+ ) : (
+
+ Time is ticking
+
+ )}
-
- :
-
-
-
- {String(countdown.minutes).padStart(2, "0")}
-
+
+ {/* Countdown Timer */}
+
+
+
+ {String(countdown.days).padStart(2, "0")}
+
+
+ D
+
+
- M
+ :
-
-
- :
-
-
+
+
+ {String(countdown.hours).padStart(2, "0")}
+
+
+ H
+
+
- {String(countdown.seconds).padStart(2, "0")}
+ :
+
+
+ {String(countdown.minutes).padStart(2, "0")}
+
+
+ M
+
+
- S
+ :
+
+
+ {String(countdown.seconds).padStart(2, "0")}
+
+
+ S
+
+
-
- {/* Right: CTA Button */}
-
-
- {isEventLive ? "View Submissions" : "Join Now"}
-
-
-
-
+ {/* Right: CTA Button */}
+
+
+ {isEventLive ? "View Submissions" : "Join Now"}
+
+
+
+
+ )}
diff --git a/app/student/profile/page.tsx b/app/student/profile/page.tsx
index 6f56ad61..aeb53446 100644
--- a/app/student/profile/page.tsx
+++ b/app/student/profile/page.tsx
@@ -154,7 +154,7 @@ export default function ProfilePage() {
!isProfileBaseComplete(profile.data) ||
profile.data?.acknowledged_auto_apply === false
) {
- router.push(`profile/complete-profile?dest=forms`);
+ router.push(`/forms`);
}
}, []);
diff --git a/components/features/hire/dashboard/ApplicationsCommandBar.tsx b/components/features/hire/dashboard/ApplicationsCommandBar.tsx
index 6747a574..036506ec 100644
--- a/components/features/hire/dashboard/ApplicationsCommandBar.tsx
+++ b/components/features/hire/dashboard/ApplicationsCommandBar.tsx
@@ -86,7 +86,7 @@ export function ApplicationsCommandBar({
{visible && (
<>
) : (
-
+
@@ -73,10 +74,10 @@ export default function JobHeader({
-
-
-
-
+
+
-
-
- Preview
-
-
-
+ Applicants
+
+
+
+
-
-
- Edit
-
-
+
+ Preview
+
+
+
-
- Delete
+
+ Edit
-
+
+
+
+ Delete
+
diff --git a/components/features/hire/listings/jobDetails.tsx b/components/features/hire/listings/jobDetails.tsx
index 2575b280..3f671665 100644
--- a/components/features/hire/listings/jobDetails.tsx
+++ b/components/features/hire/listings/jobDetails.tsx
@@ -37,12 +37,6 @@ const JobDetailsPage = ({
isMobile ? "px-1" : ""
)}
>
-
-
-
{
@@ -21,15 +21,20 @@ export const ShareJobButton = ({ id }: { id: string }) => {
return (
- {clicked ? : }
- {clicked ? "Copied link" : "Share"}
+ {clicked ? : }
+ {clicked ? "Copied link" : "Copy link"}
);
};
diff --git a/components/features/student/resume-parser/ResumeUpload.tsx b/components/features/student/resume-parser/ResumeUpload.tsx
index 0b763e24..93a059b6 100644
--- a/components/features/student/resume-parser/ResumeUpload.tsx
+++ b/components/features/student/resume-parser/ResumeUpload.tsx
@@ -1,7 +1,10 @@
import { RefObject, useRef, useState } from "react";
import { AnimatePresence } from "framer-motion";
import { ProcessingTransition } from "./ProcessingTransition";
-import { UploadIcon } from "lucide-react";
+import { TriangleAlert, UploadIcon } from "lucide-react";
+import { Toast } from "@/components/ui/toast";
+import { cn } from "@/lib/utils";
+import { useAppContext } from "@/lib/ctx-app";
const ResumeUpload = ({
ref,
@@ -10,7 +13,7 @@ const ResumeUpload = ({
onComplete,
isParsing,
accept = ".pdf,application/pdf",
- maxSizeMB = 5,
+ maxSizeMB = 2.5,
}: {
ref: RefObject;
promise?: Promise;
@@ -22,6 +25,8 @@ const ResumeUpload = ({
}) => {
const [file, setFile] = useState(null);
const [isDragging, setIsDragging] = useState(false);
+ const [fileTooBig, setFileTooBig] = useState(false);
+ const [noFile, setNoFile] = useState(false);
const dragCounter = useRef(0);
const triggerFileDialog = () => ref.current?.click();
@@ -31,7 +36,7 @@ const ResumeUpload = ({
// simple checks
const tooBig = f.size > maxSizeMB * 1024 * 1024;
if (tooBig) {
- alert(`File must be ≤ ${maxSizeMB}MB.`);
+ setFileTooBig(true);
return;
}
// accept check (relies on extension or mime)
@@ -43,10 +48,12 @@ const ResumeUpload = ({
(a) => !a.startsWith(".") && f.type.toLowerCase() === a
);
if (!(nameOk || mimeOk)) {
- alert("Please upload a PDF file.");
+ setNoFile(true);
return;
}
+ setFileTooBig(false);
+ setNoFile(false);
setFile(f);
onSelect(f);
};
@@ -83,12 +90,32 @@ const ResumeUpload = ({
handleChosenFile(f);
};
+ const { isMobile } = useAppContext();
+
return (
{isParsing ? (
) : (
+ {fileTooBig &&
+
+
+ Please upload a file smaller than 2.5 MB.
+
+ }
+ {noFile &&
+
+
+ Please upload your resume.
+
+ }
+
{children}
);
diff --git a/components/ui/command-menu.tsx b/components/ui/command-menu.tsx
index 546e65f6..44c0597f 100644
--- a/components/ui/command-menu.tsx
+++ b/components/ui/command-menu.tsx
@@ -43,7 +43,7 @@ export const CommandMenu = ({
onClick={item.onClick}
disabled={item.disabled}
className={cn(
- "flex justify-center items-center rounded-sm gap-2 p-2",
+ "flex justify-center items-center rounded-[0.33em] gap-2 p-2",
"data-[button-layout=vertical]:flex-col",
item.destructive
? "text-red-700 hover:bg-red-300/50 active:bg-red-400/75"
@@ -75,7 +75,7 @@ export const CommandMenu = ({
role="toolbar"
onClick={(e) => e.stopPropagation()}
className={cn(
- "flex gap-4 px-6 py-2 justify-center items-stretch text-xs bg-white border border-gray-300 rounded-md transition bg-clip-padding bg-clip-border",
+ "flex gap-4 px-6 py-2 justify-center items-stretch text-xs bg-white border border-gray-300 rounded-[0.33em] transition bg-clip-padding bg-clip-border",
className,
)}
>
diff --git a/components/ui/labels.tsx b/components/ui/labels.tsx
index cf731a34..d5057251 100644
--- a/components/ui/labels.tsx
+++ b/components/ui/labels.tsx
@@ -165,7 +165,7 @@ export const EmployerPropertyLabel: ValueComponent = ({
export const ErrorLabel: ValueComponent = ({ value, fallback }) => {
return value ? (
-
+
diff --git a/components/ui/status-badge.tsx b/components/ui/status-badge.tsx
index 872fc9d6..9846798f 100644
--- a/components/ui/status-badge.tsx
+++ b/components/ui/status-badge.tsx
@@ -18,7 +18,7 @@ export default function StatusBadge({
return (
diff --git a/hooks/use-file.tsx b/hooks/use-file.tsx
index 026ffd42..2d8016ef 100644
--- a/hooks/use-file.tsx
+++ b/hooks/use-file.tsx
@@ -196,19 +196,32 @@ export const useFileUpload = ({
*/
const upload = async (file?: File | null) => {
if (!file) return false;
-
- // Perform the file upload
- setIsUploading(true);
- const form = FileUploadFormBuilder.new(filename);
- form.file(file);
-
- // Check for success
- const result = (await uploader(form.build())) as { success?: boolean };
- setResponse(result);
-
- if (!result.success) return;
- if (!silent) alert("File uploaded successfully!");
- setIsUploading(false);
+
+ try {
+ // Perform the file upload
+ setIsUploading(true);
+ const form = FileUploadFormBuilder.new(filename);
+ form.file(file);
+
+ // Check for success
+ const result = (await uploader(form.build())) as { success?: boolean };
+ setResponse(result);
+
+ if (!result.success) {
+ if (!silent) alert("Upload failed.");
+ return false;
+ };
+
+ if (!silent) alert("File uploaded successfully!");
+ return true;
+ } catch (error) {
+ console.error("Upload failed: ", error);
+ setResponse({ success: false });
+
+ if (!silent) alert("Upload failed. Please try again later.");
+ } finally {
+ setIsUploading(false);
+ }
};
return {