feat(dashboard): add My Portfolio to all professional role dashboards
- Add full single-page portfolio preview in DashboardDesignPreview with profile header, about, packages, gallery, experience, testimonials, and FAQs sections matching Department Management design language - Fix sidebar persona resolution to always include My Portfolio for all professional subtypes (Photographer, Graphic Designer, Makeup Artist, Tutor, Developer, Video Editor, Social Media Manager, Fitness Trainer, Catering Services) via a three-layer fallback: roleId lookup → role key from roles list → formRoleKey stored directly from dashboard record - Add personaFromKey() helper so any professional role key maps to PROFESSIONAL sidebar even when the roles API returns empty data Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
24353ef27a
commit
8a24700e73
2 changed files with 575 additions and 14 deletions
|
|
@ -1,21 +1,29 @@
|
||||||
import { For, Show, createEffect, createMemo, createSignal } from 'solid-js';
|
import { For, Show, createEffect, createMemo, createSignal } from 'solid-js';
|
||||||
import {
|
import {
|
||||||
|
Award,
|
||||||
Bell,
|
Bell,
|
||||||
BadgeCheck,
|
BadgeCheck,
|
||||||
Bookmark,
|
Bookmark,
|
||||||
BriefcaseBusiness,
|
BriefcaseBusiness,
|
||||||
Camera,
|
Camera,
|
||||||
|
CheckCircle2,
|
||||||
|
ChevronDown,
|
||||||
|
ChevronUp,
|
||||||
Clapperboard,
|
Clapperboard,
|
||||||
Compass,
|
Compass,
|
||||||
Coins,
|
Coins,
|
||||||
Code2,
|
Code2,
|
||||||
Dumbbell,
|
Dumbbell,
|
||||||
|
Edit2,
|
||||||
|
Eye,
|
||||||
FileText,
|
FileText,
|
||||||
GraduationCap,
|
GraduationCap,
|
||||||
HandHelping,
|
HandHelping,
|
||||||
HeadphonesIcon,
|
HeadphonesIcon,
|
||||||
|
Image,
|
||||||
LayoutGrid,
|
LayoutGrid,
|
||||||
LogOut,
|
LogOut,
|
||||||
|
MapPin,
|
||||||
Megaphone,
|
Megaphone,
|
||||||
PenTool,
|
PenTool,
|
||||||
RefreshCw,
|
RefreshCw,
|
||||||
|
|
@ -25,11 +33,13 @@ import {
|
||||||
Settings,
|
Settings,
|
||||||
Settings2,
|
Settings2,
|
||||||
ShieldCheck,
|
ShieldCheck,
|
||||||
|
Star,
|
||||||
TrendingUp,
|
TrendingUp,
|
||||||
UtensilsCrossed,
|
UtensilsCrossed,
|
||||||
User,
|
User,
|
||||||
UserCircle2,
|
UserCircle2,
|
||||||
Users,
|
Users,
|
||||||
|
X,
|
||||||
} from 'lucide-solid';
|
} from 'lucide-solid';
|
||||||
|
|
||||||
function titleCase(value: string) {
|
function titleCase(value: string) {
|
||||||
|
|
@ -52,6 +62,7 @@ function StatusBadge(props: { status: 'ACTIVE' | 'INACTIVE' }) {
|
||||||
function sidebarIcon(label: string) {
|
function sidebarIcon(label: string) {
|
||||||
const key = String(label || '').toLowerCase().trim();
|
const key = String(label || '').toLowerCase().trim();
|
||||||
if (key.includes('dashboard')) return LayoutGrid;
|
if (key.includes('dashboard')) return LayoutGrid;
|
||||||
|
if (key.includes('portfolio')) return Image;
|
||||||
if (key.includes('profile')) return UserCircle2;
|
if (key.includes('profile')) return UserCircle2;
|
||||||
if (key.includes('lead')) return HandHelping;
|
if (key.includes('lead')) return HandHelping;
|
||||||
if (key.includes('response')) return FileText;
|
if (key.includes('response')) return FileText;
|
||||||
|
|
@ -274,6 +285,244 @@ function profileSpecForRole(roleKey: string): ProfileSpec {
|
||||||
return PROFILE_SPECS[normalized] || PROFILE_SPECS.PROFESSIONAL;
|
return PROFILE_SPECS[normalized] || PROFILE_SPECS.PROFESSIONAL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PortfolioSpec = {
|
||||||
|
roleLabel: string;
|
||||||
|
tabs: string[];
|
||||||
|
specialties: string[];
|
||||||
|
statsLabels: string[];
|
||||||
|
equipmentLabel: string;
|
||||||
|
galleryTabLabel: string;
|
||||||
|
experienceTabLabel: string;
|
||||||
|
serviceTabLabel: string;
|
||||||
|
faqItems: Array<{ q: string; a: string }>;
|
||||||
|
packages: Array<{ name: string; price: string; items: string[] }>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const PORTFOLIO_SPECS: Record<string, PortfolioSpec> = {
|
||||||
|
PHOTOGRAPHER: {
|
||||||
|
roleLabel: 'Photographer',
|
||||||
|
tabs: ['overview', 'about', 'services & pricing', 'portfolio gallery', 'experience & equipment', 'testimonials', 'faqs'],
|
||||||
|
specialties: ['Wedding', 'Pre-Wedding', 'Candid', 'Event', 'Portrait', 'Lifestyle'],
|
||||||
|
statsLabels: ['Shoots Done', 'Years Exp', 'Verified Pro', 'Last Delivery', 'Will Travel'],
|
||||||
|
equipmentLabel: 'Camera & Equipment',
|
||||||
|
galleryTabLabel: 'portfolio gallery',
|
||||||
|
experienceTabLabel: 'experience & equipment',
|
||||||
|
serviceTabLabel: 'services & pricing',
|
||||||
|
faqItems: [
|
||||||
|
{ q: 'Do you travel for shoots?', a: 'Yes, I cover Pan-India and select international locations. Travel charges apply beyond 50km.' },
|
||||||
|
{ q: 'How long before I receive the final photos?', a: 'Edited photos are delivered within 10–14 working days after the shoot.' },
|
||||||
|
{ q: 'What equipment do you use?', a: 'I shoot with Canon EOS R6 and Sony A7IV, with a full range of prime and zoom lenses.' },
|
||||||
|
{ q: 'Do you provide raw files?', a: 'Raw files are not included by default. They can be added as an optional upgrade.' },
|
||||||
|
],
|
||||||
|
packages: [
|
||||||
|
{ name: 'Essential', price: '₹15,000', items: ['4-hour shoot', '100 edited photos', 'Online gallery', '1 location'] },
|
||||||
|
{ name: 'Premium', price: '₹28,000', items: ['8-hour shoot', '250 edited photos', 'Online gallery', '2 locations', 'Drone shots'] },
|
||||||
|
{ name: 'Signature', price: '₹50,000', items: ['Full-day shoot', '500 edited photos', 'USB delivery', '3 locations', 'Drone + Reel'] },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
MAKEUP_ARTIST: {
|
||||||
|
roleLabel: 'Makeup Artist',
|
||||||
|
tabs: ['overview', 'about', 'services & pricing', 'gallery', 'experience & certifications', 'testimonials', 'faqs'],
|
||||||
|
specialties: ['Bridal', 'Party', 'Editorial', 'SFX', 'Airbrush', 'Natural'],
|
||||||
|
statsLabels: ['Sessions Done', 'Years Exp', 'Verified Pro', 'Last Booking', 'Will Travel'],
|
||||||
|
equipmentLabel: 'Kit & Products',
|
||||||
|
galleryTabLabel: 'gallery',
|
||||||
|
experienceTabLabel: 'experience & certifications',
|
||||||
|
serviceTabLabel: 'services & pricing',
|
||||||
|
faqItems: [
|
||||||
|
{ q: 'Do you do trials before the wedding?', a: 'Yes, a bridal trial is mandatory. It helps finalise the look and ensures comfort on the big day.' },
|
||||||
|
{ q: 'Which brands do you use?', a: 'I work with MAC, Huda Beauty, Fenty, and NARS for face. Products are selected based on skin type.' },
|
||||||
|
{ q: 'Do you travel for events?', a: 'Yes. Travel is available with prior notice. Charges apply for locations beyond 30km.' },
|
||||||
|
{ q: 'How long does bridal makeup take?', a: 'Typically 2.5–3 hours for full bridal. Party makeup takes around 45–60 minutes.' },
|
||||||
|
],
|
||||||
|
packages: [
|
||||||
|
{ name: 'Party', price: '₹3,500', items: ['Full face makeup', 'Hair styling', '45-min session', 'Touch-up kit'] },
|
||||||
|
{ name: 'Bridal', price: '₹18,000', items: ['Trial session', 'Full bridal look', 'HD & airbrush', 'Touch-up on-site'] },
|
||||||
|
{ name: 'Bridal Party', price: '₹35,000', items: ['Bride + 4 family', 'Full day', 'Airbrush finish', 'On-site artist'] },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
TUTOR: {
|
||||||
|
roleLabel: 'Tutor',
|
||||||
|
tabs: ['overview', 'about', 'subjects & pricing', 'student work', 'qualifications', 'testimonials', 'faqs'],
|
||||||
|
specialties: ['Mathematics', 'Physics', 'Chemistry', 'Biology', 'English', 'Programming'],
|
||||||
|
statsLabels: ['Students Taught', 'Years Exp', 'Verified Pro', 'Next Slot', 'Online OK'],
|
||||||
|
equipmentLabel: 'Teaching Tools',
|
||||||
|
galleryTabLabel: 'student work',
|
||||||
|
experienceTabLabel: 'qualifications',
|
||||||
|
serviceTabLabel: 'subjects & pricing',
|
||||||
|
faqItems: [
|
||||||
|
{ q: 'Do you teach online or in-person?', a: 'Both modes available. Online via Zoom or Google Meet. In-person in Chennai and surrounding areas.' },
|
||||||
|
{ q: 'What boards do you cover?', a: 'CBSE, ICSE, State Board, IB, and A-Levels. I also prepare students for JEE and NEET.' },
|
||||||
|
{ q: 'How many students per batch?', a: 'Group batches have max 5 students. Individual sessions are 1-on-1 for focused learning.' },
|
||||||
|
{ q: 'Do you provide study material?', a: 'Yes. Custom notes, practice sheets, and mock tests are included in all plans.' },
|
||||||
|
],
|
||||||
|
packages: [
|
||||||
|
{ name: 'Crash Course', price: '₹5,000/mo', items: ['10 sessions/month', '1 subject', 'Practice tests', 'Online only'] },
|
||||||
|
{ name: 'Core Plan', price: '₹8,500/mo', items: ['16 sessions/month', '2 subjects', 'Study materials', 'Doubt clearing'] },
|
||||||
|
{ name: 'Intensive', price: '₹14,000/mo', items: ['24 sessions/month', '3 subjects', 'Mock exams', 'Progress reports'] },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
DEVELOPER: {
|
||||||
|
roleLabel: 'Developer',
|
||||||
|
tabs: ['overview', 'about', 'services & pricing', 'projects', 'tech stack & experience', 'testimonials', 'faqs'],
|
||||||
|
specialties: ['Web Development', 'Mobile Apps', 'API / Backend', 'UI/UX', 'DevOps', 'Consulting'],
|
||||||
|
statsLabels: ['Projects Done', 'Years Exp', 'Verified Pro', 'Last Delivery', 'Remote OK'],
|
||||||
|
equipmentLabel: 'Tech Stack',
|
||||||
|
galleryTabLabel: 'projects',
|
||||||
|
experienceTabLabel: 'tech stack & experience',
|
||||||
|
serviceTabLabel: 'services & pricing',
|
||||||
|
faqItems: [
|
||||||
|
{ q: 'Do you work on fixed price or hourly?', a: 'Both models available. Small projects are fixed-price; larger engagements use hourly or milestone billing.' },
|
||||||
|
{ q: 'Do you sign NDAs?', a: 'Yes. NDAs are standard for all client projects involving proprietary data or unreleased products.' },
|
||||||
|
{ q: 'Can you handle full-stack?', a: 'Yes. I cover React/SolidJS frontend, Rust/Node backend, PostgreSQL databases, and cloud deployment.' },
|
||||||
|
{ q: 'What is your typical delivery time?', a: 'Landing pages in 3–5 days. Full apps depend on scope. Detailed estimate provided after briefing.' },
|
||||||
|
],
|
||||||
|
packages: [
|
||||||
|
{ name: 'Starter', price: '₹20,000', items: ['Landing page / MVP', '5-day delivery', '2 revisions', 'Basic SEO'] },
|
||||||
|
{ name: 'Business', price: '₹65,000', items: ['Full web app', '3-week delivery', 'API integration', 'Admin panel', 'Deployment'] },
|
||||||
|
{ name: 'Enterprise', price: 'Custom', items: ['Complex systems', 'Dedicated sprint', 'Team collaboration', 'SLA included'] },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
VIDEO_EDITOR: {
|
||||||
|
roleLabel: 'Video Editor',
|
||||||
|
tabs: ['overview', 'about', 'services & pricing', 'showreel', 'experience & tools', 'testimonials', 'faqs'],
|
||||||
|
specialties: ['Wedding Films', 'Corporate', 'Music Videos', 'Reels', 'Documentary', 'Product'],
|
||||||
|
statsLabels: ['Videos Done', 'Years Exp', 'Verified Pro', 'Last Delivery', 'Remote OK'],
|
||||||
|
equipmentLabel: 'Editing Suite',
|
||||||
|
galleryTabLabel: 'showreel',
|
||||||
|
experienceTabLabel: 'experience & tools',
|
||||||
|
serviceTabLabel: 'services & pricing',
|
||||||
|
faqItems: [
|
||||||
|
{ q: 'Which editing software do you use?', a: 'Adobe Premiere Pro, DaVinci Resolve, and After Effects for motion graphics and colour grading.' },
|
||||||
|
{ q: 'What is your turnaround time?', a: 'Short reels in 2–3 days. Full event films take 10–15 working days depending on footage length.' },
|
||||||
|
{ q: 'Do you accept raw footage from other cameras?', a: 'Yes. Any format is accepted — I handle transcoding as part of the workflow.' },
|
||||||
|
{ q: 'How many revisions are included?', a: '2 revisions are included in all packages. Additional revisions are charged at ₹500 per round.' },
|
||||||
|
],
|
||||||
|
packages: [
|
||||||
|
{ name: 'Reel Edit', price: '₹3,000', items: ['Up to 60s reel', 'Music sync', '2 revisions', '48hr delivery'] },
|
||||||
|
{ name: 'Event Film', price: '₹12,000', items: ['5-min highlight', 'Colour grade', 'Titles + music', '10-day delivery'] },
|
||||||
|
{ name: 'Feature Film', price: '₹28,000', items: ['Full-length film', 'Cinematic grade', 'Motion graphics', 'Multi-format export'] },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
GRAPHIC_DESIGNER: {
|
||||||
|
roleLabel: 'Graphic Designer',
|
||||||
|
tabs: ['overview', 'about', 'services & pricing', 'portfolio', 'experience & tools', 'testimonials', 'faqs'],
|
||||||
|
specialties: ['Brand Identity', 'UI/UX Design', 'Print', 'Social Media', 'Motion', 'Packaging'],
|
||||||
|
statsLabels: ['Projects Done', 'Years Exp', 'Verified Pro', 'Last Delivery', 'Remote OK'],
|
||||||
|
equipmentLabel: 'Design Tools',
|
||||||
|
galleryTabLabel: 'portfolio',
|
||||||
|
experienceTabLabel: 'experience & tools',
|
||||||
|
serviceTabLabel: 'services & pricing',
|
||||||
|
faqItems: [
|
||||||
|
{ q: 'What file formats do you deliver?', a: 'Final files in AI, PDF, PNG, SVG, and any format required. Print-ready and web-optimised variants included.' },
|
||||||
|
{ q: 'Do you create brand guidelines?', a: 'Yes. Brand identity packages include a full style guide covering colour, typography, and usage rules.' },
|
||||||
|
{ q: 'How many concepts do you provide initially?', a: '3 initial concepts are presented for branding. UI/UX starts with wireframes before visual designs.' },
|
||||||
|
{ q: 'Do you handle printing coordination?', a: 'Yes. I work with trusted print vendors and can manage end-to-end print production.' },
|
||||||
|
],
|
||||||
|
packages: [
|
||||||
|
{ name: 'Logo Pack', price: '₹8,000', items: ['3 logo concepts', '2 revisions', 'All file formats', 'Usage rights'] },
|
||||||
|
{ name: 'Brand Kit', price: '₹22,000', items: ['Logo + palette', 'Typography', 'Brand guidelines', 'Social templates'] },
|
||||||
|
{ name: 'Full Identity', price: '₹45,000', items: ['Complete brand', 'UI kit', 'Print collateral', 'Motion logo'] },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
SOCIAL_MEDIA_MANAGER: {
|
||||||
|
roleLabel: 'Social Media Manager',
|
||||||
|
tabs: ['overview', 'about', 'services & pricing', 'case studies', 'experience & tools', 'testimonials', 'faqs'],
|
||||||
|
specialties: ['Instagram', 'YouTube', 'LinkedIn', 'Twitter/X', 'Facebook', 'Content Strategy'],
|
||||||
|
statsLabels: ['Brands Managed', 'Years Exp', 'Verified Pro', 'Last Campaign', 'Remote OK'],
|
||||||
|
equipmentLabel: 'Platforms & Tools',
|
||||||
|
galleryTabLabel: 'case studies',
|
||||||
|
experienceTabLabel: 'experience & tools',
|
||||||
|
serviceTabLabel: 'services & pricing',
|
||||||
|
faqItems: [
|
||||||
|
{ q: 'Do you create the content too?', a: 'Yes. Content creation — including copy, graphics, and reels — is included in all monthly retainer plans.' },
|
||||||
|
{ q: 'How do you measure results?', a: 'Monthly reports covering reach, engagement rate, follower growth, and link clicks are shared via dashboard.' },
|
||||||
|
{ q: 'Do you handle paid ads?', a: 'Yes. Meta and Google ad campaigns are available as add-ons with dedicated reporting.' },
|
||||||
|
{ q: 'What industries do you specialise in?', a: 'Fashion, F&B, real estate, personal brands, and D2C e-commerce. Industry-specific content strategy provided.' },
|
||||||
|
],
|
||||||
|
packages: [
|
||||||
|
{ name: 'Starter', price: '₹12,000/mo', items: ['2 platforms', '12 posts/month', 'Captions + hashtags', 'Monthly report'] },
|
||||||
|
{ name: 'Growth', price: '₹22,000/mo', items: ['3 platforms', '24 posts/month', 'Reels + Stories', 'Ad management'] },
|
||||||
|
{ name: 'Premium', price: '₹40,000/mo', items: ['5 platforms', 'Daily posting', 'Full content calendar', 'Influencer outreach'] },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
FITNESS_TRAINER: {
|
||||||
|
roleLabel: 'Fitness Trainer',
|
||||||
|
tabs: ['overview', 'about', 'training plans', 'client results', 'certifications', 'testimonials', 'faqs'],
|
||||||
|
specialties: ['Weight Loss', 'Strength', 'Yoga', 'HIIT', 'Nutrition', 'Rehabilitation'],
|
||||||
|
statsLabels: ['Clients Trained', 'Years Exp', 'Certified Pro', 'Next Slot', 'Online OK'],
|
||||||
|
equipmentLabel: 'Certifications & Tools',
|
||||||
|
galleryTabLabel: 'client results',
|
||||||
|
experienceTabLabel: 'certifications',
|
||||||
|
serviceTabLabel: 'training plans',
|
||||||
|
faqItems: [
|
||||||
|
{ q: 'Do you offer online training?', a: 'Yes. Online sessions via Zoom with personalised programs tracked through fitness apps.' },
|
||||||
|
{ q: 'Is nutrition guidance included?', a: 'Basic nutrition guidance is included. A detailed meal plan is available as an add-on.' },
|
||||||
|
{ q: 'How long until I see results?', a: 'Visible progress typically begins in 4–6 weeks with consistent training and diet adherence.' },
|
||||||
|
{ q: 'Do you design custom workout plans?', a: 'Yes. Every client gets a custom plan based on fitness level, goals, and equipment access.' },
|
||||||
|
],
|
||||||
|
packages: [
|
||||||
|
{ name: 'Starter', price: '₹4,000/mo', items: ['8 sessions/month', 'Workout plan', 'WhatsApp check-ins', 'Progress tracking'] },
|
||||||
|
{ name: 'Transform', price: '₹8,000/mo', items: ['16 sessions/month', 'Custom diet plan', 'Weekly reviews', 'App access'] },
|
||||||
|
{ name: 'Elite', price: '₹15,000/mo', items: ['Daily check-in', 'Full nutrition plan', 'Body composition tracking', 'Priority access'] },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
CATERING_SERVICES: {
|
||||||
|
roleLabel: 'Catering Services',
|
||||||
|
tabs: ['overview', 'about', 'packages & pricing', 'gallery', 'experience & certifications', 'testimonials', 'faqs'],
|
||||||
|
specialties: ['Weddings', 'Corporate Events', 'Private Parties', 'Buffet', 'Live Counters', 'Theme Catering'],
|
||||||
|
statsLabels: ['Events Done', 'Years Active', 'Certified', 'Last Event', 'Pan-India'],
|
||||||
|
equipmentLabel: 'Equipment & Certifications',
|
||||||
|
galleryTabLabel: 'gallery',
|
||||||
|
experienceTabLabel: 'experience & certifications',
|
||||||
|
serviceTabLabel: 'packages & pricing',
|
||||||
|
faqItems: [
|
||||||
|
{ q: 'What is your minimum guest count?', a: 'Minimum 50 guests for standard bookings. Intimate private dining available for 20+ guests.' },
|
||||||
|
{ q: 'Do you handle equipment setup?', a: 'Yes. Full setup including chafing dishes, serving counters, and staff is included in all packages.' },
|
||||||
|
{ q: 'Are custom menus available?', a: 'Absolutely. Menus are fully customisable — dietary restrictions, regional cuisines, and themed menus accommodated.' },
|
||||||
|
{ q: 'How far in advance should I book?', a: 'At least 3–4 weeks for events under 200 guests; 6–8 weeks for large-scale events.' },
|
||||||
|
],
|
||||||
|
packages: [
|
||||||
|
{ name: 'Essential', price: '₹350/plate', items: ['3-course menu', 'Serving staff', 'Basic setup', 'Buffet style'] },
|
||||||
|
{ name: 'Premium', price: '₹650/plate', items: ['5-course menu', 'Live counters', 'Themed decor', 'Dedicated manager'] },
|
||||||
|
{ name: 'Royal', price: '₹1,200/plate', items: ['Custom menu', 'Chef station', 'Premium décor', 'Full-day service'] },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
PROFESSIONAL: {
|
||||||
|
roleLabel: 'Professional',
|
||||||
|
tabs: ['overview', 'about', 'services & pricing', 'portfolio', 'experience', 'testimonials', 'faqs'],
|
||||||
|
specialties: ['Primary Service', 'Secondary Service', 'Consulting', 'Training', 'Events', 'Custom'],
|
||||||
|
statsLabels: ['Projects Done', 'Years Exp', 'Verified Pro', 'Last Delivery', 'Remote OK'],
|
||||||
|
equipmentLabel: 'Tools & Equipment',
|
||||||
|
galleryTabLabel: 'portfolio',
|
||||||
|
experienceTabLabel: 'experience',
|
||||||
|
serviceTabLabel: 'services & pricing',
|
||||||
|
faqItems: [
|
||||||
|
{ q: 'How do I get started?', a: 'Send a requirement, review the professional\'s profile, and accept the request to receive their contact details.' },
|
||||||
|
{ q: 'What is your availability?', a: 'Availability is updated regularly on the profile. Contact for specific date confirmation.' },
|
||||||
|
{ q: 'Do you provide a contract?', a: 'Yes. A service agreement is shared before any project begins for mutual clarity.' },
|
||||||
|
{ q: 'What is your revision policy?', a: 'Revisions are included as per the selected package. Additional revisions are billed separately.' },
|
||||||
|
],
|
||||||
|
packages: [
|
||||||
|
{ name: 'Basic', price: 'From ₹5,000', items: ['Core deliverables', 'Standard timeline', '1 revision', 'Email support'] },
|
||||||
|
{ name: 'Standard', price: 'From ₹15,000', items: ['Full scope', 'Priority delivery', '3 revisions', 'Call support'] },
|
||||||
|
{ name: 'Premium', price: 'Custom', items: ['Custom scope', 'Dedicated support', 'Unlimited revisions', 'SLA'] },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function portfolioSpecForRole(roleKey: string): PortfolioSpec {
|
||||||
|
const normalized = normalizeRoleKey(roleKey);
|
||||||
|
return PORTFOLIO_SPECS[normalized] || PORTFOLIO_SPECS.PROFESSIONAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PORTFOLIO_TESTIMONIALS = [
|
||||||
|
{ name: 'Priya S.', rating: 5, text: 'Absolutely brilliant work. The results exceeded every expectation. Highly recommended.', category: 'Verified Booking' },
|
||||||
|
{ name: 'Rohan M.', rating: 5, text: 'Professional, punctual, and highly creative. Loved the final deliverables.', category: 'Verified Booking' },
|
||||||
|
{ name: 'Ananya K.', rating: 4, text: 'Great communication throughout the project. Delivered on time with excellent quality.', category: 'Verified Booking' },
|
||||||
|
{ name: 'Kiran T.', rating: 5, text: 'An absolute pleasure to work with. Will definitely hire again for our next project.', category: 'Verified Booking' },
|
||||||
|
];
|
||||||
|
|
||||||
function customerViewFor(sidebar: string, roleKey: string): CustomerView {
|
function customerViewFor(sidebar: string, roleKey: string): CustomerView {
|
||||||
const key = String(sidebar || '').toLowerCase().trim();
|
const key = String(sidebar || '').toLowerCase().trim();
|
||||||
if (key === 'my dashboard') return { title: 'Customer Dashboard Overview', subtitle: 'Manage your enterprise requirements and track professional responses in real-time.', tabs: ['overview', 'recent requirements', 'quick actions'], cta: 'Post New Requirement' };
|
if (key === 'my dashboard') return { title: 'Customer Dashboard Overview', subtitle: 'Manage your enterprise requirements and track professional responses in real-time.', tabs: ['overview', 'recent requirements', 'quick actions'], cta: 'Post New Requirement' };
|
||||||
|
|
@ -284,6 +533,10 @@ function customerViewFor(sidebar: string, roleKey: string): CustomerView {
|
||||||
const spec = profileSpecForRole(roleKey);
|
const spec = profileSpecForRole(roleKey);
|
||||||
return { title: spec.title, subtitle: spec.subtitle, tabs: spec.tabs, cta: 'Save Changes' };
|
return { title: spec.title, subtitle: spec.subtitle, tabs: spec.tabs, cta: 'Save Changes' };
|
||||||
}
|
}
|
||||||
|
if (key === 'my portfolio') {
|
||||||
|
const spec = portfolioSpecForRole(roleKey);
|
||||||
|
return { title: `${spec.roleLabel} Portfolio`, subtitle: 'Manage your public portfolio, showcase your work, and control what customers see before they accept your contact request.', tabs: [], cta: 'Edit Portfolio' };
|
||||||
|
}
|
||||||
if (key === 'credits') return { title: 'Credits & Billing', subtitle: 'View credit balance, top-up packages, and transaction history.', tabs: ['overview', 'buy credits', 'transactions', 'usage history'], cta: 'Buy Credits' };
|
if (key === 'credits') return { title: 'Credits & Billing', subtitle: 'View credit balance, top-up packages, and transaction history.', tabs: ['overview', 'buy credits', 'transactions', 'usage history'], cta: 'Buy Credits' };
|
||||||
if (key === 'explore nxtgauge') return { title: 'Explore Marketplace', subtitle: 'Discover top-tier professionals and specialized services.', tabs: [] };
|
if (key === 'explore nxtgauge') return { title: 'Explore Marketplace', subtitle: 'Discover top-tier professionals and specialized services.', tabs: [] };
|
||||||
if (key === 'verification') return { title: 'Verification Portal', subtitle: 'Track verification progress, documents, and updates.', tabs: ['approval status', 'documents', 'activity'] };
|
if (key === 'verification') return { title: 'Verification Portal', subtitle: 'Track verification progress, documents, and updates.', tabs: ['approval status', 'documents', 'activity'] };
|
||||||
|
|
@ -438,6 +691,11 @@ export default function DashboardDesignPreview(props: {
|
||||||
if (isCustomerExternalMode()) return customerView().tabs;
|
if (isCustomerExternalMode()) return customerView().tabs;
|
||||||
return props.tabs.length ? props.tabs : ['overview'];
|
return props.tabs.length ? props.tabs : ['overview'];
|
||||||
});
|
});
|
||||||
|
const resolvedTabKey = createMemo(() => {
|
||||||
|
const tabs = previewTabs();
|
||||||
|
const key = activeTabKey();
|
||||||
|
return tabs.some((item) => normalizeTabKey(item) === key) ? key : normalizeTabKey(tabs[0] || '');
|
||||||
|
});
|
||||||
const previewWidgets = createMemo(() => (props.widgets.length ? props.widgets : ['total_requirements', 'open', 'closed', 'responses', 'saved_pros']));
|
const previewWidgets = createMemo(() => (props.widgets.length ? props.widgets : ['total_requirements', 'open', 'closed', 'responses', 'saved_pros']));
|
||||||
const previewFields = createMemo(() => (props.fields.length ? props.fields : ['full_name', 'email', 'verification_status', 'approval_status']));
|
const previewFields = createMemo(() => (props.fields.length ? props.fields : ['full_name', 'email', 'verification_status', 'approval_status']));
|
||||||
const customerKey = createMemo(() => String(props.activeSidebar || '').toLowerCase().trim());
|
const customerKey = createMemo(() => String(props.activeSidebar || '').toLowerCase().trim());
|
||||||
|
|
@ -472,6 +730,7 @@ export default function DashboardDesignPreview(props: {
|
||||||
const [viewTicketFiles, setViewTicketFiles] = createSignal<string[]>([]);
|
const [viewTicketFiles, setViewTicketFiles] = createSignal<string[]>([]);
|
||||||
const [profileSettingsTab, setProfileSettingsTab] = createSignal<'change_password' | 'notifications' | 'privacy'>('change_password');
|
const [profileSettingsTab, setProfileSettingsTab] = createSignal<'change_password' | 'notifications' | 'privacy'>('change_password');
|
||||||
const [showDeleteAccountModal, setShowDeleteAccountModal] = createSignal(false);
|
const [showDeleteAccountModal, setShowDeleteAccountModal] = createSignal(false);
|
||||||
|
const [showPortfolioPreview, setShowPortfolioPreview] = createSignal(false);
|
||||||
const [lastSidebarKey, setLastSidebarKey] = createSignal('');
|
const [lastSidebarKey, setLastSidebarKey] = createSignal('');
|
||||||
const [profileSettingToggles, setProfileSettingToggles] = createSignal<Record<string, boolean>>({
|
const [profileSettingToggles, setProfileSettingToggles] = createSignal<Record<string, boolean>>({
|
||||||
email_updates: true,
|
email_updates: true,
|
||||||
|
|
@ -498,11 +757,6 @@ export default function DashboardDesignPreview(props: {
|
||||||
setViewTicketFiles([]);
|
setViewTicketFiles([]);
|
||||||
};
|
};
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
const tabs = previewTabs();
|
|
||||||
const hasMatch = tabs.some((item) => normalizeTabKey(item) === activeTabKey());
|
|
||||||
if (!hasMatch && tabs[0]) props.onTabSelect(tabs[0]);
|
|
||||||
});
|
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const key = customerKey();
|
const key = customerKey();
|
||||||
|
|
@ -554,8 +808,199 @@ export default function DashboardDesignPreview(props: {
|
||||||
return { bg: '#F3F4F6', c: '#6B7280' };
|
return { bg: '#F3F4F6', c: '#6B7280' };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const renderPortfolioContent = () => {
|
||||||
|
const spec = portfolioSpecForRole(props.roleKey || '');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style="display:flex;flex-direction:column;gap:10px">
|
||||||
|
|
||||||
|
{/* Profile Header */}
|
||||||
|
<div style="border-radius:12px;border:1px solid #E5E7EB;background:white;box-shadow:0 1px 3px rgba(0,0,0,0.06);overflow:hidden">
|
||||||
|
<div style="padding:16px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;justify-content:space-between;gap:12px">
|
||||||
|
<div style="display:flex;align-items:center;gap:12px">
|
||||||
|
<div style="width:52px;height:52px;border-radius:50%;background:#F3F4F6;border:1px solid #E5E7EB;display:flex;align-items:center;justify-content:center;flex-shrink:0">
|
||||||
|
<User size={22} style="color:#9CA3AF" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div style="display:flex;align-items:center;gap:8px">
|
||||||
|
<p style="margin:0;font-size:15px;font-weight:700;color:#111827">Alex Morgan</p>
|
||||||
|
<span style="font-size:11px;font-weight:600;color:#374151;background:#F3F4F6;border:1px solid #E5E7EB;border-radius:6px;padding:1px 7px">Verified</span>
|
||||||
|
</div>
|
||||||
|
<p style="margin:2px 0 0;font-size:12px;color:#6B7280">{spec.roleLabel} · Mumbai, Maharashtra</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex;gap:8px;flex-shrink:0">
|
||||||
|
<button type="button" style="height:34px;border-radius:8px;border:1px solid #E5E7EB;background:white;color:#374151;padding:0 14px;font-size:12px;font-weight:600;cursor:pointer">Edit Portfolio</button>
|
||||||
|
<button type="button" onClick={() => setShowPortfolioPreview(true)} style="height:34px;border-radius:8px;border:none;background:#0D0D2A;color:white;padding:0 14px;font-size:12px;font-weight:600;cursor:pointer">Preview Portfolio</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex">
|
||||||
|
{spec.statsLabels.map((label, i) => (
|
||||||
|
<div style={`flex:1;padding:12px 16px;${i < spec.statsLabels.length - 1 ? 'border-right:1px solid #F3F4F6' : ''}`}>
|
||||||
|
<p style="margin:0;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#9CA3AF">{label}</p>
|
||||||
|
<p style="margin:4px 0 0;font-size:14px;font-weight:700;color:#111827">{i===0?'248':i===1?'7+':i===2?'Verified':i===3?'2 days':'Yes'}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* About + Specialties */}
|
||||||
|
<div style="display:grid;grid-template-columns:2fr 1fr;gap:10px">
|
||||||
|
<div style="border-radius:12px;border:1px solid #E5E7EB;background:white;box-shadow:0 1px 3px rgba(0,0,0,0.06);overflow:hidden">
|
||||||
|
<div style="padding:12px 16px;border-bottom:1px solid #E5E7EB">
|
||||||
|
<p style="margin:0;font-size:13px;font-weight:700;color:#111827">About</p>
|
||||||
|
</div>
|
||||||
|
<div style="padding:14px 16px">
|
||||||
|
<p style="margin:0;font-size:13px;color:#374151;line-height:1.6">Professional {spec.roleLabel.toLowerCase()} with 7+ years of experience delivering high-quality work across India. Committed to excellence, creativity, and client satisfaction on every project.</p>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex;border-top:1px solid #F3F4F6">
|
||||||
|
{[['7+','Years Exp'],['248','Projects'],['4.9','Rating'],['128','Reviews']].map(([val,lbl], i) => (
|
||||||
|
<div style={`flex:1;padding:12px 16px;${i<3?'border-right:1px solid #F3F4F6':''}`}>
|
||||||
|
<p style="margin:0;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#9CA3AF">{lbl}</p>
|
||||||
|
<p style="margin:4px 0 0;font-size:14px;font-weight:700;color:#111827">{val}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="border-radius:12px;border:1px solid #E5E7EB;background:white;box-shadow:0 1px 3px rgba(0,0,0,0.06);overflow:hidden">
|
||||||
|
<div style="padding:12px 16px;border-bottom:1px solid #E5E7EB">
|
||||||
|
<p style="margin:0;font-size:13px;font-weight:700;color:#111827">Specialties</p>
|
||||||
|
</div>
|
||||||
|
<div style="padding:14px 16px;display:flex;flex-wrap:wrap;gap:6px">
|
||||||
|
{spec.specialties.map((s) => (
|
||||||
|
<span style="height:24px;padding:0 8px;border-radius:6px;border:1px solid #E5E7EB;background:#F9FAFB;font-size:11px;font-weight:600;color:#374151;display:inline-flex;align-items:center">{s}</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div style="padding:0 16px 14px;border-top:1px solid #F3F4F6;margin-top:4px">
|
||||||
|
<p style="margin:10px 0 6px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#9CA3AF">Languages</p>
|
||||||
|
<div style="display:flex;gap:5px">
|
||||||
|
{['English','Hindi','Marathi'].map((l) => (
|
||||||
|
<span style="height:22px;padding:0 8px;border-radius:6px;border:1px solid #E5E7EB;background:#F9FAFB;font-size:11px;font-weight:600;color:#374151;display:inline-flex;align-items:center">{l}</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<p style="margin:10px 0 6px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#9CA3AF">Service Areas</p>
|
||||||
|
<div style="display:flex;flex-wrap:wrap;gap:5px">
|
||||||
|
{['Mumbai','Pune','Thane','Nashik'].map((c) => (
|
||||||
|
<span style="height:22px;padding:0 8px;border-radius:6px;border:1px solid #E5E7EB;background:#F9FAFB;font-size:11px;font-weight:600;color:#374151;display:inline-flex;align-items:center">{c}</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Packages */}
|
||||||
|
<div style="border-radius:12px;border:1px solid #E5E7EB;background:white;box-shadow:0 1px 3px rgba(0,0,0,0.06);overflow:hidden">
|
||||||
|
<div style="padding:12px 16px;border-bottom:1px solid #E5E7EB">
|
||||||
|
<p style="margin:0;font-size:13px;font-weight:700;color:#111827">{titleCase(spec.serviceTabLabel)}</p>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex">
|
||||||
|
{spec.packages.map((pkg, i) => (
|
||||||
|
<div style={`flex:1;padding:16px;${i<2?'border-right:1px solid #F3F4F6':''}`}>
|
||||||
|
<p style="margin:0;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#9CA3AF">{pkg.name}</p>
|
||||||
|
<p style="margin:4px 0 0;font-size:16px;font-weight:700;color:#111827">{pkg.price}</p>
|
||||||
|
<div style="margin-top:8px;display:grid;gap:4px">
|
||||||
|
{pkg.items.map((item) => (
|
||||||
|
<div style="display:flex;align-items:center;gap:6px;font-size:12px;color:#374151">
|
||||||
|
<CheckCircle2 size={11} style="color:#9CA3AF;flex-shrink:0" /> {item}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Work Gallery */}
|
||||||
|
<div style="border-radius:12px;border:1px solid #E5E7EB;background:white;box-shadow:0 1px 3px rgba(0,0,0,0.06);overflow:hidden">
|
||||||
|
<div style="padding:12px 16px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;justify-content:space-between">
|
||||||
|
<p style="margin:0;font-size:13px;font-weight:700;color:#111827">{titleCase(spec.galleryTabLabel)}</p>
|
||||||
|
<button type="button" style="height:30px;border-radius:8px;border:none;background:#0D0D2A;color:white;padding:0 12px;font-size:11px;font-weight:600;cursor:pointer">Upload Work</button>
|
||||||
|
</div>
|
||||||
|
<div style="padding:14px 16px;display:grid;grid-template-columns:repeat(4,1fr);gap:8px">
|
||||||
|
{[0,1,2,3,4,5,6,7].map((i) => (
|
||||||
|
<div style="height:72px;border-radius:8px;border:1px solid #E5E7EB;background:#F9FAFB;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:4px">
|
||||||
|
<Image size={18} style="color:#D1D5DB" />
|
||||||
|
<span style="font-size:10px;color:#9CA3AF">{spec.specialties[i % spec.specialties.length]}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Experience */}
|
||||||
|
<div style="border-radius:12px;border:1px solid #E5E7EB;background:white;box-shadow:0 1px 3px rgba(0,0,0,0.06);overflow:hidden">
|
||||||
|
<div style="padding:12px 16px;border-bottom:1px solid #E5E7EB">
|
||||||
|
<p style="margin:0;font-size:13px;font-weight:700;color:#111827">{titleCase(spec.experienceTabLabel)}</p>
|
||||||
|
</div>
|
||||||
|
<div style="padding:14px 16px">
|
||||||
|
<p style="margin:0 0 8px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#9CA3AF">{spec.equipmentLabel}</p>
|
||||||
|
<div style="display:flex;flex-wrap:wrap;gap:6px">
|
||||||
|
{spec.specialties.map((item) => (
|
||||||
|
<span style="height:24px;padding:0 8px;border-radius:6px;border:1px solid #E5E7EB;background:#F9FAFB;font-size:11px;font-weight:600;color:#374151;display:inline-flex;align-items:center">{item}</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="border-top:1px solid #F3F4F6">
|
||||||
|
{[['2018','Started professional career as '+spec.roleLabel],['2020','Completed 100+ successful projects'],['2022','Won regional industry recognition'],['2024','Joined Nxtgauge marketplace']].map(([yr, desc], i, arr) => (
|
||||||
|
<div style={`display:flex;gap:16px;align-items:center;padding:10px 16px;${i<arr.length-1?'border-bottom:1px solid #F3F4F6':''}`}>
|
||||||
|
<p style="margin:0;font-size:11px;font-weight:700;color:#9CA3AF;min-width:32px">{yr}</p>
|
||||||
|
<p style="margin:0;font-size:13px;color:#374151">{desc}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Testimonials */}
|
||||||
|
<div style="border-radius:12px;border:1px solid #E5E7EB;background:white;box-shadow:0 1px 3px rgba(0,0,0,0.06);overflow:hidden">
|
||||||
|
<div style="padding:12px 16px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:10px">
|
||||||
|
<p style="margin:0;font-size:13px;font-weight:700;color:#111827">Testimonials</p>
|
||||||
|
<span style="font-size:11px;font-weight:600;color:#374151;background:#F3F4F6;border:1px solid #E5E7EB;border-radius:6px;padding:1px 7px">4.9 · 128 reviews</span>
|
||||||
|
</div>
|
||||||
|
<div style="display:grid;grid-template-columns:1fr 1fr">
|
||||||
|
{PORTFOLIO_TESTIMONIALS.map((t, i) => (
|
||||||
|
<div style={`padding:14px 16px;${i%2===0?'border-right:1px solid #F3F4F6':''};${i<2?'border-bottom:1px solid #F3F4F6':''}`}>
|
||||||
|
<div style="display:flex;justify-content:space-between;align-items:center">
|
||||||
|
<p style="margin:0;font-size:13px;font-weight:700;color:#111827">{t.name}</p>
|
||||||
|
<div style="display:flex;gap:1px">
|
||||||
|
{Array.from({length: t.rating}).map(() => <Star size={11} style="color:#F59E0B" fill="#F59E0B" />)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p style="margin:2px 0 0;font-size:11px;color:#9CA3AF">{t.category}</p>
|
||||||
|
<p style="margin:8px 0 0;font-size:12px;color:#374151;line-height:1.5">{t.text}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* FAQs */}
|
||||||
|
<div style="border-radius:12px;border:1px solid #E5E7EB;background:white;box-shadow:0 1px 3px rgba(0,0,0,0.06);overflow:hidden">
|
||||||
|
<div style="padding:12px 16px;border-bottom:1px solid #E5E7EB">
|
||||||
|
<p style="margin:0;font-size:13px;font-weight:700;color:#111827">Frequently Asked Questions</p>
|
||||||
|
</div>
|
||||||
|
<For each={spec.faqItems}>{(faq, i) => (
|
||||||
|
<div style={`border-bottom:${i() < spec.faqItems.length - 1 ? '1px solid #F3F4F6' : 'none'}`}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setOpenFaqIndex(openFaqIndex()===i() ? null : i())}
|
||||||
|
style="width:100%;display:flex;justify-content:space-between;align-items:center;padding:12px 16px;background:none;border:none;cursor:pointer;text-align:left"
|
||||||
|
>
|
||||||
|
<span style="font-size:13px;font-weight:600;color:#111827">{faq.q}</span>
|
||||||
|
{openFaqIndex()===i() ? <ChevronUp size={15} style="color:#374151;flex-shrink:0" /> : <ChevronDown size={15} style="color:#9CA3AF;flex-shrink:0" />}
|
||||||
|
</button>
|
||||||
|
<Show when={openFaqIndex()===i()}>
|
||||||
|
<div style="padding:0 16px 12px">
|
||||||
|
<p style="margin:0;font-size:13px;color:#374151;line-height:1.6">{faq.a}</p>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
)}</For>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const renderCustomerContent = () => {
|
const renderCustomerContent = () => {
|
||||||
const tab = props.activeTab.toLowerCase();
|
const tab = resolvedTabKey();
|
||||||
|
|
||||||
if (customerKey() === 'my dashboard') {
|
if (customerKey() === 'my dashboard') {
|
||||||
if (tab === 'recent requirements') {
|
if (tab === 'recent requirements') {
|
||||||
|
|
@ -1942,6 +2387,8 @@ export default function DashboardDesignPreview(props: {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (customerKey() === 'my portfolio') return renderPortfolioContent();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style="border:1px solid #E5E7EB;background:white;border-radius:16px;padding:14px;box-shadow:0 1px 4px rgba(0,0,0,0.06)">
|
<div style="border:1px solid #E5E7EB;background:white;border-radius:16px;padding:14px;box-shadow:0 1px 4px rgba(0,0,0,0.06)">
|
||||||
<p style="margin:0;font-size:13px;font-weight:700;color:#111827">Screen Preview</p>
|
<p style="margin:0;font-size:13px;font-weight:700;color:#111827">Screen Preview</p>
|
||||||
|
|
@ -2009,7 +2456,7 @@ export default function DashboardDesignPreview(props: {
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => props.onTabSelect(item)}
|
onClick={() => props.onTabSelect(item)}
|
||||||
style={`padding-bottom:10px;font-size:13px;font-weight:500;background:none;border:none;cursor:pointer;${activeTabKey() === normalizeTabKey(item) ? 'color:#FF5E13;border-bottom:2px solid #FF5E13;margin-bottom:-1px' : 'color:#6B7280'}`}
|
style={`padding-bottom:10px;font-size:13px;font-weight:500;background:none;border:none;cursor:pointer;${resolvedTabKey() === normalizeTabKey(item) ? 'color:#FF5E13;border-bottom:2px solid #FF5E13;margin-bottom:-1px' : 'color:#6B7280'}`}
|
||||||
>
|
>
|
||||||
{titleCase(item)}
|
{titleCase(item)}
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -2037,6 +2484,97 @@ export default function DashboardDesignPreview(props: {
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Show when={showPortfolioPreview()}>
|
||||||
|
<div
|
||||||
|
style="position:fixed;inset:0;background:rgba(15,23,42,0.5);display:flex;align-items:center;justify-content:center;z-index:50;padding:16px"
|
||||||
|
onClick={() => setShowPortfolioPreview(false)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style="width:min(540px,100%);max-height:88vh;overflow-y:auto;border:1px solid #E5E7EB;background:white;border-radius:16px;box-shadow:0 20px 50px rgba(0,0,0,0.22)"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<div style="background:#0D0D2A;padding:12px 16px;border-radius:16px 16px 0 0;display:flex;justify-content:space-between;align-items:center">
|
||||||
|
<div>
|
||||||
|
<span style="font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:0.08em;color:#FF5E13">Customer View</span>
|
||||||
|
<p style="margin:2px 0 0;font-size:12px;color:#9CA3AF">What customers see before accepting your contact request</p>
|
||||||
|
</div>
|
||||||
|
<button type="button" onClick={() => setShowPortfolioPreview(false)} style="width:28px;height:28px;border-radius:8px;border:1px solid rgba(255,255,255,0.15);background:transparent;display:flex;align-items:center;justify-content:center;cursor:pointer;color:#E5E7EB">
|
||||||
|
<X size={14} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div style="padding:16px;display:flex;flex-direction:column;gap:12px">
|
||||||
|
<div style="display:flex;align-items:center;gap:12px">
|
||||||
|
<div style="width:60px;height:60px;border-radius:50%;background:#F3F4F6;border:2px solid #E5E7EB;display:flex;align-items:center;justify-content:center;flex-shrink:0">
|
||||||
|
<User size={24} style="color:#9CA3AF" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div style="display:flex;align-items:center;gap:7px">
|
||||||
|
<p style="margin:0;font-size:17px;font-weight:800;color:#111827">Alex Morgan</p>
|
||||||
|
<span style="display:inline-flex;align-items:center;gap:3px;height:18px;padding:0 7px;border-radius:999px;background:#ECFDF3;border:1px solid #6EE7B7;font-size:10px;font-weight:700;color:#059669">
|
||||||
|
<BadgeCheck size={9} /> Verified
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p style="margin:3px 0 0;font-size:12px;color:#FF5E13;font-weight:600">{portfolioSpecForRole(props.roleKey||'').roleLabel}</p>
|
||||||
|
<div style="display:flex;align-items:center;gap:3px;margin-top:2px">
|
||||||
|
<MapPin size={11} style="color:#9CA3AF;flex-shrink:0" />
|
||||||
|
<span style="font-size:11px;color:#6B7280">Mumbai, Maharashtra</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex;align-items:center;gap:5px">
|
||||||
|
{[1,2,3,4,5].map(() => <Star size={13} style="color:#F59E0B" fill="#F59E0B" />)}
|
||||||
|
<span style="font-size:13px;font-weight:700;color:#111827">4.9</span>
|
||||||
|
<span style="font-size:12px;color:#6B7280">(128 reviews)</span>
|
||||||
|
</div>
|
||||||
|
<p style="margin:0;font-size:13px;color:#374151;line-height:1.5">
|
||||||
|
Passionate {portfolioSpecForRole(props.roleKey||'').roleLabel.toLowerCase()} with 7+ years of experience delivering exceptional results. Specialising in {portfolioSpecForRole(props.roleKey||'').specialties.slice(0,3).join(', ')}.
|
||||||
|
</p>
|
||||||
|
<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:8px;padding:10px;border:1px solid #E5E7EB;border-radius:10px;background:#F9FAFB">
|
||||||
|
{[['248','Projects'],['7+','Yrs Exp'],['4.9','Rating'],['128','Reviews']].map(([val,lbl]) => (
|
||||||
|
<div style="text-align:center">
|
||||||
|
<p style="margin:0;font-size:15px;font-weight:800;color:#111827">{val}</p>
|
||||||
|
<p style="margin:1px 0 0;font-size:10px;color:#6B7280;text-transform:uppercase;letter-spacing:0.04em">{lbl}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p style="margin:0 0 6px;font-size:11px;font-weight:700;color:#6B7280;text-transform:uppercase;letter-spacing:0.04em">Specialties</p>
|
||||||
|
<div style="display:flex;flex-wrap:wrap;gap:5px">
|
||||||
|
{portfolioSpecForRole(props.roleKey||'').specialties.map((s) => (
|
||||||
|
<span style="height:24px;padding:0 8px;border-radius:999px;border:1px solid #FFE0CC;background:#FFF4EE;color:#FF5E13;font-size:11px;font-weight:600;display:inline-flex;align-items:center">{s}</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p style="margin:0 0 6px;font-size:11px;font-weight:700;color:#6B7280;text-transform:uppercase;letter-spacing:0.04em">Featured Package</p>
|
||||||
|
{(() => {
|
||||||
|
const pkg = portfolioSpecForRole(props.roleKey||'').packages[1];
|
||||||
|
return (
|
||||||
|
<div style="border:2px solid #FF5E13;border-radius:10px;padding:12px;background:#FFF8F4">
|
||||||
|
<div style="display:flex;justify-content:space-between;align-items:center">
|
||||||
|
<p style="margin:0;font-size:13px;font-weight:800;color:#111827">{pkg.name}</p>
|
||||||
|
<span style="font-size:16px;font-weight:800;color:#FF5E13">{pkg.price}</span>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex;flex-wrap:wrap;gap:6px;margin-top:8px">
|
||||||
|
{pkg.items.map((item) => (
|
||||||
|
<span style="font-size:11px;color:#374151;display:inline-flex;align-items:center;gap:4px">
|
||||||
|
<CheckCircle2 size={11} style="color:#10B981;flex-shrink:0" /> {item}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
<div style="display:flex;gap:8px;padding-top:4px;border-top:1px solid #E5E7EB">
|
||||||
|
<button type="button" style="flex:1;height:36px;border-radius:8px;border:none;background:#FF5E13;color:white;font-size:13px;font-weight:700;cursor:pointer">Accept Request</button>
|
||||||
|
<button type="button" onClick={() => setShowPortfolioPreview(false)} style="flex:1;height:36px;border-radius:8px;border:1px solid #D1D5DB;background:white;color:#374151;font-size:13px;font-weight:700;cursor:pointer">Close Preview</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ const ROLE_BASED_SIDEBAR: Record<'PROFESSIONAL' | 'COMPANY' | 'JOB_SEEKER' | 'CU
|
||||||
PROFESSIONAL: [
|
PROFESSIONAL: [
|
||||||
'My Dashboard',
|
'My Dashboard',
|
||||||
'My Profile',
|
'My Profile',
|
||||||
|
'My Portfolio',
|
||||||
'Leads',
|
'Leads',
|
||||||
'My Responses',
|
'My Responses',
|
||||||
'Credits',
|
'Credits',
|
||||||
|
|
@ -182,6 +183,7 @@ export default function ExternalDashboardManagementPage() {
|
||||||
const [name, setName] = createSignal('');
|
const [name, setName] = createSignal('');
|
||||||
const [code, setCode] = createSignal('');
|
const [code, setCode] = createSignal('');
|
||||||
const [roleId, setRoleId] = createSignal('');
|
const [roleId, setRoleId] = createSignal('');
|
||||||
|
const [formRoleKey, setFormRoleKey] = createSignal('');
|
||||||
const [widgets, setWidgets] = createSignal<string[]>([]);
|
const [widgets, setWidgets] = createSignal<string[]>([]);
|
||||||
const [tabs, setTabs] = createSignal<string[]>([]);
|
const [tabs, setTabs] = createSignal<string[]>([]);
|
||||||
const [sidebarItems, setSidebarItems] = createSignal<string[]>([]);
|
const [sidebarItems, setSidebarItems] = createSignal<string[]>([]);
|
||||||
|
|
@ -204,6 +206,15 @@ export default function ExternalDashboardManagementPage() {
|
||||||
return map;
|
return map;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const personaFromKey = (key: string): 'PROFESSIONAL' | 'COMPANY' | 'JOB_SEEKER' | 'CUSTOMER' | null => {
|
||||||
|
const k = String(key || '').toUpperCase();
|
||||||
|
if (!k) return null;
|
||||||
|
if (k.includes('COMPANY')) return 'COMPANY';
|
||||||
|
if (k.includes('CUSTOMER')) return 'CUSTOMER';
|
||||||
|
if (k.includes('JOB_SEEKER') || k.includes('JOBSEEKER')) return 'JOB_SEEKER';
|
||||||
|
return 'PROFESSIONAL'; // photographer, makeup, tutor, developer, video, graphic, social, fitness, catering, etc.
|
||||||
|
};
|
||||||
|
|
||||||
const sidebarLooksCustomer = createMemo(() => {
|
const sidebarLooksCustomer = createMemo(() => {
|
||||||
const joined = sidebarItems().join(' ').toLowerCase();
|
const joined = sidebarItems().join(' ').toLowerCase();
|
||||||
return joined.includes('my requirements')
|
return joined.includes('my requirements')
|
||||||
|
|
@ -211,19 +222,24 @@ export default function ExternalDashboardManagementPage() {
|
||||||
|| joined.includes('shortlisted responses');
|
|| joined.includes('shortlisted responses');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const selectedRoleKey = createMemo(() => {
|
||||||
|
const selected = roles().find((r) => r.id === roleId());
|
||||||
|
return selected?.key || '';
|
||||||
|
});
|
||||||
|
|
||||||
const previewSidebarItems = createMemo(() => {
|
const previewSidebarItems = createMemo(() => {
|
||||||
const selectedRoleId = roleId();
|
const selectedRoleId = roleId();
|
||||||
const persona = rolePersonaById()[selectedRoleId];
|
// Primary: look up persona by role ID (from loaded roles list)
|
||||||
|
const personaById = rolePersonaById()[selectedRoleId];
|
||||||
|
// Fallback: derive persona directly from the stored role key (works when roles API is empty or roleId unmatched)
|
||||||
|
const personaByKey = personaFromKey(selectedRoleKey() || formRoleKey());
|
||||||
|
const persona = personaById ?? personaByKey;
|
||||||
if (persona && ROLE_BASED_SIDEBAR[persona]?.length) return ROLE_BASED_SIDEBAR[persona];
|
if (persona && ROLE_BASED_SIDEBAR[persona]?.length) return ROLE_BASED_SIDEBAR[persona];
|
||||||
if (sidebarLooksCustomer()) return ROLE_BASED_SIDEBAR.CUSTOMER;
|
if (sidebarLooksCustomer()) return ROLE_BASED_SIDEBAR.CUSTOMER;
|
||||||
if (sidebarItems().length) return sidebarItems();
|
if (sidebarItems().length) return sidebarItems();
|
||||||
return ['My Dashboard', 'My Profile', 'Switch Services', 'Logout'];
|
return ['My Dashboard', 'My Profile', 'Switch Services', 'Logout'];
|
||||||
});
|
});
|
||||||
const previewTabs = createMemo(() => (tabs().length ? tabs() : ['overview']));
|
const previewTabs = createMemo(() => (tabs().length ? tabs() : ['overview']));
|
||||||
const selectedRoleKey = createMemo(() => {
|
|
||||||
const selected = roles().find((r) => r.id === roleId());
|
|
||||||
return selected?.key || '';
|
|
||||||
});
|
|
||||||
|
|
||||||
const authHeaders = () => {
|
const authHeaders = () => {
|
||||||
const token = typeof sessionStorage !== 'undefined' ? sessionStorage.getItem('nxtgauge_admin_access_token') || '' : '';
|
const token = typeof sessionStorage !== 'undefined' ? sessionStorage.getItem('nxtgauge_admin_access_token') || '' : '';
|
||||||
|
|
@ -326,7 +342,9 @@ export default function ExternalDashboardManagementPage() {
|
||||||
setFormTab('preview');
|
setFormTab('preview');
|
||||||
setName('');
|
setName('');
|
||||||
setCode('');
|
setCode('');
|
||||||
setRoleId(roles()[0]?.id || '');
|
const firstRole = roles()[0];
|
||||||
|
setRoleId(firstRole?.id || '');
|
||||||
|
setFormRoleKey(firstRole?.key || '');
|
||||||
setWidgets([]);
|
setWidgets([]);
|
||||||
setTabs([]);
|
setTabs([]);
|
||||||
setSidebarItems([]);
|
setSidebarItems([]);
|
||||||
|
|
@ -345,6 +363,7 @@ export default function ExternalDashboardManagementPage() {
|
||||||
setName(row.name);
|
setName(row.name);
|
||||||
setCode(row.code);
|
setCode(row.code);
|
||||||
setRoleId(row.roleId);
|
setRoleId(row.roleId);
|
||||||
|
setFormRoleKey(row.roleKey);
|
||||||
setWidgets(row.widgets);
|
setWidgets(row.widgets);
|
||||||
setTabs(row.tabs);
|
setTabs(row.tabs);
|
||||||
setSidebarItems(row.sidebarItems);
|
setSidebarItems(row.sidebarItems);
|
||||||
|
|
@ -360,6 +379,9 @@ export default function ExternalDashboardManagementPage() {
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const selected = roleId();
|
const selected = roleId();
|
||||||
if (!selected || view() !== 'form' || editingId()) return;
|
if (!selected || view() !== 'form' || editingId()) return;
|
||||||
|
// Sync formRoleKey whenever roleId changes (so persona fallback stays fresh)
|
||||||
|
const matchedRole = roles().find((r) => r.id === selected);
|
||||||
|
if (matchedRole) setFormRoleKey(matchedRole.key);
|
||||||
applySidebarPresetForRole(selected, false);
|
applySidebarPresetForRole(selected, false);
|
||||||
applyPreviewPathForRole(selected, false);
|
applyPreviewPathForRole(selected, false);
|
||||||
});
|
});
|
||||||
|
|
@ -370,7 +392,8 @@ export default function ExternalDashboardManagementPage() {
|
||||||
});
|
});
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const list = previewTabs();
|
const list = tabs(); // use configured tabs only — not the fallback ['overview']
|
||||||
|
if (!list.length) return; // sidebar-driven tabs (profile, portfolio, etc.) manage validation internally
|
||||||
const active = normalizeToken(activePreviewTab());
|
const active = normalizeToken(activePreviewTab());
|
||||||
if (!list.some((item) => normalizeToken(item) === active)) setActivePreviewTab(list[0] || '');
|
if (!list.some((item) => normalizeToken(item) === active)) setActivePreviewTab(list[0] || '');
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue