Prettier format pending files

This commit is contained in:
Builder.io 2025-11-13 03:24:02 +00:00
parent 355d2319dc
commit 381a7dff27
15 changed files with 266 additions and 135 deletions

View file

@ -256,7 +256,10 @@ const App = () => (
<Route path="/web3-callback" element={<Web3Callback />} />
<Route path="/discord-verify" element={<DiscordVerify />} />
<Route path="/discord" element={<DiscordActivity />} />
<Route path="/discord/callback" element={<DiscordOAuthCallback />} />
<Route
path="/discord/callback"
element={<DiscordOAuthCallback />}
/>
{/* Creator Network routes */}
<Route path="/creators" element={<CreatorDirectory />} />

View file

@ -93,7 +93,9 @@ export default function AdminBlogManager() {
const handleDeleteBlogPost = useCallback(async (slug: string) => {
setDeleting(slug);
try {
const res = await fetch(`${API_BASE}/api/blog/${slug}`, { method: "DELETE" });
const res = await fetch(`${API_BASE}/api/blog/${slug}`, {
method: "DELETE",
});
if (res.ok) {
setBlogPosts((posts) => posts.filter((p) => p.slug !== slug));
aethexToast.success({

View file

@ -157,9 +157,12 @@ export function AdminDiscordManagement() {
const handleDeleteMapping = async (id: string) => {
try {
const response = await fetch(`${API_BASE}/api/discord/role-mappings?id=${id}`, {
method: "DELETE",
});
const response = await fetch(
`${API_BASE}/api/discord/role-mappings?id=${id}`,
{
method: "DELETE",
},
);
if (!response.ok) throw new Error("Failed to delete mapping");
setMappings(mappings.filter((m) => m.id !== id));
@ -193,13 +196,16 @@ export function AdminDiscordManagement() {
console.log("[Discord] Registering commands with token...");
const response = await fetch(`${API_BASE}/api/discord/admin-register-commands`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${adminToken}`,
const response = await fetch(
`${API_BASE}/api/discord/admin-register-commands`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${adminToken}`,
},
},
});
);
console.log("[Discord] Response status:", response.status);

View file

@ -126,7 +126,9 @@ export default function AdminFoundationManager() {
const fetchAchievements = async () => {
try {
setLoadingAchievements(true);
const response = await fetch(`${API_BASE}/api/admin/foundation/achievements`);
const response = await fetch(
`${API_BASE}/api/admin/foundation/achievements`,
);
if (!response.ok) throw new Error("Failed to fetch achievements");
const data = await response.json();
setAchievements(data || []);

View file

@ -216,9 +216,12 @@ export default function AdminStaffDirectory() {
try {
setIsDeleting(true);
const response = await fetch(`${API_BASE}/api/staff/members-detail?id=${memberId}`, {
method: "DELETE",
});
const response = await fetch(
`${API_BASE}/api/staff/members-detail?id=${memberId}`,
{
method: "DELETE",
},
);
const result = await response.json();

View file

@ -506,11 +506,14 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({
) {
try {
// Check if this email is linked to another account
const response = await fetch(`${API_BASE}/api/user/resolve-linked-email`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email }),
});
const response = await fetch(
`${API_BASE}/api/user/resolve-linked-email`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email }),
},
);
if (response.ok) {
const { primaryEmail } = await response.json();

View file

@ -259,7 +259,9 @@ export const aethexSocialService = {
async listMentorshipRequests(userId: string, role?: "mentor" | "mentee") {
const qs = new URLSearchParams({ user_id: userId });
if (role) qs.set("role", role);
const resp = await fetch(`${API_BASE}/api/mentorship/requests?${qs.toString()}`);
const resp = await fetch(
`${API_BASE}/api/mentorship/requests?${qs.toString()}`,
);
if (!resp.ok) return [] as any[];
return (await resp.json()) as any[];
},

View file

@ -28,44 +28,46 @@ export default function BlogPost() {
try {
if (!slug) return;
// Primary: try server API
let res = await fetch(`${API_BASE}/api/blog/${encodeURIComponent(slug)}`);
let res = await fetch(
`${API_BASE}/api/blog/${encodeURIComponent(slug)}`,
);
let data: any = null;
try {
// Attempt to parse JSON response from server route
if (res.ok) data = await res.json();
} catch (e) {
// If server returned HTML (dev server) or invalid JSON, fall back to Supabase REST
try {
const sbUrl = import.meta.env.VITE_SUPABASE_URL;
const sbKey = import.meta.env.VITE_SUPABASE_ANON_KEY;
if (sbUrl && sbKey) {
const url = `${sbUrl.replace(/\/$/, "")}/rest/v1/blog_posts?slug=eq.${encodeURIComponent(
String(slug),
)}&select=id,slug,title,excerpt,author,date,read_time,category,image,body_html,published_at`;
const sbRes = await fetch(url, {
headers: {
apikey: sbKey as string,
Authorization: `Bearer ${sbKey}`,
},
});
if (sbRes.ok) {
const arr = await sbRes.json();
data = Array.isArray(arr) && arr.length ? arr[0] : null;
// Attempt to parse JSON response from server route
if (res.ok) data = await res.json();
} catch (e) {
// If server returned HTML (dev server) or invalid JSON, fall back to Supabase REST
try {
const sbUrl = import.meta.env.VITE_SUPABASE_URL;
const sbKey = import.meta.env.VITE_SUPABASE_ANON_KEY;
if (sbUrl && sbKey) {
const url = `${sbUrl.replace(/\/$/, "")}/rest/v1/blog_posts?slug=eq.${encodeURIComponent(
String(slug),
)}&select=id,slug,title,excerpt,author,date,read_time,category,image,body_html,published_at`;
const sbRes = await fetch(url, {
headers: {
apikey: sbKey as string,
Authorization: `Bearer ${sbKey}`,
},
});
if (sbRes.ok) {
const arr = await sbRes.json();
data = Array.isArray(arr) && arr.length ? arr[0] : null;
}
}
} catch (err) {
console.warn("Supabase fallback fetch failed:", err);
}
} catch (err) {
console.warn("Supabase fallback fetch failed:", err);
}
}
// If API and Supabase both fail, try seed data
if (!data) {
const seedPost = blogSeedPosts.find((p) => p.slug === slug);
if (seedPost) {
data = seedPost;
// If API and Supabase both fail, try seed data
if (!data) {
const seedPost = blogSeedPosts.find((p) => p.slug === slug);
if (seedPost) {
data = seedPost;
}
}
}
if (!cancelled) setPost(data);
} catch (e) {

View file

@ -1132,9 +1132,12 @@ export default function Dashboard() {
onChange={async (e) => {
const ensureBuckets = async () => {
try {
await fetch(`${API_BASE}/api/storage/ensure-buckets`, {
method: "POST",
});
await fetch(
`${API_BASE}/api/storage/ensure-buckets`,
{
method: "POST",
},
);
} catch {}
};
const file = e.target.files?.[0];
@ -1210,9 +1213,12 @@ export default function Dashboard() {
onChange={async (e) => {
const ensureBuckets = async () => {
try {
await fetch(`${API_BASE}/api/storage/ensure-buckets`, {
method: "POST",
});
await fetch(
`${API_BASE}/api/storage/ensure-buckets`,
{
method: "POST",
},
);
} catch {}
};
const file = e.target.files?.[0];

View file

@ -1,7 +1,13 @@
import { useEffect, useState } from "react";
import { useAuth } from "@/contexts/AuthContext";
import { useAethexToast } from "@/hooks/use-aethex-toast";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
@ -50,10 +56,13 @@ export default function AdminEthosVerification() {
const [requests, setRequests] = useState<VerificationRequest[]>([]);
const [loading, setLoading] = useState(true);
const [activeTab, setActiveTab] = useState("pending");
const [selectedRequest, setSelectedRequest] = useState<VerificationRequest | null>(null);
const [selectedRequest, setSelectedRequest] =
useState<VerificationRequest | null>(null);
const [rejectionReason, setRejectionReason] = useState("");
const [isConfirming, setIsConfirming] = useState(false);
const [confirmAction, setConfirmAction] = useState<"approve" | "reject" | null>(null);
const [confirmAction, setConfirmAction] = useState<
"approve" | "reject" | null
>(null);
useEffect(() => {
fetchRequests();
@ -62,11 +71,14 @@ export default function AdminEthosVerification() {
const fetchRequests = async () => {
try {
setLoading(true);
const response = await fetch(`${API_BASE}/api/ethos/verification?status=${activeTab}`, {
headers: {
"x-user-id": user?.id || "",
const response = await fetch(
`${API_BASE}/api/ethos/verification?status=${activeTab}`,
{
headers: {
"x-user-id": user?.id || "",
},
},
});
);
if (!response.ok) throw new Error("Failed to fetch requests");
@ -152,7 +164,9 @@ export default function AdminEthosVerification() {
<Music className="w-8 h-8 text-pink-500" />
Ethos Guild Artist Verification
</h1>
<p className="text-gray-400">Manage artist verification applications and approve verified creators</p>
<p className="text-gray-400">
Manage artist verification applications and approve verified creators
</p>
</div>
{/* Stats Cards */}
@ -162,8 +176,12 @@ export default function AdminEthosVerification() {
<CardTitle className="text-lg">Pending</CardTitle>
</CardHeader>
<CardContent>
<div className="text-3xl font-bold text-yellow-500">{stats.pending}</div>
<p className="text-sm text-gray-400">Applications awaiting review</p>
<div className="text-3xl font-bold text-yellow-500">
{stats.pending}
</div>
<p className="text-sm text-gray-400">
Applications awaiting review
</p>
</CardContent>
</Card>
@ -172,7 +190,9 @@ export default function AdminEthosVerification() {
<CardTitle className="text-lg">Approved</CardTitle>
</CardHeader>
<CardContent>
<div className="text-3xl font-bold text-green-500">{stats.approved}</div>
<div className="text-3xl font-bold text-green-500">
{stats.approved}
</div>
<p className="text-sm text-gray-400">Verified artists</p>
</CardContent>
</Card>
@ -182,7 +202,9 @@ export default function AdminEthosVerification() {
<CardTitle className="text-lg">Rejected</CardTitle>
</CardHeader>
<CardContent>
<div className="text-3xl font-bold text-red-500">{stats.rejected}</div>
<div className="text-3xl font-bold text-red-500">
{stats.rejected}
</div>
<p className="text-sm text-gray-400">Declined applications</p>
</CardContent>
</Card>
@ -192,21 +214,33 @@ export default function AdminEthosVerification() {
<Card>
<CardHeader>
<CardTitle>Verification Requests</CardTitle>
<CardDescription>Review and approve artist applications</CardDescription>
<CardDescription>
Review and approve artist applications
</CardDescription>
</CardHeader>
<CardContent>
<Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList className="mb-6">
<TabsTrigger value="pending">Pending ({stats.pending})</TabsTrigger>
<TabsTrigger value="approved">Approved ({stats.approved})</TabsTrigger>
<TabsTrigger value="rejected">Rejected ({stats.rejected})</TabsTrigger>
<TabsTrigger value="pending">
Pending ({stats.pending})
</TabsTrigger>
<TabsTrigger value="approved">
Approved ({stats.approved})
</TabsTrigger>
<TabsTrigger value="rejected">
Rejected ({stats.rejected})
</TabsTrigger>
</TabsList>
<TabsContent value={activeTab} className="space-y-4">
{loading ? (
<div className="text-center py-8 text-gray-400">Loading requests...</div>
<div className="text-center py-8 text-gray-400">
Loading requests...
</div>
) : requests.length === 0 ? (
<div className="text-center py-8 text-gray-400">No {activeTab} verification requests</div>
<div className="text-center py-8 text-gray-400">
No {activeTab} verification requests
</div>
) : (
requests.map((request) => (
<VerificationRequestCard
@ -237,7 +271,9 @@ export default function AdminEthosVerification() {
{confirmAction === "reject" && (
<div className="mt-4">
<label className="block text-sm font-medium mb-2">Rejection Reason (optional)</label>
<label className="block text-sm font-medium mb-2">
Rejection Reason (optional)
</label>
<Textarea
placeholder="Provide feedback to help them improve their application..."
value={rejectionReason}
@ -301,14 +337,23 @@ function VerificationRequestCard({
)}
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
<h3 className="font-semibold">{request.user_profiles?.full_name}</h3>
<Badge variant="outline" className="text-xs flex items-center gap-1">
<h3 className="font-semibold">
{request.user_profiles?.full_name}
</h3>
<Badge
variant="outline"
className="text-xs flex items-center gap-1"
>
{getStatusIcon(request.status)}
{request.status}
</Badge>
</div>
<p className="text-sm text-gray-400">{request.user_profiles?.email}</p>
<p className="text-xs text-gray-500 mt-1">Applied: {formattedDate}</p>
<p className="text-sm text-gray-400">
{request.user_profiles?.email}
</p>
<p className="text-xs text-gray-500 mt-1">
Applied: {formattedDate}
</p>
</div>
</div>
@ -323,7 +368,11 @@ function VerificationRequestCard({
<XCircle className="w-4 h-4 mr-1" />
Reject
</Button>
<Button size="sm" className="bg-green-600 hover:bg-green-700" onClick={() => onApprove(request)}>
<Button
size="sm"
className="bg-green-600 hover:bg-green-700"
onClick={() => onApprove(request)}
>
<CheckCircle className="w-4 h-4 mr-1" />
Verify
</Button>
@ -335,7 +384,9 @@ function VerificationRequestCard({
{request.ethos_artist_profiles?.bio && (
<div>
<p className="text-xs font-medium text-gray-400 mb-1">Bio</p>
<p className="text-sm text-gray-300">{request.ethos_artist_profiles.bio}</p>
<p className="text-sm text-gray-300">
{request.ethos_artist_profiles.bio}
</p>
</div>
)}
@ -354,14 +405,18 @@ function VerificationRequestCard({
{request.submission_notes && (
<div>
<p className="text-xs font-medium text-gray-400 mb-1">Application Notes</p>
<p className="text-xs font-medium text-gray-400 mb-1">
Application Notes
</p>
<p className="text-sm text-gray-300">{request.submission_notes}</p>
</div>
)}
{request.portfolio_links && request.portfolio_links.length > 0 && (
<div>
<p className="text-xs font-medium text-gray-400 mb-2">Portfolio Links</p>
<p className="text-xs font-medium text-gray-400 mb-2">
Portfolio Links
</p>
<div className="flex flex-wrap gap-2">
{request.portfolio_links.map((link, idx) => (
<a
@ -381,7 +436,9 @@ function VerificationRequestCard({
{request.rejection_reason && request.status === "rejected" && (
<div className="mt-4 p-3 bg-red-500/10 border border-red-500/20 rounded">
<p className="text-xs font-medium text-red-400 mb-1">Rejection Reason</p>
<p className="text-xs font-medium text-red-400 mb-1">
Rejection Reason
</p>
<p className="text-sm text-red-300">{request.rejection_reason}</p>
</div>
)}

View file

@ -68,11 +68,19 @@ export default function ArtistProfile() {
}, [userId]);
if (loading) {
return <Layout><div className="py-20 text-center">Loading artist profile...</div></Layout>;
return (
<Layout>
<div className="py-20 text-center">Loading artist profile...</div>
</Layout>
);
}
if (!artist) {
return <Layout><div className="py-20 text-center">Artist not found</div></Layout>;
return (
<Layout>
<div className="py-20 text-center">Artist not found</div>
</Layout>
);
}
const memberSince = new Date(artist.created_at).toLocaleDateString("en-US", {
@ -130,7 +138,9 @@ export default function ArtistProfile() {
</div>
<div>
<p className="text-slate-500">Member Since</p>
<p className="text-xl font-bold text-white">{memberSince}</p>
<p className="text-xl font-bold text-white">
{memberSince}
</p>
</div>
</div>
@ -235,7 +245,9 @@ export default function ArtistProfile() {
>
<CardContent className="p-4 flex items-center justify-between">
<div className="flex-1">
<h3 className="text-white font-semibold">{track.title}</h3>
<h3 className="text-white font-semibold">
{track.title}
</h3>
<div className="flex gap-2 mt-1">
{track.genre.map((g) => (
<Badge

View file

@ -118,9 +118,12 @@ export default function ArtistSettings() {
}
// Fetch verification status
const verRes = await fetch(`${API_BASE}/api/ethos/verification?status=pending`, {
headers: { "x-user-id": user.id },
});
const verRes = await fetch(
`${API_BASE}/api/ethos/verification?status=pending`,
{
headers: { "x-user-id": user.id },
},
);
if (verRes.ok) {
const { data: requests } = await verRes.json();

View file

@ -11,12 +11,7 @@ import {
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from "@/components/ui/tabs";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { useAuth } from "@/contexts/AuthContext";
import { useAethexToast } from "@/hooks/use-aethex-toast";
import { CheckCircle2, Clock, FileText, AlertCircle } from "lucide-react";
@ -84,20 +79,21 @@ export default function LicensingDashboard() {
const handleApprove = async (id: string) => {
try {
const res = await fetch(`${API_BASE}/api/ethos/licensing-agreements?id=${id}`, {
method: "PUT",
headers: {
"x-user-id": user!.id,
"Content-Type": "application/json",
const res = await fetch(
`${API_BASE}/api/ethos/licensing-agreements?id=${id}`,
{
method: "PUT",
headers: {
"x-user-id": user!.id,
"Content-Type": "application/json",
},
body: JSON.stringify({ approved: true }),
},
body: JSON.stringify({ approved: true }),
});
);
if (res.ok) {
setAgreements((prev) =>
prev.map((a) =>
a.id === id ? { ...a, approved: true } : a,
),
prev.map((a) => (a.id === id ? { ...a, approved: true } : a)),
);
toast.success({
title: "Agreement approved",
@ -134,7 +130,11 @@ export default function LicensingDashboard() {
};
if (loading) {
return <Layout><div className="py-20 text-center">Loading agreements...</div></Layout>;
return (
<Layout>
<div className="py-20 text-center">Loading agreements...</div>
</Layout>
);
}
const pendingCount = agreements.filter((a) => !a.approved).length;
@ -172,7 +172,9 @@ export default function LicensingDashboard() {
<Card className="bg-slate-900/50 border-slate-800">
<CardContent className="pt-6">
<p className="text-slate-400 text-xs uppercase mb-2">Pending</p>
<p className="text-slate-400 text-xs uppercase mb-2">
Pending
</p>
<p className="text-2xl font-bold text-yellow-400">
{pendingCount}
</p>
@ -181,7 +183,9 @@ export default function LicensingDashboard() {
<Card className="bg-slate-900/50 border-slate-800">
<CardContent className="pt-6">
<p className="text-slate-400 text-xs uppercase mb-2">Approved</p>
<p className="text-slate-400 text-xs uppercase mb-2">
Approved
</p>
<p className="text-2xl font-bold text-green-400">
{approvedCount}
</p>
@ -287,11 +291,7 @@ interface AgreementCardProps {
onDelete: () => void;
}
function AgreementCard({
agreement,
onApprove,
onDelete,
}: AgreementCardProps) {
function AgreementCard({ agreement, onApprove, onDelete }: AgreementCardProps) {
const formatDate = (date: string) => {
return new Date(date).toLocaleDateString("en-US", {
year: "numeric",
@ -300,11 +300,12 @@ function AgreementCard({
});
};
const licenseTypeLabel = {
commercial_one_time: "One-time License",
commercial_exclusive: "Exclusive License",
broadcast: "Broadcast License",
}[agreement.license_type] || agreement.license_type;
const licenseTypeLabel =
{
commercial_one_time: "One-time License",
commercial_exclusive: "Exclusive License",
broadcast: "Broadcast License",
}[agreement.license_type] || agreement.license_type;
return (
<Card className="bg-slate-900/50 border-slate-800">
@ -347,7 +348,9 @@ function AgreementCard({
{agreement.expires_at && (
<div>
<p className="text-slate-500">Expires</p>
<p className="text-white">{formatDate(agreement.expires_at)}</p>
<p className="text-white">
{formatDate(agreement.expires_at)}
</p>
</div>
)}
</div>
@ -360,7 +363,11 @@ function AgreementCard({
asChild
className="border-slate-700"
>
<a href={agreement.agreement_url} target="_blank" rel="noreferrer">
<a
href={agreement.agreement_url}
target="_blank"
rel="noreferrer"
>
View Contract
</a>
</Button>

View file

@ -69,15 +69,21 @@ export default function TrackLibrary() {
params.append("limit", "100");
if (searchQuery) params.append("search", searchQuery);
if (selectedGenre !== "All Genres") params.append("genre", selectedGenre);
if (licenseFilter !== "all") params.append("licenseType", licenseFilter);
if (selectedGenre !== "All Genres")
params.append("genre", selectedGenre);
if (licenseFilter !== "all")
params.append("licenseType", licenseFilter);
const res = await fetch(`${API_BASE}/api/ethos/tracks?${params}`);
const { data } = await res.json();
let sorted = [...data];
if (sortBy === "newest") {
sorted.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
sorted.sort(
(a, b) =>
new Date(b.created_at).getTime() -
new Date(a.created_at).getTime(),
);
} else if (sortBy === "popular") {
sorted.sort((a, b) => b.download_count - a.download_count);
}
@ -122,8 +128,9 @@ export default function TrackLibrary() {
Discover Ethos Music & SFX
</h1>
<p className="text-lg text-slate-400 max-w-2xl">
Browse original music and sound effects created by Ethos Guild artists.
Use freely in your projects or license commercially.
Browse original music and sound effects created by Ethos
Guild artists. Use freely in your projects or license
commercially.
</p>
</div>
@ -144,7 +151,10 @@ export default function TrackLibrary() {
<label className="text-xs uppercase text-slate-500 mb-2 block">
Genre
</label>
<Select value={selectedGenre} onValueChange={setSelectedGenre}>
<Select
value={selectedGenre}
onValueChange={setSelectedGenre}
>
<SelectTrigger className="bg-slate-800 border-slate-700">
<SelectValue />
</SelectTrigger>
@ -162,14 +172,21 @@ export default function TrackLibrary() {
<label className="text-xs uppercase text-slate-500 mb-2 block">
License Type
</label>
<Select value={licenseFilter} onValueChange={setLicenseFilter}>
<Select
value={licenseFilter}
onValueChange={setLicenseFilter}
>
<SelectTrigger className="bg-slate-800 border-slate-700">
<SelectValue />
</SelectTrigger>
<SelectContent className="bg-slate-800 border-slate-700">
<SelectItem value="all">All Licenses</SelectItem>
<SelectItem value="ecosystem">Ecosystem Free</SelectItem>
<SelectItem value="commercial_sample">Commercial Demo</SelectItem>
<SelectItem value="ecosystem">
Ecosystem Free
</SelectItem>
<SelectItem value="commercial_sample">
Commercial Demo
</SelectItem>
</SelectContent>
</Select>
</div>
@ -207,7 +224,9 @@ export default function TrackLibrary() {
) : tracks.length === 0 ? (
<div className="text-center py-12">
<Music className="h-12 w-12 text-slate-600 mx-auto mb-4" />
<p className="text-slate-400">No tracks found. Try adjusting your filters.</p>
<p className="text-slate-400">
No tracks found. Try adjusting your filters.
</p>
</div>
) : (
<div className="grid gap-4">
@ -241,7 +260,9 @@ export default function TrackLibrary() {
: "bg-blue-500/10 border-blue-500/30"
}
>
{track.license_type === "ecosystem" ? "Free" : "Commercial"}
{track.license_type === "ecosystem"
? "Free"
: "Commercial"}
</Badge>
</div>

View file

@ -72,7 +72,9 @@ export default function FoundationCurriculum() {
if (selectedCategory) params.set("category", selectedCategory);
if (selectedDifficulty) params.set("difficulty", selectedDifficulty);
const response = await fetch(`${API_BASE}/api/foundation/courses?${params}`);
const response = await fetch(
`${API_BASE}/api/foundation/courses?${params}`,
);
if (!response.ok) throw new Error("Failed to fetch courses");
let data = await response.json();