Profile Function & Social
Updated: 2025-10-22
Objectives
- Create Profile page into a functional social surface for a small, admin‑only org (≈20 users).
- Support creating posts with rich text and media, YouTube auto‑embeds, likes, comments, share links, and user discovery.
- Provide org/DSW and Reggie presence, plus a sidebar list of other profiles.
- Deliver real‑time notifications (bell, badges, mini‑log) for mentions, comments, and likes via the existing logs/notifications/SSE system.
High‑level Architecture
-
Frontend (admin/src/html/profile.html + admin/src/js/pages/profile.js)
- Tabs: POSTS, PHOTOS, VIDEOS, PROFILES, and a right‑aligned CREATE control.
- Composer: Summernote editor, Blueimp multi‑file upload; posts created via API.
- Feed widgets: infinite scroll on POSTS, cover modal open by deep‑link #post=
, periodic refresh. - Social actions: Like/Unlike, collapsible comments with counts, share copy, hover liker list.
- User discovery: Left sidebar “Other Profiles” list (DSW, Reggie, others), PROFILES grid view.
-
Backend (backend/routes/admin-profiles.js, profile-photo.js, users.js)
- Dedicated adminprofiles schema for posts and media linking.
- Uploads stored under storage_user/
/posts/YYYY/MM/{uuid}.{ext}. - Media streaming via API to avoid SPA fallback/static routing issues.
- Mentions/comments/likes emit logs routed to bell and badges as per spec.
Data Model (Postgres)
Schema: adminprofiles
-
posts
- id UUID PK
- owner_user_id UUID (FK users.id)
- title text, body text
- is_pinned bool default false
- is_deleted bool default false
- created_at, updated_at timestamptz (trigger updates on change)
- Indexes: owner_user_id, created_at DESC, is_deleted; optional GIN full‑text on title+body
-
media_files
- id UUID PK
- owner_user_id UUID (FK users.id)
- rel_path text (storage_user relative), mime_type text, bytes int, sha256 text
- created_at timestamptz, unique(rel_path)
-
post_media
- post_id UUID, media_id UUID, position int, caption text NULL
- PK (post_id, position); FK to posts/media_files; cascade on delete
-
post_comments
- id UUID PK
- post_id UUID (FK posts)
- owner_user_id UUID (FK users)
- body text, is_deleted bool default false
- created_at, updated_at timestamptz (trigger updates on change)
- Indexes: post_id, owner_user_id
-
post_likes
- post_id UUID, user_id UUID, created_at timestamptz default now()
- PK (post_id, user_id); Indexes on (post_id, created_at DESC) and (user_id, created_at DESC)
Storage layout:
- storage_user/
/posts/YYYY/MM/ . computed from user first+last fallback to email local part or id (normalized, safe)
Backend API Surface
Base path: /api/v1/admin-profiles
Posts
-
POST /posts
- Body: { title?, body?, media_ids?: UUID[], owner_user_id? }
- Owner override allowed for roles: exec, senior management
- Emits mention notifications for @handles in body
-
GET /posts?user_id=
&page= &page_size= - Returns each post with:
- media: [{ id, rel_path, mime_type, bytes }] (ordered by position)
- comment_count, like_count
- viewer_has_liked (for current user)
- likers: up to 8 recent users [{ id, first_name, last_name }]
- Returns each post with:
-
GET /posts/:id
- Same decorations as list
-
PATCH /posts/:id
- Owner‑only: { title?, body?, is_pinned?, is_deleted? }
-
DELETE /posts/:id
- Owner‑only hard delete (cascades via FKs if configured)
Comments
-
POST /posts/:id/comments
- Body: { body }
- Notifies post owner (if not self) and any mentioned users
-
GET /posts/:id/comments
- Lists visible comments ordered by created_at
-
PATCH /posts/:postId/comments/:commentId
- Owner of comment OR post owner may edit/delete: { body?, is_deleted? }
Likes
- POST /posts/:id/like — idempotent like; notifies post owner (if not self)
- DELETE /posts/:id/like — idempotent unlike
User Media
- POST /user-media (multipart form‑data; field: file)
- Stores to storage_user, registers media_files row; returns { id, rel_path, mime_type, bytes }
- GET /user-media/:id — streams file with correct Content‑Type
- GET /user-media/:id/thumb — generates 480px JPEG thumbnail for images; 204 for non‑images
- DELETE /user-media/:id — owner‑only remove (attempts to unlink file and row)
Profile Photos (separate route)
- GET /api/v1/profile-photo/serve/:kind?user_id=UUID&token=...
- kind: resolved|ai|original
- Resolution preference: AI → generated.png → gender generic → robot generic
Users (for discovery)
- GET /api/users (existing) used for left sidebar and profiles grid
Mentions and Handle Rules
- A mention is @handle, where handle is:
- Lowercase alphanumeric concatenation of first_name + last_name (e.g., Brett Watson → @brettwatson), or
- The email local part (before @) for backward compatibility.
- On post/comment create, backend parses mentions and resolves user ids; for each resolved user, a notification is emitted.
Notifications, Badges, and SSE
All notifications use emitLog() and follow the System Logs & Notifications spec.
-
Context kinds used:
- profile_mention — “You were mentioned in a post/comment”
- profile_comment — “New comment on your post”
- profile_like — “Your post was liked”
-
Types: ["profile", "profile_mention" | "profile_comment" | "profile_like"]
-
Platform: "Backend"
-
Targets: ["user:
"] -
Deliver: "normal" (matrix applies; tile routing to profile enables badges)
-
Tile routing: via Settings rule for tile "profile" by context.kind in [profile_comment, profile_mention, profile_share, profile_like]
-
SSE events emitted accordingly: event: log (mini‑log), event: badge (counters)
Frontend Behavior
- Composer: hidden until CREATE is activated (only on own profile). Uses Summernote for HTML and Blueimp for uploads.
- Posts List: paginated; infinite scroll; each card includes:
- Rich body with YouTube URL auto‑embed
- Media gallery (thumbs) via /user-media
- Actions: Like (toggles, count, hover liker list), Comments (collapsible, count badge), Share (copy deep link)
- Comments: inline add/edit/delete; comment count badge increments on add.
- Share: copies profile.html?user=<owner_id>#post=<post_id> to clipboard.
- Profiles Sidebar: shows DSW (org/exec fallback), Reggie, then others alphabetically, each with VIEW button linking by id.
- PROFILES Grid: cards for users; each shows profile pic, name, and post count.
- THE FEED: right column card header renamed; list shows recent posts; click opens cover modal; refreshes periodically.
Security & Access
- Admin‑only surface; backend uses authenticate middleware; routes assume internal admin usage.
- Owner checks:
- Posts: only owner can PATCH/DELETE (exec/senior mgmt may override owner_user_id when creating DSW posts).
- Comments: comment owner OR post owner may edit/delete.
- Media delete: owner‑only; attempts to remove from disk; resilient if missing.
Error Handling & Resilience
- Upload size limits (50MB per file) and type checks for thumbs.
- All streams set correct Content‑Type.
- Frontend guards for missing panes; safe defaults if API pages are empty.
Known Enhancements (Backlog)
- Real‑time UI updates for likes/comments via SSE subscription on profile page (currently relies on refresh/poll).
- Richer @mention autocomplete, user hovercards, and deep link to comment anchors.
- Full‑text search across posts/comments (GIN TSVECTOR index present in earlier drafts).
- Media types: animated thumbnails for video; PDF/image viewer modal.
- Moderation tools for exec/senior management (pin/hide posts).
Plugin WIdget and Code Examples
- card widget
<link href="assets/plugins/lity/dist/lity.min.css" rel="stylesheet">
<script src="assets/plugins/lity/dist/lity.min.js"></script>
<div class="card p-1">
<div style="max-height: 250px" class="overflow-hidden z-1">
<img src="assets/img/gallery/widget-cover-1.jpg" alt="" class="card-img" />
</div>
<div class="card-img-overlay d-flex flex-column bg-body bg-opacity-50 m-1 z-2">
<div class="flex-fill">
<div class="d-flex align-items-center">
<h6>YOUTUBE</h6>
<div class="dropdown dropdown-icon ms-auto">
<a href="#" class="text-white" data-bs-toggle="dropdown"><i class="fa fs-14px fa-ellipsis-h"></i></a>
<div class="dropdown-menu dropdown-menu-end" data-bs-theme="light">
<a href="//www.youtube.com/watch?v=_AS5nu4u1ss" class="dropdown-item">View</a>
</div>
</div>
</div>
</div>
<div>
<a href="//www.youtube.com/watch?v=_AS5nu4u1ss" data-lity class="text-white text-decoration-none d-flex align-items-center">
<div class="bg-white bg-opacity-35 text-white w-35px h-35px d-flex align-items-center justify-content-center">
<i class="fa fa-play fa-lg"></i>
</div>
<div class="ms-3 flex-1">
<div class="fw-bold">NEW VIDEOS - BEHIND THE WARS</div>
<div class="text-body text-opacity-75">
<i class="far fa-eye"></i> 892 views
<i class="far fa-clock ms-3"></i> 39min ago
</div>
</div>
</a>
</div>
</div>
</div>
- User List widget (for hover over post likes etc)
<div class="widget-user-list">
<div class="widget-user-list-item"><a href="#" class="widget-user-list-link"><img src="assets/img/user/user-1.jpg" alt="" /></a></div>
<div class="widget-user-list-item"><a href="#" class="widget-user-list-link"><img src="assets/img/user/user-2.jpg" alt="" /></a></div>
<div class="widget-user-list-item"><a href="#" class="widget-user-list-link"><img src="assets/img/user/user-3.jpg" alt="" /></a></div>
<div class="widget-user-list-item"><a href="#" class="widget-user-list-link"><img src="assets/img/user/user-4.jpg" alt="" /></a></div>
<div class="widget-user-list-item"><a href="#" class="widget-user-list-link"><img src="assets/img/user/user-5.jpg" alt="" /></a></div>
<div class="widget-user-list-item"><a href="#" class="widget-user-list-link"><img src="assets/img/user/user-6.jpg" alt="" /></a></div>
<div class="widget-user-list-item"><a href="#" class="widget-user-list-link"><img src="assets/img/user/user-7.jpg" alt="" /></a></div>
<div class="widget-user-list-item"><a href="#" class="widget-user-list-link bg-body text-body fw-bold">+26</a></div>
</div>
- Profile Widget (for the last tab in the top navigation of profiles)
```<div class="card">
<div class="m-1">
<div class="position-relative overflow-hidden" style="height: 150px">
<img src="assets/img/gallery/widget-cover-2.jpg" class="card-img" alt="" />
<div class="card-img-overlay text-white text-center bg-body bg-opacity-75">
<div class="my-2">
<img src="assets/img/user/user.jpg" alt="" width="80" class="rounded-circle" />
</div>
<div>
<div class="fw-bold">MAURICE PATTERSON</div>
<div class="text-body text-opacity-75">Never give up</div>
</div>
</div>
</div>
<div class="card-body py-2 mt-1">
<div class="row text-center">
<div class="col-4">
<div class="fw-bold fs-5">415</div>
<div class="text-body text-opacity-50 fw-semibold small">POSTS</div>
</div>
<div class="col-4">
<div class="fw-bold fs-5">140k</div>
<div class="text-body text-opacity-50 fw-semibold small">FOLLOWERS</div>
</div>
<div class="col-4">
<div class="fw-bold fs-5">697</div>
<div class="text-body text-opacity-50 fw-semibold small">FOLLOWING</div>
</div>
</div>
</div>
</div>
</div>```
- Modal with options
```<div class="modal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">MODAL TITLE</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p>Modal body text goes here.</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-bs-dismiss="modal">Close</button>
<button type="button" class="btn btn-outline-theme">Save changes</button>
</div>
</div>
</div>
</div>
- Modal Cover (amazing view for full screen in your face odels like whenyou need to submit so many forms but oransing tham i dont have tine fr)
Modal Cover Modal cover is an extended feature from Bootstrap modal. It provide the same background color as the modal content. Place a .modal-cover css class on .modal for full cover modal.
<button type="button" class="btn btn-outline-theme me-2" data-bs-toggle="modal" data-bs-target="#modalCoverExample">Modal Cover</button>
<div class="modal modal-cover fade" id="modalCoverExample">
<div class="modal-dialog">
<div class="modal-content">
...
</div>
</div>
</div>
-Toast
<div class="toast" data-autohide="false">
<div class="toast-header text-theme gap-2">
<i class="fa fa-fw fa-bell opacity-5"></i>
<strong class="flex-1">BOOTSTRAP</strong>
<small class="text-muted">11 mins ago</small>
<button type="button" class="btn-close" data-bs-dismiss="toast"></button>
</div>
<div class="toast-body">
Hello, world! This is a toast message.
</div>
</div>
<script>
$('.toast').toast('show');
</script>
- Toasts Container - Toasts container is an extended ui from Bootstrap toasts. Wrap the toasts with .toasts-container will allow toast to float within the right top position.