Highly ambitious ATProtocol AppView service and sdks
0
fork

Configure Feed

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

fix avatar input on the profile settings form

+148 -58
+1 -1
frontend/src/features/docs/templates/DocsIndexPage.tsx
··· 15 15 export function DocsIndexPage({ docs, currentUser }: DocsIndexPageProps) { 16 16 return ( 17 17 <Layout title="Documentation - Slices" currentUser={currentUser}> 18 - <div className="max-w-4xl mx-auto py-8 px-4"> 18 + <div className="py-8 px-4"> 19 19 <div className="mb-8"> 20 20 <h1 className="text-3xl font-bold text-zinc-900 mb-2"> 21 21 Documentation
+1 -1
frontend/src/features/docs/templates/DocsPage.tsx
··· 18 18 export function DocsPage({ title, content, docs, currentSlug, currentUser }: DocsPageProps) { 19 19 return ( 20 20 <Layout title={`${title} - Slices`} currentUser={currentUser}> 21 - <div className="max-w-6xl mx-auto py-4 sm:py-8 px-4"> 21 + <div className="py-4 sm:py-8 px-4"> 22 22 {/* Mobile navigation dropdown */} 23 23 <div className="sm:hidden mb-6"> 24 24 <label htmlFor="docs-nav" className="block text-sm font-medium text-zinc-700 mb-2">
+4 -1
frontend/src/features/settings/handlers.tsx
··· 6 6 import { buildAtUri } from "../../utils/at-uri.ts"; 7 7 import type { SocialSlicesActorProfile } from "../../client.ts"; 8 8 import { SettingsPage } from "./templates/SettingsPage.tsx"; 9 + import { recordBlobToCdnUrl } from "@slices/client"; 9 10 10 11 async function handleSettingsPage(req: Request): Promise<Response> { 11 12 const context = await withAuth(req); ··· 39 40 profile = { 40 41 displayName: profileRecord.value.displayName, 41 42 description: profileRecord.value.description, 42 - avatar: profileRecord.value.avatar?.toString(), 43 + avatar: profileRecord.value.avatar 44 + ? recordBlobToCdnUrl(profileRecord, profileRecord.value.avatar, "avatar") 45 + : undefined, 43 46 }; 44 47 } 45 48 } catch (error) {
+4 -12
frontend/src/features/settings/templates/fragments/SettingsForm.tsx
··· 1 1 import { Input } from "../../../../shared/fragments/Input.tsx"; 2 2 import { Textarea } from "../../../../shared/fragments/Textarea.tsx"; 3 3 import { Button } from "../../../../shared/fragments/Button.tsx"; 4 + import { AvatarInput } from "../../../../shared/fragments/AvatarInput.tsx"; 4 5 5 6 interface SettingsFormProps { 6 7 profile?: { 7 8 displayName?: string; 8 9 description?: string; 9 10 avatar?: string; 11 + handle?: string; 10 12 }; 11 13 } 12 14 ··· 24 26 hx-encoding="multipart/form-data" 25 27 className="space-y-4" 26 28 > 29 + <AvatarInput profile={profile} /> 30 + 27 31 <Input 28 32 type="text" 29 33 name="displayName" ··· 39 43 placeholder="Free-form profile description text" 40 44 defaultValue={profile?.description || ""} 41 45 /> 42 - 43 - <div> 44 - <label className="block text-sm font-medium text-zinc-700 mb-2"> 45 - Avatar 46 - </label> 47 - <input 48 - type="file" 49 - name="avatar" 50 - accept="image/*" 51 - className="block w-full border border-zinc-300 rounded-md px-3 py-2" 52 - /> 53 - </div> 54 46 55 47 <Button 56 48 type="submit"
+36 -21
frontend/src/routes/middleware.ts
··· 1 1 import { sessionStore, atprotoClient } from "../config.ts"; 2 2 import { recordBlobToCdnUrl } from "@slices/client"; 3 + import { getSliceActor } from "../lib/api.ts"; 3 4 4 5 export interface AuthenticatedUser { 5 6 handle?: string; 6 7 sub?: string; 7 8 isAuthenticated: boolean; 8 9 avatar?: string; 10 + displayName?: string; 9 11 } 10 12 11 13 export interface RouteContext { ··· 16 18 // Get current user info from session store 17 19 const currentUser = await sessionStore.getCurrentUser(req); 18 20 19 - // If user is authenticated, try to fetch their Bluesky profile avatar 21 + // If user is authenticated, try to fetch their profile data 20 22 if (currentUser.isAuthenticated && currentUser.sub) { 21 23 try { 22 - // Try to get the user's Bluesky profile from external collections 23 - const profileRecords = 24 - await atprotoClient.app.bsky.actor.profile.getRecords({ 25 - where: { 26 - did: { eq: currentUser.sub }, 27 - }, 28 - limit: 1, 29 - }); 24 + // Get the user's profile from network.slices.actor.profile 25 + const profile = await getSliceActor(atprotoClient, currentUser.sub); 26 + if (profile) { 27 + currentUser.displayName = profile.displayName; 28 + currentUser.avatar = profile.avatar; 29 + } 30 + } catch (error) { 31 + console.log("Could not fetch user profile:", error); 32 + // Continue without profile data - this is non-critical 33 + } 34 + 35 + // Fallback to Bluesky profile for avatar if not found in slices profile 36 + if (!currentUser.avatar) { 37 + try { 38 + const profileRecords = 39 + await atprotoClient.app.bsky.actor.profile.getRecords({ 40 + where: { 41 + did: { eq: currentUser.sub }, 42 + }, 43 + limit: 1, 44 + }); 30 45 31 - if (profileRecords.records && profileRecords.records.length > 0) { 32 - const profileRecord = profileRecords.records[0]; 33 - if (profileRecord.value.avatar) { 34 - // Convert BlobRef to CDN URL for avatar 35 - currentUser.avatar = recordBlobToCdnUrl( 36 - profileRecord, 37 - profileRecord.value.avatar, 38 - "avatar" 39 - ); 46 + if (profileRecords.records && profileRecords.records.length > 0) { 47 + const profileRecord = profileRecords.records[0]; 48 + if (profileRecord.value.avatar) { 49 + currentUser.avatar = recordBlobToCdnUrl( 50 + profileRecord, 51 + profileRecord.value.avatar, 52 + "avatar" 53 + ); 54 + } 40 55 } 56 + } catch (error) { 57 + console.log("Could not fetch user avatar:", error); 58 + // Continue without avatar - this is non-critical 41 59 } 42 - } catch (error) { 43 - console.log("Could not fetch user avatar:", error); 44 - // Continue without avatar - this is non-critical 45 60 } 46 61 } 47 62
+54
frontend/src/shared/fragments/AvatarInput.tsx
··· 1 + import { ActorAvatar } from "./ActorAvatar.tsx"; 2 + 3 + interface AvatarInputProps { 4 + profile?: { 5 + displayName?: string; 6 + description?: string; 7 + avatar?: string; 8 + handle?: string; 9 + }; 10 + } 11 + 12 + export function AvatarInput({ profile }: AvatarInputProps) { 13 + return ( 14 + <div> 15 + <label className="block text-sm font-medium text-zinc-700 mb-2"> 16 + Avatar 17 + </label> 18 + <label htmlFor="avatar" className="cursor-pointer"> 19 + <div className="border rounded-full border-zinc-300 w-16 h-16 mx-auto mb-2 relative hover:border-zinc-400 transition-colors"> 20 + <div className="absolute bottom-0 right-0 bg-zinc-800 rounded-full w-5 h-5 flex items-center justify-center z-10"> 21 + <svg className="w-3 h-3 text-white" fill="currentColor" viewBox="0 0 20 20"> 22 + <path fillRule="evenodd" d="M4 5a2 2 0 00-2 2v8a2 2 0 002 2h12a2 2 0 002-2V7a2 2 0 00-2-2h-1.586a1 1 0 01-.707-.293l-1.121-1.121A2 2 0 0011.172 3H8.828a2 2 0 00-1.414.586L6.293 4.707A1 1 0 015.586 5H4zm6 9a3 3 0 100-6 3 3 0 000 6z" clipRule="evenodd" /> 23 + </svg> 24 + </div> 25 + <div id="image-preview" className="w-full h-full rounded-full overflow-hidden"> 26 + {profile ? ( 27 + <ActorAvatar profile={profile} size={64} className="w-full h-full" /> 28 + ) : ( 29 + <div className="w-full h-full bg-zinc-100 flex items-center justify-center"> 30 + <svg className="w-8 h-8 text-zinc-400" fill="currentColor" viewBox="0 0 20 20"> 31 + <path fillRule="evenodd" d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z" clipRule="evenodd" /> 32 + </svg> 33 + </div> 34 + )} 35 + </div> 36 + </div> 37 + </label> 38 + <input 39 + type="file" 40 + id="avatar" 41 + name="avatar" 42 + accept="image/*" 43 + className="hidden" 44 + _="on change 45 + if my.files.length > 0 46 + set file to my.files[0] 47 + call URL.createObjectURL(file) 48 + set imageUrl to the result 49 + put `<img src='${imageUrl}' class='w-full h-full object-cover rounded-full' alt='Avatar preview' />` into #image-preview 50 + end" 51 + /> 52 + </div> 53 + ); 54 + }
+48 -22
frontend/src/shared/fragments/Layout.tsx
··· 75 75 Docs 76 76 </a> 77 77 </div> 78 - <div className="flex space-x-2"> 78 + <div className="flex items-center space-x-2"> 79 79 {currentUser?.isAuthenticated ? ( 80 80 <div className="flex items-center space-x-2"> 81 81 <a 82 - href="/settings" 82 + href={`/profile/${currentUser.handle}`} 83 83 className="px-3 py-1.5 text-sm text-zinc-600 hover:text-zinc-900 hover:bg-zinc-100 rounded-md transition-colors" 84 84 > 85 - Settings 85 + Dashboard 86 86 </a> 87 - <form method="post" action="/logout" className="inline"> 88 - <button 89 - type="submit" 90 - className="px-3 py-1.5 text-sm bg-zinc-100 hover:bg-zinc-200 text-zinc-700 rounded-md transition-colors" 91 - > 92 - Sign out 93 - </button> 94 - </form> 95 - {currentUser.avatar ? ( 96 - <a href={`/profile/${currentUser.handle}`}> 87 + <div className="relative"> 88 + <button 89 + type="button" 90 + className="flex items-center p-1 rounded-full hover:bg-zinc-100 transition-colors" 91 + _="on click toggle .hidden on #avatar-dropdown 92 + on click from document 93 + if not me.contains(event.target) and not #avatar-dropdown.contains(event.target) 94 + add .hidden to #avatar-dropdown" 95 + > 96 + {currentUser.avatar ? ( 97 97 <img 98 98 src={currentUser.avatar} 99 99 alt="Profile avatar" 100 100 className="w-8 h-8 rounded-full" 101 101 /> 102 - </a> 103 - ) : ( 104 - <div className="flex items-center space-x-1"> 105 - <span className="text-sm text-zinc-600"> 106 - {currentUser.handle 107 - ? `@${currentUser.handle}` 108 - : "User"} 109 - </span> 102 + ) : ( 103 + <div className="w-8 h-8 bg-zinc-300 rounded-full flex items-center justify-center"> 104 + <span className="text-sm text-zinc-600 font-medium"> 105 + {currentUser.handle?.charAt(0).toUpperCase() || "U"} 106 + </span> 107 + </div> 108 + )} 109 + </button> 110 + 111 + <div id="avatar-dropdown" className="hidden absolute right-0 mt-2 w-64 bg-white border border-zinc-200 rounded-md shadow-lg z-50"> 112 + <div className="py-1"> 113 + <div className="px-4 py-3 border-b border-zinc-100"> 114 + <div className="text-sm font-medium text-zinc-900"> 115 + {currentUser.displayName || currentUser.handle || "User"} 116 + </div> 117 + <div className="text-sm text-zinc-500"> 118 + {currentUser.handle ? `@${currentUser.handle}` : ""} 119 + </div> 120 + </div> 121 + <a 122 + href="/settings" 123 + className="block px-4 py-2 text-sm text-zinc-700 hover:bg-zinc-100 transition-colors" 124 + > 125 + Settings 126 + </a> 127 + <form method="post" action="/logout" className="block"> 128 + <button 129 + type="submit" 130 + className="w-full text-left px-4 py-2 text-sm text-zinc-700 hover:bg-zinc-100 transition-colors" 131 + > 132 + Sign out 133 + </button> 134 + </form> 110 135 </div> 111 - )} 136 + </div> 137 + </div> 112 138 </div> 113 139 ) : ( 114 140 <div className="flex items-center space-x-2">