this repo has no description
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add publishing owner form

Hilke Ros 539a80cb 12f7fdf9

+360 -7
+4 -2
app/api/artist/route.ts
··· 60 60 const lexClient = new Client(oauthSession); 61 61 62 62 const createdAt = new Date().toISOString(); 63 - const res = await lexClient.create(ch.indiemusi.alpha.actor.artist, { 63 + const createdData = { 64 64 name, 65 65 createdAt, 66 - }); 66 + }; 67 + const res = await lexClient.create(ch.indiemusi.alpha.actor.artist, createdData); 67 68 68 69 return NextResponse.json({ 69 70 success: true, 70 71 uri: res.uri, 72 + artist: createdData, 71 73 }); 72 74 }
+92
app/api/publishing-owner/route.ts
··· 1 + import { NextRequest, NextResponse } from "next/server"; 2 + import { Client } from "@atproto/lex"; 3 + import { getSession } from "@/lib/auth/session"; 4 + import { getOAuthClient } from "@/lib/auth/client"; 5 + import * as ch from "@/src/lexicons/ch"; 6 + 7 + export async function GET(request: NextRequest) { 8 + const session = await getSession(); 9 + if (!session) { 10 + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); 11 + } 12 + 13 + try { 14 + const client = await getOAuthClient(); 15 + const oauthSession = await client.restore(session.did); 16 + const lexClient = new Client(oauthSession); 17 + 18 + const query = await lexClient.list(ch.indiemusi.alpha.actor.publishingOwner, { 19 + limit: 10, 20 + repo: session.did, 21 + }) 22 + 23 + if (query.records.length > 0) { 24 + const record = query.records[0]; 25 + console.log("Fetched publishing owner records:", record.value); 26 + return NextResponse.json({ 27 + success: true, 28 + publishingOwner: record.value, 29 + uri: record.uri, 30 + }); 31 + } 32 + 33 + return NextResponse.json({ 34 + success: true, 35 + publishingOwner: null, 36 + }); 37 + } catch (error) { 38 + console.error("Failed to fetch publishing owner:", error); 39 + return NextResponse.json( 40 + { error: "Failed to fetch publishing owner" }, 41 + { status: 500 } 42 + ); 43 + } 44 + } 45 + 46 + export async function POST(request: NextRequest) { 47 + const session = await getSession(); 48 + if (!session) { 49 + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); 50 + } 51 + 52 + const { firstName, lastName, companyName, ipi, collectingSociety } = await request.json(); 53 + 54 + if (!firstName && !lastName && !companyName) { 55 + return NextResponse.json( 56 + { error: "At least one name field is required" }, 57 + { status: 400 } 58 + ); 59 + } 60 + 61 + // Validate and clean IPI number 62 + let cleanedIpi: string | undefined; 63 + if (ipi) { 64 + cleanedIpi = ipi.replace(/\s/g, ''); 65 + 66 + if (!/^\d{11}$/.test(cleanedIpi)) { 67 + return NextResponse.json( 68 + { error: "IPI number must be exactly 11 digits" }, 69 + { status: 400 } 70 + ); 71 + } 72 + } 73 + 74 + const client = await getOAuthClient(); 75 + const oauthSession = await client.restore(session.did); 76 + const lexClient = new Client(oauthSession); 77 + 78 + const data: any = {}; 79 + if (firstName) data.firstName = firstName; 80 + if (lastName) data.lastName = lastName; 81 + if (companyName) data.companyName = companyName; 82 + if (cleanedIpi) data.ipi = cleanedIpi; 83 + if (collectingSociety) data.collectingSociety = collectingSociety; 84 + 85 + const res = await lexClient.create(ch.indiemusi.alpha.actor.publishingOwner, data); 86 + 87 + return NextResponse.json({ 88 + success: true, 89 + uri: res.uri, 90 + publishingOwner: data, 91 + }); 92 + }
+6 -4
app/page.tsx
··· 2 2 import { LoginForm } from "@/components/LoginForm"; 3 3 import { LogoutButton } from "@/components/LogoutButton"; 4 4 import { ArtistForm } from "@/components/ArtistForm"; 5 + import { PublishingOwnerForm } from "@/components/PublishingOwnerForm"; 6 + import { ProfileTabs } from "@/components/ProfileTabs"; 5 7 6 8 export default async function Home() { 7 9 const session = await getSession(); 8 10 9 11 return ( 10 - <div className="flex min-h-screen items-center justify-center bg-zinc-50 dark:bg-zinc-950"> 11 - <main className="w-full max-w-md mx-auto p-8"> 12 + <div className="min-h-screen bg-zinc-50 dark:bg-zinc-950"> 13 + <div className="max-w-md mx-auto px-8 py-12"> 12 14 <div className="text-center mb-8"> 13 15 <h1 className="text-3xl font-bold text-zinc-900 dark:text-zinc-100 mb-2"> 14 16 indiemusi.ch ··· 27 29 </p> 28 30 <LogoutButton /> 29 31 </div> 30 - <ArtistForm /> 32 + <ProfileTabs /> 31 33 </div> 32 34 ) : ( 33 35 <LoginForm /> 34 36 )} 35 37 </div> 36 - </main> 38 + </div> 37 39 </div> 38 40 ); 39 41 }
+5
components/ArtistForm.tsx
··· 54 54 throw new Error("Failed to update artist"); 55 55 } 56 56 57 + const data = await res.json(); 58 + if (data.artist) { 59 + setExistingArtist(data.artist); 60 + } 61 + 57 62 router.refresh(); 58 63 } catch (err) { 59 64 console.error("Failed to update artist:", err);
+41
components/ProfileTabs.tsx
··· 1 + "use client"; 2 + 3 + import { useState } from "react"; 4 + import { ArtistForm } from "@/components/ArtistForm"; 5 + import { PublishingOwnerForm } from "@/components/PublishingOwnerForm"; 6 + 7 + export function ProfileTabs() { 8 + const [activeTab, setActiveTab] = useState<"artist" | "owner">("artist"); 9 + 10 + return ( 11 + <div className="space-y-4"> 12 + <div className="flex gap-2 border-b border-zinc-200 dark:border-zinc-800"> 13 + <button 14 + onClick={() => setActiveTab("artist")} 15 + className={`px-4 py-2 text-sm font-medium border-b-2 transition-colors ${ 16 + activeTab === "artist" 17 + ? "border-blue-600 text-blue-600 dark:text-blue-400" 18 + : "border-transparent text-zinc-600 dark:text-zinc-400 hover:text-zinc-900 dark:hover:text-zinc-200" 19 + }`} 20 + > 21 + Artist 22 + </button> 23 + <button 24 + onClick={() => setActiveTab("owner")} 25 + className={`px-4 py-2 text-sm font-medium border-b-2 transition-colors ${ 26 + activeTab === "owner" 27 + ? "border-blue-600 text-blue-600 dark:text-blue-400" 28 + : "border-transparent text-zinc-600 dark:text-zinc-400 hover:text-zinc-900 dark:hover:text-zinc-200" 29 + }`} 30 + > 31 + Publishing Owner 32 + </button> 33 + </div> 34 + 35 + <div> 36 + {activeTab === "artist" && <ArtistForm />} 37 + {activeTab === "owner" && <PublishingOwnerForm />} 38 + </div> 39 + </div> 40 + ); 41 + }
+211
components/PublishingOwnerForm.tsx
··· 1 + "use client"; 2 + 3 + import { useState, useEffect } from "react"; 4 + import { useRouter } from "next/navigation"; 5 + 6 + interface PublishingOwnerProfile { 7 + firstName?: string; 8 + lastName?: string; 9 + companyName?: string; 10 + ipi?: string; 11 + collectingSociety?: string; 12 + } 13 + 14 + export function PublishingOwnerForm() { 15 + const router = useRouter(); 16 + const [firstName, setFirstName] = useState(""); 17 + const [lastName, setLastName] = useState(""); 18 + const [companyName, setCompanyName] = useState(""); 19 + const [ipi, setIpi] = useState(""); 20 + const [collectingSociety, setCollectingSociety] = useState(""); 21 + const [loading, setLoading] = useState(false); 22 + const [error, setError] = useState<string | null>(null); 23 + const [existingOwner, setExistingOwner] = useState<PublishingOwnerProfile | null>(null); 24 + const [isLoadingOwner, setIsLoadingOwner] = useState(true); 25 + 26 + // Fetch existing publishing owner profile on mount 27 + useEffect(() => { 28 + async function fetchOwner() { 29 + try { 30 + const res = await fetch("/api/publishing-owner"); 31 + if (!res.ok) { 32 + throw new Error("Failed to fetch publishing owner"); 33 + } 34 + const data = await res.json(); 35 + if (data.publishingOwner) { 36 + setExistingOwner(data.publishingOwner); 37 + } 38 + } catch (err) { 39 + console.error("Failed to fetch publishing owner:", err); 40 + } finally { 41 + setIsLoadingOwner(false); 42 + } 43 + } 44 + 45 + fetchOwner(); 46 + }, []); 47 + 48 + async function handleSubmit(e: React.FormEvent) { 49 + e.preventDefault(); 50 + setLoading(true); 51 + setError(null); 52 + 53 + // Validate IPI format before submitting 54 + if (ipi) { 55 + const cleanedIpi = ipi.replace(/\s/g, ''); 56 + if (!/^\d{11}$/.test(cleanedIpi)) { 57 + setError("IPI number must be exactly 11 digits"); 58 + setLoading(false); 59 + return; 60 + } 61 + } 62 + 63 + try { 64 + const res = await fetch("/api/publishing-owner", { 65 + method: "POST", 66 + headers: { "Content-Type": "application/json" }, 67 + body: JSON.stringify({ 68 + firstName, 69 + lastName, 70 + companyName, 71 + ipi, 72 + collectingSociety, 73 + }), 74 + }); 75 + 76 + if (!res.ok) { 77 + const data = await res.json(); 78 + throw new Error(data.error || "Failed to update publishing owner"); 79 + } 80 + 81 + const data = await res.json(); 82 + if (data.publishingOwner) { 83 + setExistingOwner(data.publishingOwner); 84 + } 85 + 86 + router.refresh(); 87 + } catch (err) { 88 + console.error("Failed to update publishing owner:", err); 89 + setError((err as Error).message || "Failed to save publishing owner information"); 90 + } finally { 91 + setLoading(false); 92 + } 93 + } 94 + 95 + // Show loading state while fetching owner 96 + if (isLoadingOwner) { 97 + return <div className="text-zinc-500">Loading publishing owner profile...</div>; 98 + } 99 + 100 + // Show existing owner profile 101 + if (existingOwner) { 102 + return ( 103 + <div className="space-y-4"> 104 + <div className="p-4 bg-green-50 dark:bg-green-900/20 rounded-lg border border-green-200 dark:border-green-800"> 105 + <h3 className="text-sm font-medium text-green-900 dark:text-green-100 mb-2"> 106 + Publishing Owner Profile Found 107 + </h3> 108 + <div className="text-sm text-green-800 dark:text-green-200 space-y-1"> 109 + {existingOwner.firstName && <p><strong>First Name:</strong> {existingOwner.firstName}</p>} 110 + {existingOwner.lastName && <p><strong>Last Name:</strong> {existingOwner.lastName}</p>} 111 + {existingOwner.companyName && <p><strong>Company:</strong> {existingOwner.companyName}</p>} 112 + {existingOwner.ipi && <p><strong>IPI:</strong> {existingOwner.ipi}</p>} 113 + {existingOwner.collectingSociety && <p><strong>Collecting Society:</strong> {existingOwner.collectingSociety}</p>} 114 + </div> 115 + </div> 116 + </div> 117 + ); 118 + } 119 + 120 + // Show form if no existing owner 121 + return ( 122 + <form onSubmit={handleSubmit} className="space-y-4"> 123 + <div className="grid grid-cols-2 gap-3"> 124 + <div> 125 + <label className="block text-sm font-medium text-zinc-700 dark:text-zinc-300 mb-1"> 126 + First Name 127 + </label> 128 + <input 129 + type="text" 130 + value={firstName} 131 + onChange={(e) => setFirstName(e.target.value)} 132 + className="w-full px-3 py-2 border border-zinc-300 dark:border-zinc-700 rounded-lg bg-white dark:bg-zinc-800 text-zinc-900 dark:text-zinc-100" 133 + disabled={loading} 134 + /> 135 + </div> 136 + <div> 137 + <label className="block text-sm font-medium text-zinc-700 dark:text-zinc-300 mb-1"> 138 + Last Name 139 + </label> 140 + <input 141 + type="text" 142 + value={lastName} 143 + onChange={(e) => setLastName(e.target.value)} 144 + className="w-full px-3 py-2 border border-zinc-300 dark:border-zinc-700 rounded-lg bg-white dark:bg-zinc-800 text-zinc-900 dark:text-zinc-100" 145 + disabled={loading} 146 + /> 147 + </div> 148 + </div> 149 + 150 + <div> 151 + <label className="block text-sm font-medium text-zinc-700 dark:text-zinc-300 mb-1"> 152 + Company Name 153 + </label> 154 + <input 155 + type="text" 156 + value={companyName} 157 + onChange={(e) => setCompanyName(e.target.value)} 158 + className="w-full px-3 py-2 border border-zinc-300 dark:border-zinc-700 rounded-lg bg-white dark:bg-zinc-800 text-zinc-900 dark:text-zinc-100" 159 + disabled={loading} 160 + /> 161 + </div> 162 + 163 + <div className="grid grid-cols-2 gap-3"> 164 + <div> 165 + <label className="block text-sm font-medium text-zinc-700 dark:text-zinc-300 mb-1"> 166 + IPI Number 167 + </label> 168 + <input 169 + type="text" 170 + value={ipi} 171 + onChange={(e) => setIpi(e.target.value)} 172 + className={`w-full px-3 py-2 border rounded-lg bg-white dark:bg-zinc-800 text-zinc-900 dark:text-zinc-100 ${ 173 + ipi && !/^\d{0,11}(\s*\d{0,11})*$/.test(ipi.replace(/\s/g, '')) 174 + ? "border-red-300 dark:border-red-700" 175 + : "border-zinc-300 dark:border-zinc-700" 176 + }`} 177 + disabled={loading} 178 + placeholder="11 digits" 179 + /> 180 + {ipi && !/^\d{11}$/.test(ipi.replace(/\s/g, '')) && ( 181 + <p className="text-red-500 text-xs mt-1"> 182 + Must be exactly 11 digits 183 + </p> 184 + )} 185 + </div> 186 + <div> 187 + <label className="block text-sm font-medium text-zinc-700 dark:text-zinc-300 mb-1"> 188 + Collecting Society 189 + </label> 190 + <input 191 + type="text" 192 + value={collectingSociety} 193 + onChange={(e) => setCollectingSociety(e.target.value)} 194 + className="w-full px-3 py-2 border border-zinc-300 dark:border-zinc-700 rounded-lg bg-white dark:bg-zinc-800 text-zinc-900 dark:text-zinc-100" 195 + disabled={loading} 196 + /> 197 + </div> 198 + </div> 199 + 200 + {error && <p className="text-red-500 text-sm">{error}</p>} 201 + 202 + <button 203 + type="submit" 204 + disabled={loading || (!firstName && !lastName && !companyName) || (ipi && !/^\d{11}$/.test(ipi.replace(/\s/g, '')))} 205 + className="w-full py-2 px-4 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50" 206 + > 207 + {loading ? "Saving..." : "Save Publishing Owner"} 208 + </button> 209 + </form> 210 + ); 211 + }
+1 -1
lib/auth/client.ts
··· 8 8 } from "@atproto/oauth-client-node"; 9 9 import { getDb } from "../db"; 10 10 11 - export const SCOPE = "atproto repo:ch.indiemusi.alpha.actor.artist"; 11 + export const SCOPE = "atproto repo:ch.indiemusi.alpha.actor.artist repo:ch.indiemusi.alpha.actor.publishingOwner"; 12 12 13 13 // Use globalThis to persist across Next.js hot reloads 14 14 const globalAuth = globalThis as unknown as {