;; Profile and actor timeline components (defcomp ~federation-actor-profile-header (&key avatar display-name username domain summary follow) (div :class "bg-white rounded-lg shadow-sm border border-stone-200 p-6 mb-6" (div :class "flex items-center gap-4" avatar (div :class "flex-1" (h1 :class "text-xl font-bold" display-name) (div :class "text-stone-500" "@" username "@" domain) summary) follow))) (defcomp ~federation-actor-timeline-layout (&key header timeline) header (div :id "timeline" timeline)) (defcomp ~federation-follow-form (&key action csrf actor-url label cls) (div :class "flex-shrink-0" (form :method "post" :action action (input :type "hidden" :name "csrf_token" :value csrf) (input :type "hidden" :name "actor_url" :value actor-url) (button :type "submit" :class cls label)))) (defcomp ~federation-profile-summary (&key summary) (div :class "text-sm text-stone-600 mt-2" (~rich-text :html summary))) ;; Public profile page (defcomp ~federation-activity-obj-type (&key obj-type) (span :class "text-sm text-stone-500" obj-type)) (defcomp ~federation-activity-card (&key activity-type published obj-type) (div :class "bg-white rounded-lg shadow p-4" (div :class "flex justify-between items-start" (span :class "font-medium" activity-type) (span :class "text-sm text-stone-400" published)) obj-type)) (defcomp ~federation-activities-list (&key items) (div :class "space-y-4" items)) (defcomp ~federation-activities-empty () (p :class "text-stone-500" "No activities yet.")) (defcomp ~federation-profile-page (&key display-name username domain summary activities-heading activities) (div :class "py-8" (div :class "bg-white rounded-lg shadow p-6 mb-6" (h1 :class "text-2xl font-bold" display-name) (p :class "text-stone-500" "@" username "@" domain) summary) (h2 :class "text-xl font-bold mb-4" activities-heading) activities)) (defcomp ~federation-profile-summary-text (&key text) (p :class "mt-2" text)) ;; Assembled actor timeline content — replaces Python _actor_timeline_content_sx (defcomp ~federation-actor-timeline-content (&key remote-actor items is-following actor) (let* ((display-name (or (get remote-actor "display_name") (get remote-actor "preferred_username") "")) (icon-url (get remote-actor "icon_url")) (summary (get remote-actor "summary")) (actor-url (or (get remote-actor "actor_url") "")) (csrf (csrf-token)) (initial (if (and (not icon-url) display-name) (upper (slice display-name 0 1)) "?"))) (~federation-actor-timeline-layout :header (~federation-actor-profile-header :avatar (~avatar :src icon-url :cls (if icon-url "w-16 h-16 rounded-full" "w-16 h-16 rounded-full bg-stone-300 flex items-center justify-center text-stone-600 font-bold text-xl") :initial (when (not icon-url) initial)) :display-name (escape display-name) :username (escape (or (get remote-actor "preferred_username") "")) :domain (escape (or (get remote-actor "domain") "")) :summary (when summary (~federation-profile-summary :summary summary)) :follow (when actor (if is-following (~federation-follow-form :action (url-for "social.unfollow") :csrf csrf :actor-url actor-url :label "Unfollow" :cls "border border-stone-300 rounded px-4 py-2 hover:bg-stone-100") (~federation-follow-form :action (url-for "social.follow") :csrf csrf :actor-url actor-url :label "Follow" :cls "bg-stone-800 text-white rounded px-4 py-2 hover:bg-stone-700")))) :timeline (~federation-timeline-items :items items :timeline-type "actor" :actor actor :next-url (when (not (empty? items)) (url-for "social.actor_timeline_page" :id (get remote-actor "id") :before (get (last items) "before_cursor"))))))) ;; Data-driven activities list (replaces Python loop in render_profile_page) (defcomp ~federation-activities-from-data (&key activities) (if (empty? (or activities (list))) (~federation-activities-empty) (~federation-activities-list :items (<> (map (lambda (a) (~federation-activity-card :activity-type (get a "activity_type") :published (get a "published") :obj-type (when (get a "object_type") (~federation-activity-obj-type :obj-type (get a "object_type"))))) activities)))))