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;
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
@ -39,7 +41,9 @@ export default async function handler(req: any, res: any) {
if (artistError && artistError.code !== "PGRST116") throw artistError;
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({

View file

@ -29,7 +29,8 @@ export default async function handler(req: any, res: any) {
if (!artist_id || !service_type || !description) {
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();
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
@ -96,7 +99,9 @@ export default async function handler(req: any, res: any) {
if (requester_id) dbQuery = dbQuery.eq("requester_id", requester_id);
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;
return res.json({ data });
@ -109,7 +114,9 @@ export default async function handler(req: any, res: any) {
const { status, notes } = body;
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

View file

@ -53,10 +53,15 @@ export default async function handler(req: any, res: any) {
if (genre) dbQuery = dbQuery.contains("genre", [genre]);
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
.range(Number(offset), Number(offset) + Number(limit) - 1);
const { data, error, count } = await dbQuery.range(
Number(offset),
Number(offset) + Number(limit) - 1,
);
if (error) throw error;

View file

@ -45,7 +45,8 @@ export default function EcosystemLicenseModal({
🎵 Welcome to the Ethos Library
</DialogTitle>
<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>
</DialogHeader>
@ -54,23 +55,28 @@ export default function EcosystemLicenseModal({
<Alert className="bg-blue-500/10 border-blue-500/30">
<AlertCircle className="h-4 w-4 text-blue-400" />
<AlertDescription className="text-blue-300">
This is a one-time agreement. By contributing to the Ethos Library, you're helping
build a vibrant community of creators. We're transparent about how your work will be
used.
This is a one-time agreement. By contributing to the Ethos
Library, you're helping build a vibrant community of creators.
We're transparent about how your work will be used.
</AlertDescription>
</Alert>
{/* License Terms Section */}
<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">
<p>
<strong className="text-white">What is the Ecosystem License?</strong>
<strong className="text-white">
What is the Ecosystem License?
</strong>
</p>
<p>
The Ecosystem License allows AeThex development teams (specifically our GameForge
arm) to use your track for free in non-commercial projects. This includes:
The Ecosystem License allows AeThex development teams
(specifically our GameForge arm) to use your track for free in
non-commercial projects. This includes:
</p>
<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">
<li>100% ownership of your music</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>
</ul>
@ -94,24 +102,28 @@ export default function EcosystemLicenseModal({
<strong className="text-white">Commercial Use:</strong>
</p>
<p>
If you want to license your track for commercial use (outside our ecosystem), you
can set your own price on the NEXUS marketplace or negotiate directly with clients.
If you want to license your track for commercial use (outside
our ecosystem), you can set your own price on the NEXUS
marketplace or negotiate directly with clients.
</p>
<p>
<strong className="text-white">Getting Help:</strong>
</p>
<p>
Our CORP arm can help negotiate high-value commercial licenses and connect you with
enterprise clients.
Our CORP arm can help negotiate high-value commercial licenses
and connect you with enterprise clients.
</p>
<hr className="border-slate-600 my-4" />
<p className="text-xs text-slate-400">
Version 1.0 | Effective Date: {new Date().toLocaleDateString()} | For complete
legal terms, see our{" "}
<a href="/docs/legal/knd-008" className="text-pink-400 hover:text-pink-300">
Version 1.0 | Effective Date: {new Date().toLocaleDateString()}{" "}
| For complete legal terms, see our{" "}
<a
href="/docs/legal/knd-008"
className="text-pink-400 hover:text-pink-300"
>
full agreement
</a>
</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">
<Checkbox
checked={hasReadTerms}
onCheckedChange={(checked) => setHasReadTerms(checked as boolean)}
onCheckedChange={(checked) =>
setHasReadTerms(checked as boolean)
}
className="mt-1 border-slate-600"
/>
<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">
<Checkbox
checked={agreeToTerms}
onCheckedChange={(checked) => setAgreeToTerms(checked as boolean)}
onCheckedChange={(checked) =>
setAgreeToTerms(checked as boolean)
}
className="mt-1 border-slate-600"
/>
<span className="text-sm text-slate-300">
I agree to the KND-008 Ecosystem License terms and allow AeThex to use my music
for non-commercial projects
I agree to the KND-008 Ecosystem License terms and allow AeThex
to use my music for non-commercial projects
</span>
</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">
<Checkbox
checked={confirmOriginal}
onCheckedChange={(checked) => setConfirmOriginal(checked as boolean)}
onCheckedChange={(checked) =>
setConfirmOriginal(checked as boolean)
}
className="mt-1 border-slate-600"
/>
<span className="text-sm text-slate-300">
I confirm that this is my original work and I have the right to grant these
licenses
I confirm that this is my original work and I have the right to
grant these licenses
</span>
</label>
</div>
@ -160,7 +178,9 @@ export default function EcosystemLicenseModal({
{allChecked && (
<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" />
<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>
)}

View file

@ -10,7 +10,14 @@ import {
SelectTrigger,
SelectValue,
} 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";
interface ArtistService {
@ -54,7 +61,9 @@ export default function AudioServicesForHire() {
try {
setLoading(true);
// 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) {
const result = await response.json();
const artistsData = result.data || result || [];
@ -102,7 +111,7 @@ export default function AudioServicesForHire() {
const matchesSkill =
!selectedSkill ||
artist.skills.some((skill) =>
skill.toLowerCase().includes(selectedSkill.toLowerCase())
skill.toLowerCase().includes(selectedSkill.toLowerCase()),
);
const matchesService =
@ -116,7 +125,7 @@ export default function AudioServicesForHire() {
// Get all unique skills from artists
const allSkills = Array.from(
new Set(artists.flatMap((artist) => artist.skills))
new Set(artists.flatMap((artist) => artist.skills)),
).sort();
return (
@ -134,7 +143,10 @@ export default function AudioServicesForHire() {
</div>
<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">
<SelectValue placeholder="Skill" />
</SelectTrigger>
@ -148,7 +160,10 @@ export default function AudioServicesForHire() {
</SelectContent>
</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">
<SelectValue placeholder="Service" />
</SelectTrigger>
@ -162,7 +177,10 @@ export default function AudioServicesForHire() {
</SelectContent>
</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">
<SelectValue placeholder="Min Rating" />
</SelectTrigger>
@ -191,12 +209,16 @@ export default function AudioServicesForHire() {
{/* Results Count */}
<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>
{/* Artists Grid */}
{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 ? (
<div className="text-center py-12 text-slate-400">
No artists found matching your criteria. Try adjusting your filters.
@ -226,7 +248,9 @@ export default function AudioServicesForHire() {
</CardTitle>
<div className="flex items-center gap-1 mt-1">
<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>
@ -234,7 +258,9 @@ export default function AudioServicesForHire() {
<CardContent className="space-y-4">
{/* 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 */}
@ -242,7 +268,11 @@ export default function AudioServicesForHire() {
<p className="text-xs font-medium text-slate-400">Skills</p>
<div className="flex flex-wrap gap-2">
{artist.skills.slice(0, 3).map((skill) => (
<Badge key={skill} variant="secondary" className="text-xs">
<Badge
key={skill}
variant="secondary"
className="text-xs"
>
{skill}
</Badge>
))}

View file

@ -66,7 +66,7 @@ export default function AudioTracksForSale() {
if (selectedLicense) params.append("licenseType", selectedLicense);
const response = await fetch(
`/api/ethos/tracks?${params.toString()}&limit=20`
`/api/ethos/tracks?${params.toString()}&limit=20`,
);
if (response.ok) {
const { data } = await response.json();
@ -116,7 +116,10 @@ export default function AudioTracksForSale() {
</div>
<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">
<SelectValue placeholder="Genre" />
</SelectTrigger>
@ -130,7 +133,10 @@ export default function AudioTracksForSale() {
</SelectContent>
</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">
<SelectValue placeholder="License Type" />
</SelectTrigger>
@ -178,7 +184,9 @@ export default function AudioTracksForSale() {
{/* Tracks Grid */}
{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 ? (
<div className="text-center py-12 text-slate-400">
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 gap-1">
<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 className="flex items-center gap-1 text-slate-400">
<Download className="h-4 w-4" />

View file

@ -224,18 +224,27 @@ export default function Nexus() {
Ethos Guild - Music & Audio Services
</h2>
<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>
</div>
{/* Tabs for Tracks & Services */}
<Tabs defaultValue="tracks" className="w-full">
<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" />
Tracks for Sale
</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" />
Hire Artists
</TabsTrigger>
@ -248,7 +257,9 @@ export default function Nexus() {
Browse Pre-made Music
</h3>
<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>
</div>
<AudioTracksForSale />
@ -261,7 +272,10 @@ export default function Nexus() {
Hire Verified Artists
</h3>
<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>
</div>
<AudioServicesForHire />
@ -277,7 +291,8 @@ export default function Nexus() {
</CardTitle>
</CardHeader>
<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>
</Card>
@ -288,7 +303,8 @@ export default function Nexus() {
</CardTitle>
</CardHeader>
<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>
</Card>
@ -299,7 +315,8 @@ export default function Nexus() {
</CardTitle>
</CardHeader>
<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>
</Card>
</div>

View file

@ -22,7 +22,14 @@ import {
} from "@/lib/aethex-database-adapter";
import { useAuth } from "@/contexts/AuthContext";
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";
interface ProjectPreview {
@ -231,20 +238,21 @@ const ProfilePassport = () => {
authProfile.username.toLowerCase() ===
resolvedProfile.username.toLowerCase());
const [achievementList, interestList, projectList, ethos] = await Promise.all([
aethexAchievementService
.getUserAchievements(resolvedId)
.catch(() => [] as AethexAchievement[]),
aethexUserService
.getUserInterests(resolvedId)
.catch(() => [] as string[]),
aethexProjectService
.getUserProjects(resolvedId)
.catch(() => [] as ProjectPreview[]),
fetch(`/api/ethos/artists?id=${resolvedId}`)
.then(res => res.ok ? res.json() : { tracks: [] })
.catch(() => ({ tracks: [] })),
]);
const [achievementList, interestList, projectList, ethos] =
await Promise.all([
aethexAchievementService
.getUserAchievements(resolvedId)
.catch(() => [] as AethexAchievement[]),
aethexUserService
.getUserInterests(resolvedId)
.catch(() => [] as string[]),
aethexProjectService
.getUserProjects(resolvedId)
.catch(() => [] as ProjectPreview[]),
fetch(`/api/ethos/artists?id=${resolvedId}`)
.then((res) => (res.ok ? res.json() : { tracks: [] }))
.catch(() => ({ tracks: [] })),
]);
if (cancelled) {
return;
@ -428,13 +436,21 @@ const ProfilePassport = () => {
</div>
{ethosProfile.skills && ethosProfile.skills.length > 0 && (
<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">
{ethosProfile.skills.slice(0, 5).map((skill: string) => (
<Badge key={skill} variant="secondary" className="text-xs">
{skill}
</Badge>
))}
{ethosProfile.skills
.slice(0, 5)
.map((skill: string) => (
<Badge
key={skill}
variant="secondary"
className="text-xs"
>
{skill}
</Badge>
))}
{ethosProfile.skills.length > 5 && (
<Badge variant="secondary" className="text-xs">
+{ethosProfile.skills.length - 5} more
@ -454,16 +470,26 @@ const ProfilePassport = () => {
</h3>
<div className="grid gap-2">
{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">
<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">
{track.genre && track.genre.slice(0, 2).map((g: string) => (
<Badge key={g} variant="secondary" className="text-xs">
{g}
</Badge>
))}
{track.genre &&
track.genre.slice(0, 2).map((g: string) => (
<Badge
key={g}
variant="secondary"
className="text-xs"
>
{g}
</Badge>
))}
</div>
</div>
<Button

View file

@ -11,7 +11,13 @@ import { useAuth } from "@/contexts/AuthContext";
import TrackUploadModal from "@/components/ethos/TrackUploadModal";
import TrackMetadataForm from "@/components/ethos/TrackMetadataForm";
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 { useAethexToast } from "@/hooks/use-aethex-toast";
import { Upload, Music, Settings, CheckCircle, Clock } from "lucide-react";
@ -70,10 +76,12 @@ export default function ArtistSettings() {
const [uploadModalOpen, setUploadModalOpen] = useState(false);
const [currentFile, setCurrentFile] = useState<File | null>(null);
const [showMetadataForm, setShowMetadataForm] = useState(false);
const [verificationStatus, setVerificationStatus] = useState<VerificationStatus>({
status: "none",
});
const [isSubmittingVerification, setIsSubmittingVerification] = useState(false);
const [verificationStatus, setVerificationStatus] =
useState<VerificationStatus>({
status: "none",
});
const [isSubmittingVerification, setIsSubmittingVerification] =
useState(false);
const [submissionNotes, setSubmissionNotes] = useState("");
const [portfolioLinks, setPortfolioLinks] = useState("");
const [showLicenseModal, setShowLicenseModal] = useState(false);
@ -323,7 +331,8 @@ export default function ArtistSettings() {
if (res.ok) {
toast.success({
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);
setCurrentFile(null);
@ -340,7 +349,11 @@ export default function ArtistSettings() {
};
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 (
@ -364,7 +377,9 @@ export default function ArtistSettings() {
{/* Profile Section */}
<Card className="bg-slate-900/50 border-slate-800">
<CardHeader>
<CardTitle className="text-white">Profile Information</CardTitle>
<CardTitle className="text-white">
Profile Information
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
@ -384,7 +399,10 @@ export default function ArtistSettings() {
<Input
value={profile.portfolio_url || ""}
onChange={(e) =>
setProfile({ ...profile, portfolio_url: e.target.value })
setProfile({
...profile,
portfolio_url: e.target.value,
})
}
placeholder="https://yourportfolio.com"
type="url"
@ -411,7 +429,9 @@ export default function ArtistSettings() {
<Card className="bg-slate-900/50 border-slate-800">
<CardHeader>
<CardTitle className="text-white">Skills</CardTitle>
<CardDescription>Select the skills you specialize in</CardDescription>
<CardDescription>
Select the skills you specialize in
</CardDescription>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 md:grid-cols-3 gap-3">
@ -436,9 +456,12 @@ export default function ArtistSettings() {
{profile.for_hire && (
<Card className="bg-slate-900/50 border-slate-800">
<CardHeader>
<CardTitle className="text-white">Services & Pricing</CardTitle>
<CardTitle className="text-white">
Services & Pricing
</CardTitle>
<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>
</CardHeader>
<CardContent className="space-y-4">
@ -461,7 +484,9 @@ export default function ArtistSettings() {
className="bg-slate-800 border-slate-700"
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 className="space-y-2">
@ -482,7 +507,9 @@ export default function ArtistSettings() {
className="bg-slate-800 border-slate-700"
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 className="space-y-2">
@ -503,7 +530,9 @@ export default function ArtistSettings() {
className="bg-slate-800 border-slate-700"
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 className="space-y-2">
@ -524,26 +553,33 @@ export default function ArtistSettings() {
className="bg-slate-800 border-slate-700"
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 className="space-y-2">
<Label className="text-white">Turnaround Time (days)</Label>
<Label className="text-white">
Turnaround Time (days)
</Label>
<Input
type="number"
value={profile.turnaround_days || ""}
onChange={(e) =>
setProfile({
...profile,
turnaround_days: Number(e.target.value) || undefined,
turnaround_days:
Number(e.target.value) || undefined,
})
}
placeholder="5"
className="bg-slate-800 border-slate-700"
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>
<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"
/>
<span className="text-sm text-slate-300">
High-value projects (Enterprise clients): "Contact for Quote"
High-value projects (Enterprise clients): "Contact for
Quote"
</span>
</label>
</CardContent>
@ -575,7 +612,9 @@ export default function ArtistSettings() {
<Music className="h-5 w-5" />
Upload Track
</CardTitle>
<CardDescription>Add a new track to your portfolio</CardDescription>
<CardDescription>
Add a new track to your portfolio
</CardDescription>
</CardHeader>
<CardContent>
<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">
<CheckCircle className="h-5 w-5 text-green-500 flex-shrink-0 mt-0.5" />
<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">
You are a verified Ethos Guild artist. You can upload tracks and accept
commercial licensing requests.
You are a verified Ethos Guild artist. You can upload
tracks and accept commercial licensing requests.
</p>
</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">
<Clock className="h-5 w-5 text-yellow-500 flex-shrink-0 mt-0.5" />
<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">
Your verification request is under review. We'll email you when there's an
update.
Your verification request is under review. We'll email
you when there's an update.
</p>
{verificationStatus.submitted_at && (
<p className="text-xs text-yellow-300/70 mt-2">
Submitted:{" "}
{new Date(verificationStatus.submitted_at).toLocaleDateString()}
{new Date(
verificationStatus.submitted_at,
).toLocaleDateString()}
</p>
)}
</div>
</div>
) : verificationStatus.status === "rejected" ? (
<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 && (
<p className="text-sm text-red-300 mt-2">
{verificationStatus.rejection_reason}
</p>
)}
<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>
</div>
) : (
<>
<div className="space-y-2">
<Label className="text-white">Application Notes (optional)</Label>
<Label className="text-white">
Application Notes (optional)
</Label>
<Textarea
value={submissionNotes}
onChange={(e) => setSubmissionNotes(e.target.value)}
@ -653,7 +703,9 @@ export default function ArtistSettings() {
</div>
<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
value={portfolioLinks}
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
### Phase 1: Artist Verification Workflow ✅
- **Admin Dashboard**: `/admin` → "Ethos Verification" tab
- **Artist Submission**: `/ethos/settings` → Verification form
- **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
### Phase 2: Supabase Storage Integration ✅
- **Track Upload**: Audio files stored in `ethos-tracks` bucket
- **Public Access**: Tracks are publicly readable for streaming
- **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
### Phase 3: Email Notifications ✅
- **SMTP Configuration**: Hostinger SMTP (smtp.hostinger.com:465)
- **Templates**: Verification, licensing, and status notifications
- **Delivery**: Reliable email delivery via Nodemailer
- **Async Processing**: Non-blocking email sending
### Phase 4: Ecosystem License Agreement ✅
- **Modal Interface**: Click-wrap agreement on first track upload
- **License Tracking**: `ethos_ecosystem_licenses` table
- **Track Linking**: Accepted licenses linked to specific tracks
- **Re-acceptance**: Artists can accept license once
### Phase 5: Artist Services & Pricing ✅
- **Flexible Pricing**: JSON-based `price_list` structure
- **Service Types**: Custom tracks, SFX packs, full scores, day rates
- **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
### Phase 6: NEXUS Marketplace Integration ✅
- **Two Components**: AudioTracksForSale and AudioServicesForHire
- **Artist Directory**: Filter by skills, ratings, services
- **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
### Phase 7: Artist Portfolio ✅
- **Route**: `/passport/me` (personal) and `/passport/:username` (public)
- **Sections**: Ethos Guild info, tracks, skills, verification status
- **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
### Artist Management
```
GET /api/ethos/artists?id=<id> - Get single artist
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
```
GET /api/ethos/artist-services/:artist_id - Get artist's service pricing
```
### Service Requests
```
POST /api/ethos/service-requests - Create service request
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
```
GET /api/ethos/tracks - List published tracks
POST /api/ethos/tracks - Upload new track (with auto-license linking)
```
### Verification
```
GET /api/ethos/verification - List verification requests (admin)
POST /api/ethos/verification - Submit or manage verification
```
### Licensing
```
GET /api/ethos/licensing-agreements - List agreements
POST /api/ethos/licensing-notifications - Send notifications
@ -98,6 +111,7 @@ POST /api/ethos/licensing-notifications - Send notifications
### Main Tables
**ethos_artist_profiles**
- `user_id` (PK): References user_profiles
- `for_hire`: Boolean flag for marketplace visibility
- `verified`: Verification status
@ -107,6 +121,7 @@ POST /api/ethos/licensing-notifications - Send notifications
- RLS: Users see all, own updates only
**ethos_tracks**
- `id` (PK): UUID
- `user_id`: Artist who uploaded
- `title`, `description`: Track info
@ -118,6 +133,7 @@ POST /api/ethos/licensing-notifications - Send notifications
- RLS: Public read, user write/delete own
**ethos_ecosystem_licenses**
- `id` (PK): UUID
- `track_id`: Which track accepted license
- `artist_id`: Which artist accepted
@ -125,6 +141,7 @@ POST /api/ethos/licensing-notifications - Send notifications
- RLS: User sees own, admins see all
**ethos_verification_requests**
- `id` (PK): UUID
- `user_id`: Artist requesting verification
- `status`: "pending", "approved", "rejected"
@ -133,6 +150,7 @@ POST /api/ethos/licensing-notifications - Send notifications
- RLS: Artists see own, admins see all
**ethos_service_requests**
- `id` (PK): UUID
- `artist_id`: Requested artist
- `requester_id`: Client requesting service
@ -142,7 +160,9 @@ POST /api/ethos/licensing-notifications - Send notifications
- RLS: Both parties can view, artist updates
### Storage Bucket
**ethos-tracks** (Public)
- Path: `/{user_id}/{track_id}/audio.mp3`
- RLS: Authenticated users can upload to own folder, public read, user delete own
- Policy: Users isolated to their own folder, public streaming access
@ -150,6 +170,7 @@ POST /api/ethos/licensing-notifications - Send notifications
## User Flows
### Artist Upload Flow
1. Artist goes to `/ethos/settings`
2. Clicks "Upload Track" button
3. Selects audio file
@ -164,6 +185,7 @@ POST /api/ethos/licensing-notifications - Send notifications
12. Success toast shown, track appears in library
### Verification Flow
1. Artist goes to `/ethos/settings` → "Request Verification"
2. Fills form: bio, skills, portfolio links, submission notes
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
### Marketplace Discovery Flow
1. User goes to `/nexus`
2. Clicks "Services for Hire" tab
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
### Artist Portfolio View
1. Artist goes to `/passport/me` (personal portfolio)
2. **Ethos Guild Section Shows**:
- Verified Artist badge (if applicable)
@ -214,6 +238,7 @@ POST /api/ethos/licensing-notifications - Send notifications
## Deployment Checklist
### Database
- [ ] Apply migration: `20250206_add_ethos_guild.sql`
- [ ] Apply migration: `20250210_add_ethos_artist_verification.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`
### Storage Setup
- [ ] Create Supabase Storage bucket: "ethos-tracks"
- [ ] Make bucket PUBLIC
- [ ] Apply RLS policies (see migration comments)
- [ ] Test upload from browser
### Environment Variables
```
VITE_SUPABASE_URL=https://kmdeisowhtsalsekkzqd.supabase.co
VITE_SUPABASE_ANON_KEY=<your-anon-key>
@ -239,6 +266,7 @@ SMTP_FROM_EMAIL=no-reply@aethex.tech
```
### Testing Steps
1. [ ] Create user account
2. [ ] Go to `/ethos/settings`
3. [ ] Upload track → Accept license → Fill metadata → Upload to storage
@ -257,24 +285,29 @@ SMTP_FROM_EMAIL=no-reply@aethex.tech
## Technical Details
### License Linking Logic
When a track is uploaded with `license_type: "ecosystem"`:
1. Track record created in `ethos_tracks`
2. Immediately after insert, code creates record in `ethos_ecosystem_licenses`
3. Links: track_id, artist_id, accepted_at (current timestamp)
4. This establishes the relationship between license agreement and track
### Storage Path Format
```
ethos-tracks/
{user_id}/
{track_id}/
audio.mp3
```
- User ID isolates folders for RLS
- Track ID groups related files
- Flat structure easy to manage
### Service Pricing Structure
```json
{
"price_list": {
@ -286,11 +319,13 @@ ethos-tracks/
}
}
```
- Flexible JSON for future service types
- `null` values mean service not available
- Boolean flag for custom quotes
### Verification Workflow
```
pending → admin review → approved/rejected
↓ ↓
@ -314,6 +349,7 @@ pending → admin review → approved/rejected
## Support
For issues or questions about Ethos Guild:
1. Check `/docs` section for tutorials
2. Review `/ethos/library` for example tracks
3. See `/admin` → "Ethos Verification" for status