fix(admin): add missing detail pages for 8 profession admin lists
All checks were successful
build-and-release / build (push) Successful in 44s
All checks were successful
build-and-release / build (push) Successful in 44s
Each of these profession admin list pages (makeup artist, tutor, developer,
video editor, graphic designer, social media manager, fitness trainer,
catering services) linked "View Profile" to /admin/{role}/{id}, but only
Photographer and Company had a real [id].tsx route — every other role fell
through to the legacy catch-all module and showed a placeholder instead of
the applicant's actual profile. Converts each flat list route into a
folder (index.tsx + [id].tsx) and adds a shared ProfessionDetailPage
component, modeled on the working Photographer detail page.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
7ae59fee20
commit
0866b7a069
17 changed files with 259 additions and 0 deletions
163
src/components/admin/ProfessionDetailPage.tsx
Normal file
163
src/components/admin/ProfessionDetailPage.tsx
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
import { A, useParams } from '@solidjs/router';
|
||||
import { For, createMemo, createResource, Show } from 'solid-js';
|
||||
|
||||
const API = '';
|
||||
|
||||
type ProfessionProfile = {
|
||||
id: string;
|
||||
name?: string;
|
||||
full_name?: string;
|
||||
first_name?: string;
|
||||
last_name?: string;
|
||||
email?: string;
|
||||
phone?: string;
|
||||
status?: string;
|
||||
created_at?: string;
|
||||
createdAt?: string;
|
||||
city?: string;
|
||||
state?: string;
|
||||
experience?: string;
|
||||
portfolio_url?: string;
|
||||
phoneNumber?: string;
|
||||
location?: string;
|
||||
gender?: string;
|
||||
availability?: string;
|
||||
permanentAddress?: string;
|
||||
hometown?: string;
|
||||
pincode?: string;
|
||||
languages?: Array<{ language?: string; proficiency?: string }>;
|
||||
};
|
||||
|
||||
async function fetchProfile(id: string, apiSlug: string): Promise<ProfessionProfile | null> {
|
||||
try {
|
||||
const res = await fetch(`${API}/api/admin/users/${id}`);
|
||||
if (res.ok) return res.json();
|
||||
const fallback = await fetch(`${API}/api/users/${id}`);
|
||||
if (fallback.ok) return fallback.json();
|
||||
const roleRes = await fetch(`${API}/api/${apiSlug}/${id}`);
|
||||
if (!roleRes.ok) return null;
|
||||
return roleRes.json();
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic admin detail/drill-down page shared by every profession's "View Profile"
|
||||
* link. Modeled on the Photographer detail page's fetch pattern (which already
|
||||
* works via /api/admin/users/{id}) so each profession doesn't need its own
|
||||
* bespoke backend endpoint just to render a read-only snapshot.
|
||||
*/
|
||||
export default function ProfessionDetailPage(props: {
|
||||
roleLabel: string;
|
||||
apiSlug: string;
|
||||
backHref: string;
|
||||
backLabel: string;
|
||||
}) {
|
||||
const params = useParams();
|
||||
const [profile] = createResource(() => params.id, (id) => fetchProfile(id, props.apiSlug));
|
||||
|
||||
const name = createMemo(() => {
|
||||
const p = profile();
|
||||
if (!p) return props.roleLabel;
|
||||
return (
|
||||
p.name ||
|
||||
p.full_name ||
|
||||
[p.first_name, p.last_name].filter(Boolean).join(' ') ||
|
||||
props.roleLabel
|
||||
);
|
||||
});
|
||||
const created = createMemo(() => profile()?.createdAt || profile()?.created_at || '');
|
||||
|
||||
return (
|
||||
<div class="flex flex-col -mx-6 -mt-6 min-h-full">
|
||||
<div class="bg-white border-b border-gray-200 px-6 py-4 flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-xl font-semibold text-gray-900">{props.roleLabel} Detail</h1>
|
||||
<p class="text-sm text-gray-500 mt-0.5">View profile snapshot and account metadata for one {props.roleLabel.toLowerCase()}.</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<A class="rounded-lg border border-gray-200 px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" href={props.backHref}>{props.backLabel}</A>
|
||||
<A class="rounded-lg border border-gray-200 px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" href={`/admin/users/details/${params.id}`}>Open User Detail</A>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-6 flex-1">
|
||||
<Show when={profile.loading}>
|
||||
<div class="rounded-xl border border-gray-200 bg-white shadow-sm"><p class="notice">Loading {props.roleLabel.toLowerCase()}...</p></div>
|
||||
</Show>
|
||||
<Show when={!profile.loading && !profile()}>
|
||||
<div class="rounded-xl border border-gray-200 bg-white shadow-sm"><p class="notice">{props.roleLabel} not found.</p></div>
|
||||
</Show>
|
||||
|
||||
<Show when={profile()}>
|
||||
<section class="rounded-xl border border-gray-200 bg-white shadow-sm">
|
||||
<div class="mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<div class="kv-item">
|
||||
<p class="kv-label">Name</p>
|
||||
<p class="kv-value">{name()}</p>
|
||||
</div>
|
||||
<div class="kv-item">
|
||||
<p class="kv-label">Email</p>
|
||||
<p class="kv-value">{profile()!.email || '—'}</p>
|
||||
</div>
|
||||
<div class="kv-item">
|
||||
<p class="kv-label">Phone</p>
|
||||
<p class="kv-value">{profile()!.phone || profile()!.phoneNumber || '—'}</p>
|
||||
</div>
|
||||
<div class="kv-item">
|
||||
<p class="kv-label">Location</p>
|
||||
<p class="kv-value">{profile()!.location || [profile()!.city, profile()!.state].filter(Boolean).join(', ') || '—'}</p>
|
||||
</div>
|
||||
<div class="kv-item">
|
||||
<p class="kv-label">Gender</p>
|
||||
<p class="kv-value">{profile()!.gender || '—'}</p>
|
||||
</div>
|
||||
<div class="kv-item">
|
||||
<p class="kv-label">Experience</p>
|
||||
<p class="kv-value">{profile()!.experience || '—'}</p>
|
||||
</div>
|
||||
<div class="kv-item">
|
||||
<p class="kv-label">Availability</p>
|
||||
<p class="kv-value">{profile()!.availability || '—'}</p>
|
||||
</div>
|
||||
<div class="kv-item">
|
||||
<p class="kv-label">Permanent Address</p>
|
||||
<p class="kv-value">{profile()!.permanentAddress || '—'}</p>
|
||||
</div>
|
||||
<div class="kv-item">
|
||||
<p class="kv-label">Hometown</p>
|
||||
<p class="kv-value">{profile()!.hometown || '—'}</p>
|
||||
</div>
|
||||
<div class="kv-item">
|
||||
<p class="kv-label">Pincode</p>
|
||||
<p class="kv-value">{profile()!.pincode || '—'}</p>
|
||||
</div>
|
||||
<div class="kv-item">
|
||||
<p class="kv-label">Status</p>
|
||||
<p class="kv-value">{profile()!.status || '—'}</p>
|
||||
</div>
|
||||
<div class="kv-item">
|
||||
<p class="kv-label">Created</p>
|
||||
<p class="kv-value">{created() ? new Date(created()).toLocaleString() : '—'}</p>
|
||||
</div>
|
||||
<div class="kv-item" style="grid-column:1 / -1">
|
||||
<p class="kv-label">Portfolio</p>
|
||||
<p class="kv-value">{profile()!.portfolio_url || '—'}</p>
|
||||
</div>
|
||||
<Show when={profile()!.languages && profile()!.languages!.length > 0}>
|
||||
<div class="kv-item" style="grid-column:1 / -1">
|
||||
<p class="kv-label">Languages</p>
|
||||
<ul class="notice" style="margin:8px 0 0;padding-left:18px">
|
||||
<For each={profile()!.languages!}>
|
||||
{(item) => <li>{item.language || 'Unknown'} - {item.proficiency || 'N/A'}</li>}
|
||||
</For>
|
||||
</ul>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</section>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
12
src/routes/admin/catering-services/[id].tsx
Normal file
12
src/routes/admin/catering-services/[id].tsx
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import ProfessionDetailPage from '~/components/admin/ProfessionDetailPage';
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<ProfessionDetailPage
|
||||
roleLabel="Catering Service"
|
||||
apiSlug="catering-services"
|
||||
backHref="/admin/catering-services"
|
||||
backLabel="Back to Catering Services List"
|
||||
/>
|
||||
);
|
||||
}
|
||||
12
src/routes/admin/developers/[id].tsx
Normal file
12
src/routes/admin/developers/[id].tsx
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import ProfessionDetailPage from '~/components/admin/ProfessionDetailPage';
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<ProfessionDetailPage
|
||||
roleLabel="Developer"
|
||||
apiSlug="developers"
|
||||
backHref="/admin/developers"
|
||||
backLabel="Back to Developers List"
|
||||
/>
|
||||
);
|
||||
}
|
||||
12
src/routes/admin/fitness-trainers/[id].tsx
Normal file
12
src/routes/admin/fitness-trainers/[id].tsx
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import ProfessionDetailPage from '~/components/admin/ProfessionDetailPage';
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<ProfessionDetailPage
|
||||
roleLabel="Fitness Trainer"
|
||||
apiSlug="fitness-trainers"
|
||||
backHref="/admin/fitness-trainers"
|
||||
backLabel="Back to Fitness Trainers List"
|
||||
/>
|
||||
);
|
||||
}
|
||||
12
src/routes/admin/graphic-designers/[id].tsx
Normal file
12
src/routes/admin/graphic-designers/[id].tsx
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import ProfessionDetailPage from '~/components/admin/ProfessionDetailPage';
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<ProfessionDetailPage
|
||||
roleLabel="Graphic Designer"
|
||||
apiSlug="graphic-designers"
|
||||
backHref="/admin/graphic-designers"
|
||||
backLabel="Back to Graphic Designers List"
|
||||
/>
|
||||
);
|
||||
}
|
||||
12
src/routes/admin/makeup-artist/[id].tsx
Normal file
12
src/routes/admin/makeup-artist/[id].tsx
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import ProfessionDetailPage from '~/components/admin/ProfessionDetailPage';
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<ProfessionDetailPage
|
||||
roleLabel="Makeup Artist"
|
||||
apiSlug="makeup-artists"
|
||||
backHref="/admin/makeup-artist"
|
||||
backLabel="Back to Makeup Artist List"
|
||||
/>
|
||||
);
|
||||
}
|
||||
12
src/routes/admin/social-media-managers/[id].tsx
Normal file
12
src/routes/admin/social-media-managers/[id].tsx
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import ProfessionDetailPage from '~/components/admin/ProfessionDetailPage';
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<ProfessionDetailPage
|
||||
roleLabel="Social Media Manager"
|
||||
apiSlug="social-media-managers"
|
||||
backHref="/admin/social-media-managers"
|
||||
backLabel="Back to Social Media Managers List"
|
||||
/>
|
||||
);
|
||||
}
|
||||
12
src/routes/admin/tutors/[id].tsx
Normal file
12
src/routes/admin/tutors/[id].tsx
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import ProfessionDetailPage from '~/components/admin/ProfessionDetailPage';
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<ProfessionDetailPage
|
||||
roleLabel="Tutor"
|
||||
apiSlug="tutors"
|
||||
backHref="/admin/tutors"
|
||||
backLabel="Back to Tutors List"
|
||||
/>
|
||||
);
|
||||
}
|
||||
12
src/routes/admin/video-editors/[id].tsx
Normal file
12
src/routes/admin/video-editors/[id].tsx
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import ProfessionDetailPage from '~/components/admin/ProfessionDetailPage';
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<ProfessionDetailPage
|
||||
roleLabel="Video Editor"
|
||||
apiSlug="video-editors"
|
||||
backHref="/admin/video-editors"
|
||||
backLabel="Back to Video Editors List"
|
||||
/>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue