Your music, beautifully tracked. All yours. (coming soon) teal.fm
teal-fm atproto

pfp thing init

+145 -71
+134 -62
apps/amethyst/components/actor/actorView.tsx
··· 1 - import { View, Image } from 'react-native'; 2 - import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; 3 - import { CardTitle } from '../../components/ui/card'; 4 - import { Text } from '@/components/ui/text'; 5 - import { useStore } from '@/stores/mainStore'; 1 + import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; 2 + import { Text } from "@/components/ui/text"; 3 + import { useStore } from "@/stores/mainStore"; 4 + import { Image, View } from "react-native"; 5 + import { CardTitle } from "../../components/ui/card"; 6 6 7 - import ActorPlaysView from '@/components/play/actorPlaysView'; 8 - import { Button } from '@/components/ui/button'; 9 - import { Icon } from '@/lib/icons/iconWithClassName'; 10 - import { MoreHorizontal, Pen, Plus } from 'lucide-react-native'; 11 - import { Agent } from '@atproto/api'; 12 - import { useState, useEffect } from 'react'; 13 - import EditProfileModal from './editProfileView'; 7 + import ActorPlaysView from "@/components/play/actorPlaysView"; 8 + import { Button } from "@/components/ui/button"; 9 + import { Icon } from "@/lib/icons/iconWithClassName"; 10 + import { Agent } from "@atproto/api"; 11 + import { MoreHorizontal, Pen, Plus } from "lucide-react-native"; 12 + import { useEffect, useState } from "react"; 13 + import EditProfileModal from "./editProfileView"; 14 14 15 - import { Record as ProfileRecord } from '@teal/lexicons/src/types/fm/teal/alpha/actor/profile'; 16 - import { OutputSchema as GetProfileOutputSchema } from '@teal/lexicons/src/types/fm/teal/alpha/actor/getProfile'; 17 - import getImageCdnLink from '@/lib/atp/getImageCdnLink'; 15 + import getImageCdnLink from "@/lib/atp/getImageCdnLink"; 16 + import { OutputSchema as GetProfileOutputSchema } from "@teal/lexicons/src/types/fm/teal/alpha/actor/getProfile"; 17 + import { Record as ProfileRecord } from "@teal/lexicons/src/types/fm/teal/alpha/actor/profile"; 18 + 19 + const topAlbums = [ 20 + { 21 + name: "Album 1", 22 + artist: "Artist 1", 23 + image: 24 + "https://at.uwu.wang/did:plc:tas6hj2xjrqben5653v5kohk/bafkreihxjfnq7r6tst33pf34lh6tojeh6tw6kt3p23dmhxmb2klxckjxx4", 25 + }, 26 + { 27 + name: "Album 2", 28 + artist: "Artist 2", 29 + image: 30 + "https://at.uwu.wang/did:plc:tas6hj2xjrqben5653v5kohk/bafkreihxjfnq7r6tst33pf34lh6tojeh6tw6kt3p23dmhxmb2klxckjxx4", 31 + }, 32 + { 33 + name: "Album 3", 34 + artist: "Artist 3", 35 + image: 36 + "https://at.uwu.wang/did:plc:tas6hj2xjrqben5653v5kohk/bafkreihxjfnq7r6tst33pf34lh6tojeh6tw6kt3p23dmhxmb2klxckjxx4", 37 + }, 38 + { 39 + name: "Album 4", 40 + artist: "Artist 4", 41 + image: 42 + "https://at.uwu.wang/did:plc:tas6hj2xjrqben5653v5kohk/bafkreihxjfnq7r6tst33pf34lh6tojeh6tw6kt3p23dmhxmb2klxckjxx4", 43 + }, 44 + { 45 + name: "Album 5", 46 + artist: "Artist 5", 47 + image: 48 + "https://at.uwu.wang/did:plc:tas6hj2xjrqben5653v5kohk/bafkreihxjfnq7r6tst33pf34lh6tojeh6tw6kt3p23dmhxmb2klxckjxx4", 49 + }, 50 + ]; 18 51 19 52 const GITHUB_AVATAR_URI = 20 - 'https://i.pinimg.com/originals/ef/a2/8d/efa28d18a04e7fa40ed49eeb0ab660db.jpg'; 53 + "https://i.pinimg.com/originals/ef/a2/8d/efa28d18a04e7fa40ed49eeb0ab660db.jpg"; 21 54 22 55 export interface ActorViewProps { 23 56 actorDid: string; ··· 27 60 export default function ActorView({ actorDid, pdsAgent }: ActorViewProps) { 28 61 const [isEditing, setIsEditing] = useState(false); 29 62 const [profile, setProfile] = useState< 30 - GetProfileOutputSchema['actor'] | null 63 + GetProfileOutputSchema["actor"] | null 31 64 >(null); 32 65 33 66 const tealDid = useStore((state) => state.tealDid); ··· 41 74 } 42 75 try { 43 76 let res = await pdsAgent.call( 44 - 'fm.teal.alpha.actor.getProfile', 77 + "fm.teal.alpha.actor.getProfile", 45 78 { actor: actorDid }, 46 79 {}, 47 - { headers: { 'atproto-proxy': tealDid + '#teal_fm_appview' } }, 80 + { headers: { "atproto-proxy": tealDid + "#teal_fm_appview" } } 48 81 ); 49 82 if (isMounted) { 50 - setProfile(res.data['actor'] as GetProfileOutputSchema['actor']); 83 + setProfile(res.data["actor"] as GetProfileOutputSchema["actor"]); 51 84 } 52 85 } catch (error) { 53 - console.error('Error fetching profile:', error); 86 + console.error("Error fetching profile:", error); 54 87 } 55 88 }; 56 89 ··· 61 94 }; 62 95 }, [pdsAgent, actorDid, tealDid]); 63 96 64 - const isSelf = actorDid === (pdsAgent?.did || ''); 97 + const isSelf = actorDid === (pdsAgent?.did || ""); 65 98 66 99 const handleSave = async ( 67 100 updatedProfile: { displayName: any; description: any }, 68 101 newAvatarUri: string, 69 - newBannerUri: string, 102 + newBannerUri: string 70 103 ) => { 71 104 if (!pdsAgent) { 72 105 return; 73 106 } 74 107 // Implement your save logic here (e.g., update your database or state) 75 - console.log('Saving profile:', updatedProfile, newAvatarUri, newBannerUri); 108 + console.log("Saving profile:", updatedProfile, newAvatarUri, newBannerUri); 76 109 77 110 // Update the local profile data 78 111 setProfile((prevProfile) => ({ ··· 87 120 let currentUser: ProfileRecord | undefined; 88 121 let cid: string | undefined; 89 122 try { 90 - const res = await pdsAgent.call('com.atproto.repo.getRecord', { 123 + const res = await pdsAgent.call("com.atproto.repo.getRecord", { 91 124 repo: pdsAgent.did, 92 - collection: 'fm.teal.alpha.actor.profile', 93 - rkey: 'self', 125 + collection: "fm.teal.alpha.actor.profile", 126 + rkey: "self", 94 127 }); 95 128 currentUser = res.data.value; 96 129 cid = res.data.cid; 97 130 } catch (error) { 98 - console.error('Error fetching user profile:', error); 131 + console.error("Error fetching user profile:", error); 99 132 } 100 133 101 134 // upload blobs if necessary ··· 103 136 let newBannerBlob = currentUser?.banner ?? undefined; 104 137 if (newAvatarUri) { 105 138 // if it is http/s url then do nothing 106 - if (!newAvatarUri.startsWith('http')) { 107 - console.log('Uploading avatar'); 139 + if (!newAvatarUri.startsWith("http")) { 140 + console.log("Uploading avatar"); 108 141 // its a b64 encoded data uri, decode it and get a blob 109 142 const data = await fetch(newAvatarUri).then((r) => r.blob()); 110 - const fileType = newAvatarUri.split(';')[0].split(':')[1]; 143 + const fileType = newAvatarUri.split(";")[0].split(":")[1]; 111 144 console.log(fileType); 112 145 const blob = new Blob([data], { type: fileType }); 113 146 newAvatarBlob = (await pdsAgent.uploadBlob(blob)).data.blob; 114 147 } 115 148 } 116 149 if (newBannerUri) { 117 - if (!newBannerUri.startsWith('http')) { 118 - console.log('Uploading banner'); 150 + if (!newBannerUri.startsWith("http")) { 151 + console.log("Uploading banner"); 119 152 const data = await fetch(newBannerUri).then((r) => r.blob()); 120 - const fileType = newBannerUri.split(';')[0].split(':')[1]; 153 + const fileType = newBannerUri.split(";")[0].split(":")[1]; 121 154 console.log(fileType); 122 155 const blob = new Blob([data], { type: fileType }); 123 156 newBannerBlob = (await pdsAgent.uploadBlob(blob)).data.blob; 124 157 } 125 158 } 126 159 127 - console.log('done uploading'); 160 + console.log("done uploading"); 128 161 129 162 let record: ProfileRecord = { 130 163 displayName: updatedProfile.displayName, ··· 137 170 138 171 if (cid) { 139 172 post = await pdsAgent.call( 140 - 'com.atproto.repo.putRecord', 173 + "com.atproto.repo.putRecord", 141 174 {}, 142 175 { 143 176 repo: pdsAgent.did, 144 - collection: 'fm.teal.alpha.actor.profile', 145 - rkey: 'self', 177 + collection: "fm.teal.alpha.actor.profile", 178 + rkey: "self", 146 179 record, 147 180 swapRecord: cid, 148 - }, 181 + } 149 182 ); 150 183 } else { 151 184 post = await pdsAgent.call( 152 - 'com.atproto.repo.createRecord', 185 + "com.atproto.repo.createRecord", 153 186 {}, 154 187 { 155 188 repo: pdsAgent.did, 156 - collection: 'fm.teal.alpha.actor.profile', 157 - rkey: 'self', 189 + collection: "fm.teal.alpha.actor.profile", 190 + rkey: "self", 158 191 record, 159 - }, 192 + } 160 193 ); 161 194 } 162 195 ··· 184 217 <View className="flex flex-col items-left justify-start text-left max-w-2xl w-screen gap-1 p-4 px-8"> 185 218 <View className="flex flex-row justify-between items-center"> 186 219 <View className="flex justify-between"> 187 - <Avatar alt="Rick Sanchez's Avatar" className="w-24 h-24"> 188 - <AvatarImage 189 - source={{ 190 - uri: 191 - (profile.avatar && 192 - getImageCdnLink({ 193 - did: profile.did!, 194 - hash: profile.avatar, 195 - })) || 196 - GITHUB_AVATAR_URI, 197 - }} 198 - /> 199 - <AvatarFallback> 200 - <Text>{profile.displayName?.substring(0, 1) ?? 'R'}</Text> 201 - </AvatarFallback> 202 - </Avatar> 220 + <View className="group"> 221 + <Avatar alt="Rick Sanchez's Avatar" className="w-24 h-24"> 222 + <AvatarImage 223 + source={{ 224 + uri: 225 + (profile.avatar && 226 + getImageCdnLink({ 227 + did: profile.did!, 228 + hash: profile.avatar, 229 + })) || 230 + GITHUB_AVATAR_URI, 231 + }} 232 + /> 233 + <AvatarFallback> 234 + <Text>{profile.displayName?.substring(0, 1) ?? "R"}</Text> 235 + </AvatarFallback> 236 + </Avatar> 237 + <View> 238 + {topAlbums.map((album, i: number) => ( 239 + <View 240 + key={album.name} 241 + className={`absolute ${ 242 + i === 0 243 + ? // #1, middle, yellow border 244 + `-z-10 group-hover:-translate-y-[9.3rem] group-hover:translate-x-5 group-hover:rotate-0 group-hover:bg-yellow-200 group-hover:p-[0.125rem]` 245 + : i === 1 246 + ? // #2, left of #1, silver border 247 + `-z-[11] group-hover:-translate-y-[8.8rem] group-hover:-translate-x-2 group-hover:-rotate-12 group-hover:bg-stone-200 group-hover:p-[0.125rem]` 248 + : i === 2 249 + ? // #3, right of #1, brown border 250 + `-z-[11] group-hover:-translate-y-[8.8rem] group-hover:translate-x-[3.25rem] group-hover:rotate-12 group-hover:bg-yellow-900 group-hover:p-[0.125rem]` 251 + : i === 3 252 + ? // #4, very left 253 + `-z-[12] group-hover:-translate-x-8 group-hover:-translate-y-[7.5rem] group-hover:-rotate-45` 254 + : // #5, very right 255 + `-z-[12] group-hover:-translate-y-[7.5rem] group-hover:translate-x-[5.5rem] group-hover:rotate-45` 256 + } -translate-y-[5.6rem] translate-x-5 rotate-0 rounded-xl opacity-0 transition-all duration-300 group-hover:opacity-100`} 257 + > 258 + {album.image ? ( 259 + <Image 260 + source={{ 261 + uri: album.image, 262 + width: 50, 263 + height: 50, 264 + }} 265 + alt={`${album.name} by ${album.artist}`} 266 + className="rounded-xl" 267 + /> 268 + ) : ( 269 + <Text>XD</Text> 270 + )} 271 + </View> 272 + ))} 273 + </View> 274 + </View> 203 275 <CardTitle className="text-left flex w-full justify-between mt-2"> 204 - {profile.displayName ?? ' Richard'} 276 + {profile.displayName ?? " Richard"} 205 277 </CardTitle> 206 278 </View> 207 279 <View className="mt-2 flex-row gap-2"> ··· 236 308 </View> 237 309 <View> 238 310 {profile 239 - ? profile.description?.split('\n').map((str, i) => ( 311 + ? profile.description?.split("\n").map((str, i) => ( 240 312 <Text 241 313 className="text-start self-start place-self-start" 242 314 key={i} ··· 244 316 {str} 245 317 </Text> 246 318 )) || <Text>'A very mysterious person'</Text> 247 - : 'Loading...'} 319 + : "Loading..."} 248 320 </View> 249 321 </View> 250 322 <View className="max-w-2xl w-full gap-4 py-4 pl-8">
+10 -8
compose.yaml
··· 1 1 services: 2 - aqua-api: 3 - image: ghcr.io/teal-fm/aqua:latest 4 - container_name: aqua-api 5 - # pass through db.sqlite 6 - volumes: 7 - - ./db.sqlite:/app/db.sqlite 2 + cadet: 3 + image: ghcr.io/mmattbtw/cadet:asdf 8 4 ports: 9 - - "3000:3000" 5 + - "9000:9000" 10 6 env_file: 11 - - .env 7 + - .env 8 + satellite: 9 + image: ghcr.io/mmattbtw/satellite:asdf 10 + ports: 11 + - "3132:3000" 12 + env_file: 13 + - .env
+1 -1
package.json
··· 2 2 "name": "teal", 3 3 "private": true, 4 4 "version": "0.0.0", 5 - "packageManager": "pnpm@9.15.0+sha256.09a8fe31a34fda706354680619f4002f4ccef6dadff93240d24ef6c831f0fd28", 5 + "packageManager": "pnpm@10.10.0", 6 6 "scripts": { 7 7 "dev": "turbo dev", 8 8 "build": "pnpm turbo run build --filter='./packages/*' --filter='./apps/*'",