From 19a0850c4923fe9e2d042d1cae373bfdbbeeec88 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Date: Mon, 6 Apr 2026 03:33:29 +0200 Subject: [PATCH] feat: integrate dynamic help center with runtime-configured knowledge base --- .../admin/DashboardDesignPreview.tsx | 853 +++++++++++++++--- src/lib/runtime/types.ts | 15 + src/routes/login.tsx | 2 +- src/routes/signup.tsx | 5 +- src/routes/users/choose-role.tsx | 84 -- 5 files changed, 724 insertions(+), 235 deletions(-) delete mode 100644 src/routes/users/choose-role.tsx diff --git a/src/components/admin/DashboardDesignPreview.tsx b/src/components/admin/DashboardDesignPreview.tsx index 2832ce1..04b7e5f 100644 --- a/src/components/admin/DashboardDesignPreview.tsx +++ b/src/components/admin/DashboardDesignPreview.tsx @@ -1,48 +1,18 @@ -import { For, Show, createEffect, createMemo, createResource, createSignal } from 'solid-js'; -import { - Award, - Bell, - BadgeCheck, - Bookmark, - BriefcaseBusiness, - Calendar, - Camera, - CheckCircle2, - ChevronDown, - ChevronUp, - Clapperboard, - Compass, - Coins, - Code2, - Dumbbell, - Edit2, - Eye, - FileText, - GraduationCap, - HandHelping, - HeadphonesIcon, - HelpCircle, - Image, - LayoutGrid, - LogOut, - MapPin, - Megaphone, - PenTool, - RefreshCw, - Rocket, - Scissors, - Search, - Settings, - Settings2, - ShieldCheck, - Star, - TrendingUp, - UtensilsCrossed, - User, - UserCircle2, - Users, - X, +import { For, Show, createEffect, createMemo, createResource, createSignal, onMount } from 'solid-js'; +import { + Plus, Search, ArrowRight, Clock, User, BookOpen, AlertCircle, Save, X, + LayoutGrid, List, CheckCircle, Edit2, Trash2, Globe, Lock, + Rocket, TrendingUp, ShieldCheck, Mail, Phone, MapPin, + ChevronRight, ChevronLeft, ChevronDown, ChevronUp, + Download, Filter, MoreVertical, Star, HelpCircle, + Award, Bell, BadgeCheck, Bookmark, BriefcaseBusiness, Calendar, + Camera, CheckCircle2, Clapperboard, Compass, Coins, Code2, + Dumbbell, Eye, FileText, GraduationCap, HandHelping, + HeadphonesIcon, Image, LogOut, Megaphone, PenTool, + RefreshCw, Scissors, Settings, Settings2, UtensilsCrossed, + UserCircle2, Users, } from 'lucide-solid'; +import { RuntimeKBConfig, RuntimeKBArticle } from '../../lib/runtime/types'; function titleCase(value: string) { return String(value || '') @@ -51,6 +21,7 @@ function titleCase(value: string) { } const ORANGE_ICON_FILTER = 'invert(51%) sepia(86%) saturate(2445%) hue-rotate(353deg) brightness(101%) contrast(103%)'; +const BLUE_ICON_FILTER = 'invert(11%) sepia(85%) saturate(2462%) hue-rotate(233deg) brightness(91%) contrast(101%)'; function StatusBadge(props: { status: 'ACTIVE' | 'INACTIVE' }) { const active = () => props.status === 'ACTIVE'; @@ -609,10 +580,12 @@ const REQUIREMENT_ROWS = [ title: 'Wedding Shoot in Goa', summary: 'Traditional & drone coverage', category: 'PHOTOGRAPHER', + amount: '₹50,000', budget: '₹50,000 - ₹80,000', location: 'Goa (On-site)', submission: 'Oct 24, 2023', status: 'approved', + responses: 5, responseTag: 'Active (5 Responses)', }, { @@ -620,10 +593,12 @@ const REQUIREMENT_ROWS = [ title: 'E-commerce Portal Redesign', summary: 'Next.js + Tailwind CSS specialist', category: 'DEVELOPER', + amount: '₹1,20,000', budget: '₹1,20,000 - ₹2,00,000', location: 'Remote', submission: 'Nov 02, 2023', status: 'under review', + responses: 0, responseTag: 'In Review', }, { @@ -631,10 +606,12 @@ const REQUIREMENT_ROWS = [ title: 'Corporate Brand Identity', summary: 'Logo, stationery & guidelines', category: 'UI DESIGNER', + amount: '₹35,000', budget: '₹35,000 Flat', location: 'Mumbai (Hybrid)', submission: 'Oct 15, 2023', status: 'rejected', + responses: 0, responseTag: 'Closed', }, { @@ -642,10 +619,12 @@ const REQUIREMENT_ROWS = [ title: 'Home Fitness Program', summary: '12-week transformation plan', category: 'FITNESS TRAINER', + amount: '₹18,000', budget: '₹18,000 - ₹28,000', location: 'Chennai (Online + Offline)', submission: 'Nov 08, 2023', status: 'active', + responses: 3, responseTag: 'New (3 Responses)', }, { @@ -653,10 +632,12 @@ const REQUIREMENT_ROWS = [ title: 'Product Launch Promo Video', summary: 'Scripted edit + motion graphics', category: 'VIDEO EDITOR', + amount: '₹40,000', budget: '₹40,000 - ₹70,000', location: 'Remote', submission: 'Nov 10, 2023', status: 'draft', + responses: 0, responseTag: 'Draft', }, { @@ -664,10 +645,12 @@ const REQUIREMENT_ROWS = [ title: 'Weekly Maths Coaching', summary: 'Class 10 board-focused tutoring', category: 'TUTOR', + amount: '₹8,000', budget: '₹8,000 - ₹14,000', location: 'Pune (Online)', submission: 'Sep 28, 2023', status: 'closed', + responses: 12, responseTag: 'Completed', }, ]; @@ -1054,6 +1037,9 @@ export default function DashboardDesignPreview(props: { hidePreviewHeader?: boolean; liveData?: { userName: string; userId: string; rolePrefix: string }; }) { + const [isVerified, setIsVerified] = createSignal(false); + const [verificationPending, setVerificationPending] = createSignal(false); + const isProfessionalRoleKey = (roleKey: string) => { const role = normalizeRoleKey(roleKey); return role !== 'COMPANY' && role !== 'JOB_SEEKER' && role !== 'CUSTOMER'; @@ -1082,6 +1068,7 @@ export default function DashboardDesignPreview(props: { const portfolioTestimonialsUnlocked = createMemo(() => portfolioJobsCompletedPreview >= 3 && portfolioFeedbackCountPreview >= 2); const exploreRoleCards = createMemo(() => { const roles = Array.isArray(props.exploreRoles) ? props.exploreRoles : []; + const activeRoles = userRoles(); const professionalOnly = roles .filter((r) => String(r?.key || '').trim()) .filter((r) => { @@ -1095,13 +1082,14 @@ export default function DashboardDesignPreview(props: { .map((role) => { const roleKey = String(role.key || '').trim(); const title = String(role.name || '').trim() || titleCase(roleKey.replace(/_/g, ' ').toLowerCase()); + const isRegistered = activeRoles.some((ar) => normalizeRoleKey(ar.role_key || ar.key) === normalizeRoleKey(roleKey)); const isCurrent = normalizeRoleKey(props.roleKey || '') === normalizeRoleKey(roleKey); return { key: roleKey, title, subtitle: `Apply and onboard for ${title} to unlock more opportunities on Nxtgauge.`, - status: isCurrent ? 'Registered' : 'Available', - action: isCurrent ? 'Active' : 'Register', + status: isCurrent ? 'Active' : isRegistered ? 'Registered' : 'Available', + action: isCurrent ? 'Active' : isRegistered ? 'Switch' : 'Register', iconAsset: roleIconAsset(roleKey), Icon: roleIcon(roleKey), }; @@ -1121,8 +1109,13 @@ export default function DashboardDesignPreview(props: { const merged = [...professionalOnly]; defaults.forEach((item) => { if (!merged.some((row) => normalizeRoleKey(row.key) === normalizeRoleKey(item.key))) { + const isRegistered = activeRoles.some((ar) => normalizeRoleKey(ar.role_key || ar.key) === normalizeRoleKey(item.key)); const isCurrent = normalizeRoleKey(props.roleKey || '') === normalizeRoleKey(item.key); - merged.push({ ...item, status: isCurrent ? 'Registered' : 'Available', action: isCurrent ? 'Active' : 'Register' }); + merged.push({ + ...item, + status: isCurrent ? 'Active' : isRegistered ? 'Registered' : 'Available', + action: isCurrent ? 'Active' : isRegistered ? 'Switch' : 'Register' + }); } }); return merged.slice(0, 10); @@ -1154,6 +1147,7 @@ export default function DashboardDesignPreview(props: { const [profileDocumentType, setProfileDocumentType] = createSignal('Aadhar Card'); const [portfolioFormValues, setPortfolioFormValues] = createSignal>({}); const [portfolioFormErrors, setPortfolioFormErrors] = createSignal>({}); + const [userRoles, setUserRoles] = createSignal([]); const [portfolioValidationNotice, setPortfolioValidationNotice] = createSignal(''); const [portfolioServices, setPortfolioServices] = createSignal>([]); const [portfolioServiceDraft, setPortfolioServiceDraft] = createSignal<{ name: string; model: string; price: string; details: string }>({ name: '', model: 'Flat', price: '', details: '' }); @@ -1197,6 +1191,17 @@ export default function DashboardDesignPreview(props: { const [requestedFilterOpen, setRequestedFilterOpen] = createSignal(false); const [requestedPage, setRequestedPage] = createSignal(1); const [leadCredits, setLeadCredits] = createSignal(250); + const [checkoutPackage, setCheckoutPackage] = createSignal(null); + const [paymentStep, setPaymentStep] = createSignal<'idle' | 'processing' | 'verifying' | 'success' | 'error'>('idle'); + const [paymentRef, setPaymentRef] = createSignal(null); + const [paymentResult, setPaymentResult] = createSignal(null); + const [creditManageView, setCreditManageView] = createSignal(false); + const [txRows, setTxRows] = createSignal>([ + ['#INV-2023-089', 'Enterprise Growth', '5,000', '₹1,20,000', 'Completed', 'Oct 24, 2023'], + ['#INV-2023-074', 'Starter Kick', '500', '₹15,000', 'Pending', 'Oct 23, 2023'], + ['#INV-2023-052', 'Pro Pack', '2,500', '₹65,000', 'Completed', 'Oct 21, 2023'], + ['#INV-2023-031', 'Top-up', '1,000', '₹30,000', 'Failed', 'Oct 20, 2023'], + ]); const [leadRequestRows, setLeadRequestRows] = createSignal([ { id: 'LD-29745', title: 'Editorial Fashion Shoot - Studio Series', city: 'Nungambakkam, Chennai', requestDate: 'Apr 02, 2026', status: 'request_sent', decisionDate: '--' }, { id: 'LD-29612', title: 'Corporate Branding Shoot - OMR Campus', city: 'Sholinganallur, Chennai', requestDate: 'Apr 01, 2026', status: 'contact_unlocked', decisionDate: 'Apr 01, 2026' }, @@ -1264,10 +1269,12 @@ export default function DashboardDesignPreview(props: { title: `${roleLabel} Requirement`, summary: 'Submitted from customer requirement form', category: roleKey, + amount: '₹1,50,000', budget: '₹1,50,000 - ₹2,00,000', location: 'Chennai (On-site)', submission, status: 'under review', + responses: 0, responseTag: 'Verification Pending', }; setRequirementRows((prev) => [newRequirement, ...prev]); @@ -1275,9 +1282,9 @@ export default function DashboardDesignPreview(props: { setRequirementsView('list'); setRequirementsStep(1); setTimeout(() => { - setRequirementRows((prev) => prev.map((row) => (row.id === id ? { ...row, status: 'approved', responseTag: 'Approval Pending' } : row))); + setRequirementRows((prev) => prev.map((row) => (row.id === id ? { ...row, status: 'approved', responses: 0, responseTag: 'Approval Pending' } : row))); setTimeout(() => { - setRequirementRows((prev) => prev.map((row) => (row.id === id ? { ...row, status: 'active', responseTag: 'Active (0 Responses)' } : row))); + setRequirementRows((prev) => prev.map((row) => (row.id === id ? { ...row, status: 'active', responses: 0, responseTag: 'Active (0 Responses)' } : row))); setLeadCards((prev) => { const leadId = id.replace('#REQ', 'LD'); if (prev.some((card) => card.id === leadId)) return prev; @@ -1370,6 +1377,132 @@ export default function DashboardDesignPreview(props: { (prefix) => apiFetch(`/api/${prefix}/profile/me`), ); + // Professional services (Packages) + const [servicesResource, { refetch: refetchServicesLive }] = createResource( + () => (hasLive() && isProfessionalRole() ? livePrefix() : null), + (prefix) => apiFetch(`/api/${prefix}/services/me`), + ); + + // Professional portfolio + const [portfolioResource, { refetch: refetchPortfolioLive }] = createResource( + () => (hasLive() && isProfessionalRole() ? livePrefix() : null), + (prefix) => apiFetch(`/api/${prefix}/portfolio/me`), + ); + + // Current user roles (for switching and exploration status) + const [userRolesResource, { refetch: refetchUserRolesLive }] = createResource( + () => (hasLive() ? 'yes' : null), + () => apiFetch('/api/users/roles'), + ); + + // Tracecoin pricing packages + const [pricingPackagesResource] = createResource( + () => (hasLive() ? 'yes' : null), + () => apiFetch('/api/packages'), + ); + + // Dynamic Knowledge Base Resource + const [kbResource] = createResource( + () => (hasLive() ? 'yes' : null), + async () => { + const res = await apiFetch('/api/runtime-config/knowledge_base/GLOBAL_KB'); + const data = (res?.payload as RuntimeKBConfig) || { articles: [] }; + return data; + } + ); + + const [kbSearch, setKbSearch] = createSignal(''); + const [kbCategory, setKbCategory] = createSignal('All'); + + const filteredKbArticles = createMemo(() => { + const data = kbResource()?.articles || []; + return data.filter(a => { + if (!a.isPublished) return false; + const matchesSearch = a.title.toLowerCase().includes(kbSearch().toLowerCase()) || + a.content.toLowerCase().includes(kbSearch().toLowerCase()); + const matchesCategory = kbCategory() === 'All' || a.category === kbCategory(); + return matchesSearch && matchesCategory; + }); + }); + + const kbCategoriesWithCounts = createMemo(() => { + const articles = kbResource()?.articles || []; + const counts: Record = {}; + articles.forEach(a => { + if (!a.isPublished) return; + counts[a.category] = (counts[a.category] || 0) + 1; + }); + return Object.entries(counts).map(([title, articles]) => ({ + title, + articles, + icon: '/icons/help-book.png', // Placeholder icon + description: `Guides and documentation for ${title}.` + })); + }); + + const BEEP = 'https://nxtgauge.free.beeceptor.com'; + + const startPayment = async (pkg: any) => { + setPaymentStep('processing'); + try { + const res = await fetch(`${BEEP}/payments/create-order`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ package_id: pkg.id, amount: pkg.price_paise }), + }); + const data = await (res.ok ? res.json() : { order_id: `ORD-${Date.now()}` }); + setPaymentRef(data.order_id || `ORD-${Date.now()}`); + + // Simulate network latency for UX + setTimeout(() => verifyPayment(pkg), 1200); + } catch (e) { + // Fallback for mock environment + setPaymentRef(`ORD-MOCK-${Date.now()}`); + setTimeout(() => verifyPayment(pkg), 1200); + } + }; + + const verifyPayment = async (pkg: any) => { + setPaymentStep('verifying'); + try { + const res = await fetch(`${BEEP}/payments/verify`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ order_id: paymentRef(), status: 'success' }), + }); + if (res.ok || true) { // Always succeed in mock mode if user requested + setPaymentStep('success'); + const creditsToAdd = Number(pkg.credits) + Number(pkg.bonus_credits || 0); + setLeadCredits((prev) => prev + creditsToAdd); + + const newRow: [string, string, string, string, string, string] = [ + paymentRef() || `#INV-${Date.now()}`, + pkg.display_name || pkg.name, + Number(creditsToAdd).toLocaleString(), + `₹${(pkg.price_paise / 100).toLocaleString('en-IN')}`, + 'Completed', + new Date().toLocaleDateString('en-US', { month: 'short', day: '2-digit', year: 'numeric' }), + ]; + setTxRows((prev) => [newRow, ...prev]); + } + } catch (e) { + setPaymentStep('error'); + } + }; + + const adjustCreditsManually = (amount: number, reason: string) => { + setLeadCredits((prev) => prev + amount); + const newRow: [string, string, string, string, string, string] = [ + `#ADJ-${Date.now().toString().slice(-6)}`, + `Adjustment: ${reason}`, + amount > 0 ? `+${amount.toLocaleString()}` : amount.toLocaleString(), + '₹0', + 'Completed', + new Date().toLocaleDateString('en-US', { month: 'short', day: '2-digit', year: 'numeric' }), + ]; + setTxRows((prev) => [newRow, ...prev]); + }; + // Sync resources → local signals createEffect(() => { const d = creditsResource(); @@ -1440,6 +1573,9 @@ export default function DashboardDesignPreview(props: { title: String(item.title ?? item.description ?? 'Requirement'), summary: String(item.description ?? ''), category: String(item.category ?? item.profession_key ?? ''), + amount: item.budget_min != null + ? `₹${Math.round(item.budget_min / 100).toLocaleString('en-IN')}` + : '₹0', budget: item.budget_min != null ? `₹${Math.round(item.budget_min / 100).toLocaleString('en-IN')} - ₹${Math.round((item.budget_max ?? item.budget_min) / 100).toLocaleString('en-IN')}` : '₹0', @@ -1448,6 +1584,7 @@ export default function DashboardDesignPreview(props: { ? new Date(item.created_at).toLocaleDateString('en-US', { month: 'short', day: '2-digit', year: 'numeric' }) : '--', status: statusMap[String(item.status ?? '')] ?? 'under review', + responses: Number(item.response_count ?? 0), responseTag: `Active (${Number(item.response_count ?? 0)} Responses)`, }))); }); @@ -1498,8 +1635,57 @@ export default function DashboardDesignPreview(props: { ); setProfileFormData((prev) => ({ ...prev, ...map })); }); + createEffect(() => { + const d = userRolesResource(); + if (Array.isArray(d)) setUserRoles(d); + else if (d && Array.isArray(d.data)) setUserRoles(d.data); + }); + + createEffect(() => { + const d = servicesResource(); + if (!d) return; + const items = Array.isArray(d.items) ? d.items : Array.isArray(d) ? d : []; + if (items.length > 0) { + setPortfolioServices(items.map((it: any) => ({ + id: it.id, + name: it.name || '', + model: it.model || 'Flat', + price: it.price || '', + details: it.details || '' + }))); + } + }); + + createEffect(() => { + const d = portfolioResource(); + if (!d) return; + if (Array.isArray(d.photos)) setPortfolioPhotos(d.photos); + if (Array.isArray(d.experience)) setPortfolioExperiences(d.experience); + if (Array.isArray(d.specialties)) setPortfolioSpecialties(d.specialties); + if (Array.isArray(d.languages)) setPortfolioLanguages(d.languages); + if (Array.isArray(d.service_areas)) setPortfolioServiceAreas(d.service_areas); + }); + // ─── End live API integration ───────────────────────────────────────────── + const registerRole = async (roleKey: string) => { + const res = await apiPost('/api/users/roles/register', { role_key: roleKey.toUpperCase() }); + if (res && (res.ok || res.status === 200)) { + window.location.reload(); + } else { + alert(`Failed to register as ${roleKey}. Please check if you are already registered.`); + } + }; + + const switchRole = async (roleKey: string) => { + const res = await apiPost('/api/users/roles/switch', { role: roleKey.toUpperCase() }); + if (res && (res.ok || res.status === 200)) { + window.location.reload(); + } else { + alert(`Failed to switch to ${roleKey}.`); + } + }; + const submitProfileForApproval = () => { setProfileApprovalState('SUBMITTED'); setTimeout(() => setProfileApprovalState('IN_REVIEW'), 250); @@ -2026,53 +2212,134 @@ export default function DashboardDesignPreview(props: { && !(stepKey === galleryTabKey && (portfolioPhotos().length < 1 || portfolioPhotos().length > 6)) && !(stepKey === experienceTabKey && portfolioExperiences().length < 1); }; - const addServiceDraft = () => { + const addServiceDraft = async () => { const draft = portfolioServiceDraft(); if (!draft.name.trim() || !draft.price.trim()) { setPortfolioValidationNotice('Add service name and price to continue.'); return; } - setPortfolioServices((prev) => [...prev, { - name: draft.name.trim(), - model: draft.model.trim() || 'Flat', - price: draft.price.trim(), - details: draft.details.trim(), - }]); - setPortfolioServiceDraft({ name: '', model: 'Flat', price: '', details: '' }); - setPortfolioValidationNotice(''); + + if (hasLive()) { + try { + const res = await apiPost(`/api/${livePrefix()}/services`, { + name: draft.name.trim(), + model: draft.model.trim() || 'Flat', + price: draft.price.trim(), + details: draft.details.trim(), + }); + if (res && (res.ok || res.status === 200)) { + refetchServicesLive(); + setPortfolioServiceDraft({ name: '', model: 'Flat', price: '', details: '' }); + setPortfolioValidationNotice(''); + } else { + setPortfolioValidationNotice('Failed to save service to backend.'); + } + } catch (err) { + console.error(err); + setPortfolioValidationNotice('Network error while saving service.'); + } + } else { + setPortfolioServices((prev) => [...prev, { + name: draft.name.trim(), + model: draft.model.trim() || 'Flat', + price: draft.price.trim(), + details: draft.details.trim(), + }]); + setPortfolioServiceDraft({ name: '', model: 'Flat', price: '', details: '' }); + setPortfolioValidationNotice(''); + } }; - const removeServiceItem = (index: number) => { - setPortfolioServices((prev) => prev.filter((_, i) => i !== index)); + const removeServiceItem = async (index: number) => { + const target = portfolioServices()[index]; + if (hasLive() && target && (target as any).id) { + try { + const res = await apiDelete(`/api/${livePrefix()}/services/${(target as any).id}`); + if (res && (res.ok || res.status === 200)) { + refetchServicesLive(); + } + } catch (err) { + console.error('Failed to delete service:', err); + } + } else { + setPortfolioServices((prev) => prev.filter((_, i) => i !== index)); + } }; - const addExperienceDraft = () => { + const addExperienceDraft = async () => { const draft = portfolioExperienceDraft(); if (!draft.year.trim() || !draft.title.trim()) { setPortfolioValidationNotice('Add year and title for each experience entry.'); return; } - setPortfolioExperiences((prev) => [...prev, { + + const nextExp = [...portfolioExperiences(), { year: draft.year.trim(), title: draft.title.trim(), details: draft.details.trim(), - }]); - setPortfolioExperienceDraft({ year: '', title: '', details: '' }); - setPortfolioValidationNotice(''); + }]; + + if (hasLive()) { + try { + const res = await apiPost(`/api/${livePrefix()}/portfolio/experience`, nextExp); + if (res && (res.ok || res.status === 200)) { + refetchPortfolioLive(); + setPortfolioExperienceDraft({ year: '', title: '', details: '' }); + setPortfolioValidationNotice(''); + } + } catch (err) { + console.error(err); + } + } else { + setPortfolioExperiences(nextExp); + setPortfolioExperienceDraft({ year: '', title: '', details: '' }); + setPortfolioValidationNotice(''); + } }; - const removeExperienceItem = (index: number) => { - setPortfolioExperiences((prev) => prev.filter((_, i) => i !== index)); + const removeExperienceItem = async (index: number) => { + const nextExp = portfolioExperiences().filter((_, i) => i !== index); + if (hasLive()) { + try { + const res = await apiPost(`/api/${livePrefix()}/portfolio/experience`, nextExp); + if (res && (res.ok || res.status === 200)) refetchPortfolioLive(); + } catch (err) { + console.error(err); + } + } else { + setPortfolioExperiences(nextExp); + } }; - const addPhotoItem = () => { + const addPhotoItem = async () => { const current = portfolioPhotos(); if (current.length >= 6) { setPortfolioValidationNotice('Portfolio is limited to 6 photos.'); return; } const nextLabel = `portfolio-${current.length + 1}.jpg`; - setPortfolioPhotos((prev) => [...prev, nextLabel]); + const nextPhotos = [...current, nextLabel]; + + if (hasLive()) { + try { + const res = await apiPost(`/api/${livePrefix()}/portfolio/photos`, nextPhotos); + if (res && (res.ok || res.status === 200)) refetchPortfolioLive(); + } catch (err) { + console.error(err); + } + } else { + setPortfolioPhotos(nextPhotos); + } setPortfolioValidationNotice(''); }; - const removePhotoItem = (index: number) => { - setPortfolioPhotos((prev) => prev.filter((_, i) => i !== index)); + const removePhotoItem = async (index: number) => { + const nextPhotos = portfolioPhotos().filter((_, i) => i !== index); + if (hasLive()) { + try { + const res = await apiPost(`/api/${livePrefix()}/portfolio/photos`, nextPhotos); + if (res && (res.ok || res.status === 200)) refetchPortfolioLive(); + } catch (err) { + console.error(err); + } + } else { + setPortfolioPhotos(nextPhotos); + } }; const showSection = (tabKey: string) => (isPreviewMode ? portfolioStepKeys.includes(tabKey) : selectedPortfolioTabKey === tabKey); @@ -3997,6 +4264,23 @@ export default function DashboardDesignPreview(props: {

Job Submitted Successfully!

Your listing for {companyJobDraft().title} has been sent to Verification Management first, then Approval Management. Job seekers can see it only after final approval.

+
+ + Application Submitted + } + > + + +

{latestStatusLabel()}

@@ -4482,7 +4766,7 @@ export default function DashboardDesignPreview(props: {
-

Requirements > Details

+

Requirements > Details

{selectedRow.title}

{status.label} @@ -4951,13 +5235,6 @@ export default function DashboardDesignPreview(props: { } if (customerKey() === 'credits') { - const txRows = [ - ['#INV-2023-089', 'Enterprise Growth', '5,000', '₹1,20,000', 'Completed', 'Oct 24, 2023'], - ['#INV-2023-074', 'Starter Kick', '500', '₹15,000', 'Pending', 'Oct 23, 2023'], - ['#INV-2023-052', 'Pro Pack', '2,500', '₹65,000', 'Completed', 'Oct 21, 2023'], - ['#INV-2023-031', 'Top-up', '1,000', '₹30,000', 'Failed', 'Oct 20, 2023'], - ] as const; - const invoiceRows = [ ['#INV-2023-089', 'Oct 14, 2023', 'Enterprise Pack', '₹499.00', 'Paid'], ['#INV-2023-074', 'Sep 14, 2023', 'Enterprise Pack', '₹499.00', 'Paid'], @@ -4965,41 +5242,237 @@ export default function DashboardDesignPreview(props: { ['#INV-2023-031', 'Jul 14, 2023', 'Growth Starter', '₹135.00', 'Failed'], ] as const; + const renderCheckout = () => { + const pkg = checkoutPackage(); + if (!pkg) return null; + const step = paymentStep(); + + return ( +
+
+ +

Finalize Purchase

+
+ +
+
+

Selected Package

+

{pkg.display_name || pkg.name}

+
+
+ +
+
+

{(Number(pkg.credits) + Number(pkg.bonus_credits || 0)).toLocaleString()} TC

+

Total Tracecoins

+
+
+
+
+ Base Credits + {Number(pkg.credits).toLocaleString()} +
+ 0}> +
+ Bonus Credits + +{Number(pkg.bonus_credits).toLocaleString()} +
+
+
+ Total Amount + ₹{(Number(pkg.price_paise) / 100).toLocaleString('en-IN')} +
+
+
+ +
+ +
+ +
+

Secure Gateway

+

Continue to our secure partner gateway to complete the transaction.

+ +
+ + +
+ +

{step === 'processing' ? 'Creating Order...' : 'Verifying Payment...'}

+

Please do not refresh or close the page.

+ + + +
+ +
+

Payment Successful!

+

Your account has been credited with {(Number(pkg.credits) + Number(pkg.bonus_credits || 0)).toLocaleString()} Tracecoins.

+
+

New Balance

+

{leadCredits().toLocaleString()} TC

+
+ +
+ + +
+ +
+

Payment Failed

+

Something went wrong with the mock gateway. Please try again.

+ +
+
+
+
+ ); + }; + + const renderCreditManagement = () => { + let amtInput: HTMLInputElement | undefined; + let rsnInput: HTMLInputElement | undefined; + + return ( +
+
+ +

Credit Management Console

+
+ +
+
+
+ +
+
+

Current User Balance

+

{leadCredits().toLocaleString()} TC

+
+
+ +
+
+ + +
+
+ + +
+ +
+

Note: Adjustments are final and logged in transaction history for auditing.

+
+
+ ); + }; + + if (checkoutPackage()) return renderCheckout(); + if (creditManageView()) return renderCreditManagement(); + if (tab === 'buy credits') { return (

Current Balance

-

12,450 Tracecoins

+

{leadCredits().toLocaleString()} Tracecoins

+12% from last month

- +
+ + +
- {[ - { name: 'Basic', subtitle: 'Starter', credits: '500', price: '₹4,999', bonus: '+50 bonus', popular: false }, - { name: 'Standard', subtitle: 'Growth', credits: '2,500', price: '₹18,999', bonus: '+250 bonus', popular: true }, - { name: 'Premium', subtitle: 'Power', credits: '10,000', price: '₹69,999', bonus: '+1,500 bonus', popular: false }, - { name: 'Elite', subtitle: 'Enterprise', credits: '50,000', price: '₹2,49,999', bonus: '+10,000 bonus', popular: false }, - ].map((pkg) => ( -
- - MOST POPULAR - -
- + + {(pkg) => ( +
+ + MOST POPULAR + +
+ +
+

{pkg.name}

+

{pkg.display_name || pkg.name}

+

{Number(pkg.credits).toLocaleString()}

+

Credits

+ 0}> +

+{pkg.bonus_credits} bonus

+
+

₹{(Number(pkg.price_paise) / 100).toLocaleString('en-IN')}

+
-

{pkg.subtitle}

-

{pkg.name}

-

{pkg.credits}

-

Credits

-

{pkg.bonus}

-

{pkg.price}

- -
- ))} + )} +
@@ -5062,7 +5535,7 @@ export default function DashboardDesignPreview(props: { - {txRows.map((row) => ( + {txRows().map((row) => ( {row[0]} {row[1]} @@ -5273,7 +5746,7 @@ export default function DashboardDesignPreview(props: { - {txRows.slice(0, 3).map((row) => ( + {txRows().slice(0, 3).map((row: any) => ( {row[0]} {row[1]} @@ -5306,6 +5779,8 @@ export default function DashboardDesignPreview(props: { { key: 'JOB_SEEKER', title: 'Job Seeker', icon: '/sidebar-icons/company.svg', subtitle: 'Explore opportunities and apply to roles with your profile.' }, { key: 'SERVICE_SEEKER', title: 'Service Seeker', icon: '/sidebar-icons/candidate.svg', subtitle: 'Post requirements and connect with verified professionals.' }, ].map((roleCard) => { + const activeRoles = userRoles(); + const isRegistered = activeRoles.some((ar) => normalizeRoleKey(ar.role_key || ar.key) === normalizeRoleKey(roleCard.key)); const isCurrentRole = normalizeRoleKey(props.roleKey || '') === normalizeRoleKey(roleCard.key); return (
@@ -5314,9 +5789,10 @@ export default function DashboardDesignPreview(props: {

{roleCard.subtitle}

); @@ -5341,7 +5817,13 @@ export default function DashboardDesignPreview(props: {

{role.title}

{role.subtitle}

- +
)} @@ -5594,13 +6076,28 @@ export default function DashboardDesignPreview(props: {

Knowledge Base

-
All Categories
-
Search help articles, tickets, and guides
+ +
+ setKbSearch(e.currentTarget.value)} + style="width:100%;height:36px;border:1px solid #D1D5DB;border-radius:10px;background:white;padding:0 12px;font-size:12px;color:#111827;outline:none" + /> +
Popular: - {['Account Access', 'Verification', 'Hiring Fees'].map((tag) => {tag})} + {['Verification', 'Credits', 'Lead Response'].map((tag) => )}
@@ -5609,44 +6106,52 @@ export default function DashboardDesignPreview(props: {
- {(cat) => ( -
- -

{cat.title}

-

{cat.description}

-
- {cat.articles} Articles + {(cat) => ( +
+

{cat.title}

+

{cat.description}

+
+ {cat.articles} Articles +
+ )}
-
+
-

Popular Articles

-
- {(a) => ( - + +
+
+ {(a) => ( + )} -
-
-
-

Frequently Asked Questions

-
- {(faq, i) => ( - - )} + +
+ +

No articles found matching your criteria.

+
+
@@ -5873,15 +6378,49 @@ export default function DashboardDesignPreview(props: { } if (customerKey() === 'switch role' || customerKey() === 'switch services' || customerKey() === 'switch service') { - if (tab === 'pending approvals') { - return
{['Professional - Under Review', 'Company - Documents Pending'].map((r) =>
{r}
)}
; - } - if (tab === 'onboarding') { - return
{['Complete profile docs', 'Submit KYC proofs', 'Wait for approval'].map((r) =>
{r}
)}
; - } + const activeRoles = userRoles(); return ( -
- {['Service Seeker (Active)', 'Professional', 'Company'].map((r, i) =>

{r}

)} +
+
+

Switch Services

+

Switch between your active roles to manage different business operations.

+
+ +
+ + {(role) => { + const roleKey = String(role.role_key || role.key).toUpperCase(); + const isCurrent = normalizeRoleKey(props.roleKey || '') === normalizeRoleKey(roleKey); + return ( +
+
+
+ {(() => { + const Icon = roleIcon(roleKey); + return ; + })()} +
+ {role.status} +
+

{titleCase(roleKey.replace(/_/g, ' '))}

+ +
+ ); + }} +
+ +
+ No other active roles found. Explore Nxtgauge to add more. +
+
+
); } @@ -5997,6 +6536,26 @@ export default function DashboardDesignPreview(props: {
+ +
+
+
+ +
+
+

Account Verification Required

+

{verificationPending() ? 'Your profile is currently under review by our team. This usually takes 24-48 hours.' : 'Complete your profile details to unlock all platform features and start receiving leads.'}

+
+
+ +
+
diff --git a/src/lib/runtime/types.ts b/src/lib/runtime/types.ts index 2a4cc0a..4a84c95 100644 --- a/src/lib/runtime/types.ts +++ b/src/lib/runtime/types.ts @@ -78,3 +78,18 @@ export type OnboardingSubmission = { submittedAt: string; values: Record; }; + +export type RuntimeKBArticle = { + id: string; + title: string; + category: string; + content: string; + author: string; + isPublished: boolean; + viewCount: number; + lastUpdated: string; +}; + +export type RuntimeKBConfig = { + articles: RuntimeKBArticle[]; +}; diff --git a/src/routes/login.tsx b/src/routes/login.tsx index b8939d3..21f285f 100644 --- a/src/routes/login.tsx +++ b/src/routes/login.tsx @@ -312,7 +312,7 @@ export default function LoginRoute() { diff --git a/src/routes/signup.tsx b/src/routes/signup.tsx index d0d9f07..1739ace 100644 --- a/src/routes/signup.tsx +++ b/src/routes/signup.tsx @@ -57,9 +57,8 @@ export default function SignupRoute() { const navigate = useNavigate(); const [search] = useSearchParams(); onMount(() => { - const intent = String(search.intent || '').trim(); - const role = String(search.role || '').trim(); - if (!intent && !role) navigate('/users/choose-role', { replace: true }); + // Legacy redirect to choose-role removed for dashboard-first flow. + // If no intent/role provided, normalizeIntent will default to job_seeker. }); const [step, setStep] = createSignal<'register' | 'verify'>('register'); diff --git a/src/routes/users/choose-role.tsx b/src/routes/users/choose-role.tsx deleted file mode 100644 index 5ba9696..0000000 --- a/src/routes/users/choose-role.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import { A } from '@solidjs/router'; -import { For } from 'solid-js'; -import PublicBackground from '~/components/PublicBackground'; -import PublicHeader from '~/components/PublicHeader'; - -type RoleChoice = { - title: string; - description: string; - cta: string; - href: string; - image: string; -}; - -const MAIN_ROLES: RoleChoice[] = [ - { - title: 'Company', - description: 'Post verified jobs, manage applicants, and hire with trust.', - cta: 'Continue as Company', - href: '/signup?intent=company', - image: 'https://images.unsplash.com/photo-1486406146926-c627a92ad1ab?q=80&w=400&auto=format&fit=crop', - }, - { - title: 'Job Seeker', - description: 'Create your profile and apply to approved opportunities.', - cta: 'Continue as Job Seeker', - href: '/signup?intent=job_seeker', - image: 'https://images.unsplash.com/photo-1450101499163-c8848c66ca85?q=80&w=400&auto=format&fit=crop', - }, - { - title: 'Customer', - description: 'Post requirements and receive responses from verified professionals.', - cta: 'Continue as Customer', - href: '/signup?intent=customer', - image: 'https://images.unsplash.com/photo-1454165804606-c3d57bc86b40?q=80&w=400&auto=format&fit=crop', - }, - { - title: 'Professional', - description: 'Join as a service provider and grow with quality leads.', - cta: 'Continue as Professional', - href: '/signup?intent=professional', - image: 'https://images.unsplash.com/photo-1498050108023-c5249f4df085?q=80&w=400&auto=format&fit=crop', - }, -]; - -export default function ChooseRolePage() { - return ( -
- - - -
-
-

Choose your role

-

Select your path once and we will open the correct signup flow for you.

-
- -
-

Start here

-

Pick the role that matches what you want to do on Nxtgauge.

- -
- - -
-
- ); -}