736 lines
33 KiB
TypeScript
736 lines
33 KiB
TypeScript
import { A } from '@solidjs/router';
|
|
import { createEffect, createMemo, createSignal, For, onCleanup, onMount, Show } from 'solid-js';
|
|
import OpportunityGraph from '~/components/OpportunityGraph';
|
|
import PublicHeader from '~/components/PublicHeader';
|
|
import PublicFooter from '~/components/PublicFooter';
|
|
|
|
type PathCard = {
|
|
title: string;
|
|
description: string;
|
|
button: string;
|
|
chip: string;
|
|
icon: 'briefcase' | 'user' | 'users' | 'code' | 'camera' | 'sparkles' | 'grad' | 'film' | 'pen' | 'share' | 'dumbbell' | 'utensils';
|
|
href: string;
|
|
image: string;
|
|
audience: 'customer' | 'professional' | 'company' | 'job_seeker';
|
|
};
|
|
|
|
type Flow = {
|
|
label: string;
|
|
title: string;
|
|
description: string;
|
|
image: string;
|
|
steps: Array<{ title: string; description: string }>;
|
|
};
|
|
|
|
const pathCards: PathCard[] = [
|
|
{
|
|
title: 'Post a Job',
|
|
description: 'Create verified job openings and find the right talent faster.',
|
|
button: 'Start as Company',
|
|
chip: 'Company',
|
|
icon: 'briefcase',
|
|
href: '/auth/register?intent=company&redirect=/users/onboarding/company',
|
|
audience: 'company',
|
|
image: 'https://images.unsplash.com/photo-1484480974693-6ca0a78fb36b?q=80&w=800&auto=format&fit=crop',
|
|
},
|
|
{
|
|
title: 'Apply for Jobs',
|
|
description: 'Build your profile and apply to approved opportunities quickly.',
|
|
button: 'Start as Job Seeker',
|
|
chip: 'Job Seeker',
|
|
icon: 'user',
|
|
href: '/auth/register?intent=job_seeker&redirect=/users/onboarding/job-seeker',
|
|
audience: 'job_seeker',
|
|
image: 'https://images.unsplash.com/photo-1586281380349-632531db7ed4?q=80&w=800&auto=format&fit=crop',
|
|
},
|
|
{
|
|
title: 'Hire a Professional',
|
|
description: 'Post your requirement and discover trusted specialists.',
|
|
button: 'Start as Customer',
|
|
chip: 'Customer',
|
|
icon: 'users',
|
|
href: '/auth/register?intent=customer&redirect=/users/onboarding/customer',
|
|
audience: 'customer',
|
|
image: 'https://images.unsplash.com/photo-1450101499163-c8848c66ca85?q=80&w=800&auto=format&fit=crop',
|
|
},
|
|
{
|
|
title: 'Join as Developer',
|
|
description: 'Build products and grow with verified client demand.',
|
|
button: 'Join Developer',
|
|
chip: 'Professional',
|
|
icon: 'code',
|
|
href: '/auth/register?intent=professional&redirect=/users/onboarding/professional&profession=developer',
|
|
audience: 'professional',
|
|
image: 'https://images.unsplash.com/photo-1498050108023-c5249f4df085?q=80&w=800&auto=format&fit=crop',
|
|
},
|
|
{
|
|
title: 'Join as Photographer',
|
|
description: 'Capture events and campaigns with trusted bookings.',
|
|
button: 'Join Photographer',
|
|
chip: 'Professional',
|
|
icon: 'camera',
|
|
href: '/auth/register?intent=professional&redirect=/users/onboarding/professional&profession=photographer',
|
|
audience: 'professional',
|
|
image: 'https://images.unsplash.com/photo-1516035069371-29a1b244cc32?q=80&w=800&auto=format&fit=crop',
|
|
},
|
|
{
|
|
title: 'Join as Makeup Artist',
|
|
description: 'Offer styling services with profile-based trust signals.',
|
|
button: 'Join Makeup Artist',
|
|
chip: 'Professional',
|
|
icon: 'sparkles',
|
|
href: '/auth/register?intent=professional&redirect=/users/onboarding/professional&profession=makeup_artist',
|
|
audience: 'professional',
|
|
image: 'https://images.unsplash.com/photo-1522335789203-aabd1fc54bc9?q=80&w=800&auto=format&fit=crop',
|
|
},
|
|
{
|
|
title: 'Join as Tutor',
|
|
description: 'Teach learners and build your reputation with verified profiles.',
|
|
button: 'Join Tutor',
|
|
chip: 'Professional',
|
|
icon: 'grad',
|
|
href: '/auth/register?intent=professional&redirect=/users/onboarding/professional&profession=tutor',
|
|
audience: 'professional',
|
|
image: 'https://images.unsplash.com/photo-1497633762265-9d179a990aa6?q=80&w=800&auto=format&fit=crop',
|
|
},
|
|
{
|
|
title: 'Join as Video Editor',
|
|
description: 'Create compelling edits and work with quality clients.',
|
|
button: 'Join Video Editor',
|
|
chip: 'Professional',
|
|
icon: 'film',
|
|
href: '/auth/register?intent=professional&redirect=/users/onboarding/professional&profession=video_editor',
|
|
audience: 'professional',
|
|
image: 'https://images.unsplash.com/photo-1574717024653-61fd2cf4d44d?q=80&w=800&auto=format&fit=crop',
|
|
},
|
|
{
|
|
title: 'Join as Graphic Designer',
|
|
description: 'Design brand-ready visuals and collaborate with growing businesses.',
|
|
button: 'Join Graphic Designer',
|
|
chip: 'Professional',
|
|
icon: 'pen',
|
|
href: '/auth/register?intent=professional&redirect=/users/onboarding/professional&profession=graphic_designer',
|
|
audience: 'professional',
|
|
image: 'https://images.unsplash.com/photo-1558655146-d09347e92766?q=80&w=800&auto=format&fit=crop',
|
|
},
|
|
{
|
|
title: 'Join as Social Media Manager',
|
|
description: 'Plan campaigns and scale audience growth for clients.',
|
|
button: 'Join Social Manager',
|
|
chip: 'Professional',
|
|
icon: 'share',
|
|
href: '/auth/register?intent=professional&redirect=/users/onboarding/professional&profession=social_media_manager',
|
|
audience: 'professional',
|
|
image: 'https://images.unsplash.com/photo-1611162618071-b39a2ec055fb?q=80&w=800&auto=format&fit=crop',
|
|
},
|
|
{
|
|
title: 'Join as Fitness Trainer',
|
|
description: 'Coach clients with structured plans and trusted profiles.',
|
|
button: 'Join Trainer',
|
|
chip: 'Professional',
|
|
icon: 'dumbbell',
|
|
href: '/auth/register?intent=professional&redirect=/users/onboarding/professional&profession=fitness_trainer',
|
|
audience: 'professional',
|
|
image: 'https://images.unsplash.com/photo-1517836357463-d25dfeac3438?q=80&w=800&auto=format&fit=crop',
|
|
},
|
|
{
|
|
title: 'Join as Catering Services',
|
|
description: 'Showcase event-ready menus to customers and companies.',
|
|
button: 'Join Catering',
|
|
chip: 'Professional',
|
|
icon: 'utensils',
|
|
href: '/auth/register?intent=professional&redirect=/users/onboarding/professional&profession=catering_services',
|
|
audience: 'professional',
|
|
image: 'https://images.unsplash.com/photo-1555244162-803834f70033?q=80&w=800&auto=format&fit=crop',
|
|
},
|
|
];
|
|
|
|
const benefits = [
|
|
{ title: 'Verified profiles & businesses', body: 'Identity and profile checks reduce fake submissions and spam.', icon: 'shield-check' },
|
|
{ title: 'Approval-based quality (24-48 hours)', body: 'Profiles, requirements, and jobs are reviewed before visibility.', icon: 'zap' },
|
|
{ title: 'Smart matching using tags/skills', body: 'Tag-based relevance helps surface better opportunities faster.', icon: 'hash' },
|
|
{ title: 'Focused discovery with filters', body: 'Search and filter tools keep opportunity discovery focused.', icon: 'search' },
|
|
{ title: 'Controlled contact visibility', body: 'Sensitive contact flow remains controlled by role and workflow.', icon: 'lock' },
|
|
{ title: 'Notifications & updates', body: 'Track approvals, responses, applications, and key updates.', icon: 'bell' },
|
|
] as const;
|
|
|
|
const flows: Flow[] = [
|
|
{
|
|
label: 'Customers',
|
|
title: 'Need to solution, with verified trust in the middle',
|
|
description: 'Post your requirement, pass quality checks, and receive focused responses.',
|
|
image: 'https://images.unsplash.com/photo-1454165804606-c3d57bc86b40?q=80&w=1200&auto=format&fit=crop',
|
|
steps: [
|
|
{ title: 'Create account', description: 'Sign up quickly with customer intent.' },
|
|
{ title: 'Share requirement', description: 'Add budget, scope, and timing.' },
|
|
{ title: 'Review and verify', description: 'Quality checks run before visibility.' },
|
|
{ title: 'Track responses', description: 'Monitor replies and status updates.' },
|
|
],
|
|
},
|
|
{
|
|
label: 'Professionals',
|
|
title: 'Profile first, opportunity next',
|
|
description: 'Build profile quality once, then receive better-fit lead discovery.',
|
|
image: 'https://images.unsplash.com/photo-1461749280684-dccba630e2f6?q=80&w=1200&auto=format&fit=crop',
|
|
steps: [
|
|
{ title: 'Pick role and skills', description: 'Choose service category and specialization.' },
|
|
{ title: 'Build profile', description: 'Add portfolio, strengths, and proof.' },
|
|
{ title: 'Pass verification', description: 'Approval protects platform trust.' },
|
|
{ title: 'Respond to opportunities', description: 'Manage matching leads in one flow.' },
|
|
],
|
|
},
|
|
{
|
|
label: 'Companies',
|
|
title: 'Hiring workflows without chaos',
|
|
description: 'Publish requirements, keep quality high, and track applicants clearly.',
|
|
image: 'https://images.unsplash.com/photo-1486406146926-c627a92ad1ab?q=80&w=1200&auto=format&fit=crop',
|
|
steps: [
|
|
{ title: 'Create company profile', description: 'Set hiring context and org details.' },
|
|
{ title: 'Post requirements', description: 'Publish role details and expectations.' },
|
|
{ title: 'Approval before publish', description: 'Listings are reviewed first.' },
|
|
{ title: 'Manage pipeline', description: 'Track candidates and responses.' },
|
|
],
|
|
},
|
|
{
|
|
label: 'Job Seekers',
|
|
title: 'Onboard once, apply with confidence',
|
|
description: 'Get visible only after approval, then discover jobs with targeted filters.',
|
|
image: 'https://images.unsplash.com/photo-1450101499163-c8848c66ca85?q=80&w=1200&auto=format&fit=crop',
|
|
steps: [
|
|
{ title: 'Set up profile', description: 'Complete essentials and work preferences.' },
|
|
{ title: 'Approval check', description: 'Review keeps quality consistent.' },
|
|
{ title: 'Explore matching jobs', description: 'Filter by practical criteria.' },
|
|
{ title: 'Apply and track', description: 'Submit applications and watch status.' },
|
|
],
|
|
},
|
|
];
|
|
|
|
const faqs = [
|
|
{
|
|
q: 'What is Nxtgauge?',
|
|
a: 'Nxtgauge connects customers, professionals, companies, and job seekers in one trusted platform.',
|
|
},
|
|
{
|
|
q: 'Who can join Nxtgauge?',
|
|
a: 'Customers, service professionals, companies, and job seekers can create an account and choose their path.',
|
|
},
|
|
{
|
|
q: 'Why does Nxtgauge require verification?',
|
|
a: 'Verification helps reduce fake profiles and improves trust across the platform.',
|
|
},
|
|
{
|
|
q: 'How long does account approval take?',
|
|
a: 'Most profile approvals are completed within 24-48 hours.',
|
|
},
|
|
{
|
|
q: 'Do I need to choose my role during signup?',
|
|
a: 'No. You can sign up once and continue with the role flow that fits you best.',
|
|
},
|
|
] as const;
|
|
|
|
const chipNodes = [
|
|
{ kind: 'code', left: '2%', top: '14%', size: 44, cls: 'lp-chip-slow' },
|
|
{ kind: 'camera', left: '94%', top: '22%', size: 46, cls: 'lp-chip-mid' },
|
|
{ kind: 'briefcase', left: '3%', top: '76%', size: 46, cls: 'lp-chip-fast' },
|
|
{ kind: 'bell', left: '93%', top: '80%', size: 42, cls: 'lp-chip-slow' },
|
|
{ kind: 'sparkles', left: '50%', top: '6%', size: 40, cls: 'lp-chip-mid' },
|
|
] as const;
|
|
|
|
function ChipIcon(props: { kind: (typeof chipNodes)[number]['kind'] }) {
|
|
const common = { fill: 'none', stroke: 'currentColor', 'stroke-width': '2', 'stroke-linecap': 'round', 'stroke-linejoin': 'round' } as const;
|
|
if (props.kind === 'camera') {
|
|
return (
|
|
<svg viewBox="0 0 24 24" aria-hidden="true" {...common}>
|
|
<path d="M14 4a2 2 0 0 1 1.76 1.05l.49.9A2 2 0 0 0 18 7h2a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V9a2 2 0 0 1 2-2h2a2 2 0 0 0 1.76-1.05l.49-.9A2 2 0 0 1 10 4z" />
|
|
<circle cx="12" cy="13" r="3" />
|
|
</svg>
|
|
);
|
|
}
|
|
if (props.kind === 'briefcase') {
|
|
return (
|
|
<svg viewBox="0 0 24 24" aria-hidden="true" {...common}>
|
|
<path d="M16 6V4a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v2" />
|
|
<path d="M22 13a18 18 0 0 1-20 0" />
|
|
<rect x="2" y="6" width="20" height="14" rx="2" />
|
|
</svg>
|
|
);
|
|
}
|
|
if (props.kind === 'bell') {
|
|
return (
|
|
<svg viewBox="0 0 24 24" aria-hidden="true" {...common}>
|
|
<path d="M10.3 21a2 2 0 0 0 3.4 0" />
|
|
<path d="M3.3 15.3A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.67C19.4 14 18 12.5 18 8A6 6 0 0 0 6 8c0 4.5-1.4 6-2.7 7.3" />
|
|
</svg>
|
|
);
|
|
}
|
|
if (props.kind === 'sparkles') {
|
|
return (
|
|
<svg viewBox="0 0 24 24" aria-hidden="true" {...common}>
|
|
<path d="M11 2.8a1 1 0 0 1 2 0l1 5.6a2 2 0 0 0 1.6 1.6l5.6 1a1 1 0 0 1 0 2l-5.6 1a2 2 0 0 0-1.6 1.6l-1 5.6a1 1 0 0 1-2 0l-1-5.6A2 2 0 0 0 8.4 14l-5.6-1a1 1 0 0 1 0-2l5.6-1A2 2 0 0 0 10 8.4z" />
|
|
<path d="M20 2v4M22 4h-4" />
|
|
</svg>
|
|
);
|
|
}
|
|
return (
|
|
<svg viewBox="0 0 24 24" aria-hidden="true" {...common}>
|
|
<path d="m18 16 4-4-4-4" />
|
|
<path d="m6 8-4 4 4 4" />
|
|
<path d="m14.5 4-5 16" />
|
|
</svg>
|
|
);
|
|
}
|
|
|
|
function PathRoleIcon(props: { kind: PathCard['icon'] }) {
|
|
const common = { fill: 'none', stroke: 'currentColor', 'stroke-width': '2', 'stroke-linecap': 'round', 'stroke-linejoin': 'round' } as const;
|
|
if (props.kind === 'briefcase') return <svg viewBox="0 0 24 24" aria-hidden="true" {...common}><path d="M16 6V4a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v2" /><path d="M22 13a18 18 0 0 1-20 0" /><rect x="2" y="6" width="20" height="14" rx="2" /></svg>;
|
|
if (props.kind === 'user') return <svg viewBox="0 0 24 24" aria-hidden="true" {...common}><circle cx="12" cy="8" r="4" /><path d="M6 20c1.4-3.2 3.4-5 6-5s4.6 1.8 6 5" /></svg>;
|
|
if (props.kind === 'users') return <svg viewBox="0 0 24 24" aria-hidden="true" {...common}><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" /><circle cx="9" cy="7" r="4" /><path d="M22 21v-2a4 4 0 0 0-3-3.87" /><path d="M16 3.13a4 4 0 0 1 0 7.75" /></svg>;
|
|
if (props.kind === 'code') return <svg viewBox="0 0 24 24" aria-hidden="true" {...common}><path d="m18 16 4-4-4-4" /><path d="m6 8-4 4 4 4" /><path d="m14.5 4-5 16" /></svg>;
|
|
if (props.kind === 'camera') return <svg viewBox="0 0 24 24" aria-hidden="true" {...common}><path d="M14 4a2 2 0 0 1 1.76 1.05l.49.9A2 2 0 0 0 18 7h2a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V9a2 2 0 0 1 2-2h2a2 2 0 0 0 1.76-1.05l.49-.9A2 2 0 0 1 10 4z" /><circle cx="12" cy="13" r="3" /></svg>;
|
|
if (props.kind === 'sparkles') return <svg viewBox="0 0 24 24" aria-hidden="true" {...common}><path d="M11 2.8a1 1 0 0 1 2 0l1 5.6a2 2 0 0 0 1.6 1.6l5.6 1a1 1 0 0 1 0 2l-5.6 1a2 2 0 0 0-1.6 1.6l-1 5.6a1 1 0 0 1-2 0l-1-5.6A2 2 0 0 0 8.4 14l-5.6-1a1 1 0 0 1 0-2l5.6-1A2 2 0 0 0 10 8.4z" /><path d="M20 2v4M22 4h-4" /></svg>;
|
|
if (props.kind === 'grad') return <svg viewBox="0 0 24 24" aria-hidden="true" {...common}><path d="m22 10-10-5-10 5 10 5 10-5z" /><path d="M6 12v5c3 2 9 2 12 0v-5" /></svg>;
|
|
if (props.kind === 'film') return <svg viewBox="0 0 24 24" aria-hidden="true" {...common}><rect x="2" y="2" width="20" height="20" rx="2" /><path d="M7 2v20M17 2v20M2 7h20M2 17h20" /></svg>;
|
|
if (props.kind === 'pen') return <svg viewBox="0 0 24 24" aria-hidden="true" {...common}><path d="M12 20h9" /><path d="m16.5 3.5 4 4L7 21H3v-4z" /></svg>;
|
|
if (props.kind === 'share') return <svg viewBox="0 0 24 24" aria-hidden="true" {...common}><circle cx="18" cy="5" r="3" /><circle cx="6" cy="12" r="3" /><circle cx="18" cy="19" r="3" /><path d="M8.6 13.4 15.4 17M15.4 7 8.6 10.6" /></svg>;
|
|
if (props.kind === 'dumbbell') return <svg viewBox="0 0 24 24" aria-hidden="true" {...common}><path d="M6 10v4M4 9v6M2 8v8M18 10v4M20 9v6M22 8v8M6 12h12" /></svg>;
|
|
return <svg viewBox="0 0 24 24" aria-hidden="true" {...common}><path d="M3 4h18" /><path d="M6 4v16h12V4" /><path d="M9 10h6M9 14h6" /></svg>;
|
|
}
|
|
|
|
function CheckBadgeIcon() {
|
|
return (
|
|
<svg viewBox="0 0 24 24" aria-hidden="true">
|
|
<path d="M9 12.4l2 2 4-4" />
|
|
<circle cx="12" cy="12" r="9" />
|
|
</svg>
|
|
);
|
|
}
|
|
|
|
function WhyBenefitIcon(props: { kind: (typeof benefits)[number]['icon'] }) {
|
|
const common = { fill: 'none', stroke: 'currentColor', 'stroke-width': '2', 'stroke-linecap': 'round', 'stroke-linejoin': 'round' } as const;
|
|
if (props.kind === 'shield-check') {
|
|
return (
|
|
<svg viewBox="0 0 24 24" aria-hidden="true" {...common}>
|
|
<path d="M12 2 4 6v6c0 5 3.5 8.5 8 10 4.5-1.5 8-5 8-10V6z" />
|
|
<path d="m9 12 2 2 4-4" />
|
|
</svg>
|
|
);
|
|
}
|
|
if (props.kind === 'zap') {
|
|
return (
|
|
<svg viewBox="0 0 24 24" aria-hidden="true" {...common}>
|
|
<path d="M13 2 3 14h7l-1 8 10-12h-7z" />
|
|
</svg>
|
|
);
|
|
}
|
|
if (props.kind === 'hash') {
|
|
return (
|
|
<svg viewBox="0 0 24 24" aria-hidden="true" {...common}>
|
|
<path d="M4 9h16M3 15h16M10 3 8 21M16 3l-2 18" />
|
|
</svg>
|
|
);
|
|
}
|
|
if (props.kind === 'search') {
|
|
return (
|
|
<svg viewBox="0 0 24 24" aria-hidden="true" {...common}>
|
|
<circle cx="11" cy="11" r="7" />
|
|
<path d="m21 21-4.35-4.35" />
|
|
</svg>
|
|
);
|
|
}
|
|
if (props.kind === 'lock') {
|
|
return (
|
|
<svg viewBox="0 0 24 24" aria-hidden="true" {...common}>
|
|
<rect x="4" y="11" width="16" height="10" rx="2" />
|
|
<path d="M8 11V7a4 4 0 0 1 8 0v4" />
|
|
</svg>
|
|
);
|
|
}
|
|
return (
|
|
<svg viewBox="0 0 24 24" aria-hidden="true" {...common}>
|
|
<path d="M10.3 21a2 2 0 0 0 3.4 0" />
|
|
<path d="M3.3 15.3A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.67C19.4 14 18 12.5 18 8A6 6 0 0 0 6 8c0 4.5-1.4 6-2.7 7.3" />
|
|
</svg>
|
|
);
|
|
}
|
|
|
|
export default function PublicLanding() {
|
|
const [reduceMotion, setReduceMotion] = createSignal(false);
|
|
const [scrollY, setScrollY] = createSignal(0);
|
|
const [heroTilt, setHeroTilt] = createSignal({ x: 0, y: 0 });
|
|
const [pathPage, setPathPage] = createSignal(0);
|
|
const [cardsPerPage, setCardsPerPage] = createSignal(3);
|
|
const [pathPaused, setPathPaused] = createSignal(false);
|
|
const [pathTouchStartX, setPathTouchStartX] = createSignal<number | null>(null);
|
|
const [benefitIdx, setBenefitIdx] = createSignal(0);
|
|
const [flowIndex, setFlowIndex] = createSignal(0);
|
|
const [flowStepIndex, setFlowStepIndex] = createSignal(0);
|
|
const [openFaq, setOpenFaq] = createSignal(0);
|
|
const [showBackToTop, setShowBackToTop] = createSignal(false);
|
|
|
|
onMount(() => {
|
|
const media = window.matchMedia('(prefers-reduced-motion: reduce)');
|
|
const syncMotion = () => setReduceMotion(media.matches);
|
|
syncMotion();
|
|
|
|
let ticking = false;
|
|
const onScroll = () => {
|
|
setShowBackToTop(window.scrollY > 500);
|
|
if (media.matches) return;
|
|
if (ticking) return;
|
|
ticking = true;
|
|
requestAnimationFrame(() => {
|
|
setScrollY(window.scrollY || 0);
|
|
ticking = false;
|
|
});
|
|
};
|
|
|
|
const syncCardsPerPage = () => {
|
|
const w = window.innerWidth;
|
|
if (w < 640) {
|
|
setCardsPerPage(1);
|
|
return;
|
|
}
|
|
if (w < 1024) {
|
|
setCardsPerPage(2);
|
|
return;
|
|
}
|
|
setCardsPerPage(3);
|
|
};
|
|
|
|
onScroll();
|
|
syncCardsPerPage();
|
|
window.addEventListener('scroll', onScroll, { passive: true });
|
|
window.addEventListener('resize', syncCardsPerPage);
|
|
media.addEventListener('change', syncMotion);
|
|
|
|
const benefitTimer = window.setInterval(() => setBenefitIdx((x) => (x + 1) % benefits.length), 4200);
|
|
const flowTimer = window.setInterval(() => {
|
|
const active = flows[flowIndex()];
|
|
setFlowStepIndex((prev) => {
|
|
const next = prev + 1;
|
|
if (next < active.steps.length) return next;
|
|
setFlowIndex((f) => (f + 1) % flows.length);
|
|
return 0;
|
|
});
|
|
}, 3000);
|
|
|
|
onCleanup(() => {
|
|
window.removeEventListener('scroll', onScroll);
|
|
window.removeEventListener('resize', syncCardsPerPage);
|
|
media.removeEventListener('change', syncMotion);
|
|
window.clearInterval(benefitTimer);
|
|
window.clearInterval(flowTimer);
|
|
});
|
|
});
|
|
|
|
const pagedPaths = createMemo(() => {
|
|
const cards = pathCards;
|
|
const per = cardsPerPage();
|
|
const pages: PathCard[][] = [];
|
|
for (let i = 0; i < cards.length; i += per) pages.push(cards.slice(i, i + per));
|
|
return pages;
|
|
});
|
|
|
|
const activePathPage = createMemo(() => {
|
|
const pages = pagedPaths();
|
|
if (pages.length === 0) return 0;
|
|
return pathPage() % pages.length;
|
|
});
|
|
|
|
createEffect(() => {
|
|
const pagesLen = pagedPaths().length;
|
|
if (reduceMotion() || pathPaused() || pagesLen <= 1) return;
|
|
const timer = window.setInterval(() => {
|
|
setPathPage((prev) => {
|
|
const next = prev + 1;
|
|
return next >= pagesLen ? 0 : next;
|
|
});
|
|
}, 4200);
|
|
onCleanup(() => window.clearInterval(timer));
|
|
});
|
|
|
|
return (
|
|
<main class="lp-main">
|
|
<div class="lp-bg" aria-hidden="true">
|
|
<div class="lp-dark-base" />
|
|
<div class="lp-mesh" style={{ transform: `translate3d(0, ${reduceMotion() ? 0 : Math.min(36, scrollY() * 0.1)}px, 0)` }} />
|
|
<div class="lp-ribbon" style={{ transform: `translate3d(0, ${reduceMotion() ? 0 : Math.min(58, scrollY() * 0.18)}px, 0)` }} />
|
|
<div class="lp-chips" style={{ transform: `translate3d(0, ${reduceMotion() ? 0 : Math.min(80, scrollY() * 0.24)}px, 0)` }}>
|
|
<For each={chipNodes}>
|
|
{(chip) => (
|
|
<span class={`lp-chip ${chip.cls}`} style={{ left: chip.left, top: chip.top, width: `${chip.size}px`, height: `${chip.size}px` }}>
|
|
<ChipIcon kind={chip.kind} />
|
|
</span>
|
|
)}
|
|
</For>
|
|
</div>
|
|
<div class="lp-noise" />
|
|
</div>
|
|
|
|
<div class="lp-content">
|
|
<PublicHeader />
|
|
|
|
<section
|
|
class="public-hero scene-dark lp-section-hero"
|
|
onMouseMove={(event) => {
|
|
if (reduceMotion()) return;
|
|
const rect = event.currentTarget.getBoundingClientRect();
|
|
const px = (event.clientX - rect.left) / rect.width - 0.5;
|
|
const py = (event.clientY - rect.top) / rect.height - 0.5;
|
|
setHeroTilt({ x: px * 14, y: py * 12 });
|
|
}}
|
|
onMouseLeave={() => setHeroTilt({ x: 0, y: 0 })}
|
|
>
|
|
<div class="container lp-hero-grid">
|
|
<div>
|
|
<h1 class="lp-hero-title">Hire verified professionals. Post jobs. Get approvals in 24-48 hours.</h1>
|
|
<p class="lp-hero-copy">
|
|
Nxtgauge connects customers, companies, job seekers, and professionals through a trusted approval workflow.
|
|
</p>
|
|
<div class="hero-actions">
|
|
<A class="lp-primary-btn" href="/auth/register?intent=customer&redirect=/users/onboarding/customer">Hire a Professional</A>
|
|
<A class="lp-ghost-btn lp-ghost-btn-dark" href="/auth/register?intent=job_seeker&redirect=/users/onboarding/job-seeker">Apply for Jobs</A>
|
|
</div>
|
|
</div>
|
|
<div
|
|
class="lp-hero-graph"
|
|
style={{
|
|
transform: reduceMotion() ? 'none' : `translate3d(${heroTilt().x}px, ${heroTilt().y}px, 0)`,
|
|
}}
|
|
>
|
|
<OpportunityGraph reduceMotion={reduceMotion()} />
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section id="choose-path" class="public-section scene-dark lp-section">
|
|
<div class="container panel choose-path-panel">
|
|
<div class="section-head">
|
|
<div>
|
|
<h2>Choose Your Path</h2>
|
|
<p class="sub">One account, multiple journeys. Pick your goal and get started.</p>
|
|
</div>
|
|
<div class="lp-path-controls desktop-only">
|
|
<button
|
|
class="lp-path-arrow"
|
|
onClick={() => setPathPage((x) => Math.max(0, x - 1))}
|
|
aria-label="Previous cards"
|
|
disabled={activePathPage() === 0}
|
|
>
|
|
←
|
|
</button>
|
|
<button
|
|
class="lp-path-arrow"
|
|
onClick={() => setPathPage((x) => Math.min(Math.max(0, pagedPaths().length - 1), x + 1))}
|
|
aria-label="Next cards"
|
|
disabled={activePathPage() >= Math.max(0, pagedPaths().length - 1)}
|
|
>
|
|
→
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="path-carousel-shell">
|
|
<div
|
|
class={`path-carousel-track ${pathPaused() ? 'path-carousel-track-hover' : ''}`}
|
|
style={{ transform: `translate3d(-${activePathPage() * 100}%, 0, 0)` }}
|
|
onMouseEnter={() => setPathPaused(true)}
|
|
onMouseLeave={() => setPathPaused(false)}
|
|
onTouchStart={(event) => {
|
|
setPathPaused(true);
|
|
setPathTouchStartX(event.changedTouches[0]?.clientX ?? null);
|
|
}}
|
|
onTouchEnd={(event) => {
|
|
setPathPaused(false);
|
|
const startX = pathTouchStartX();
|
|
const endX = event.changedTouches[0]?.clientX ?? null;
|
|
if (startX == null || endX == null) return;
|
|
const deltaX = endX - startX;
|
|
if (Math.abs(deltaX) < 40) return;
|
|
if (deltaX < 0) {
|
|
setPathPage((x) => Math.min(Math.max(0, pagedPaths().length - 1), x + 1));
|
|
return;
|
|
}
|
|
setPathPage((x) => Math.max(0, x - 1));
|
|
}}
|
|
>
|
|
<For each={pagedPaths()}>
|
|
{(group) => (
|
|
<div class="path-page">
|
|
<div class={`path-grid path-grid-${cardsPerPage()}`}>
|
|
<For each={group}>
|
|
{(card) => (
|
|
<article class="path-card path-card-hero">
|
|
<div class="path-media">
|
|
<img src={card.image} alt={card.title} loading="lazy" />
|
|
<div class="path-media-overlay" />
|
|
</div>
|
|
<div class="path-body">
|
|
<div class="path-head-row">
|
|
<span class="path-icon"><PathRoleIcon kind={card.icon} /></span>
|
|
<span class="path-chip"><CheckBadgeIcon />{card.chip}</span>
|
|
</div>
|
|
<h3>{card.title}</h3>
|
|
<p>{card.description}</p>
|
|
<A class="path-secondary-btn" href={card.href}>{card.button}</A>
|
|
</div>
|
|
</article>
|
|
)}
|
|
</For>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</For>
|
|
</div>
|
|
</div>
|
|
<div class="lp-path-dots">
|
|
<For each={pagedPaths()}>
|
|
{(_, idx) => (
|
|
<button
|
|
class={`lp-path-dot ${idx() === activePathPage() ? 'active' : ''}`}
|
|
onClick={() => setPathPage(idx())}
|
|
aria-label={`Go to page ${idx() + 1}`}
|
|
/>
|
|
)}
|
|
</For>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section id="why-nxtgauge" class="public-section scene-dark lp-section">
|
|
<div class="container panel panel-dark">
|
|
<div class="center">
|
|
<p class="eyebrow">Why Nxtgauge</p>
|
|
<h2>Trust, approvals, and better matching in one flow.</h2>
|
|
</div>
|
|
<div class="whySliderWrap">
|
|
<article class="whyHeroCard whyHeroCardPulse">
|
|
<div class="whyCardGlow" />
|
|
<div class="whyHaloA" />
|
|
<div class="whyHaloB" />
|
|
<div class="whyCardContent whyContentSwap">
|
|
<p class="whyMetaKicker">Capability {benefitIdx() + 1} of {benefits.length}</p>
|
|
<span class="whyIconShell"><WhyBenefitIcon kind={benefits[benefitIdx()].icon} /></span>
|
|
<h3 style={{ margin: '12px 0 0', color: '#fff', 'font-size': '30px' }}>{benefits[benefitIdx()].title}</h3>
|
|
<p style={{ margin: '8px 0 0', color: 'rgba(255,255,255,0.9)', 'font-size': '17px' }}>{benefits[benefitIdx()].body}</p>
|
|
</div>
|
|
<span class="whyAutoTrack" />
|
|
</article>
|
|
<div class="whyOrbitalNav">
|
|
<For each={benefits}>
|
|
{(item, idx) => (
|
|
<button
|
|
class={`whyOrbitalBtn ${idx() === benefitIdx() ? 'whyOrbitalBtnActive' : ''}`}
|
|
onClick={() => setBenefitIdx(idx())}
|
|
aria-label={`Show ${item.title}`}
|
|
title={item.title}
|
|
>
|
|
<WhyBenefitIcon kind={item.icon} />
|
|
</button>
|
|
)}
|
|
</For>
|
|
</div>
|
|
<div class="whyControls">
|
|
<button class="whyControlBtn" onClick={() => setBenefitIdx((benefitIdx() - 1 + benefits.length) % benefits.length)} aria-label="Previous slide">◀</button>
|
|
<button class="whyControlBtn" onClick={() => setBenefitIdx((benefitIdx() + 1) % benefits.length)} aria-label="Next slide">▶</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section id="how-it-works" class="public-section scene-dark lp-section landing-hiw-section">
|
|
<div class="container panel panel-light">
|
|
<div class="center">
|
|
<p class="eyebrow">How it works</p>
|
|
<h2>Clear journey, zero confusion</h2>
|
|
</div>
|
|
<article class="hiwCodeCard">
|
|
<div class="hiwCodeMedia">
|
|
<div class="hiwCodeBigRect">
|
|
<img class="hiwCodePhoto" src={flows[flowIndex()].image} alt={`${flows[flowIndex()].label} role`} />
|
|
</div>
|
|
<div class="hiwCodeMediaCopy">
|
|
<p class="hiwCodeKicker">{flows[flowIndex()].label}</p>
|
|
<h3 class="hiwCodeTitle">{flows[flowIndex()].title}</h3>
|
|
<p class="hiwCodeDesc">{flows[flowIndex()].description}</p>
|
|
</div>
|
|
</div>
|
|
<div class="hiwCodeBody">
|
|
<p class="hiwCodeStepsHeading">Step Flow</p>
|
|
<div class="hiwCodeSteps">
|
|
<For each={flows[flowIndex()].steps}>
|
|
{(step, idx) => (
|
|
<div class={`hiwCodeStep ${idx() === flowStepIndex() ? 'hiwCodeStepActive' : ''}`}>
|
|
<span class="hiwCodeStepNum">{idx() + 1}</span>
|
|
<div>
|
|
<h4 class="hiwCodeStepTitle">{step.title}</h4>
|
|
<p class="hiwCodeStepDesc">{step.description}</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</For>
|
|
</div>
|
|
<div class="hiwCodeFooter">
|
|
<div class="hiwCodeArrows">
|
|
<button class="hiwCodeArrow" onClick={() => { setFlowIndex((flowIndex() - 1 + flows.length) % flows.length); setFlowStepIndex(0); }} aria-label="Previous role">←</button>
|
|
<button class="hiwCodeArrow" onClick={() => { setFlowIndex((flowIndex() + 1) % flows.length); setFlowStepIndex(0); }} aria-label="Next role">→</button>
|
|
</div>
|
|
<div class="hiwCodeDots">
|
|
<For each={flows}>
|
|
{(_, idx) => <span class={`hiwCodeDot ${idx() === flowIndex() ? 'hiwCodeDotActive' : ''}`} />}
|
|
</For>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</article>
|
|
</div>
|
|
</section>
|
|
|
|
<section id="faqs" class="public-section scene-dark lp-section landing-faq-section">
|
|
<div class="container panel panel-dark faq-wrap">
|
|
<h2 class="center">Frequently asked questions</h2>
|
|
<p class="center sub">Quick answers before you create your account.</p>
|
|
<div class="faq-list">
|
|
<For each={faqs}>
|
|
{(item, idx) => (
|
|
<article class={`faq-item ${openFaq() === idx() ? 'open' : ''}`}>
|
|
<button class="faq-q" onClick={() => setOpenFaq(openFaq() === idx() ? -1 : idx())}>
|
|
<span>{item.q}</span>
|
|
<span class={`faq-q-icon ${openFaq() === idx() ? 'open' : ''}`}>⌄</span>
|
|
</button>
|
|
<Show when={openFaq() === idx()}>
|
|
<p class="faq-a">{item.a}</p>
|
|
</Show>
|
|
</article>
|
|
)}
|
|
</For>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="public-section scene-dark lp-section">
|
|
<div class="container panel panel-dark cta-panel">
|
|
<div class="cta-glow" />
|
|
<div class="cta-copy">
|
|
<p class="eyebrow">Quick Actions</p>
|
|
<h2>Ready to get started?</h2>
|
|
<p class="sub">Pick your next action and continue with the correct role flow.</p>
|
|
</div>
|
|
<div class="hero-actions cta-actions">
|
|
<A class="lp-primary-btn pulse" href="/auth/register?intent=customer&redirect=/users/onboarding/customer">Hire a Professional</A>
|
|
<A class="lp-ghost-btn lp-ghost-btn-dark" href="/auth/register?intent=job_seeker&redirect=/users/onboarding/job-seeker">Apply for Jobs</A>
|
|
<A class="lp-ghost-btn lp-ghost-btn-dark" href="/auth/register?intent=company&redirect=/users/onboarding/company">Post a Job</A>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<PublicFooter />
|
|
<Show when={showBackToTop()}>
|
|
<button class="back-top" onClick={() => window.scrollTo({ top: 0, behavior: reduceMotion() ? 'auto' : 'smooth' })}>
|
|
↑
|
|
</button>
|
|
</Show>
|
|
</div>
|
|
</main>
|
|
);
|
|
}
|