commits
- Add missing CSS variables for Standard, keytrace, Aetheros, Roomy
apps (light + dark mode) — legend dots were transparent
- Remove [&_svg]:!w-full that stretched heatmap wider than its content
- Remove "Less ... More" intensity scale from legend (self-evident)
- Clean up unused i18n keys (less, more)
Connections, endorsements, and reactions all show 0 for every user.
Remove the stats section from the identity card until these have
real data behind them.
The end date input is now conditionally hidden when the current position
checkbox is active, and its value is cleared to prevent stale dates from
being saved.
Update BlueskyPostCard to extract thumbnails from resolved AppView
embeds. Handles three embed types:
- app.bsky.embed.images#view (photo posts)
- app.bsky.embed.external#view (link previews with thumbnails)
- app.bsky.embed.recordWithMedia#view (quote posts with media)
Previously images never displayed because the card only received raw
blob refs (not CDN URLs). Paired with sifa-api PR #152 which now
passes resolved embeds from the Bluesky AppView.
Add CardLink wrapper component using CSS overlay technique to make
entire activity cards clickable. URL resolution follows a 3-tier
hierarchy: per-item URL > per-user profile URL > not clickable.
- Add URL patterns and resolveCardUrl() to atproto-apps registry
- Create CardLink component with accessible overlay link
- Wrap all 6 card types (generic, Bluesky, Tangled, event RSVP,
KipClip, keytrace) in CardLink
- Remove redundant "View on X" footer links from all cards
- Internal links (Bluesky facets, keytrace profileUrl) use
relative z-10 to stay clickable above the overlay
- Pass authorHandle from feed/overview to cards for URL resolution
- Override fill as SVG attribute (not just CSS style) so colors render
- Set opacity as SVG attribute for correct intensity
- Force SVG to w-full h-auto to fill container width
- Fix first column Monday rollback logic
- Tooltip uses SVG title element inside cloneElement children
* fix(web): heatmap colors, tooltip, layout, and week start
- Use SVG fill instead of CSS backgroundColor for rect cells
- Replace broken foreignObject tooltip with native SVG title
- Set weekStart=1 for Monday-first grid
- Move legend below chart on all viewports
- Move summary stats above chart
- Fix month count calculation
- Pass 365 days on activity page for full-width chart
* fix(web): start heatmap grid on Monday so first column is always full
* chore(deps): add react-activity-calendar 2.7.12
* feat(web): add fetchHeatmapData API client and HeatmapResponse types
* feat(web): add heatmap color logic — dominant app, intensity levels, cell styling
* feat(web): add heatmap CSS custom properties for empty cells and color overrides
* feat(web): add heatmap tooltip component with per-app breakdown
* feat(web): add heatmap legend with intensity scale and app color key
* feat(web): add heatmap summary stats component
* feat(web): add mobile monthly stacked bar chart for heatmap
* feat(web): add desktop heatmap grid with custom cell rendering via react-activity-calendar
Wraps react-activity-calendar's ActivityCalendar with custom renderBlock
to apply getCellStyle colors, selected-date outline, and HeatmapTooltip
on hover. Fills date gaps for a complete 6-month range.
* feat(web): add main ActivityHeatmap wrapper with data fetching and responsive layout
Client component that fetches heatmap data via API, shows loading skeleton,
transforms data with transformHeatmapData, and renders desktop (HeatmapGrid +
HeatmapLegend) and mobile (HeatmapBars + HeatmapLegend) layouts. Summary stats
shown only for full variant. Empty state overlay when no data.
* feat(web): add compact 3-month heatmap to profile activity overview section
* feat(web): add full 6-month heatmap to activity page above feed
* fix(web): remove setLoading call from effect body
Fixes react-hooks/set-state-in-effect lint error by removing
the synchronous setLoading(true) inside useEffect.
* style: format heatmap components with prettier
Add zoomable world map to admin dashboard showing user distribution
by location. Uses Leaflet with OpenStreetMap tiles. Circle marker
size scales with user count per location.
The API doesn't return authorHandle in activity items, so the
Bluesky card was building URLs like bsky.app/profile/unknown/post/...
Now extracts the DID from the AT URI (at://did:plc:xxx/collection/rkey)
and uses it in the URL. bsky.app accepts DIDs in profile URLs.
Shows bookmarked URL as a clickable link with domain extracted as
display name. Full card shows domain + full URL + tags. Compact card
shows domain link + timestamp. "View on KipClip" footer link.
Shows verified links with platform icon, subject name, verification
checkmark, and link to the profile on keytrace.dev. Renders for
dev.keytrace.claim records.
Also: label changed from "Keytrace" to "keytrace.dev" per request.
The COLLECTION_TO_APP map was out of sync with the verified API registry:
- community.lexicon.* was mapped to smokesignal (caught kipclip bookmarks)
- com.picosky.* was wrong (should be social.psky.*)
- link.tangled.* was wrong (should be sh.tangled.*)
- app.linkat.* was wrong (should be blue.linkat.*)
Now split community.lexicon.calendar.* (smokesignal) from
community.lexicon.bookmarks.* (kipclip). Added all verified apps:
standard, aetheros, roomy, keytrace.
Adds teal (oklch hue 180) CSS custom properties for KipClip badges
and stripe colors in both light and dark mode. Registers KipClip
in the frontend app display registry.
* feat(web): extend ActiveApp type and add activity API client
- Add category, recentCount, latestRecordAt fields to ActiveApp interface
- Add fetchAppsRegistry() for cached app registry lookups
- Add updateActivityVisibility() for toggling app visibility
- Add AppRegistryEntry interface for registry API responses
- Add tangled and flashes to atproto-apps display registry
* feat(web): add generic activity card and card registry
- Add ActivityCardProps type interface
- Add card-registry with exact and prefix matching for collection-specific card components
- Add GenericActivityCard with full and compact variants:
- Left accent stripe colored per app
- Phosphor icon per activity category
- Content text extraction from record fields with fallback
- Relative timestamp formatting
- App badge pill in full variant
- Optional author display
- Dark mode support
- Add 9 tests covering both variants, fallback text, author visibility, and stripe colors
- Add activityCard translations to test setup
* feat(web): wire activeApps badges into profile page
* feat(web): add activity visibility settings page
Client-side page at /p/[handle]/settings/activity where authenticated
users can toggle which ATproto apps appear on their Sifa profile.
Uses the existing updateActivityVisibility API client function.
* feat(web): add GDPR profile removal request page
No-auth-required form at /privacy/removal for GDPR Art. 17 erasure
requests. Calls POST /api/privacy/suppress (placeholder endpoint,
API implementation follows in a separate task).
* feat(web): add Tangled custom activity card
* feat(web): replace activity overview placeholder with live teaser cards
Fetch teaser data from GET /api/activity/:handle/teaser on mount and
render up to 5 compact activity cards using the card registry with
GenericActivityCard fallback. Section returns null when no data is
available. Full-width "View full activity" CTA links to the activity
page.
* feat(web): add Bluesky custom activity card
Render Bluesky posts with rich text facets (links as anchors, mentions
as bold), reply labels, image thumbnails, and "View on Bluesky" deep
link. Compact variant shows truncated text with Butterfly icon.
Registered as app.bsky.feed.post in card registry.
* fix: resolve lint errors (setState in effect, redundant roles, img elements)
* feat(activity): hide category tabs when single category (#330)
Only show category filter tabs when the API reports more than one
available category. Also filters the tab list to only show categories
that actually have content, removing empty category tabs.
Closes #327
* fix(activity): show all tabs when availableCategories is empty/missing (#332)
When the API returns an empty availableCategories array or the field is
absent (backwards compatibility), show all category tabs instead of
hiding them. Only hide tabs when the API explicitly reports a single
category.
* feat(activity): enrich event RSVP cards with event details (#333)
* fix(activity): show all tabs when availableCategories is empty/missing
When the API returns an empty availableCategories array or the field is
absent (backwards compatibility), show all category tabs instead of
hiding them. Only hide tabs when the API explicitly reports a single
category.
* feat(activity): map community.lexicon to Smoke Signal in frontend
* feat(activity): add EventRsvpCard component for enriched event display
* feat(activity): register EventRsvpCard for community.lexicon.calendar.rsvp
Add buildBlobUrl helper to construct PDS blob URLs from DIDs and CIDs.
Add extractImageBlob to the generic activity card that finds image blob
references across common ATproto record shapes (image, thumbnail,
images array, embed.images). Renders the image in full (non-compact)
cards with lazy loading and graceful error hiding for unreachable PDS.
* feat: add My Network page (#317)
- Add /my-network page showing who the user follows, grouped by
Sifa profiles vs external follows
- Add fetchFollowing() API client function
- Add onUnfollow prop to SuggestionCard for unfollow action
- Add My Network link to site header (desktop) and mobile nav
- Update Find People follow toast to link to My Network
- Add i18n string for myNetwork
- Source filter tabs (All/Sifa/Bluesky/Tangled) with pagination
- Empty state with Find People CTA
Closes #317
* refactor: extract followUser/unfollowUser into API helpers
Move inline fetch calls for follow/unfollow into reusable functions
in api.ts, matching the pattern of other API helpers. Use avatarUrl
field name for consistency with API response.
* fix: use avatarUrl field name in FollowProfile interface
Align FollowProfile interface with API response field name (avatarUrl)
for consistency with SuggestionProfile and the API endpoint.
Migrate ATproto app colors from scattered Tailwind classes and hardcoded
hex values to centralized CSS custom properties in globals.css. Each app
now defines --app-{id}-stripe, --app-{id}-badge-bg, and
--app-{id}-badge-text variables in both light and dark mode using oklch
to match the existing shadcn theme convention.
- globals.css: add app color variables for 9 apps + fallback in :root and .dark
- atproto-apps.ts: replace hardcoded Tailwind classes with CSS var references,
export getAppStripeColor() helper
- generic-activity-card.tsx: remove STRIPE_COLORS map, use getAppStripeColor()
- bluesky-post-card.tsx: replace hardcoded #0285c7 with getAppStripeColor()
- tangled-card.tsx: replace hardcoded stripe/badge constants with centralized vars
- Update tests to expect CSS variable strings instead of resolved RGB values
The synthetic Bluesky entry now uses the PDS detection logic from the
identity card. Shows the correct PDS icon (Bluesky butterfly, EuroSky,
favicon for self-hosted) and a contextual label:
- Bluesky users: "Bluesky (@handle)"
- Self-hosted PDS: "Self-hosted ATProto (@handle)"
- Other providers: "EuroSky (@handle)", etc.
Previously hardcoded the Butterfly icon and "@handle" label for all
users, which was misleading for self-hosted PDS users.
The synthetic Bluesky entry had mb-2 (8px) margin below it while
the EditableSection items use space-y-4 (16px), causing inconsistent
spacing. Changed to mb-4 to match.
Updates privacy policy with:
- Cross-app activity processing disclosure (Art. 14)
- Legal basis statement (legitimate interest)
- Unclaimed profiles: no activity data collected
- Activity visibility controls explanation
- Profile removal mechanism (links to /privacy/removal)
- GDPR data subject rights section
The backend seed database includes community and security categories
but the frontend only recognized 5 categories, causing skills in those
categories to fall into "Other". Now recognizes all 7 backend categories.
Note: 323 unresolved skills still need canonical mappings in sifa-api
to fully fix the "Other" catch-all problem.
Closes #316
Replace the ambiguous "Mentoring" checkbox with two distinct options:
- "Mentoring others" — open to mentoring others
- "Being mentored" — open to being mentored
Includes backwards compatibility: old `id.sifa.defs#mentoring` values
still display as "Mentoring others" on existing profiles.
Closes #311
* feat(web): extend ActiveApp type and add activity API client
- Add category, recentCount, latestRecordAt fields to ActiveApp interface
- Add fetchAppsRegistry() for cached app registry lookups
- Add updateActivityVisibility() for toggling app visibility
- Add AppRegistryEntry interface for registry API responses
- Add tangled and flashes to atproto-apps display registry
* feat(web): add generic activity card and card registry
- Add ActivityCardProps type interface
- Add card-registry with exact and prefix matching for collection-specific card components
- Add GenericActivityCard with full and compact variants:
- Left accent stripe colored per app
- Phosphor icon per activity category
- Content text extraction from record fields with fallback
- Relative timestamp formatting
- App badge pill in full variant
- Optional author display
- Dark mode support
- Add 9 tests covering both variants, fallback text, author visibility, and stripe colors
- Add activityCard translations to test setup
* feat(web): wire activeApps badges into profile page
* feat(web): add activity visibility settings page
Client-side page at /p/[handle]/settings/activity where authenticated
users can toggle which ATproto apps appear on their Sifa profile.
Uses the existing updateActivityVisibility API client function.
* feat(web): add GDPR profile removal request page
No-auth-required form at /privacy/removal for GDPR Art. 17 erasure
requests. Calls POST /api/privacy/suppress (placeholder endpoint,
API implementation follows in a separate task).
* feat(web): add Tangled custom activity card
* feat(web): replace activity overview placeholder with live teaser cards
Fetch teaser data from GET /api/activity/:handle/teaser on mount and
render up to 5 compact activity cards using the card registry with
GenericActivityCard fallback. Section returns null when no data is
available. Full-width "View full activity" CTA links to the activity
page.
* feat(web): add Bluesky custom activity card
Render Bluesky posts with rich text facets (links as anchors, mentions
as bold), reply labels, image thumbnails, and "View on Bluesky" deep
link. Compact variant shows truncated text with Butterfly icon.
Registered as app.bsky.feed.post in card registry.
* fix: resolve lint errors (setState in effect, redundant roles, img elements)
The completion bar was checking `verifiedAccounts` (third-party verified
accounts) instead of `externalAccounts` (user-added websites/services),
causing the "Add a website or verification" suggestion to persist after
a user adds a website.
Also adds loading state to import wizard confirm button so users get
visual feedback that their click registered (#310).
Closes #314
Users who already have LinkedIn-imported data now see a prominent info
banner on the upload step explaining that re-importing fully replaces
existing positions, education, skills, and other profile data. The
notice is hidden for first-time importers.
Show the user's Bluesky handle as an always-verified entry at the top of
the Other Profiles section, derived from their AT Protocol identity.
Bluesky is excluded from the "Add" dropdown since it's automatic.
Also expand timeline entry descriptions by default (#315) — since
SectionOverflow already limits visible items to 3, double-collapsing is
redundant. Increases description text contrast from text-muted-foreground
to text-foreground/80 for better readability.
Closes #312
Closes #315
fix(ci): address pre-existing CodeQL alerts
IdentityCard is used on events, embed, and homepage pages that don't
have a ProfileEditProvider. The hook now returns safe defaults (non-owner,
no preview) instead of throwing, fixing the Docker build failure.
Add top-level `permissions: contents: read` to ci.yml to restrict
GITHUB_TOKEN scope. Pin third-party actions in deploy.yml to commit
SHAs to prevent supply-chain attacks.
Resolves CodeQL alerts #2, #3, #4, #5.
Add an Eye icon button next to Edit on own profile that toggles
preview mode, showing the profile as visitors see it:
- Hides edit buttons, Add buttons, empty sections, completion bar
- Shows section overflow disclosure (Show N more)
- Hides DataTransparencyCard and DangerZone
- Displays amber sticky preview bar with "Exit preview" button
Implemented via ProfileEditProvider context: when previewMode is
active, isOwnProfile is set to false, so all existing components
automatically switch to public view without per-component changes.
* fix(find-people): link unclaimed profiles to their Bluesky profile
* fix(suggestions): add 'new' to banner text for clarity
* fix(suggestions): add spacing between banner text and Find people link
* fix(suggestions): use 'Show me who' as banner link text
* feat(i18n): add showMore/showLess translation keys for section overflow
* feat(ui): add SectionOverflow disclosure component with tests
* feat(timeline): add optional itemCount badge to section heading
* feat(career): add section overflow disclosure (show 3, expand rest)
* feat(sections): add overflow disclosure to education, projects, volunteering
* feat(sections): add overflow disclosure to credentials, publications, awards
* feat(sections): add item count badges to skills, languages, external accounts
When a handle doesn't match any profile, the embed now renders a
compact "No profile found for <handle>" message instead of the full
404 page, which looked broken inside the small iframe preview.
Display a user-friendly error when redirected back with ?error=upstream
after a transient identity provider failure, instead of showing nothing.
* feat(web): add avatar upload and override API client methods
Add hasDisplayNameOverride, hasAvatarUrlOverride, and source.displayName/avatarUrl
to the Profile interface. Add updateProfileOverride, uploadAvatar, and
deleteAvatarOverride functions to the API client.
* feat(web): add avatar upload, display name editing, and override scope notice
Replace read-only avatar/displayName section with editable fields including
avatar upload overlay, display name input, and remove-custom-avatar button.
Add blue override scope notice banner. Save displayName via override API.
* feat(web): pass override flags and source values to profile edit dialog
Thread hasDisplayNameOverride, hasAvatarUrlOverride, sourceDisplayName,
and sourceAvatar through IdentityCard and AboutSection to ProfileEditDialog.
* feat(web): allow Next.js Image to load avatar uploads from sifa.id
Add sifa.id/uploads/** to remotePatterns so uploaded avatar images
render through the Next.js Image optimization pipeline.
- Add spinning icon to sync button during sync
- Show spinner with message during auto-sync
- Remove per-card invite button (not personalized)
- Show dismiss-only for unclaimed profiles
- Cards now show avatar/name/handle from resolved profiles
The page-level text-center was cascading into the IdentityCard,
misaligning role, location, and headline text. Add text-left to
the card wrapper.
Credentials, publications, and awards entries were visually running
together. Adds space-y-4 around the items container in EditableSection
so each entry has clear visual separation.
* feat(identity-card): display pronouns from Bluesky profile
Show pronouns next to the display name in the IdentityCard component
for both page and embed variants. Reads the pronouns field from the
API response (sourced from app.bsky.actor.profile). Also included in
the embed data API and featured profile type.
* fix(events): add pronouns to EventEntry profile type
* fix(embed): debounce handle input to prevent 404 flashing in preview
The embed builder iframe reloaded on every keystroke, causing rapid 404
flashes while typing non-existent handles. Add 500ms debounce so the
preview only loads after the user stops typing.
* style(embed): fix prettier formatting
Fetch the complete profile via fetchProfile() instead of the limited
featured endpoint data. The card now renders identically to the embed
preview, including trustStats, verifiedAccounts, activeApps, and
PDS provider.
The find-people page showed no suggestions because Bluesky follows
were never imported. Add a "Sync follows" button that triggers
the new /api/suggestions/sync endpoint, plus auto-sync on first
visit when no suggestions are found.
Stack identity card avatar above text content on mobile instead of
side-by-side columns. Stack timeline entry title/date vertically on
mobile so titles get full width instead of being squeezed by dates.
Add Previous/Next controls with page indicator. Resets to page 1 when
switching filters. Total count shown in the section header.
PDS icon now shows provider name tooltip on hover and is not clickable.
Handle remains a link to the external profile page.
Non-Bluesky PDS providers (Eurosky, Northsky, etc.) may not have working
web profile viewers, especially for users with custom handles. Since
bsky.app resolves any ATProto handle regardless of PDS, use it as the
universal profile link. Provider name and host are preserved for icon display.
* fix(home): two-column layout and avatar reel caption
- Split content below CTAs into two columns on desktop (lg breakpoint):
left column for Profile of the Day + avatar reel, right for ATmosphereConf
- Collapses back to single column when ATmosphereConf block is removed
- Add whitespace (mt-12) between CTA buttons and content columns
- Change avatar reel caption to "Other professionals..." for natural flow
after the Profile of the Day section
* fix(home): pass Bluesky followers and PDS provider to featured card
Add atprotoFollowersCount and pdsProvider to FeaturedProfile interface
and pass them through to IdentityCard so the homepage card matches
the embed preview.
CI now detects which routes changed and passes AFFECTED_PAGES env var.
Visual regression and QA screenshots only run for affected pages instead
of all 8 pages x 3 viewports on every PR. Shared file changes (layouts,
components, styles, config) still trigger all-page screenshots. Nightly
and local dev behavior unchanged (defaults to all pages).
* feat(roadmap): add public roadmap page
Add /roadmap page showing completed and upcoming headline features,
a disclaimer about timing uncertainty, and links for users to suggest
ideas, report bugs, or contribute via GitHub.
Also links to the roadmap from the about page CTA section and the
site footer.
* feat(roadmap): add job profiles and event RSVP items
Add atwork.place job profiles integration and Smoke Signals / atmo.rsvp
event RSVP integration to the planned roadmap items.
* feat(roadmap): add connections, notifications, and profile analytics
Add verified connections (mutual two-way), notifications, and profile
analytics to the planned roadmap items.
Extend profile-edit event with section property to distinguish
which part of the profile was edited: profile, position, skill,
or the specific section name from the generic edit dialog
(education, certification, project, etc).
Adds filter buttons (All / No import) to the Latest Signups section and a
visual profile completion progress bar per user. Users without a LinkedIn
import get a "No import" badge. Hovering the completion bar shows which
sections are filled (headline, about, positions, education, skills, certs).
Expand Schema.org structured data to include:
- Full position history in worksFor (all roles with dates, not just current)
- Education credentials via hasCredential (degree + field of study)
- Certifications via hasCredential (merged with education credentials)
- Volunteering via memberOf (OrganizationRole)
- Honors/awards via award
- Languages via knowsLanguage
- ProfilePage wrapper type (Google recommendation)
Also updates profile page to use buildProfilePageJsonLd and adds
21 tests covering all new mappings plus full-profile integration.
Closes singi-labs/sifa-workspace#27
Add trackEvent() wrapper and instrument 10 user actions:
signup, import-upload, import-preview, import-complete,
import-failed, share-click, profile-edit, endorsement-sent,
expert-click, and embed-load (via direct API call from
embed.js on third-party sites).
- Add missing CSS variables for Standard, keytrace, Aetheros, Roomy
apps (light + dark mode) — legend dots were transparent
- Remove [&_svg]:!w-full that stretched heatmap wider than its content
- Remove "Less ... More" intensity scale from legend (self-evident)
- Clean up unused i18n keys (less, more)
Update BlueskyPostCard to extract thumbnails from resolved AppView
embeds. Handles three embed types:
- app.bsky.embed.images#view (photo posts)
- app.bsky.embed.external#view (link previews with thumbnails)
- app.bsky.embed.recordWithMedia#view (quote posts with media)
Previously images never displayed because the card only received raw
blob refs (not CDN URLs). Paired with sifa-api PR #152 which now
passes resolved embeds from the Bluesky AppView.
Add CardLink wrapper component using CSS overlay technique to make
entire activity cards clickable. URL resolution follows a 3-tier
hierarchy: per-item URL > per-user profile URL > not clickable.
- Add URL patterns and resolveCardUrl() to atproto-apps registry
- Create CardLink component with accessible overlay link
- Wrap all 6 card types (generic, Bluesky, Tangled, event RSVP,
KipClip, keytrace) in CardLink
- Remove redundant "View on X" footer links from all cards
- Internal links (Bluesky facets, keytrace profileUrl) use
relative z-10 to stay clickable above the overlay
- Pass authorHandle from feed/overview to cards for URL resolution
* fix(web): heatmap colors, tooltip, layout, and week start
- Use SVG fill instead of CSS backgroundColor for rect cells
- Replace broken foreignObject tooltip with native SVG title
- Set weekStart=1 for Monday-first grid
- Move legend below chart on all viewports
- Move summary stats above chart
- Fix month count calculation
- Pass 365 days on activity page for full-width chart
* fix(web): start heatmap grid on Monday so first column is always full
* chore(deps): add react-activity-calendar 2.7.12
* feat(web): add fetchHeatmapData API client and HeatmapResponse types
* feat(web): add heatmap color logic — dominant app, intensity levels, cell styling
* feat(web): add heatmap CSS custom properties for empty cells and color overrides
* feat(web): add heatmap tooltip component with per-app breakdown
* feat(web): add heatmap legend with intensity scale and app color key
* feat(web): add heatmap summary stats component
* feat(web): add mobile monthly stacked bar chart for heatmap
* feat(web): add desktop heatmap grid with custom cell rendering via react-activity-calendar
Wraps react-activity-calendar's ActivityCalendar with custom renderBlock
to apply getCellStyle colors, selected-date outline, and HeatmapTooltip
on hover. Fills date gaps for a complete 6-month range.
* feat(web): add main ActivityHeatmap wrapper with data fetching and responsive layout
Client component that fetches heatmap data via API, shows loading skeleton,
transforms data with transformHeatmapData, and renders desktop (HeatmapGrid +
HeatmapLegend) and mobile (HeatmapBars + HeatmapLegend) layouts. Summary stats
shown only for full variant. Empty state overlay when no data.
* feat(web): add compact 3-month heatmap to profile activity overview section
* feat(web): add full 6-month heatmap to activity page above feed
* fix(web): remove setLoading call from effect body
Fixes react-hooks/set-state-in-effect lint error by removing
the synchronous setLoading(true) inside useEffect.
* style: format heatmap components with prettier
The COLLECTION_TO_APP map was out of sync with the verified API registry:
- community.lexicon.* was mapped to smokesignal (caught kipclip bookmarks)
- com.picosky.* was wrong (should be social.psky.*)
- link.tangled.* was wrong (should be sh.tangled.*)
- app.linkat.* was wrong (should be blue.linkat.*)
Now split community.lexicon.calendar.* (smokesignal) from
community.lexicon.bookmarks.* (kipclip). Added all verified apps:
standard, aetheros, roomy, keytrace.
* feat(web): extend ActiveApp type and add activity API client
- Add category, recentCount, latestRecordAt fields to ActiveApp interface
- Add fetchAppsRegistry() for cached app registry lookups
- Add updateActivityVisibility() for toggling app visibility
- Add AppRegistryEntry interface for registry API responses
- Add tangled and flashes to atproto-apps display registry
* feat(web): add generic activity card and card registry
- Add ActivityCardProps type interface
- Add card-registry with exact and prefix matching for collection-specific card components
- Add GenericActivityCard with full and compact variants:
- Left accent stripe colored per app
- Phosphor icon per activity category
- Content text extraction from record fields with fallback
- Relative timestamp formatting
- App badge pill in full variant
- Optional author display
- Dark mode support
- Add 9 tests covering both variants, fallback text, author visibility, and stripe colors
- Add activityCard translations to test setup
* feat(web): wire activeApps badges into profile page
* feat(web): add activity visibility settings page
Client-side page at /p/[handle]/settings/activity where authenticated
users can toggle which ATproto apps appear on their Sifa profile.
Uses the existing updateActivityVisibility API client function.
* feat(web): add GDPR profile removal request page
No-auth-required form at /privacy/removal for GDPR Art. 17 erasure
requests. Calls POST /api/privacy/suppress (placeholder endpoint,
API implementation follows in a separate task).
* feat(web): add Tangled custom activity card
* feat(web): replace activity overview placeholder with live teaser cards
Fetch teaser data from GET /api/activity/:handle/teaser on mount and
render up to 5 compact activity cards using the card registry with
GenericActivityCard fallback. Section returns null when no data is
available. Full-width "View full activity" CTA links to the activity
page.
* feat(web): add Bluesky custom activity card
Render Bluesky posts with rich text facets (links as anchors, mentions
as bold), reply labels, image thumbnails, and "View on Bluesky" deep
link. Compact variant shows truncated text with Butterfly icon.
Registered as app.bsky.feed.post in card registry.
* fix: resolve lint errors (setState in effect, redundant roles, img elements)
* feat(activity): hide category tabs when single category (#330)
Only show category filter tabs when the API reports more than one
available category. Also filters the tab list to only show categories
that actually have content, removing empty category tabs.
Closes #327
* fix(activity): show all tabs when availableCategories is empty/missing (#332)
When the API returns an empty availableCategories array or the field is
absent (backwards compatibility), show all category tabs instead of
hiding them. Only hide tabs when the API explicitly reports a single
category.
* feat(activity): enrich event RSVP cards with event details (#333)
* fix(activity): show all tabs when availableCategories is empty/missing
When the API returns an empty availableCategories array or the field is
absent (backwards compatibility), show all category tabs instead of
hiding them. Only hide tabs when the API explicitly reports a single
category.
* feat(activity): map community.lexicon to Smoke Signal in frontend
* feat(activity): add EventRsvpCard component for enriched event display
* feat(activity): register EventRsvpCard for community.lexicon.calendar.rsvp
Add buildBlobUrl helper to construct PDS blob URLs from DIDs and CIDs.
Add extractImageBlob to the generic activity card that finds image blob
references across common ATproto record shapes (image, thumbnail,
images array, embed.images). Renders the image in full (non-compact)
cards with lazy loading and graceful error hiding for unreachable PDS.
* feat: add My Network page (#317)
- Add /my-network page showing who the user follows, grouped by
Sifa profiles vs external follows
- Add fetchFollowing() API client function
- Add onUnfollow prop to SuggestionCard for unfollow action
- Add My Network link to site header (desktop) and mobile nav
- Update Find People follow toast to link to My Network
- Add i18n string for myNetwork
- Source filter tabs (All/Sifa/Bluesky/Tangled) with pagination
- Empty state with Find People CTA
Closes #317
* refactor: extract followUser/unfollowUser into API helpers
Move inline fetch calls for follow/unfollow into reusable functions
in api.ts, matching the pattern of other API helpers. Use avatarUrl
field name for consistency with API response.
* fix: use avatarUrl field name in FollowProfile interface
Align FollowProfile interface with API response field name (avatarUrl)
for consistency with SuggestionProfile and the API endpoint.
Migrate ATproto app colors from scattered Tailwind classes and hardcoded
hex values to centralized CSS custom properties in globals.css. Each app
now defines --app-{id}-stripe, --app-{id}-badge-bg, and
--app-{id}-badge-text variables in both light and dark mode using oklch
to match the existing shadcn theme convention.
- globals.css: add app color variables for 9 apps + fallback in :root and .dark
- atproto-apps.ts: replace hardcoded Tailwind classes with CSS var references,
export getAppStripeColor() helper
- generic-activity-card.tsx: remove STRIPE_COLORS map, use getAppStripeColor()
- bluesky-post-card.tsx: replace hardcoded #0285c7 with getAppStripeColor()
- tangled-card.tsx: replace hardcoded stripe/badge constants with centralized vars
- Update tests to expect CSS variable strings instead of resolved RGB values
The synthetic Bluesky entry now uses the PDS detection logic from the
identity card. Shows the correct PDS icon (Bluesky butterfly, EuroSky,
favicon for self-hosted) and a contextual label:
- Bluesky users: "Bluesky (@handle)"
- Self-hosted PDS: "Self-hosted ATProto (@handle)"
- Other providers: "EuroSky (@handle)", etc.
Previously hardcoded the Butterfly icon and "@handle" label for all
users, which was misleading for self-hosted PDS users.
Updates privacy policy with:
- Cross-app activity processing disclosure (Art. 14)
- Legal basis statement (legitimate interest)
- Unclaimed profiles: no activity data collected
- Activity visibility controls explanation
- Profile removal mechanism (links to /privacy/removal)
- GDPR data subject rights section
The backend seed database includes community and security categories
but the frontend only recognized 5 categories, causing skills in those
categories to fall into "Other". Now recognizes all 7 backend categories.
Note: 323 unresolved skills still need canonical mappings in sifa-api
to fully fix the "Other" catch-all problem.
Closes #316
* feat(web): extend ActiveApp type and add activity API client
- Add category, recentCount, latestRecordAt fields to ActiveApp interface
- Add fetchAppsRegistry() for cached app registry lookups
- Add updateActivityVisibility() for toggling app visibility
- Add AppRegistryEntry interface for registry API responses
- Add tangled and flashes to atproto-apps display registry
* feat(web): add generic activity card and card registry
- Add ActivityCardProps type interface
- Add card-registry with exact and prefix matching for collection-specific card components
- Add GenericActivityCard with full and compact variants:
- Left accent stripe colored per app
- Phosphor icon per activity category
- Content text extraction from record fields with fallback
- Relative timestamp formatting
- App badge pill in full variant
- Optional author display
- Dark mode support
- Add 9 tests covering both variants, fallback text, author visibility, and stripe colors
- Add activityCard translations to test setup
* feat(web): wire activeApps badges into profile page
* feat(web): add activity visibility settings page
Client-side page at /p/[handle]/settings/activity where authenticated
users can toggle which ATproto apps appear on their Sifa profile.
Uses the existing updateActivityVisibility API client function.
* feat(web): add GDPR profile removal request page
No-auth-required form at /privacy/removal for GDPR Art. 17 erasure
requests. Calls POST /api/privacy/suppress (placeholder endpoint,
API implementation follows in a separate task).
* feat(web): add Tangled custom activity card
* feat(web): replace activity overview placeholder with live teaser cards
Fetch teaser data from GET /api/activity/:handle/teaser on mount and
render up to 5 compact activity cards using the card registry with
GenericActivityCard fallback. Section returns null when no data is
available. Full-width "View full activity" CTA links to the activity
page.
* feat(web): add Bluesky custom activity card
Render Bluesky posts with rich text facets (links as anchors, mentions
as bold), reply labels, image thumbnails, and "View on Bluesky" deep
link. Compact variant shows truncated text with Butterfly icon.
Registered as app.bsky.feed.post in card registry.
* fix: resolve lint errors (setState in effect, redundant roles, img elements)
The completion bar was checking `verifiedAccounts` (third-party verified
accounts) instead of `externalAccounts` (user-added websites/services),
causing the "Add a website or verification" suggestion to persist after
a user adds a website.
Also adds loading state to import wizard confirm button so users get
visual feedback that their click registered (#310).
Closes #314
Show the user's Bluesky handle as an always-verified entry at the top of
the Other Profiles section, derived from their AT Protocol identity.
Bluesky is excluded from the "Add" dropdown since it's automatic.
Also expand timeline entry descriptions by default (#315) — since
SectionOverflow already limits visible items to 3, double-collapsing is
redundant. Increases description text contrast from text-muted-foreground
to text-foreground/80 for better readability.
Closes #312
Closes #315
Add an Eye icon button next to Edit on own profile that toggles
preview mode, showing the profile as visitors see it:
- Hides edit buttons, Add buttons, empty sections, completion bar
- Shows section overflow disclosure (Show N more)
- Hides DataTransparencyCard and DangerZone
- Displays amber sticky preview bar with "Exit preview" button
Implemented via ProfileEditProvider context: when previewMode is
active, isOwnProfile is set to false, so all existing components
automatically switch to public view without per-component changes.
* feat(i18n): add showMore/showLess translation keys for section overflow
* feat(ui): add SectionOverflow disclosure component with tests
* feat(timeline): add optional itemCount badge to section heading
* feat(career): add section overflow disclosure (show 3, expand rest)
* feat(sections): add overflow disclosure to education, projects, volunteering
* feat(sections): add overflow disclosure to credentials, publications, awards
* feat(sections): add item count badges to skills, languages, external accounts
* feat(web): add avatar upload and override API client methods
Add hasDisplayNameOverride, hasAvatarUrlOverride, and source.displayName/avatarUrl
to the Profile interface. Add updateProfileOverride, uploadAvatar, and
deleteAvatarOverride functions to the API client.
* feat(web): add avatar upload, display name editing, and override scope notice
Replace read-only avatar/displayName section with editable fields including
avatar upload overlay, display name input, and remove-custom-avatar button.
Add blue override scope notice banner. Save displayName via override API.
* feat(web): pass override flags and source values to profile edit dialog
Thread hasDisplayNameOverride, hasAvatarUrlOverride, sourceDisplayName,
and sourceAvatar through IdentityCard and AboutSection to ProfileEditDialog.
* feat(web): allow Next.js Image to load avatar uploads from sifa.id
Add sifa.id/uploads/** to remotePatterns so uploaded avatar images
render through the Next.js Image optimization pipeline.
* feat(identity-card): display pronouns from Bluesky profile
Show pronouns next to the display name in the IdentityCard component
for both page and embed variants. Reads the pronouns field from the
API response (sourced from app.bsky.actor.profile). Also included in
the embed data API and featured profile type.
* fix(events): add pronouns to EventEntry profile type
* fix(home): two-column layout and avatar reel caption
- Split content below CTAs into two columns on desktop (lg breakpoint):
left column for Profile of the Day + avatar reel, right for ATmosphereConf
- Collapses back to single column when ATmosphereConf block is removed
- Add whitespace (mt-12) between CTA buttons and content columns
- Change avatar reel caption to "Other professionals..." for natural flow
after the Profile of the Day section
* fix(home): pass Bluesky followers and PDS provider to featured card
Add atprotoFollowersCount and pdsProvider to FeaturedProfile interface
and pass them through to IdentityCard so the homepage card matches
the embed preview.
CI now detects which routes changed and passes AFFECTED_PAGES env var.
Visual regression and QA screenshots only run for affected pages instead
of all 8 pages x 3 viewports on every PR. Shared file changes (layouts,
components, styles, config) still trigger all-page screenshots. Nightly
and local dev behavior unchanged (defaults to all pages).
* feat(roadmap): add public roadmap page
Add /roadmap page showing completed and upcoming headline features,
a disclaimer about timing uncertainty, and links for users to suggest
ideas, report bugs, or contribute via GitHub.
Also links to the roadmap from the about page CTA section and the
site footer.
* feat(roadmap): add job profiles and event RSVP items
Add atwork.place job profiles integration and Smoke Signals / atmo.rsvp
event RSVP integration to the planned roadmap items.
* feat(roadmap): add connections, notifications, and profile analytics
Add verified connections (mutual two-way), notifications, and profile
analytics to the planned roadmap items.
Expand Schema.org structured data to include:
- Full position history in worksFor (all roles with dates, not just current)
- Education credentials via hasCredential (degree + field of study)
- Certifications via hasCredential (merged with education credentials)
- Volunteering via memberOf (OrganizationRole)
- Honors/awards via award
- Languages via knowsLanguage
- ProfilePage wrapper type (Google recommendation)
Also updates profile page to use buildProfilePageJsonLd and adds
21 tests covering all new mappings plus full-profile integration.
Closes singi-labs/sifa-workspace#27