Prettier format pending files

This commit is contained in:
Builder.io 2025-11-12 02:57:31 +00:00
parent a057dacf56
commit 669ab329a8
10 changed files with 325 additions and 118 deletions

View file

@ -13,7 +13,9 @@ export default async function handler(req: any, res: any) {
const artistId = query.artist_id; const artistId = query.artist_id;
if (!artistId) { if (!artistId) {
return res.status(400).json({ error: "artist_id query parameter is required" }); return res
.status(400)
.json({ error: "artist_id query parameter is required" });
} }
const { data: artist, error: artistError } = await supabase const { data: artist, error: artistError } = await supabase
@ -39,7 +41,9 @@ export default async function handler(req: any, res: any) {
if (artistError && artistError.code !== "PGRST116") throw artistError; if (artistError && artistError.code !== "PGRST116") throw artistError;
if (!artist || !artist.for_hire) { if (!artist || !artist.for_hire) {
return res.status(404).json({ error: "Artist not found or not available for hire" }); return res
.status(404)
.json({ error: "Artist not found or not available for hire" });
} }
return res.json({ return res.json({

View file

@ -29,7 +29,8 @@ export default async function handler(req: any, res: any) {
if (!artist_id || !service_type || !description) { if (!artist_id || !service_type || !description) {
return res.status(400).json({ return res.status(400).json({
error: "Missing required fields: artist_id, service_type, description", error:
"Missing required fields: artist_id, service_type, description",
}); });
} }
@ -41,7 +42,9 @@ export default async function handler(req: any, res: any) {
.single(); .single();
if (artistError || !artist || !artist.for_hire) { if (artistError || !artist || !artist.for_hire) {
return res.status(404).json({ error: "Artist not found or not available for hire" }); return res
.status(404)
.json({ error: "Artist not found or not available for hire" });
} }
// Create service request // Create service request
@ -96,7 +99,9 @@ export default async function handler(req: any, res: any) {
if (requester_id) dbQuery = dbQuery.eq("requester_id", requester_id); if (requester_id) dbQuery = dbQuery.eq("requester_id", requester_id);
if (status) dbQuery = dbQuery.eq("status", status); if (status) dbQuery = dbQuery.eq("status", status);
const { data, error } = await dbQuery.order("created_at", { ascending: false }); const { data, error } = await dbQuery.order("created_at", {
ascending: false,
});
if (error) throw error; if (error) throw error;
return res.json({ data }); return res.json({ data });
@ -109,7 +114,9 @@ export default async function handler(req: any, res: any) {
const { status, notes } = body; const { status, notes } = body;
if (!id || !status) { if (!id || !status) {
return res.status(400).json({ error: "Missing required fields: id, status" }); return res
.status(400)
.json({ error: "Missing required fields: id, status" });
} }
const { data, error } = await supabase const { data, error } = await supabase

View file

@ -53,10 +53,15 @@ export default async function handler(req: any, res: any) {
if (genre) dbQuery = dbQuery.contains("genre", [genre]); if (genre) dbQuery = dbQuery.contains("genre", [genre]);
if (licenseType) dbQuery = dbQuery.eq("license_type", licenseType); if (licenseType) dbQuery = dbQuery.eq("license_type", licenseType);
if (search) dbQuery = dbQuery.or(`title.ilike.%${search}%,description.ilike.%${search}%`); if (search)
dbQuery = dbQuery.or(
`title.ilike.%${search}%,description.ilike.%${search}%`,
);
const { data, error, count } = await dbQuery const { data, error, count } = await dbQuery.range(
.range(Number(offset), Number(offset) + Number(limit) - 1); Number(offset),
Number(offset) + Number(limit) - 1,
);
if (error) throw error; if (error) throw error;

View file

@ -45,7 +45,8 @@ export default function EcosystemLicenseModal({
🎵 Welcome to the Ethos Library 🎵 Welcome to the Ethos Library
</DialogTitle> </DialogTitle>
<DialogDescription className="text-slate-400"> <DialogDescription className="text-slate-400">
Before you upload your first track, please review and accept the Ecosystem License Before you upload your first track, please review and accept the
Ecosystem License
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
@ -54,23 +55,28 @@ export default function EcosystemLicenseModal({
<Alert className="bg-blue-500/10 border-blue-500/30"> <Alert className="bg-blue-500/10 border-blue-500/30">
<AlertCircle className="h-4 w-4 text-blue-400" /> <AlertCircle className="h-4 w-4 text-blue-400" />
<AlertDescription className="text-blue-300"> <AlertDescription className="text-blue-300">
This is a one-time agreement. By contributing to the Ethos Library, you're helping This is a one-time agreement. By contributing to the Ethos
build a vibrant community of creators. We're transparent about how your work will be Library, you're helping build a vibrant community of creators.
used. We're transparent about how your work will be used.
</AlertDescription> </AlertDescription>
</Alert> </Alert>
{/* License Terms Section */} {/* License Terms Section */}
<div className="space-y-3"> <div className="space-y-3">
<h3 className="font-semibold text-white">KND-008: AeThex Ecosystem License</h3> <h3 className="font-semibold text-white">
KND-008: AeThex Ecosystem License
</h3>
<div className="bg-slate-800/50 border border-slate-700 rounded-lg p-4 space-y-3 text-sm text-slate-300"> <div className="bg-slate-800/50 border border-slate-700 rounded-lg p-4 space-y-3 text-sm text-slate-300">
<p> <p>
<strong className="text-white">What is the Ecosystem License?</strong> <strong className="text-white">
What is the Ecosystem License?
</strong>
</p> </p>
<p> <p>
The Ecosystem License allows AeThex development teams (specifically our GameForge The Ecosystem License allows AeThex development teams
arm) to use your track for free in non-commercial projects. This includes: (specifically our GameForge arm) to use your track for free in
non-commercial projects. This includes:
</p> </p>
<ul className="list-disc list-inside space-y-2 ml-2"> <ul className="list-disc list-inside space-y-2 ml-2">
@ -86,7 +92,9 @@ export default function EcosystemLicenseModal({
<ul className="list-disc list-inside space-y-2 ml-2"> <ul className="list-disc list-inside space-y-2 ml-2">
<li>100% ownership of your music</li> <li>100% ownership of your music</li>
<li>Full credit and attribution</li> <li>Full credit and attribution</li>
<li>Right to license commercially (outside AeThex ecosystem)</li> <li>
Right to license commercially (outside AeThex ecosystem)
</li>
<li>Ability to use elsewhere without restriction</li> <li>Ability to use elsewhere without restriction</li>
</ul> </ul>
@ -94,24 +102,28 @@ export default function EcosystemLicenseModal({
<strong className="text-white">Commercial Use:</strong> <strong className="text-white">Commercial Use:</strong>
</p> </p>
<p> <p>
If you want to license your track for commercial use (outside our ecosystem), you If you want to license your track for commercial use (outside
can set your own price on the NEXUS marketplace or negotiate directly with clients. our ecosystem), you can set your own price on the NEXUS
marketplace or negotiate directly with clients.
</p> </p>
<p> <p>
<strong className="text-white">Getting Help:</strong> <strong className="text-white">Getting Help:</strong>
</p> </p>
<p> <p>
Our CORP arm can help negotiate high-value commercial licenses and connect you with Our CORP arm can help negotiate high-value commercial licenses
enterprise clients. and connect you with enterprise clients.
</p> </p>
<hr className="border-slate-600 my-4" /> <hr className="border-slate-600 my-4" />
<p className="text-xs text-slate-400"> <p className="text-xs text-slate-400">
Version 1.0 | Effective Date: {new Date().toLocaleDateString()} | For complete Version 1.0 | Effective Date: {new Date().toLocaleDateString()}{" "}
legal terms, see our{" "} | For complete legal terms, see our{" "}
<a href="/docs/legal/knd-008" className="text-pink-400 hover:text-pink-300"> <a
href="/docs/legal/knd-008"
className="text-pink-400 hover:text-pink-300"
>
full agreement full agreement
</a> </a>
</p> </p>
@ -123,7 +135,9 @@ export default function EcosystemLicenseModal({
<label className="flex items-start gap-3 p-3 bg-slate-800/50 rounded-lg border border-slate-700 cursor-pointer hover:bg-slate-800 transition"> <label className="flex items-start gap-3 p-3 bg-slate-800/50 rounded-lg border border-slate-700 cursor-pointer hover:bg-slate-800 transition">
<Checkbox <Checkbox
checked={hasReadTerms} checked={hasReadTerms}
onCheckedChange={(checked) => setHasReadTerms(checked as boolean)} onCheckedChange={(checked) =>
setHasReadTerms(checked as boolean)
}
className="mt-1 border-slate-600" className="mt-1 border-slate-600"
/> />
<span className="text-sm text-slate-300"> <span className="text-sm text-slate-300">
@ -134,24 +148,28 @@ export default function EcosystemLicenseModal({
<label className="flex items-start gap-3 p-3 bg-slate-800/50 rounded-lg border border-slate-700 cursor-pointer hover:bg-slate-800 transition"> <label className="flex items-start gap-3 p-3 bg-slate-800/50 rounded-lg border border-slate-700 cursor-pointer hover:bg-slate-800 transition">
<Checkbox <Checkbox
checked={agreeToTerms} checked={agreeToTerms}
onCheckedChange={(checked) => setAgreeToTerms(checked as boolean)} onCheckedChange={(checked) =>
setAgreeToTerms(checked as boolean)
}
className="mt-1 border-slate-600" className="mt-1 border-slate-600"
/> />
<span className="text-sm text-slate-300"> <span className="text-sm text-slate-300">
I agree to the KND-008 Ecosystem License terms and allow AeThex to use my music I agree to the KND-008 Ecosystem License terms and allow AeThex
for non-commercial projects to use my music for non-commercial projects
</span> </span>
</label> </label>
<label className="flex items-start gap-3 p-3 bg-slate-800/50 rounded-lg border border-slate-700 cursor-pointer hover:bg-slate-800 transition"> <label className="flex items-start gap-3 p-3 bg-slate-800/50 rounded-lg border border-slate-700 cursor-pointer hover:bg-slate-800 transition">
<Checkbox <Checkbox
checked={confirmOriginal} checked={confirmOriginal}
onCheckedChange={(checked) => setConfirmOriginal(checked as boolean)} onCheckedChange={(checked) =>
setConfirmOriginal(checked as boolean)
}
className="mt-1 border-slate-600" className="mt-1 border-slate-600"
/> />
<span className="text-sm text-slate-300"> <span className="text-sm text-slate-300">
I confirm that this is my original work and I have the right to grant these I confirm that this is my original work and I have the right to
licenses grant these licenses
</span> </span>
</label> </label>
</div> </div>
@ -160,7 +178,9 @@ export default function EcosystemLicenseModal({
{allChecked && ( {allChecked && (
<div className="flex items-center gap-2 p-3 bg-green-500/10 border border-green-500/30 rounded-lg"> <div className="flex items-center gap-2 p-3 bg-green-500/10 border border-green-500/30 rounded-lg">
<CheckCircle2 className="h-4 w-4 text-green-500" /> <CheckCircle2 className="h-4 w-4 text-green-500" />
<span className="text-sm text-green-400">All terms accepted. Ready to continue.</span> <span className="text-sm text-green-400">
All terms accepted. Ready to continue.
</span>
</div> </div>
)} )}

View file

@ -10,7 +10,14 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { Search, Star, Clock, DollarSign, CheckCircle, Music } from "lucide-react"; import {
Search,
Star,
Clock,
DollarSign,
CheckCircle,
Music,
} from "lucide-react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
interface ArtistService { interface ArtistService {
@ -54,7 +61,9 @@ export default function AudioServicesForHire() {
try { try {
setLoading(true); setLoading(true);
// Fetch artists who are for_hire // Fetch artists who are for_hire
const response = await fetch(`/api/ethos/artists?forHire=true&limit=50`); const response = await fetch(
`/api/ethos/artists?forHire=true&limit=50`,
);
if (response.ok) { if (response.ok) {
const result = await response.json(); const result = await response.json();
const artistsData = result.data || result || []; const artistsData = result.data || result || [];
@ -102,7 +111,7 @@ export default function AudioServicesForHire() {
const matchesSkill = const matchesSkill =
!selectedSkill || !selectedSkill ||
artist.skills.some((skill) => artist.skills.some((skill) =>
skill.toLowerCase().includes(selectedSkill.toLowerCase()) skill.toLowerCase().includes(selectedSkill.toLowerCase()),
); );
const matchesService = const matchesService =
@ -116,7 +125,7 @@ export default function AudioServicesForHire() {
// Get all unique skills from artists // Get all unique skills from artists
const allSkills = Array.from( const allSkills = Array.from(
new Set(artists.flatMap((artist) => artist.skills)) new Set(artists.flatMap((artist) => artist.skills)),
).sort(); ).sort();
return ( return (
@ -134,7 +143,10 @@ export default function AudioServicesForHire() {
</div> </div>
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-3"> <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-3">
<Select value={selectedSkill || ""} onValueChange={(val) => setSelectedSkill(val || null)}> <Select
value={selectedSkill || ""}
onValueChange={(val) => setSelectedSkill(val || null)}
>
<SelectTrigger className="bg-slate-800 border-slate-700"> <SelectTrigger className="bg-slate-800 border-slate-700">
<SelectValue placeholder="Skill" /> <SelectValue placeholder="Skill" />
</SelectTrigger> </SelectTrigger>
@ -148,7 +160,10 @@ export default function AudioServicesForHire() {
</SelectContent> </SelectContent>
</Select> </Select>
<Select value={selectedService || ""} onValueChange={(val) => setSelectedService(val || null)}> <Select
value={selectedService || ""}
onValueChange={(val) => setSelectedService(val || null)}
>
<SelectTrigger className="bg-slate-800 border-slate-700"> <SelectTrigger className="bg-slate-800 border-slate-700">
<SelectValue placeholder="Service" /> <SelectValue placeholder="Service" />
</SelectTrigger> </SelectTrigger>
@ -162,7 +177,10 @@ export default function AudioServicesForHire() {
</SelectContent> </SelectContent>
</Select> </Select>
<Select value={minRating.toString()} onValueChange={(val) => setMinRating(Number(val))}> <Select
value={minRating.toString()}
onValueChange={(val) => setMinRating(Number(val))}
>
<SelectTrigger className="bg-slate-800 border-slate-700"> <SelectTrigger className="bg-slate-800 border-slate-700">
<SelectValue placeholder="Min Rating" /> <SelectValue placeholder="Min Rating" />
</SelectTrigger> </SelectTrigger>
@ -191,12 +209,16 @@ export default function AudioServicesForHire() {
{/* Results Count */} {/* Results Count */}
<div className="text-sm text-slate-400"> <div className="text-sm text-slate-400">
{loading ? "Loading..." : `${filteredArtists.length} artist${filteredArtists.length !== 1 ? "s" : ""} available`} {loading
? "Loading..."
: `${filteredArtists.length} artist${filteredArtists.length !== 1 ? "s" : ""} available`}
</div> </div>
{/* Artists Grid */} {/* Artists Grid */}
{loading ? ( {loading ? (
<div className="text-center py-12 text-slate-400">Loading artists...</div> <div className="text-center py-12 text-slate-400">
Loading artists...
</div>
) : filteredArtists.length === 0 ? ( ) : filteredArtists.length === 0 ? (
<div className="text-center py-12 text-slate-400"> <div className="text-center py-12 text-slate-400">
No artists found matching your criteria. Try adjusting your filters. No artists found matching your criteria. Try adjusting your filters.
@ -226,7 +248,9 @@ export default function AudioServicesForHire() {
</CardTitle> </CardTitle>
<div className="flex items-center gap-1 mt-1"> <div className="flex items-center gap-1 mt-1">
<Star className="h-4 w-4 text-yellow-500" /> <Star className="h-4 w-4 text-yellow-500" />
<span className="text-sm text-slate-300">{artist.rating.toFixed(1)}</span> <span className="text-sm text-slate-300">
{artist.rating.toFixed(1)}
</span>
</div> </div>
</div> </div>
</div> </div>
@ -234,7 +258,9 @@ export default function AudioServicesForHire() {
<CardContent className="space-y-4"> <CardContent className="space-y-4">
{/* Bio */} {/* Bio */}
{artist.bio && ( {artist.bio && (
<p className="text-sm text-slate-300 line-clamp-2">{artist.bio}</p> <p className="text-sm text-slate-300 line-clamp-2">
{artist.bio}
</p>
)} )}
{/* Skills */} {/* Skills */}
@ -242,7 +268,11 @@ export default function AudioServicesForHire() {
<p className="text-xs font-medium text-slate-400">Skills</p> <p className="text-xs font-medium text-slate-400">Skills</p>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{artist.skills.slice(0, 3).map((skill) => ( {artist.skills.slice(0, 3).map((skill) => (
<Badge key={skill} variant="secondary" className="text-xs"> <Badge
key={skill}
variant="secondary"
className="text-xs"
>
{skill} {skill}
</Badge> </Badge>
))} ))}

View file

@ -66,7 +66,7 @@ export default function AudioTracksForSale() {
if (selectedLicense) params.append("licenseType", selectedLicense); if (selectedLicense) params.append("licenseType", selectedLicense);
const response = await fetch( const response = await fetch(
`/api/ethos/tracks?${params.toString()}&limit=20` `/api/ethos/tracks?${params.toString()}&limit=20`,
); );
if (response.ok) { if (response.ok) {
const { data } = await response.json(); const { data } = await response.json();
@ -116,7 +116,10 @@ export default function AudioTracksForSale() {
</div> </div>
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-3"> <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-3">
<Select value={selectedGenre || ""} onValueChange={(val) => setSelectedGenre(val || null)}> <Select
value={selectedGenre || ""}
onValueChange={(val) => setSelectedGenre(val || null)}
>
<SelectTrigger className="bg-slate-800 border-slate-700"> <SelectTrigger className="bg-slate-800 border-slate-700">
<SelectValue placeholder="Genre" /> <SelectValue placeholder="Genre" />
</SelectTrigger> </SelectTrigger>
@ -130,7 +133,10 @@ export default function AudioTracksForSale() {
</SelectContent> </SelectContent>
</Select> </Select>
<Select value={selectedLicense || ""} onValueChange={(val) => setSelectedLicense(val || null)}> <Select
value={selectedLicense || ""}
onValueChange={(val) => setSelectedLicense(val || null)}
>
<SelectTrigger className="bg-slate-800 border-slate-700"> <SelectTrigger className="bg-slate-800 border-slate-700">
<SelectValue placeholder="License Type" /> <SelectValue placeholder="License Type" />
</SelectTrigger> </SelectTrigger>
@ -178,7 +184,9 @@ export default function AudioTracksForSale() {
{/* Tracks Grid */} {/* Tracks Grid */}
{loading ? ( {loading ? (
<div className="text-center py-12 text-slate-400">Loading tracks...</div> <div className="text-center py-12 text-slate-400">
Loading tracks...
</div>
) : sortedTracks.length === 0 ? ( ) : sortedTracks.length === 0 ? (
<div className="text-center py-12 text-slate-400"> <div className="text-center py-12 text-slate-400">
No tracks found. Try adjusting your filters. No tracks found. Try adjusting your filters.
@ -239,7 +247,9 @@ export default function AudioTracksForSale() {
<div className="flex items-center justify-between text-sm"> <div className="flex items-center justify-between text-sm">
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<Star className="h-4 w-4 text-yellow-500" /> <Star className="h-4 w-4 text-yellow-500" />
<span className="text-slate-300">{track.rating || 5.0}</span> <span className="text-slate-300">
{track.rating || 5.0}
</span>
</div> </div>
<div className="flex items-center gap-1 text-slate-400"> <div className="flex items-center gap-1 text-slate-400">
<Download className="h-4 w-4" /> <Download className="h-4 w-4" />

View file

@ -224,18 +224,27 @@ export default function Nexus() {
Ethos Guild - Music & Audio Services Ethos Guild - Music & Audio Services
</h2> </h2>
<p className="text-purple-200/70 max-w-2xl mx-auto"> <p className="text-purple-200/70 max-w-2xl mx-auto">
Discover original tracks and hire verified audio artists for composition, SFX design, and sound engineering. Support independent creators and get high-quality audio for your projects. Discover original tracks and hire verified audio artists for
composition, SFX design, and sound engineering. Support
independent creators and get high-quality audio for your
projects.
</p> </p>
</div> </div>
{/* Tabs for Tracks & Services */} {/* Tabs for Tracks & Services */}
<Tabs defaultValue="tracks" className="w-full"> <Tabs defaultValue="tracks" className="w-full">
<TabsList className="mb-8 bg-slate-800/50 border border-slate-700"> <TabsList className="mb-8 bg-slate-800/50 border border-slate-700">
<TabsTrigger value="tracks" className="flex items-center gap-2"> <TabsTrigger
value="tracks"
className="flex items-center gap-2"
>
<Music className="h-4 w-4" /> <Music className="h-4 w-4" />
Tracks for Sale Tracks for Sale
</TabsTrigger> </TabsTrigger>
<TabsTrigger value="artists" className="flex items-center gap-2"> <TabsTrigger
value="artists"
className="flex items-center gap-2"
>
<Users className="h-4 w-4" /> <Users className="h-4 w-4" />
Hire Artists Hire Artists
</TabsTrigger> </TabsTrigger>
@ -248,7 +257,9 @@ export default function Nexus() {
Browse Pre-made Music Browse Pre-made Music
</h3> </h3>
<p className="text-slate-400 text-sm"> <p className="text-slate-400 text-sm">
Find original tracks available under ecosystem licenses (free for non-commercial use) or commercial licenses (for games, films, content). Find original tracks available under ecosystem licenses
(free for non-commercial use) or commercial licenses (for
games, films, content).
</p> </p>
</div> </div>
<AudioTracksForSale /> <AudioTracksForSale />
@ -261,7 +272,10 @@ export default function Nexus() {
Hire Verified Artists Hire Verified Artists
</h3> </h3>
<p className="text-slate-400 text-sm"> <p className="text-slate-400 text-sm">
Work directly with Ethos Guild artists for custom compositions, SFX packs, game scores, and audio production services. Artists set their own prices and maintain full creative control. Work directly with Ethos Guild artists for custom
compositions, SFX packs, game scores, and audio production
services. Artists set their own prices and maintain full
creative control.
</p> </p>
</div> </div>
<AudioServicesForHire /> <AudioServicesForHire />
@ -277,7 +291,8 @@ export default function Nexus() {
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent className="text-sm text-purple-200/70"> <CardContent className="text-sm text-purple-200/70">
Artists keep 80% of licensing revenue. AeThex takes 20% to support the platform and help artists grow. Artists keep 80% of licensing revenue. AeThex takes 20% to
support the platform and help artists grow.
</CardContent> </CardContent>
</Card> </Card>
@ -288,7 +303,8 @@ export default function Nexus() {
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent className="text-sm text-purple-200/70"> <CardContent className="text-sm text-purple-200/70">
Artists retain 100% ownership of their music. License on NEXUS, elsewhere, or both. You decide. Artists retain 100% ownership of their music. License on
NEXUS, elsewhere, or both. You decide.
</CardContent> </CardContent>
</Card> </Card>
@ -299,7 +315,8 @@ export default function Nexus() {
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent className="text-sm text-purple-200/70"> <CardContent className="text-sm text-purple-200/70">
Build your portfolio in the FOUNDATION community before launching on NEXUS. Get mentorship and feedback from peers. Build your portfolio in the FOUNDATION community before
launching on NEXUS. Get mentorship and feedback from peers.
</CardContent> </CardContent>
</Card> </Card>
</div> </div>

View file

@ -22,7 +22,14 @@ import {
} from "@/lib/aethex-database-adapter"; } from "@/lib/aethex-database-adapter";
import { useAuth } from "@/contexts/AuthContext"; import { useAuth } from "@/contexts/AuthContext";
import FourOhFourPage from "@/pages/404"; import FourOhFourPage from "@/pages/404";
import { Clock, Rocket, Target, ExternalLink, Award, Music } from "lucide-react"; import {
Clock,
Rocket,
Target,
ExternalLink,
Award,
Music,
} from "lucide-react";
import { aethexSocialService } from "@/lib/aethex-social-service"; import { aethexSocialService } from "@/lib/aethex-social-service";
interface ProjectPreview { interface ProjectPreview {
@ -231,20 +238,21 @@ const ProfilePassport = () => {
authProfile.username.toLowerCase() === authProfile.username.toLowerCase() ===
resolvedProfile.username.toLowerCase()); resolvedProfile.username.toLowerCase());
const [achievementList, interestList, projectList, ethos] = await Promise.all([ const [achievementList, interestList, projectList, ethos] =
aethexAchievementService await Promise.all([
.getUserAchievements(resolvedId) aethexAchievementService
.catch(() => [] as AethexAchievement[]), .getUserAchievements(resolvedId)
aethexUserService .catch(() => [] as AethexAchievement[]),
.getUserInterests(resolvedId) aethexUserService
.catch(() => [] as string[]), .getUserInterests(resolvedId)
aethexProjectService .catch(() => [] as string[]),
.getUserProjects(resolvedId) aethexProjectService
.catch(() => [] as ProjectPreview[]), .getUserProjects(resolvedId)
fetch(`/api/ethos/artists?id=${resolvedId}`) .catch(() => [] as ProjectPreview[]),
.then(res => res.ok ? res.json() : { tracks: [] }) fetch(`/api/ethos/artists?id=${resolvedId}`)
.catch(() => ({ tracks: [] })), .then((res) => (res.ok ? res.json() : { tracks: [] }))
]); .catch(() => ({ tracks: [] })),
]);
if (cancelled) { if (cancelled) {
return; return;
@ -428,13 +436,21 @@ const ProfilePassport = () => {
</div> </div>
{ethosProfile.skills && ethosProfile.skills.length > 0 && ( {ethosProfile.skills && ethosProfile.skills.length > 0 && (
<div> <div>
<p className="text-xs font-medium text-slate-400 mb-2">Skills</p> <p className="text-xs font-medium text-slate-400 mb-2">
Skills
</p>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{ethosProfile.skills.slice(0, 5).map((skill: string) => ( {ethosProfile.skills
<Badge key={skill} variant="secondary" className="text-xs"> .slice(0, 5)
{skill} .map((skill: string) => (
</Badge> <Badge
))} key={skill}
variant="secondary"
className="text-xs"
>
{skill}
</Badge>
))}
{ethosProfile.skills.length > 5 && ( {ethosProfile.skills.length > 5 && (
<Badge variant="secondary" className="text-xs"> <Badge variant="secondary" className="text-xs">
+{ethosProfile.skills.length - 5} more +{ethosProfile.skills.length - 5} more
@ -454,16 +470,26 @@ const ProfilePassport = () => {
</h3> </h3>
<div className="grid gap-2"> <div className="grid gap-2">
{ethosTracks.slice(0, 5).map((track: any) => ( {ethosTracks.slice(0, 5).map((track: any) => (
<Card key={track.id} className="border border-slate-800 bg-slate-900/50"> <Card
key={track.id}
className="border border-slate-800 bg-slate-900/50"
>
<CardContent className="py-3 px-4 flex items-center justify-between"> <CardContent className="py-3 px-4 flex items-center justify-between">
<div className="flex-1"> <div className="flex-1">
<p className="text-sm font-medium text-white">{track.title}</p> <p className="text-sm font-medium text-white">
{track.title}
</p>
<div className="flex gap-2 mt-1"> <div className="flex gap-2 mt-1">
{track.genre && track.genre.slice(0, 2).map((g: string) => ( {track.genre &&
<Badge key={g} variant="secondary" className="text-xs"> track.genre.slice(0, 2).map((g: string) => (
{g} <Badge
</Badge> key={g}
))} variant="secondary"
className="text-xs"
>
{g}
</Badge>
))}
</div> </div>
</div> </div>
<Button <Button

View file

@ -11,7 +11,13 @@ import { useAuth } from "@/contexts/AuthContext";
import TrackUploadModal from "@/components/ethos/TrackUploadModal"; import TrackUploadModal from "@/components/ethos/TrackUploadModal";
import TrackMetadataForm from "@/components/ethos/TrackMetadataForm"; import TrackMetadataForm from "@/components/ethos/TrackMetadataForm";
import { ethosStorage, getAudioDuration } from "@/lib/ethos-storage"; import { ethosStorage, getAudioDuration } from "@/lib/ethos-storage";
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 { Badge } from "@/components/ui/badge";
import { useAethexToast } from "@/hooks/use-aethex-toast"; import { useAethexToast } from "@/hooks/use-aethex-toast";
import { Upload, Music, Settings, CheckCircle, Clock } from "lucide-react"; import { Upload, Music, Settings, CheckCircle, Clock } from "lucide-react";
@ -70,10 +76,12 @@ export default function ArtistSettings() {
const [uploadModalOpen, setUploadModalOpen] = useState(false); const [uploadModalOpen, setUploadModalOpen] = useState(false);
const [currentFile, setCurrentFile] = useState<File | null>(null); const [currentFile, setCurrentFile] = useState<File | null>(null);
const [showMetadataForm, setShowMetadataForm] = useState(false); const [showMetadataForm, setShowMetadataForm] = useState(false);
const [verificationStatus, setVerificationStatus] = useState<VerificationStatus>({ const [verificationStatus, setVerificationStatus] =
status: "none", useState<VerificationStatus>({
}); status: "none",
const [isSubmittingVerification, setIsSubmittingVerification] = useState(false); });
const [isSubmittingVerification, setIsSubmittingVerification] =
useState(false);
const [submissionNotes, setSubmissionNotes] = useState(""); const [submissionNotes, setSubmissionNotes] = useState("");
const [portfolioLinks, setPortfolioLinks] = useState(""); const [portfolioLinks, setPortfolioLinks] = useState("");
const [showLicenseModal, setShowLicenseModal] = useState(false); const [showLicenseModal, setShowLicenseModal] = useState(false);
@ -323,7 +331,8 @@ export default function ArtistSettings() {
if (res.ok) { if (res.ok) {
toast.success({ toast.success({
title: "Track uploaded successfully! 🎵", title: "Track uploaded successfully! 🎵",
description: "Your track has been added to your portfolio and is ready to share", description:
"Your track has been added to your portfolio and is ready to share",
}); });
setShowMetadataForm(false); setShowMetadataForm(false);
setCurrentFile(null); setCurrentFile(null);
@ -340,7 +349,11 @@ export default function ArtistSettings() {
}; };
if (loading) { if (loading) {
return <Layout><div className="py-20 text-center">Loading settings...</div></Layout>; return (
<Layout>
<div className="py-20 text-center">Loading settings...</div>
</Layout>
);
} }
return ( return (
@ -364,7 +377,9 @@ export default function ArtistSettings() {
{/* Profile Section */} {/* Profile Section */}
<Card className="bg-slate-900/50 border-slate-800"> <Card className="bg-slate-900/50 border-slate-800">
<CardHeader> <CardHeader>
<CardTitle className="text-white">Profile Information</CardTitle> <CardTitle className="text-white">
Profile Information
</CardTitle>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
<div className="space-y-2"> <div className="space-y-2">
@ -384,7 +399,10 @@ export default function ArtistSettings() {
<Input <Input
value={profile.portfolio_url || ""} value={profile.portfolio_url || ""}
onChange={(e) => onChange={(e) =>
setProfile({ ...profile, portfolio_url: e.target.value }) setProfile({
...profile,
portfolio_url: e.target.value,
})
} }
placeholder="https://yourportfolio.com" placeholder="https://yourportfolio.com"
type="url" type="url"
@ -411,7 +429,9 @@ export default function ArtistSettings() {
<Card className="bg-slate-900/50 border-slate-800"> <Card className="bg-slate-900/50 border-slate-800">
<CardHeader> <CardHeader>
<CardTitle className="text-white">Skills</CardTitle> <CardTitle className="text-white">Skills</CardTitle>
<CardDescription>Select the skills you specialize in</CardDescription> <CardDescription>
Select the skills you specialize in
</CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="grid grid-cols-2 md:grid-cols-3 gap-3"> <div className="grid grid-cols-2 md:grid-cols-3 gap-3">
@ -436,9 +456,12 @@ export default function ArtistSettings() {
{profile.for_hire && ( {profile.for_hire && (
<Card className="bg-slate-900/50 border-slate-800"> <Card className="bg-slate-900/50 border-slate-800">
<CardHeader> <CardHeader>
<CardTitle className="text-white">Services & Pricing</CardTitle> <CardTitle className="text-white">
Services & Pricing
</CardTitle>
<CardDescription> <CardDescription>
Set your prices for custom services. Leave blank if you prefer "Contact for Quote" Set your prices for custom services. Leave blank if you
prefer "Contact for Quote"
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
@ -461,7 +484,9 @@ export default function ArtistSettings() {
className="bg-slate-800 border-slate-700" className="bg-slate-800 border-slate-700"
min="0" min="0"
/> />
<p className="text-xs text-slate-400">Original music composition</p> <p className="text-xs text-slate-400">
Original music composition
</p>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
@ -482,7 +507,9 @@ export default function ArtistSettings() {
className="bg-slate-800 border-slate-700" className="bg-slate-800 border-slate-700"
min="0" min="0"
/> />
<p className="text-xs text-slate-400">Sound effects collection</p> <p className="text-xs text-slate-400">
Sound effects collection
</p>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
@ -503,7 +530,9 @@ export default function ArtistSettings() {
className="bg-slate-800 border-slate-700" className="bg-slate-800 border-slate-700"
min="0" min="0"
/> />
<p className="text-xs text-slate-400">Complete game/film score</p> <p className="text-xs text-slate-400">
Complete game/film score
</p>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
@ -524,26 +553,33 @@ export default function ArtistSettings() {
className="bg-slate-800 border-slate-700" className="bg-slate-800 border-slate-700"
min="0" min="0"
/> />
<p className="text-xs text-slate-400">Hourly or daily rate for consulting</p> <p className="text-xs text-slate-400">
Hourly or daily rate for consulting
</p>
</div> </div>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label className="text-white">Turnaround Time (days)</Label> <Label className="text-white">
Turnaround Time (days)
</Label>
<Input <Input
type="number" type="number"
value={profile.turnaround_days || ""} value={profile.turnaround_days || ""}
onChange={(e) => onChange={(e) =>
setProfile({ setProfile({
...profile, ...profile,
turnaround_days: Number(e.target.value) || undefined, turnaround_days:
Number(e.target.value) || undefined,
}) })
} }
placeholder="5" placeholder="5"
className="bg-slate-800 border-slate-700" className="bg-slate-800 border-slate-700"
min="1" min="1"
/> />
<p className="text-xs text-slate-400">Typical delivery time for custom work</p> <p className="text-xs text-slate-400">
Typical delivery time for custom work
</p>
</div> </div>
<label className="flex items-center gap-2 p-3 rounded-lg bg-slate-800/50 border border-slate-700 cursor-pointer"> <label className="flex items-center gap-2 p-3 rounded-lg bg-slate-800/50 border border-slate-700 cursor-pointer">
@ -561,7 +597,8 @@ export default function ArtistSettings() {
className="border-slate-600" className="border-slate-600"
/> />
<span className="text-sm text-slate-300"> <span className="text-sm text-slate-300">
High-value projects (Enterprise clients): "Contact for Quote" High-value projects (Enterprise clients): "Contact for
Quote"
</span> </span>
</label> </label>
</CardContent> </CardContent>
@ -575,7 +612,9 @@ export default function ArtistSettings() {
<Music className="h-5 w-5" /> <Music className="h-5 w-5" />
Upload Track Upload Track
</CardTitle> </CardTitle>
<CardDescription>Add a new track to your portfolio</CardDescription> <CardDescription>
Add a new track to your portfolio
</CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<Button <Button
@ -604,10 +643,12 @@ export default function ArtistSettings() {
<div className="p-4 bg-green-500/10 border border-green-500/20 rounded-lg flex items-start gap-3"> <div className="p-4 bg-green-500/10 border border-green-500/20 rounded-lg flex items-start gap-3">
<CheckCircle className="h-5 w-5 text-green-500 flex-shrink-0 mt-0.5" /> <CheckCircle className="h-5 w-5 text-green-500 flex-shrink-0 mt-0.5" />
<div> <div>
<p className="font-semibold text-green-400">Verified Artist</p> <p className="font-semibold text-green-400">
Verified Artist
</p>
<p className="text-sm text-green-300 mt-1"> <p className="text-sm text-green-300 mt-1">
You are a verified Ethos Guild artist. You can upload tracks and accept You are a verified Ethos Guild artist. You can upload
commercial licensing requests. tracks and accept commercial licensing requests.
</p> </p>
</div> </div>
</div> </div>
@ -615,35 +656,44 @@ export default function ArtistSettings() {
<div className="p-4 bg-yellow-500/10 border border-yellow-500/20 rounded-lg flex items-start gap-3"> <div className="p-4 bg-yellow-500/10 border border-yellow-500/20 rounded-lg flex items-start gap-3">
<Clock className="h-5 w-5 text-yellow-500 flex-shrink-0 mt-0.5" /> <Clock className="h-5 w-5 text-yellow-500 flex-shrink-0 mt-0.5" />
<div> <div>
<p className="font-semibold text-yellow-400">Pending Review</p> <p className="font-semibold text-yellow-400">
Pending Review
</p>
<p className="text-sm text-yellow-300 mt-1"> <p className="text-sm text-yellow-300 mt-1">
Your verification request is under review. We'll email you when there's an Your verification request is under review. We'll email
update. you when there's an update.
</p> </p>
{verificationStatus.submitted_at && ( {verificationStatus.submitted_at && (
<p className="text-xs text-yellow-300/70 mt-2"> <p className="text-xs text-yellow-300/70 mt-2">
Submitted:{" "} Submitted:{" "}
{new Date(verificationStatus.submitted_at).toLocaleDateString()} {new Date(
verificationStatus.submitted_at,
).toLocaleDateString()}
</p> </p>
)} )}
</div> </div>
</div> </div>
) : verificationStatus.status === "rejected" ? ( ) : verificationStatus.status === "rejected" ? (
<div className="p-4 bg-red-500/10 border border-red-500/20 rounded-lg"> <div className="p-4 bg-red-500/10 border border-red-500/20 rounded-lg">
<p className="font-semibold text-red-400">Application Rejected</p> <p className="font-semibold text-red-400">
Application Rejected
</p>
{verificationStatus.rejection_reason && ( {verificationStatus.rejection_reason && (
<p className="text-sm text-red-300 mt-2"> <p className="text-sm text-red-300 mt-2">
{verificationStatus.rejection_reason} {verificationStatus.rejection_reason}
</p> </p>
)} )}
<p className="text-sm text-red-300 mt-2"> <p className="text-sm text-red-300 mt-2">
You can resubmit with updates to your portfolio or qualifications. You can resubmit with updates to your portfolio or
qualifications.
</p> </p>
</div> </div>
) : ( ) : (
<> <>
<div className="space-y-2"> <div className="space-y-2">
<Label className="text-white">Application Notes (optional)</Label> <Label className="text-white">
Application Notes (optional)
</Label>
<Textarea <Textarea
value={submissionNotes} value={submissionNotes}
onChange={(e) => setSubmissionNotes(e.target.value)} onChange={(e) => setSubmissionNotes(e.target.value)}
@ -653,7 +703,9 @@ export default function ArtistSettings() {
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label className="text-white">Portfolio Links (one per line)</Label> <Label className="text-white">
Portfolio Links (one per line)
</Label>
<Textarea <Textarea
value={portfolioLinks} value={portfolioLinks}
onChange={(e) => setPortfolioLinks(e.target.value)} onChange={(e) => setPortfolioLinks(e.target.value)}

View file

@ -7,6 +7,7 @@ Ethos Guild is a music production and licensing ecosystem within AeThex. Artists
## Completed Features ## Completed Features
### Phase 1: Artist Verification Workflow ✅ ### Phase 1: Artist Verification Workflow ✅
- **Admin Dashboard**: `/admin` → "Ethos Verification" tab - **Admin Dashboard**: `/admin` → "Ethos Verification" tab
- **Artist Submission**: `/ethos/settings` → Verification form - **Artist Submission**: `/ethos/settings` → Verification form
- **Verification Process**: Manual review by admins with approval/rejection - **Verification Process**: Manual review by admins with approval/rejection
@ -14,6 +15,7 @@ Ethos Guild is a music production and licensing ecosystem within AeThex. Artists
- **Database**: `ethos_verification_requests` and `ethos_verification_audit_log` tables - **Database**: `ethos_verification_requests` and `ethos_verification_audit_log` tables
### Phase 2: Supabase Storage Integration ✅ ### Phase 2: Supabase Storage Integration ✅
- **Track Upload**: Audio files stored in `ethos-tracks` bucket - **Track Upload**: Audio files stored in `ethos-tracks` bucket
- **Public Access**: Tracks are publicly readable for streaming - **Public Access**: Tracks are publicly readable for streaming
- **User Isolation**: Each user can only upload to their own folder - **User Isolation**: Each user can only upload to their own folder
@ -21,18 +23,21 @@ Ethos Guild is a music production and licensing ecosystem within AeThex. Artists
- **File Management**: Upload, download, delete operations supported - **File Management**: Upload, download, delete operations supported
### Phase 3: Email Notifications ✅ ### Phase 3: Email Notifications ✅
- **SMTP Configuration**: Hostinger SMTP (smtp.hostinger.com:465) - **SMTP Configuration**: Hostinger SMTP (smtp.hostinger.com:465)
- **Templates**: Verification, licensing, and status notifications - **Templates**: Verification, licensing, and status notifications
- **Delivery**: Reliable email delivery via Nodemailer - **Delivery**: Reliable email delivery via Nodemailer
- **Async Processing**: Non-blocking email sending - **Async Processing**: Non-blocking email sending
### Phase 4: Ecosystem License Agreement ✅ ### Phase 4: Ecosystem License Agreement ✅
- **Modal Interface**: Click-wrap agreement on first track upload - **Modal Interface**: Click-wrap agreement on first track upload
- **License Tracking**: `ethos_ecosystem_licenses` table - **License Tracking**: `ethos_ecosystem_licenses` table
- **Track Linking**: Accepted licenses linked to specific tracks - **Track Linking**: Accepted licenses linked to specific tracks
- **Re-acceptance**: Artists can accept license once - **Re-acceptance**: Artists can accept license once
### Phase 5: Artist Services & Pricing ✅ ### Phase 5: Artist Services & Pricing ✅
- **Flexible Pricing**: JSON-based `price_list` structure - **Flexible Pricing**: JSON-based `price_list` structure
- **Service Types**: Custom tracks, SFX packs, full scores, day rates - **Service Types**: Custom tracks, SFX packs, full scores, day rates
- **For Hire Status**: Boolean flag to show in marketplace - **For Hire Status**: Boolean flag to show in marketplace
@ -40,6 +45,7 @@ Ethos Guild is a music production and licensing ecosystem within AeThex. Artists
- **Contact System**: Service request form with commission tracking - **Contact System**: Service request form with commission tracking
### Phase 6: NEXUS Marketplace Integration ✅ ### Phase 6: NEXUS Marketplace Integration ✅
- **Two Components**: AudioTracksForSale and AudioServicesForHire - **Two Components**: AudioTracksForSale and AudioServicesForHire
- **Artist Directory**: Filter by skills, ratings, services - **Artist Directory**: Filter by skills, ratings, services
- **Track Library**: Filter by genre, license type, price - **Track Library**: Filter by genre, license type, price
@ -47,6 +53,7 @@ Ethos Guild is a music production and licensing ecosystem within AeThex. Artists
- **Profile Integration**: Links to artist profiles and portfolio - **Profile Integration**: Links to artist profiles and portfolio
### Phase 7: Artist Portfolio ✅ ### Phase 7: Artist Portfolio ✅
- **Route**: `/passport/me` (personal) and `/passport/:username` (public) - **Route**: `/passport/me` (personal) and `/passport/:username` (public)
- **Sections**: Ethos Guild info, tracks, skills, verification status - **Sections**: Ethos Guild info, tracks, skills, verification status
- **Self View**: Edit link to settings for own profile - **Self View**: Edit link to settings for own profile
@ -56,6 +63,7 @@ Ethos Guild is a music production and licensing ecosystem within AeThex. Artists
## API Endpoints ## API Endpoints
### Artist Management ### Artist Management
``` ```
GET /api/ethos/artists?id=<id> - Get single artist GET /api/ethos/artists?id=<id> - Get single artist
GET /api/ethos/artists?for_hire=true - List artists available for hire GET /api/ethos/artists?for_hire=true - List artists available for hire
@ -64,11 +72,13 @@ PUT /api/ethos/artists - Update artist profile
``` ```
### Artist Services ### Artist Services
``` ```
GET /api/ethos/artist-services/:artist_id - Get artist's service pricing GET /api/ethos/artist-services/:artist_id - Get artist's service pricing
``` ```
### Service Requests ### Service Requests
``` ```
POST /api/ethos/service-requests - Create service request POST /api/ethos/service-requests - Create service request
GET /api/ethos/service-requests?artist_id=<id> - List requests for artist GET /api/ethos/service-requests?artist_id=<id> - List requests for artist
@ -76,18 +86,21 @@ PUT /api/ethos/service-requests/:id - Update request status
``` ```
### Tracks ### Tracks
``` ```
GET /api/ethos/tracks - List published tracks GET /api/ethos/tracks - List published tracks
POST /api/ethos/tracks - Upload new track (with auto-license linking) POST /api/ethos/tracks - Upload new track (with auto-license linking)
``` ```
### Verification ### Verification
``` ```
GET /api/ethos/verification - List verification requests (admin) GET /api/ethos/verification - List verification requests (admin)
POST /api/ethos/verification - Submit or manage verification POST /api/ethos/verification - Submit or manage verification
``` ```
### Licensing ### Licensing
``` ```
GET /api/ethos/licensing-agreements - List agreements GET /api/ethos/licensing-agreements - List agreements
POST /api/ethos/licensing-notifications - Send notifications POST /api/ethos/licensing-notifications - Send notifications
@ -98,6 +111,7 @@ POST /api/ethos/licensing-notifications - Send notifications
### Main Tables ### Main Tables
**ethos_artist_profiles** **ethos_artist_profiles**
- `user_id` (PK): References user_profiles - `user_id` (PK): References user_profiles
- `for_hire`: Boolean flag for marketplace visibility - `for_hire`: Boolean flag for marketplace visibility
- `verified`: Verification status - `verified`: Verification status
@ -107,6 +121,7 @@ POST /api/ethos/licensing-notifications - Send notifications
- RLS: Users see all, own updates only - RLS: Users see all, own updates only
**ethos_tracks** **ethos_tracks**
- `id` (PK): UUID - `id` (PK): UUID
- `user_id`: Artist who uploaded - `user_id`: Artist who uploaded
- `title`, `description`: Track info - `title`, `description`: Track info
@ -118,6 +133,7 @@ POST /api/ethos/licensing-notifications - Send notifications
- RLS: Public read, user write/delete own - RLS: Public read, user write/delete own
**ethos_ecosystem_licenses** **ethos_ecosystem_licenses**
- `id` (PK): UUID - `id` (PK): UUID
- `track_id`: Which track accepted license - `track_id`: Which track accepted license
- `artist_id`: Which artist accepted - `artist_id`: Which artist accepted
@ -125,6 +141,7 @@ POST /api/ethos/licensing-notifications - Send notifications
- RLS: User sees own, admins see all - RLS: User sees own, admins see all
**ethos_verification_requests** **ethos_verification_requests**
- `id` (PK): UUID - `id` (PK): UUID
- `user_id`: Artist requesting verification - `user_id`: Artist requesting verification
- `status`: "pending", "approved", "rejected" - `status`: "pending", "approved", "rejected"
@ -133,6 +150,7 @@ POST /api/ethos/licensing-notifications - Send notifications
- RLS: Artists see own, admins see all - RLS: Artists see own, admins see all
**ethos_service_requests** **ethos_service_requests**
- `id` (PK): UUID - `id` (PK): UUID
- `artist_id`: Requested artist - `artist_id`: Requested artist
- `requester_id`: Client requesting service - `requester_id`: Client requesting service
@ -142,7 +160,9 @@ POST /api/ethos/licensing-notifications - Send notifications
- RLS: Both parties can view, artist updates - RLS: Both parties can view, artist updates
### Storage Bucket ### Storage Bucket
**ethos-tracks** (Public) **ethos-tracks** (Public)
- Path: `/{user_id}/{track_id}/audio.mp3` - Path: `/{user_id}/{track_id}/audio.mp3`
- RLS: Authenticated users can upload to own folder, public read, user delete own - RLS: Authenticated users can upload to own folder, public read, user delete own
- Policy: Users isolated to their own folder, public streaming access - Policy: Users isolated to their own folder, public streaming access
@ -150,6 +170,7 @@ POST /api/ethos/licensing-notifications - Send notifications
## User Flows ## User Flows
### Artist Upload Flow ### Artist Upload Flow
1. Artist goes to `/ethos/settings` 1. Artist goes to `/ethos/settings`
2. Clicks "Upload Track" button 2. Clicks "Upload Track" button
3. Selects audio file 3. Selects audio file
@ -164,6 +185,7 @@ POST /api/ethos/licensing-notifications - Send notifications
12. Success toast shown, track appears in library 12. Success toast shown, track appears in library
### Verification Flow ### Verification Flow
1. Artist goes to `/ethos/settings` → "Request Verification" 1. Artist goes to `/ethos/settings` → "Request Verification"
2. Fills form: bio, skills, portfolio links, submission notes 2. Fills form: bio, skills, portfolio links, submission notes
3. POST to `/api/ethos/verification` with action: "submit" 3. POST to `/api/ethos/verification` with action: "submit"
@ -177,6 +199,7 @@ POST /api/ethos/licensing-notifications - Send notifications
11. Artist can see status on settings page 11. Artist can see status on settings page
### Marketplace Discovery Flow ### Marketplace Discovery Flow
1. User goes to `/nexus` 1. User goes to `/nexus`
2. Clicks "Services for Hire" tab 2. Clicks "Services for Hire" tab
3. Component fetches: `GET /api/ethos/artists?forHire=true&limit=50` 3. Component fetches: `GET /api/ethos/artists?forHire=true&limit=50`
@ -196,6 +219,7 @@ POST /api/ethos/licensing-notifications - Send notifications
12. Artist can accept/decline in dashboard 12. Artist can accept/decline in dashboard
### Artist Portfolio View ### Artist Portfolio View
1. Artist goes to `/passport/me` (personal portfolio) 1. Artist goes to `/passport/me` (personal portfolio)
2. **Ethos Guild Section Shows**: 2. **Ethos Guild Section Shows**:
- Verified Artist badge (if applicable) - Verified Artist badge (if applicable)
@ -214,6 +238,7 @@ POST /api/ethos/licensing-notifications - Send notifications
## Deployment Checklist ## Deployment Checklist
### Database ### Database
- [ ] Apply migration: `20250206_add_ethos_guild.sql` - [ ] Apply migration: `20250206_add_ethos_guild.sql`
- [ ] Apply migration: `20250210_add_ethos_artist_verification.sql` - [ ] Apply migration: `20250210_add_ethos_artist_verification.sql`
- [ ] Apply migration: `20250210_setup_ethos_storage.sql` (read-only in SQL) - [ ] Apply migration: `20250210_setup_ethos_storage.sql` (read-only in SQL)
@ -221,12 +246,14 @@ POST /api/ethos/licensing-notifications - Send notifications
- [ ] Apply migration: `20250212_add_ethos_service_requests.sql` - [ ] Apply migration: `20250212_add_ethos_service_requests.sql`
### Storage Setup ### Storage Setup
- [ ] Create Supabase Storage bucket: "ethos-tracks" - [ ] Create Supabase Storage bucket: "ethos-tracks"
- [ ] Make bucket PUBLIC - [ ] Make bucket PUBLIC
- [ ] Apply RLS policies (see migration comments) - [ ] Apply RLS policies (see migration comments)
- [ ] Test upload from browser - [ ] Test upload from browser
### Environment Variables ### Environment Variables
``` ```
VITE_SUPABASE_URL=https://kmdeisowhtsalsekkzqd.supabase.co VITE_SUPABASE_URL=https://kmdeisowhtsalsekkzqd.supabase.co
VITE_SUPABASE_ANON_KEY=<your-anon-key> VITE_SUPABASE_ANON_KEY=<your-anon-key>
@ -239,6 +266,7 @@ SMTP_FROM_EMAIL=no-reply@aethex.tech
``` ```
### Testing Steps ### Testing Steps
1. [ ] Create user account 1. [ ] Create user account
2. [ ] Go to `/ethos/settings` 2. [ ] Go to `/ethos/settings`
3. [ ] Upload track → Accept license → Fill metadata → Upload to storage 3. [ ] Upload track → Accept license → Fill metadata → Upload to storage
@ -257,24 +285,29 @@ SMTP_FROM_EMAIL=no-reply@aethex.tech
## Technical Details ## Technical Details
### License Linking Logic ### License Linking Logic
When a track is uploaded with `license_type: "ecosystem"`: When a track is uploaded with `license_type: "ecosystem"`:
1. Track record created in `ethos_tracks` 1. Track record created in `ethos_tracks`
2. Immediately after insert, code creates record in `ethos_ecosystem_licenses` 2. Immediately after insert, code creates record in `ethos_ecosystem_licenses`
3. Links: track_id, artist_id, accepted_at (current timestamp) 3. Links: track_id, artist_id, accepted_at (current timestamp)
4. This establishes the relationship between license agreement and track 4. This establishes the relationship between license agreement and track
### Storage Path Format ### Storage Path Format
``` ```
ethos-tracks/ ethos-tracks/
{user_id}/ {user_id}/
{track_id}/ {track_id}/
audio.mp3 audio.mp3
``` ```
- User ID isolates folders for RLS - User ID isolates folders for RLS
- Track ID groups related files - Track ID groups related files
- Flat structure easy to manage - Flat structure easy to manage
### Service Pricing Structure ### Service Pricing Structure
```json ```json
{ {
"price_list": { "price_list": {
@ -286,11 +319,13 @@ ethos-tracks/
} }
} }
``` ```
- Flexible JSON for future service types - Flexible JSON for future service types
- `null` values mean service not available - `null` values mean service not available
- Boolean flag for custom quotes - Boolean flag for custom quotes
### Verification Workflow ### Verification Workflow
``` ```
pending → admin review → approved/rejected pending → admin review → approved/rejected
↓ ↓ ↓ ↓
@ -314,6 +349,7 @@ pending → admin review → approved/rejected
## Support ## Support
For issues or questions about Ethos Guild: For issues or questions about Ethos Guild:
1. Check `/docs` section for tutorials 1. Check `/docs` section for tutorials
2. Review `/ethos/library` for example tracks 2. Review `/ethos/library` for example tracks
3. See `/admin` → "Ethos Verification" for status 3. See `/admin` → "Ethos Verification" for status