this repo has no description
2
fork

Configure Feed

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

Add master owner form

Hilke Ros 6a3d1326 539a80cb

+205 -6
+70
app/api/master-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 records = await lexClient.list(ch.indiemusi.alpha.actor.masterOwner, { 19 + limit: 10, 20 + repo: session.did, 21 + }) 22 + 23 + if (records.records.length > 0) { 24 + const record = records.records[0]; 25 + console.log("Fetched master owner records:", record.value); 26 + return NextResponse.json({ 27 + success: true, 28 + masterOwner: record.value, 29 + uri: record.uri, 30 + }); 31 + } 32 + 33 + return NextResponse.json({ 34 + success: true, 35 + masterOwner: null, 36 + }); 37 + } catch (error) { 38 + console.error("Failed to fetch master owner:", error); 39 + return NextResponse.json( 40 + { error: "Failed to fetch master 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 { name } = await request.json(); 53 + 54 + if (!name || typeof name !== "string") { 55 + return NextResponse.json({ error: "Name is required" }, { status: 400 }); 56 + } 57 + 58 + const client = await getOAuthClient(); 59 + const oauthSession = await client.restore(session.did); 60 + const lexClient = new Client(oauthSession); 61 + 62 + const createdData = { name }; 63 + const res = await lexClient.create(ch.indiemusi.alpha.actor.masterOwner, createdData); 64 + 65 + return NextResponse.json({ 66 + success: true, 67 + uri: res.uri, 68 + masterOwner: createdData, 69 + }); 70 + }
+4 -4
app/page.tsx
··· 10 10 11 11 return ( 12 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"> 13 + <div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-8 sm:py-12"> 14 14 <div className="text-center mb-8"> 15 15 <h1 className="text-3xl font-bold text-zinc-900 dark:text-zinc-100 mb-2"> 16 16 indiemusi.ch ··· 20 20 </p> 21 21 </div> 22 22 23 - <div className="bg-white dark:bg-zinc-900 rounded-lg border border-zinc-200 dark:border-zinc-800 p-6"> 23 + <div className="bg-white dark:bg-zinc-900 rounded-lg border border-zinc-200 dark:border-zinc-800 p-6 sm:p-8"> 24 24 {session ? ( 25 25 <div className="space-y-4"> 26 26 <div className="flex items-center justify-between mb-4"> 27 - <p className="text-sm text-zinc-500 dark:text-zinc-400"> 28 - Signed in 27 + <p className="text-sm text-zinc-600 dark:text-zinc-300"> 28 + {session.did} 29 29 </p> 30 30 <LogoutButton /> 31 31 </div>
+117
components/MasterOwnerForm.tsx
··· 1 + "use client"; 2 + 3 + import { useState, useEffect } from "react"; 4 + import { useRouter } from "next/navigation"; 5 + 6 + interface MasterOwnerProfile { 7 + name: string; 8 + } 9 + 10 + export function MasterOwnerForm() { 11 + const router = useRouter(); 12 + const [name, setName] = useState(""); 13 + const [loading, setLoading] = useState(false); 14 + const [error, setError] = useState<string | null>(null); 15 + const [existingOwner, setExistingOwner] = useState<MasterOwnerProfile | null>(null); 16 + const [isLoadingOwner, setIsLoadingOwner] = useState(true); 17 + 18 + // Fetch existing master owner profile on mount 19 + useEffect(() => { 20 + async function fetchOwner() { 21 + try { 22 + const res = await fetch("/api/master-owner"); 23 + if (!res.ok) { 24 + throw new Error("Failed to fetch master owner"); 25 + } 26 + const data = await res.json(); 27 + if (data.masterOwner) { 28 + setExistingOwner(data.masterOwner); 29 + } 30 + } catch (err) { 31 + console.error("Failed to fetch master owner:", err); 32 + } finally { 33 + setIsLoadingOwner(false); 34 + } 35 + } 36 + 37 + fetchOwner(); 38 + }, []); 39 + 40 + async function handleSubmit(e: React.FormEvent) { 41 + e.preventDefault(); 42 + setLoading(true); 43 + setError(null); 44 + 45 + try { 46 + const res = await fetch("/api/master-owner", { 47 + method: "POST", 48 + headers: { "Content-Type": "application/json" }, 49 + body: JSON.stringify({ name }), 50 + }); 51 + 52 + if (!res.ok) { 53 + throw new Error("Failed to update master owner"); 54 + } 55 + 56 + const data = await res.json(); 57 + if (data.masterOwner) { 58 + setExistingOwner(data.masterOwner); 59 + } 60 + 61 + router.refresh(); 62 + } catch (err) { 63 + console.error("Failed to update master owner:", err); 64 + } finally { 65 + setLoading(false); 66 + } 67 + } 68 + 69 + // Show loading state while fetching owner 70 + if (isLoadingOwner) { 71 + return <div className="text-zinc-500">Loading master owner profile...</div>; 72 + } 73 + 74 + // Show existing owner profile 75 + if (existingOwner) { 76 + return ( 77 + <div className="space-y-4"> 78 + <div className="p-4 bg-green-50 dark:bg-green-900/20 rounded-lg border border-green-200 dark:border-green-800"> 79 + <h3 className="text-sm font-medium text-green-900 dark:text-green-100 mb-2"> 80 + Master Owner Profile Found 81 + </h3> 82 + <p className="text-sm text-green-800 dark:text-green-200"> 83 + <strong>Name:</strong> {existingOwner.name} 84 + </p> 85 + </div> 86 + </div> 87 + ); 88 + } 89 + 90 + // Show form if no existing owner 91 + return ( 92 + <form onSubmit={handleSubmit} className="space-y-4"> 93 + <div> 94 + <label className="block text-sm font-medium text-zinc-700 dark:text-zinc-300 mb-1"> 95 + Master owner name 96 + </label> 97 + <input 98 + type="text" 99 + value={name} 100 + onChange={(e) => setName(e.target.value)} 101 + 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" 102 + disabled={loading} 103 + /> 104 + </div> 105 + 106 + {error && <p className="text-red-500 text-sm">{error}</p>} 107 + 108 + <button 109 + type="submit" 110 + disabled={loading || !name} 111 + className="w-full py-2 px-4 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50" 112 + > 113 + {loading ? "Saving..." : "Save Master Owner"} 114 + </button> 115 + </form> 116 + ); 117 + }
+13 -1
components/ProfileTabs.tsx
··· 3 3 import { useState } from "react"; 4 4 import { ArtistForm } from "@/components/ArtistForm"; 5 5 import { PublishingOwnerForm } from "@/components/PublishingOwnerForm"; 6 + import { MasterOwnerForm } from "@/components/MasterOwnerForm"; 6 7 7 8 export function ProfileTabs() { 8 - const [activeTab, setActiveTab] = useState<"artist" | "owner">("artist"); 9 + const [activeTab, setActiveTab] = useState<"artist" | "owner" | "master">("artist"); 9 10 10 11 return ( 11 12 <div className="space-y-4"> ··· 30 31 > 31 32 Publishing Owner 32 33 </button> 34 + <button 35 + onClick={() => setActiveTab("master")} 36 + className={`px-4 py-2 text-sm font-medium border-b-2 transition-colors ${ 37 + activeTab === "master" 38 + ? "border-blue-600 text-blue-600 dark:text-blue-400" 39 + : "border-transparent text-zinc-600 dark:text-zinc-400 hover:text-zinc-900 dark:hover:text-zinc-200" 40 + }`} 41 + > 42 + Master Owner 43 + </button> 33 44 </div> 34 45 35 46 <div> 36 47 {activeTab === "artist" && <ArtistForm />} 37 48 {activeTab === "owner" && <PublishingOwnerForm />} 49 + {activeTab === "master" && <MasterOwnerForm />} 38 50 </div> 39 51 </div> 40 52 );
+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 repo:ch.indiemusi.alpha.actor.publishingOwner"; 11 + export const SCOPE = "atproto repo:ch.indiemusi.alpha.actor.artist repo:ch.indiemusi.alpha.actor.publishingOwner repo:ch.indiemusi.alpha.actor.masterOwner"; 12 12 13 13 // Use globalThis to persist across Next.js hot reloads 14 14 const globalAuth = globalThis as unknown as {