Prettier format pending files
This commit is contained in:
parent
a057dacf56
commit
669ab329a8
10 changed files with 325 additions and 118 deletions
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue