diff --git a/app/error.tsx b/app/error.tsx index 86adc011..1466a062 100644 --- a/app/error.tsx +++ b/app/error.tsx @@ -2,7 +2,7 @@ import { Card } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; -import { HeaderIcon, HeaderText } from "@/components/ui/text"; +import { HeaderTitle } from "@/components/ui/text"; import { TriangleAlert } from "lucide-react"; import { useEffect } from "react"; import { SUPPORT_FACEBOOK } from "@/constants"; @@ -23,10 +23,9 @@ export default function Error({
Error -
- - Something went wrong. -
+ + Something went wrong. + Sorry for the inconvenience. Please{" "} contact us for support. diff --git a/app/hire/dashboard/page.tsx b/app/hire/dashboard/page.tsx index 4bbca010..622d676a 100644 --- a/app/hire/dashboard/page.tsx +++ b/app/hire/dashboard/page.tsx @@ -16,7 +16,7 @@ import { Briefcase, Plus } from "lucide-react"; import { useState, useRef, useEffect } from "react"; import { useAuthContext } from "../authctx"; import { Job } from "@/lib/db/db.types"; -import { HeaderIcon, HeaderText } from "@/components/ui/text"; +import { HeaderTitle } from "@/components/ui/text"; import { useRouter } from "next/navigation"; const SUPER_LISTING_CREATE_PATH = @@ -89,13 +89,9 @@ function DashboardContent() { isMobile ? "px-1" : "px-4", )} > -
- - Job listings -
+ + Job listings +
diff --git a/app/hire/forgot-password/page.tsx b/app/hire/forgot-password/page.tsx index 65c94182..974480b8 100644 --- a/app/hire/forgot-password/page.tsx +++ b/app/hire/forgot-password/page.tsx @@ -9,7 +9,7 @@ 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 { HeaderTitle } from "@/components/ui/text"; import { HelpCircle, MailCheck } from "lucide-react"; import { useBlurTransition } from "@/components/animata/blur"; @@ -73,10 +73,7 @@ const ForgotPasswordForm = () => { -
- - Reset password -
+ Reset password {error && (

{error}

diff --git a/app/hire/help/page.tsx b/app/hire/help/page.tsx index 430ed407..4e86c03a 100644 --- a/app/hire/help/page.tsx +++ b/app/hire/help/page.tsx @@ -15,7 +15,7 @@ import { } from "lucide-react"; import { AnimatePresence, motion } from "framer-motion"; import Link from "next/link"; -import { HeaderIcon, HeaderText } from "@/components/ui/text"; +import { HeaderTitle } from "@/components/ui/text"; import { useBlurTransition } from "@/components/animata/blur"; import { EMPLOYER_GUIDE, @@ -43,10 +43,7 @@ export default function HelpPage() { )} {...blurTransition} > -
- - Help -
+ Help

Contact us

diff --git a/app/hire/login/page.tsx b/app/hire/login/page.tsx index 7b83fb82..20ddbf8a 100644 --- a/app/hire/login/page.tsx +++ b/app/hire/login/page.tsx @@ -13,7 +13,7 @@ import { Card } from "@/components/ui/card"; 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"; +import { HeaderTitle } from "@/components/ui/text"; import { useBlurTransition } from "@/components/animata/blur"; export default function LoginPage() { @@ -105,10 +105,7 @@ function LoginContent() {
{/* Welcome Message */} -
- - Log in -
+ Log in {/* Error Message */} {error && (
-
- - Register -
+ Register {missingFields.length > 0 && (
{ style={{ width: `${redirectProgress}%` }} />
-
- - Password updated -
+ Password updated

{success}

@@ -135,10 +132,7 @@ const ResetPasswordForm = ({ hash }: { hash: string }) => { -
- - Reset password -
+ Reset password {error && (

{error}

diff --git a/app/student/allowLanding.tsx b/app/student/allowLanding.tsx index 1434f6d2..f3a54ef2 100644 --- a/app/student/allowLanding.tsx +++ b/app/student/allowLanding.tsx @@ -11,12 +11,23 @@ export default function AllowLanding({ }) { const pathname = usePathname(); const isStudentLanding = pathname === "/"; + const isChallengePage = + pathname.startsWith("/challenges/") || + pathname.startsWith("/student/challenges/"); const hideSharedHeader = - isStudentLanding || pathname.startsWith("/companies/"); + isStudentLanding || pathname.startsWith("/companies/") || isChallengePage; + + if (hideSharedHeader) { + return ( +
{children}
+ ); + } return (
- {!hideSharedHeader &&
} + +
+
{children}
); diff --git a/app/student/applications/page.tsx b/app/student/applications/page.tsx index 294c2c0f..9dac90bb 100644 --- a/app/student/applications/page.tsx +++ b/app/student/applications/page.tsx @@ -19,7 +19,7 @@ import { Loader } from "@/components/ui/loader"; import { Card } from "@/components/ui/card"; import { JobHead, SuperListingBadge } from "@/components/shared/jobs"; import { UserApplication } from "@/lib/db/db.types"; -import { HeaderText, HeaderIcon } from "@/components/ui/text"; +import { HeaderTitle } from "@/components/ui/text"; import { Separator } from "@/components/ui/separator"; import { PageError } from "@/components/ui/error"; import { cn } from "@/lib/utils"; @@ -61,10 +61,7 @@ export default function ApplicationsPage() {
-
- - My Applications -
+ My Applications

Track your internship applications and their status diff --git a/app/student/challenges/[id]/page.tsx b/app/student/challenges/[id]/page.tsx new file mode 100644 index 00000000..6839ab00 --- /dev/null +++ b/app/student/challenges/[id]/page.tsx @@ -0,0 +1,390 @@ +import type { Metadata } from "next"; +import Image from "next/image"; +import Link from "next/link"; +import { notFound } from "next/navigation"; +import { JetBrains_Mono, Open_Sans, Space_Grotesk } from "next/font/google"; +import { + ArrowLeft, + ArrowRight, + Building2, + CalendarDays, + CheckCircle2, + Clock3, + MapPin, + Sparkles, + Target, + Trophy, +} from "lucide-react"; + +import { Card } from "@/components/ui/card"; +import { cn } from "@/lib/utils"; +import { + challengePhChallenges, + getChallengeById, + type ChallengePhChallenge, +} from "@/app/student/challenges/data"; +import { SuperListingMapBackground } from "@/components/features/student/super-listing/philippines-infographic-map"; + +const headingFont = Space_Grotesk({ + subsets: ["latin"], + weight: ["500", "700"], + variable: "--font-challenge-ph-heading", +}); + +const monoFont = JetBrains_Mono({ + subsets: ["latin"], + weight: ["400", "600"], + variable: "--font-challenge-ph-mono", +}); + +const bodyFont = Open_Sans({ + subsets: ["latin"], + weight: ["400", "600", "700"], + variable: "--font-challenge-ph-body", +}); + +type PageProps = { + params: Promise<{ + id: string; + }>; +}; + +export function generateStaticParams() { + return challengePhChallenges.map((challenge) => ({ + id: challenge.id, + })); +} + +export async function generateMetadata({ + params, +}: PageProps): Promise { + const { id } = await params; + const challenge = getChallengeById(id); + + if (!challenge) { + return { + title: "Challenge PH | BetterInternship", + }; + } + + return { + title: `${challenge.shortTitle} | Challenge PH`, + description: challenge.summary, + }; +} + +function SectionTitle({ eyebrow, title }: { eyebrow?: string; title: string }) { + return ( +

+ {eyebrow ? ( +

+ {eyebrow} +

+ ) : null} +

+ {title} +

+
+ ); +} + +function AsteriskList({ items }: { items: readonly string[] }) { + return ( +
    + {items.map((item) => ( +
  • + + * + + {item} +
  • + ))} +
+ ); +} + +function DetailRail({ challenge }: { challenge: ChallengePhChallenge }) { + const details = [ + { + icon: Trophy, + label: "Reward", + value: challenge.reward, + emphasized: true, + }, + { + icon: Sparkles, + label: "Bounty type", + value: challenge.rewardType, + }, + { + icon: CalendarDays, + label: "Deadline", + value: challenge.deadline, + }, + { + icon: MapPin, + label: "Location", + value: challenge.location, + }, + { + icon: Clock3, + label: "Difficulty", + value: challenge.difficulty, + }, + { + icon: Building2, + label: "Sector", + value: challenge.sector, + }, + ]; + + return ( + + ); +} + +function Timeline({ challenge }: { challenge: ChallengePhChallenge }) { + return ( +
+ {challenge.timeline.map((item) => ( +
+

+ {item.label} +

+

+ {item.detail} +

+
+ ))} +
+ ); +} + +export default async function ChallengePage({ params }: PageProps) { + const { id } = await params; + const challenge = getChallengeById(id); + + if (!challenge) { + notFound(); + } + + return ( +
+
+
+ + + +
+ BetterInternship logo + + Challenge PH + +
+
+
+ +
+
+
+ +
+
+
+
+
+
+
+
+

+ {challenge.title} +

+

+ {challenge.summary} +

+
+ +
+ + Reward + + + {challenge.reward} + +
+
+
+ +
+
+
+
+
+ +
+ +
+
+
+ +

+ {challenge.problem} +

+
+ +
+ +

+ {challenge.whyItMatters} +

+
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+ {challenge.judgingCriteria.map((criterion) => ( +
+ +

+ {criterion} +

+
+ ))} +
+
+ +
+
+
+
+ +

+ Build for the brief, not for a resume screen. +

+
+

+ This placeholder page is focused on understanding the + problem and reward. Submission actions can be added once + the final Challenge PH flow is ready. +

+
+ + See all challenges + + +
+
+
+
+
+
+
+ ); +} diff --git a/app/student/challenges/data.ts b/app/student/challenges/data.ts new file mode 100644 index 00000000..9627d566 --- /dev/null +++ b/app/student/challenges/data.ts @@ -0,0 +1,400 @@ +export type ChallengePhChallenge = { + id: string; + host: string; + sector: string; + title: string; + shortTitle: string; + reward: string; + rewardType: string; + deadline: string; + location: string; + difficulty: string; + summary: string; + problem: string; + whyItMatters: string; + brief: string[]; + eligibility: string[]; + deliverables: string[]; + timeline: Array<{ + label: string; + detail: string; + }>; + judgingCriteria: string[]; + tags: string[]; + accent: string; +}; + +export const challengePhChallenges: ChallengePhChallenge[] = [ + { + id: "flood-ready-commutes", + host: "Urban Mobility Lab", + sector: "Transport and disaster response", + title: "Build a flood-ready commute planner for Metro Manila students", + shortTitle: "Flood-ready commutes", + reward: "PHP 75,000 pilot bounty", + rewardType: "Cash prize plus LGU showcase", + deadline: "June 28, 2026", + location: "Metro Manila", + difficulty: "Intermediate", + summary: + "Create a practical way for students to choose safer routes when heavy rain disrupts normal commutes.", + problem: + "Students often make commute decisions with fragmented information: weather alerts in one place, flood posts on social media, class advisories elsewhere, and no clear sense of what route is still usable.", + whyItMatters: + "Flooding can turn a routine trip into a safety risk and a financial burden. A better decision tool could help students avoid stranded routes, missed classes, and unsafe transfers.", + brief: [ + "Design a mobile-first experience that combines route options, flood severity, transport availability, and school advisories.", + "Prioritize clarity under stress: students should understand the safest next step within a few seconds.", + "Show how your solution could work even when official data is delayed or incomplete.", + ], + eligibility: [ + "Open to students and early-career builders in the Philippines.", + "Teams of one to four members are allowed.", + "No production app required, but working prototypes get stronger consideration.", + ], + deliverables: [ + "Problem framing and user assumptions.", + "Clickable prototype, demo video, or working proof of concept.", + "Short implementation plan covering data sources, limitations, and rollout.", + ], + timeline: [ + { label: "Brief opens", detail: "May 30, 2026" }, + { label: "Submission deadline", detail: "June 28, 2026" }, + { label: "Final demos", detail: "July 8, 2026" }, + ], + judgingCriteria: [ + "Usefulness during real flood conditions.", + "Quality of user flow and information hierarchy.", + "Practicality of data collection and maintenance.", + "Strength of risk and edge-case thinking.", + ], + tags: ["Mobility", "Climate", "Student safety"], + accent: "#0D6BFF", + }, + { + id: "sari-sari-stockouts", + host: "Neighborhood Retail Network", + sector: "MSME retail", + title: "Predict sari-sari store stockouts before they happen", + shortTitle: "Sari-sari stockout tracker", + reward: "PHP 50,000 bounty", + rewardType: "Cash prize plus distributor pilot", + deadline: "July 5, 2026", + location: "Nationwide", + difficulty: "Beginner friendly", + summary: + "Help small neighborhood stores avoid missed sales by forecasting which fast-moving goods need replenishment.", + problem: + "Many sari-sari stores track inventory manually, making it hard to know when staples like prepaid load, canned goods, rice, coffee sachets, and hygiene products are about to run out.", + whyItMatters: + "A stockout can mean lost income for the store and extra trips for the neighborhood. Lightweight forecasting could make everyday retail more resilient without expensive software.", + brief: [ + "Create a simple inventory assistant that can work with handwritten logs, phone photos, spreadsheet input, or chat-based updates.", + "Focus on low-friction adoption for store owners who do not want another complicated dashboard.", + "Recommend what to restock, when to restock, and why.", + ], + eligibility: [ + "Open to students interested in retail, data, operations, or product design.", + "Solo submissions and small teams are welcome.", + "Solutions should be usable by non-technical store owners.", + ], + deliverables: [ + "Prototype or workflow mockup.", + "Sample input and output using at least ten common store items.", + "Explanation of forecasting logic in plain language.", + ], + timeline: [ + { label: "Brief opens", detail: "June 3, 2026" }, + { label: "Submission deadline", detail: "July 5, 2026" }, + { label: "Pilot selection", detail: "July 15, 2026" }, + ], + judgingCriteria: [ + "Ease of use for sari-sari store owners.", + "Practicality in low-connectivity settings.", + "Quality of forecasting assumptions.", + "Potential to improve store income.", + ], + tags: ["MSME", "Retail", "Forecasting"], + accent: "#00A886", + }, + { + id: "barangay-health-queues", + host: "Community Health Systems Group", + sector: "Public health", + title: "Reduce waiting time in barangay health center queues", + shortTitle: "Barangay health queues", + reward: "PHP 40,000 plus internship shortlist", + rewardType: "Cash prize and health-tech internship path", + deadline: "July 12, 2026", + location: "Philippines", + difficulty: "Intermediate", + summary: + "Design a queue and triage workflow that helps barangay health centers serve patients faster and more fairly.", + problem: + "Barangay health centers often handle checkups, records, vaccination, referrals, and follow-ups with limited staff and mostly manual queue systems.", + whyItMatters: + "Long waits discourage people from seeking care early. Better triage and queue visibility can improve service quality without requiring a large new budget.", + brief: [ + "Map the patient journey from arrival to release or referral.", + "Design a queueing system that separates urgent cases, scheduled visits, and simple transactions.", + "Consider staff workload, privacy, paper records, and intermittent internet.", + ], + eligibility: [ + "Open to students in product, public health, operations, design, and engineering.", + "Teams may include non-technical members.", + "Submissions should respect patient privacy and avoid collecting sensitive data unnecessarily.", + ], + deliverables: [ + "Service blueprint or journey map.", + "Prototype, flow diagram, or lightweight system design.", + "Metrics for measuring reduced wait time and staff burden.", + ], + timeline: [ + { label: "Brief opens", detail: "June 8, 2026" }, + { label: "Submission deadline", detail: "July 12, 2026" }, + { label: "Review week", detail: "July 13-19, 2026" }, + ], + judgingCriteria: [ + "Fit with barangay health center constraints.", + "Patient safety and privacy awareness.", + "Operational clarity for staff.", + "Measurable improvement plan.", + ], + tags: ["Health", "Operations", "Service design"], + accent: "#E5484D", + }, + { + id: "agri-cold-chain", + host: "Agri Logistics Studio", + sector: "Agriculture and logistics", + title: "Track produce spoilage across local cold-chain gaps", + shortTitle: "Cold-chain spoilage tracker", + reward: "PHP 80,000 prototype grant", + rewardType: "Prototype grant plus mentor review", + deadline: "July 19, 2026", + location: "Luzon, Visayas, Mindanao", + difficulty: "Advanced", + summary: + "Help farmers and consolidators identify where vegetables and fruits lose value before reaching market.", + problem: + "Produce spoilage is often treated as unavoidable, but the actual loss points across transport, storage, sorting, and market handoff are poorly documented.", + whyItMatters: + "Reducing spoilage can improve farmer income, stabilize prices, and make local food systems more efficient.", + brief: [ + "Design a tracking method for spoilage events from harvest to market.", + "Make the workflow realistic for cooperatives, truckers, and market operators.", + "Show how the data would help decide where to invest in cold storage, packaging, or route changes.", + ], + eligibility: [ + "Open to students interested in agriculture, supply chain, hardware, data, or field research.", + "Teams are encouraged to include someone familiar with provincial food systems.", + "Submissions may be software, research, operations, or hybrid proposals.", + ], + deliverables: [ + "Loss-point map and stakeholder workflow.", + "Prototype dashboard, form, sensor concept, or reporting system.", + "Rollout plan for a three-month pilot.", + ], + timeline: [ + { label: "Brief opens", detail: "June 10, 2026" }, + { label: "Submission deadline", detail: "July 19, 2026" }, + { label: "Mentor review", detail: "July 29, 2026" }, + ], + judgingCriteria: [ + "Understanding of agricultural logistics.", + "Quality of field assumptions.", + "Potential to reduce waste and protect income.", + "Pilot feasibility.", + ], + tags: ["Agriculture", "Logistics", "Food waste"], + accent: "#2E7D32", + }, + { + id: "jeepney-demand-dashboard", + host: "Transit Data Collective", + sector: "Public transport", + title: "Create a demand dashboard for modern jeepney routes", + shortTitle: "Jeepney route demand", + reward: "PHP 60,000 bounty", + rewardType: "Cash prize plus transit lab internship interview", + deadline: "July 26, 2026", + location: "Metro Manila and key cities", + difficulty: "Intermediate", + summary: + "Turn messy commuter and operator signals into route-level insights that help modern jeepney fleets plan trips.", + problem: + "Operators need to know when and where demand spikes, but ridership signals are scattered across dispatcher notes, driver experience, payment data, and commuter complaints.", + whyItMatters: + "Better dispatching can reduce long waits for commuters and improve earnings for drivers without adding unnecessary trips.", + brief: [ + "Design a route dashboard that shows peak demand, undersupplied stops, and schedule gaps.", + "Include a data collection approach that does not assume every vehicle has advanced hardware.", + "Explain what actions an operator should take after seeing your dashboard.", + ], + eligibility: [ + "Open to students interested in transport, analytics, civic tech, or operations.", + "Teams may submit either a product prototype or analytics case study.", + "Use public or synthetic data unless you have permission to use real data.", + ], + deliverables: [ + "Dashboard mockup or working analytics prototype.", + "Sample route scenario and recommended decisions.", + "Data reliability and privacy notes.", + ], + timeline: [ + { label: "Brief opens", detail: "June 12, 2026" }, + { label: "Submission deadline", detail: "July 26, 2026" }, + { label: "Operator feedback", detail: "August 5, 2026" }, + ], + judgingCriteria: [ + "Decision usefulness for operators.", + "Clarity for non-technical transport teams.", + "Handling of incomplete data.", + "Impact on commuter wait time and driver economics.", + ], + tags: ["Transport", "Data", "Operations"], + accent: "#F5A400", + }, + { + id: "coastal-plastic-recovery", + host: "Blue Communities Network", + sector: "Environment and circular economy", + title: "Make coastal plastic recovery financially sustainable", + shortTitle: "Coastal plastic recovery", + reward: "PHP 70,000 challenge bounty", + rewardType: "Cash prize plus NGO accelerator slot", + deadline: "August 2, 2026", + location: "Coastal Philippines", + difficulty: "Intermediate", + summary: + "Design a system that helps coastal communities collect, sort, and sell recovered plastic with better incentives.", + problem: + "Plastic recovery efforts often depend on short-term cleanups. Communities need a repeatable model that connects collection, sorting, buyers, and transparent payouts.", + whyItMatters: + "A sustainable recovery system can protect coastal livelihoods while giving residents a reason to keep plastic out of waterways.", + brief: [ + "Design the incentive loop for households, collectors, sorters, and buyers.", + "Show how plastic quality, volume, and payout data would be tracked.", + "Account for trust, fraud prevention, and community coordination.", + ], + eligibility: [ + "Open to students in sustainability, business, product, design, or engineering.", + "Community research is welcome but not required.", + "Solutions should avoid unpaid labor assumptions.", + ], + deliverables: [ + "System model or marketplace flow.", + "Prototype, operations plan, or financial model.", + "Risk notes for fraud, safety, and long-term adoption.", + ], + timeline: [ + { label: "Brief opens", detail: "June 15, 2026" }, + { label: "Submission deadline", detail: "August 2, 2026" }, + { label: "Community review", detail: "August 12, 2026" }, + ], + judgingCriteria: [ + "Sustainability of incentives.", + "Operational fit for coastal communities.", + "Clarity of buyer and payout flow.", + "Environmental and livelihood impact.", + ], + tags: ["Environment", "Circular economy", "Community"], + accent: "#0084A8", + }, + { + id: "ofw-remittance-helper", + host: "Inclusive Finance Lab", + sector: "Fintech and family finance", + title: "Help OFW families turn remittances into monthly plans", + shortTitle: "OFW remittance planner", + reward: "PHP 55,000 bounty", + rewardType: "Cash prize plus fintech internship interview", + deadline: "August 9, 2026", + location: "Nationwide", + difficulty: "Beginner friendly", + summary: + "Build a budgeting assistant that helps families plan remittance use without shame, jargon, or complicated spreadsheets.", + problem: + "Families receiving remittances often balance bills, debt, tuition, groceries, savings, and emergencies with little shared visibility between sender and receiver.", + whyItMatters: + "A respectful planning tool could reduce money stress, improve savings behavior, and help families make decisions together.", + brief: [ + "Design a budgeting flow for both the OFW sender and the family member receiving money.", + "Emphasize trust, privacy, and emotional tone.", + "Show how the tool handles irregular income, urgent requests, and shared goals.", + ], + eligibility: [ + "Open to students interested in fintech, behavioral design, product, or family finance.", + "No finance background required.", + "Solutions must avoid manipulative savings or lending patterns.", + ], + deliverables: [ + "User flow for sender and receiver.", + "Prototype, worksheet, chatbot flow, or mobile concept.", + "Explanation of tone, privacy, and financial safeguards.", + ], + timeline: [ + { label: "Brief opens", detail: "June 18, 2026" }, + { label: "Submission deadline", detail: "August 9, 2026" }, + { label: "Final interviews", detail: "August 19, 2026" }, + ], + judgingCriteria: [ + "Empathy for family money dynamics.", + "Simplicity of planning flow.", + "Privacy and trust design.", + "Potential to improve household financial stability.", + ], + tags: ["Fintech", "OFW", "Behavioral design"], + accent: "#7C3AED", + }, + { + id: "shs-skills-mapper", + host: "Youth Workforce Bridge", + sector: "Education and employment", + title: "Map senior high school skills to real entry-level work", + shortTitle: "SHS skills mapper", + reward: "PHP 45,000 bounty", + rewardType: "Cash prize plus employer demo day", + deadline: "August 16, 2026", + location: "Philippines", + difficulty: "Beginner friendly", + summary: + "Create a tool that helps senior high school students understand what work they can already try, practice, or prepare for.", + problem: + "Many students graduate with projects, strand knowledge, and informal skills, but they struggle to translate those experiences into specific roles, portfolios, or next steps.", + whyItMatters: + "Better skill translation can help students discover opportunities earlier and help employers see potential beyond credentials.", + brief: [ + "Design a mapper that turns student projects, interests, and strand experience into role suggestions.", + "Show what evidence a student should prepare for each suggested path.", + "Make the output encouraging but honest about gaps to close.", + ], + eligibility: [ + "Open to students, educators, and early-career builders.", + "Teams may include senior high school students.", + "Solutions should be accessible on low-end phones.", + ], + deliverables: [ + "Skills-to-role matching flow.", + "Sample outputs for at least three student profiles.", + "Plan for guidance counselors, schools, or employers to use the tool.", + ], + timeline: [ + { label: "Brief opens", detail: "June 20, 2026" }, + { label: "Submission deadline", detail: "August 16, 2026" }, + { label: "Employer demo day", detail: "August 26, 2026" }, + ], + judgingCriteria: [ + "Usefulness for students with little work experience.", + "Quality of role and portfolio recommendations.", + "Accessibility for schools and low-end devices.", + "Potential employer relevance.", + ], + tags: ["Education", "Workforce", "Career discovery"], + accent: "#C2410C", + }, +]; + +export function getChallengeById(id: string) { + return challengePhChallenges.find((challenge) => challenge.id === id); +} diff --git a/app/student/forms/components/FormDashboard.tsx b/app/student/forms/components/FormDashboard.tsx index 0ec2a06b..7a6bce9c 100644 --- a/app/student/forms/components/FormDashboard.tsx +++ b/app/student/forms/components/FormDashboard.tsx @@ -28,7 +28,7 @@ import { FormActionAccordion } from "./FormActionAccordion"; import { FormSigningPartyTimeline } from "./FormSigningPartyTimeline"; import { FormMobileCloseConfirmation } from "./FormMobileCloseConfirmation"; import { useMobile } from "@/hooks/use-mobile"; -import { HeaderIcon, HeaderText } from "@/components/ui/text"; +import { HeaderTitle } from "@/components/ui/text"; import { useSearchParams } from "next/navigation"; import { toast } from "sonner"; import { useHeaderContext } from "@/lib/ctx-header"; @@ -289,14 +289,9 @@ export default function FormDashboard({ >
-
- -
- - {formGroupDescription || "Form Templates"} - -
-
+ + {formGroupDescription || "Form Templates"} +

We have a{" "} { + const requiredManualFieldGroups = useMemo(() => { const fields = noEsign ? form.formMetadata.getFieldsForClientService(undefined) : form.fields; - return fields + const radioGroupMap = new Map(); + const normalKeys: string[] = []; + + fields .filter((field) => { if (field.source !== "manual" || field.type === "signature") return false; @@ -302,7 +307,20 @@ export function FormSigningLayout({ if (noEsign) return true; return field.signing_party_id === "initiator"; }) - .map((field) => field.field); + .forEach((field) => { + const groupId = (field as any).radio_group_id as string | undefined; + if (groupId) { + if (!radioGroupMap.has(groupId)) radioGroupMap.set(groupId, []); + radioGroupMap.get(groupId)!.push(field.field); + } else { + normalKeys.push(field.field); + } + }); + + return [ + ...normalKeys.map((k) => [k]), + ...Array.from(radioGroupMap.values()), + ]; }, [form.fields, form.formMetadata, noEsign]); const previewValuesWithDerived = useMemo( () => withDerivedFormValues(form.formMetadata, previewValues), @@ -315,10 +333,10 @@ export function FormSigningLayout({ const computeRequiredFieldsComplete = useCallback( (nextValues: FormValues) => - requiredManualFieldKeys.every( - (fieldKey) => !!getFieldValue(nextValues, fieldKey), + requiredManualFieldGroups.every((group) => + group.some((fieldKey) => !!getFieldValue(nextValues, fieldKey)), ), - [requiredManualFieldKeys], + [requiredManualFieldGroups], ); const handleValuesChange = useCallback( @@ -408,8 +426,11 @@ export function FormSigningLayout({ }; const recipientEmailErrors = useMemo( - () => getRecipientEmailErrors(recipientEmails), - [recipientEmails], + () => + getRecipientEmailErrors(recipientEmails, { + studentEmail: profile.data?.email, + }), + [recipientEmails, profile.data?.email], ); const nextEnabled = useMemo(() => { @@ -630,9 +651,9 @@ export function FormSigningLayout({ setMobileFieldsTab("form"); setMobilePreviewNeedsAttention(false); setHasConfirmedDetails(false); - setAreRequiredFieldsComplete(requiredManualFieldKeys.length === 0); + setAreRequiredFieldsComplete(requiredManualFieldGroups.length === 0); setCurrentStep(initialStep); - }, [formLabel, initialStep, requiredManualFieldKeys.length, noEsign]); + }, [formLabel, initialStep, requiredManualFieldGroups.length, noEsign]); useEffect(() => { if (currentStep === "confirm") { diff --git a/app/student/forms/components/FormSigningPartyTimeline.tsx b/app/student/forms/components/FormSigningPartyTimeline.tsx index b3943f01..4e456a33 100644 --- a/app/student/forms/components/FormSigningPartyTimeline.tsx +++ b/app/student/forms/components/FormSigningPartyTimeline.tsx @@ -5,6 +5,7 @@ import { Timeline, TimelineItem } from "@/components/ui/timeline"; import { StateRecord, StateRecordActions } from "@/hooks/base/useStateRecord"; import { cn } from "@/lib/utils"; import { useEffect } from "react"; +import { useProfileData } from "@/lib/api/student.data.api"; import { getRecipientEmailErrors, RECIPIENT_EMAIL_VALIDATION_DEBOUNCE_MS, @@ -23,6 +24,7 @@ export const FormSigningPartyTimeline = ({ isConfirmingRecipients?: boolean; }) => { const form = useFormRendererContext(); + const profile = useProfileData(); const recipients = form.formMetadata.getSigningParties(); useEffect(() => { @@ -32,13 +34,16 @@ export const FormSigningPartyTimeline = ({ const validationTimeout = window.setTimeout(() => { recipientInputAPI.recipientErrorActions.overwrite( - getRecipientEmailErrors(recipientInputAPI.recipientEmails), + getRecipientEmailErrors(recipientInputAPI.recipientEmails, { + studentEmail: profile.data?.email, + }), ); }, RECIPIENT_EMAIL_VALIDATION_DEBOUNCE_MS); return () => window.clearTimeout(validationTimeout); }, [ isConfirmingRecipients, + profile.data?.email, recipientInputAPI?.recipientEmails, recipientInputAPI?.recipientErrorActions, ]); diff --git a/app/student/forms/components/recipient-email-validation.ts b/app/student/forms/components/recipient-email-validation.ts index 05aa3616..b5ecc8b7 100644 --- a/app/student/forms/components/recipient-email-validation.ts +++ b/app/student/forms/components/recipient-email-validation.ts @@ -2,8 +2,15 @@ import { isValidEmail } from "@/lib/utils/string-utils"; export const RECIPIENT_EMAIL_VALIDATION_DEBOUNCE_MS = 300; +const normalizeEmail = (email: string) => email.trim().toLowerCase(); + +type RecipientEmailValidationOptions = { + studentEmail?: string | null; +}; + export const getRecipientEmailErrors = ( recipientEmails: Record, + options: RecipientEmailValidationOptions = {}, ) => { return Object.entries(recipientEmails).reduce>( (errors, [fieldName, emailValue]) => { @@ -27,7 +34,6 @@ export const getRecipientEmailErrors = ( ...(fieldNamesByEmail.get(normalizedEmail) ?? []), fieldName, ]); - }); // Rule: all recipient emails must be unique // fieldNamesByEmail.forEach((fieldNames) => { diff --git a/app/student/saved/page.tsx b/app/student/saved/page.tsx index 9a5b2d19..c406278e 100644 --- a/app/student/saved/page.tsx +++ b/app/student/saved/page.tsx @@ -12,7 +12,7 @@ import { Loader } from "@/components/ui/loader"; import { Card } from "@/components/ui/card"; import { JobHead, SuperListingBadge } from "@/components/shared/jobs"; import { Job, SavedJob } from "@/lib/db/db.types"; -import { HeaderIcon, HeaderText } from "@/components/ui/text"; +import { HeaderTitle } from "@/components/ui/text"; import { Separator } from "@/components/ui/separator"; import { PageError } from "@/components/ui/error"; import { SaveJobButton } from "@/components/features/student/job/save-job-button"; @@ -32,10 +32,7 @@ export default function SavedJobsPage() {

-
- - Saved Jobs -
+ Saved Jobs {savedJobs.length} saved
diff --git a/app/student/super-listing/page.tsx b/app/student/super-listing/page.tsx index df93011b..c6aaa055 100644 --- a/app/student/super-listing/page.tsx +++ b/app/student/super-listing/page.tsx @@ -1,750 +1,439 @@ -"use client"; - import Link from "next/link"; import Image from "next/image"; -import { ArrowRight, Zap, Target } from "lucide-react"; -import { useRef, useState, useEffect } from "react"; +import { JetBrains_Mono, Open_Sans, Space_Grotesk } from "next/font/google"; +import { + ArrowRight, + Building2, + CalendarDays, + ChevronRight, + MapPin, +} from "lucide-react"; + +import { challengePhChallenges } from "@/app/student/challenges/data"; import { - motion, - useScroll, - useTransform, - useReducedMotion, -} from "framer-motion"; -import { InteractiveGridPattern } from "@/components/landingStudent/sections/1stSection/interactive-grid-pattern"; -import { Button } from "@/components/ui/button"; -import { Card } from "@/components/ui/card"; -import { SuperListingBadge } from "@/components/shared/jobs"; + ChallengePhInteractiveMap, + SuperListingMapBackground, +} from "@/components/features/student/super-listing/philippines-infographic-map"; import { cn } from "@/lib/utils"; -function SectionReveal({ - children, - className, -}: { - children: React.ReactNode; - className?: string; -}) { - return ( - - {children} - - ); -} - -type SuperListingBadgeSize = "body" | "heading" | "button"; +const headingFont = Space_Grotesk({ + subsets: ["latin"], + weight: ["500", "700"], + variable: "--font-challenge-ph-heading", +}); + +const monoFont = JetBrains_Mono({ + subsets: ["latin"], + weight: ["400", "600"], + variable: "--font-challenge-ph-mono", +}); + +const bodyFont = Open_Sans({ + subsets: ["latin"], + weight: ["400", "600", "700"], + variable: "--font-challenge-ph-body", +}); + +const featuredBounties = challengePhChallenges.slice(0, 4); + +function HeroSection() { + const socialProofItems = [ + { + value: 151, + label: "challenges", + }, + { + value: "6.12M", + label: "rewards", + }, + { + value: "131", + label: "successes", + }, + ]; -const superListingBadgeSizeClasses: Record = { - body: "mx-1 align-middle", - heading: "mx-2 align-middle text-[0.38em] px-[0.72em] py-[0.38em]", - button: "mx-2 align-middle text-[0.75em] px-[0.72em] py-[0.32em] text-black", -}; - -function renderTextWithSuperListingBadge( - text: string, - size: SuperListingBadgeSize = "body", -): React.ReactNode { - const parts = text.split(/(Super Listings|Super Listing)/g); - - return parts.map((part, index) => { - if (part === "Super Listings" || part === "Super Listing") { - return ( - +
+ BetterInternship - ); - } - - return {part}; - }); +
+
+
+
+
+
+
+
+
+
+
+

+ Solve 🇵🇭 challenges + + Win rewards. + Make a difference. +

+ +
+ + Explore bounties + +
+
+
+ {socialProofItems.map((item) => ( +
+

+ {item.value} +

+

+ {item.label} +

+
+ ))} +
+
+
+

+ Open to all PH students nationwide. +

+
+
+
+ +
+
+ + + ); } -/* ---------- Magnetic CTA Button ---------- */ -function MagneticButton({ - children, - className = "", +function PipelineStep({ + number, + title, + body, + accent, }: { - children: React.ReactNode; - className?: string; + number: string; + title: string; + body: string; + accent: "blue" | "gold"; }) { - const prefersReduce = useReducedMotion(); - const ref = useRef(null); - const [tx, setTx] = useState(0); - const [ty, setTy] = useState(0); - - const max = 6; // px - function onMove(e: React.MouseEvent) { - if (prefersReduce) return; - const el = ref.current; - if (!el) return; - const r = el.getBoundingClientRect(); - const x = (e.clientX - r.left) / r.width - 0.5; - const y = (e.clientY - r.top) / r.height - 0.5; - setTx(x * max * 2); - setTy(y * max * 2); - } - function onLeave() { - setTx(0); - setTy(0); - } + const isGold = accent === "gold"; return ( - - {children} - + + [{number}] + +
+

+ {title} +

+

+ {body} +

+
+ +
+ ); } -export default function SuperListingStoryPage() { - const containerRef = useRef(null); - const [randomPositions, setRandomPositions] = useState<{ - tl: { top: string; left: string }; - tr: { top: string; right: string }; - bl: { bottom: string; left: string }; - br: { bottom: string; right: string }; - } | null>(null); - - useEffect(() => { - // Generate random positions on mount - setRandomPositions({ - tl: { - top: `${15 + Math.random() * 15}%`, - left: `${5 + Math.random() * 10}%`, - }, - tr: { - top: `${20 + Math.random() * 20}%`, - right: `${5 + Math.random() * 10}%`, - }, - bl: { - bottom: `${15 + Math.random() * 15}%`, - left: `${8 + Math.random() * 12}%`, - }, - br: { - bottom: `${20 + Math.random() * 15}%`, - right: `${8 + Math.random() * 12}%`, - }, - }); - }, []); - - const { scrollYProgress } = useScroll({ - target: containerRef, - offset: ["start start", "end end"], - }); - - // Parallax values - const heroY = useTransform(scrollYProgress, [0, 0.2], [0, 100]); - const heroScale = useTransform(scrollYProgress, [0, 0.3], [1, 0.95]); - const heroOpacity = useTransform(scrollYProgress, [0.15, 0.35], [1, 0.7]); - +function MissionPipeline() { return ( -
- {/* Grid pattern background */} -
- -
- - {/* Animated orbs - brand colors */} -
- - -
+
+
+
+
+

+ Mission Pipeline +

+

+ Your path from challenge to impact +

+
+
+
+ +
+
+
+

+ MAIN CHALLENGE +

+

+ Open to all participants +

+
+
+
+ + + +
+
- {/* HEADER */} -
-
-
- - BetterInternship +
+
+

+ OPTIONAL: SUPER CHALLENGE +

+

+ For selected winners +

+
+
+
+ - - BetterInternship - - + + +
-
- - {/* Hero Section with Parallax */} - - {/* Lightning Icons - Top Left */} - - - - - {/* Lightning Icons - Top Right */} - - - - - {/* Lightning Icons - Bottom Left */} - - - - - {/* Lightning Icons - Bottom Right */} - - - - - {/* Animated background gradient */} -
- {/* Moving orbital circles */} - - +
+
+ ); +} - {/* Dynamic gradient blobs */} - - +function BountyCard({ + challenge, +}: { + challenge: (typeof challengePhChallenges)[number]; +}) { + const amount = + challenge.reward.match(/(PHP[\s\d,]+)/i)?.[1]?.trim() ?? challenge.reward; - {/* Original animated orbs */} - - + return ( +
+
+

+ {challenge.shortTitle} +

+
- {/* Pulsing accent circles */} - - -
+

+ {amount} +

+ +

+ {challenge.summary} +

+ +
+

+ + {challenge.host} +

+

+ + {challenge.deadline} +

+

+ + {challenge.location} +

+
-
- + {challenge.tags.slice(0, 2).map((tag) => ( + - - ✨ The Future of Hiring - -

- Talent -
- Doesn't Always -
- Wear a Resume -

-

- The best builders are filtered out before they get a real chance.{" "} - {renderTextWithSuperListingBadge( - "With Super Listings, capability matters. Execution wins. You win.", - )} -

- - - - - - - -
-
+ {tag} + + ))} +
- {/* Scroll indicator */} - - - - - - + + View + + + + ); +} - {/* The Problem */} - -
- +
+
+