# Social Media App — Design Spec
**Date:** 2026-07-14
**Stack:** Laravel 11 + MySQL (backend) · React + Vite + Redux Toolkit (frontend)

---

## 1. Architecture

### Request Flow
```
React (Axios) → Sanctum CSRF cookie → Controller → Service → Model → MySQL
                                                  ↓
                                             API Resource → JSON
```

### Auth Strategy
- Laravel Sanctum SPA mode (cookie-based, no JWT tokens)
- React calls `GET /sanctum/csrf-cookie` before any state-changing request
- Vite dev server proxies `/api` and `/sanctum` to `http://localhost:80` (Laragon)
- All authenticated routes protected by `auth:sanctum` middleware

---

## 2. Database Schema

### `users`
| column | type | notes |
|---|---|---|
| id | bigint PK auto | |
| first_name | varchar(100) | |
| last_name | varchar(100) | |
| email | varchar(255) | unique, indexed |
| password | varchar(255) | bcrypt |
| avatar | varchar(255) | nullable |
| remember_token | varchar(100) | nullable |
| created_at / updated_at | timestamps | |

### `posts`
| column | type | notes |
|---|---|---|
| id | bigint PK auto | |
| user_id | bigint FK | indexed |
| content | text | |
| image_path | varchar(255) | nullable |
| visibility | enum('public','private') | default 'public' |
| created_at / updated_at | timestamps | |
| deleted_at | timestamp | nullable, soft delete |

### `comments`
| column | type | notes |
|---|---|---|
| id | bigint PK auto | |
| post_id | bigint FK | indexed |
| user_id | bigint FK | indexed |
| content | text | |
| created_at / updated_at | timestamps | |
| deleted_at | timestamp | nullable, soft delete |

### `replies`
| column | type | notes |
|---|---|---|
| id | bigint PK auto | |
| comment_id | bigint FK | indexed |
| user_id | bigint FK | indexed |
| content | text | |
| created_at / updated_at | timestamps | |
| deleted_at | timestamp | nullable, soft delete |

### `likes` (polymorphic)
| column | type | notes |
|---|---|---|
| id | bigint PK auto | |
| user_id | bigint FK | indexed |
| likeable_type | varchar(255) | `App\Models\Post`, `Comment`, `Reply` |
| likeable_id | bigint | |
| created_at | timestamp | |

**Unique constraint:** `(user_id, likeable_type, likeable_id)` — prevents duplicate likes at DB level.

---

## 3. Backend Structure

```
app/
├── Http/
│   ├── Controllers/Api/
│   │   ├── AuthController       register, login, logout, me
│   │   ├── PostController       CRUD + visibility filter
│   │   ├── CommentController    store, update, destroy
│   │   ├── ReplyController      store, update, destroy
│   │   └── LikeController       toggle (post/comment/reply via polymorphic)
│   ├── Requests/
│   │   ├── RegisterRequest
│   │   ├── LoginRequest
│   │   ├── StorePostRequest
│   │   ├── UpdatePostRequest
│   │   ├── StoreCommentRequest
│   │   └── StoreReplyRequest
│   └── Resources/
│       ├── UserResource
│       ├── PostResource         includes like_count, comment_count, liked_by_me
│       ├── CommentResource      includes like_count, liked_by_me, replies
│       └── ReplyResource        includes like_count, liked_by_me
├── Models/
│   ├── User                     hasMany: posts, comments, replies
│   ├── Post                     belongsTo: user; hasMany: comments, likes (morph)
│   ├── Comment                  belongsTo: post, user; hasMany: replies, likes (morph)
│   ├── Reply                    belongsTo: comment, user; hasMany: likes (morph)
│   └── Like                     morphTo: likeable; belongsTo: user
├── Policies/
│   ├── PostPolicy               update/delete: user_id === auth()->id()
│   ├── CommentPolicy
│   └── ReplyPolicy
└── Services/
    ├── PostService              create, update, delete, paginate with visibility filter
    ├── CommentService           create, update, delete
    └── LikeService              toggle (upsert / delete)
```

### API Routes (`routes/api.php`)
```
POST   /sanctum/csrf-cookie
POST   /api/register
POST   /api/login
POST   /api/logout                 [auth:sanctum]
GET    /api/me                     [auth:sanctum]

GET    /api/posts                  [auth:sanctum] paginated, newest first
POST   /api/posts                  [auth:sanctum] multipart/form-data
GET    /api/posts/{post}           [auth:sanctum]
PUT    /api/posts/{post}           [auth:sanctum, policy]
DELETE /api/posts/{post}           [auth:sanctum, policy]

POST   /api/posts/{post}/comments  [auth:sanctum]
PUT    /api/comments/{comment}     [auth:sanctum, policy]
DELETE /api/comments/{comment}     [auth:sanctum, policy]

POST   /api/comments/{comment}/replies  [auth:sanctum]
PUT    /api/replies/{reply}             [auth:sanctum, policy]
DELETE /api/replies/{reply}             [auth:sanctum, policy]

POST   /api/posts/{post}/like      [auth:sanctum] toggles
POST   /api/comments/{comment}/like [auth:sanctum] toggles
POST   /api/replies/{reply}/like   [auth:sanctum] toggles
```

### Pagination
- `GET /api/posts` returns 10 posts per page via `?page=N`
- Responses include `meta.current_page`, `meta.last_page`, `meta.has_more`
- Comments and replies are NOT embedded in the post list response — they are fetched lazily when the user expands a post (`GET /api/posts/{post}/comments` is implicit via the CommentList component dispatch)

### Visibility Rule
- Public posts: visible to all authenticated users
- Private posts: visible only to `post.user_id === auth()->id()`
- Filter applied in `PostService::paginate()` using an Eloquent scope

### Security
- All routes behind `auth:sanctum`
- Policies enforce ownership for edit/delete
- `StorePostRequest` validates image: `mimes:jpg,jpeg,png,gif|max:5120`
- Rate limiting: 60 requests/minute on API routes
- Password: `min:8` validation, bcrypt via `Hash::make()`

---

## 4. Frontend Structure

```
D:\assesment-react\assesment-react\src\
├── api/
│   └── axios.js              baseURL, withCredentials, CSRF interceptor
├── assets/                   copied from C:\Users\shipo\Downloads\assets\assets\
│   ├── css/                  bootstrap.min.css, main.css, common.css, responsive.css
│   └── images/               all PNG/SVG design images
├── app/
│   └── store.js              RTK configureStore
├── features/
│   ├── auth/
│   │   ├── authSlice.js      user, isAuthenticated, status
│   │   ├── LoginPage.jsx     matches login.html exactly
│   │   └── RegisterPage.jsx  matches registration.html + first_name, last_name fields
│   ├── posts/
│   │   ├── postsSlice.js     posts[], currentPage, hasMore, loading
│   │   ├── FeedPage.jsx      3-column layout matching feed.html
│   │   ├── PostCard.jsx      single post with reactions, comments toggle, dropdown
│   │   ├── CreatePostModal.jsx text + image + visibility selector
│   │   └── PostComposer.jsx  the "Write something..." input in the feed
│   ├── comments/
│   │   ├── commentsSlice.js  keyed by postId
│   │   ├── CommentList.jsx
│   │   └── CommentItem.jsx   with like button + reply toggle
│   └── replies/
│       ├── repliesSlice.js   keyed by commentId
│       ├── ReplyList.jsx
│       └── ReplyItem.jsx     with like button
├── components/
│   ├── Avatar.jsx
│   ├── Modal.jsx
│   ├── Spinner.jsx
│   ├── LikeButton.jsx        reusable, takes type + id + count + liked_by_me
│   └── InfiniteScrollTrigger.jsx  IntersectionObserver sentinel div
├── layouts/
│   ├── AuthLayout.jsx        wraps login/register (shapes background, no navbar)
│   └── MainLayout.jsx        navbar + 3-column grid wrapper
├── routes/
│   ├── ProtectedRoute.jsx    redirects to /login if not authenticated
│   └── router.jsx            createBrowserRouter config
├── hooks/
│   └── useInfiniteScroll.js  IntersectionObserver hook, calls onLoadMore
└── utils/
    ├── formatDate.js         "5 minutes ago" style
    └── formatName.js         first_name + last_name → full name
```

### Pages & Routes
| Path | Component | Protected |
|---|---|---|
| `/login` | LoginPage | No (redirect to /feed if authenticated) |
| `/register` | RegisterPage | No (redirect to /feed if authenticated) |
| `/feed` | FeedPage | Yes (redirect to /login if not authenticated) |
| `/` | Redirect to /feed | — |

### State Management (Redux Toolkit)
- **`auth` slice**: `{ user, isAuthenticated, loading, error }`
  - Thunks: `loginUser`, `registerUser`, `logoutUser`, `fetchCurrentUser`
  - On app load: dispatch `fetchCurrentUser` → if 401, set `isAuthenticated: false`
- **`posts` slice**: `{ items: [], page: 1, hasMore: true, loading, error }`
  - Thunks: `fetchPosts` (append on page increment), `createPost`, `updatePost`, `deletePost`
- **`comments` slice**: `{ byPostId: { [postId]: [] }, loading }`
  - Thunks: `fetchComments`, `createComment`, `updateComment`, `deleteComment`
- **`replies` slice**: `{ byCommentId: { [commentId]: [] }, loading }`
  - Thunks: `fetchReplies`, `createReply`, `updateReply`, `deleteReply`

### Optimistic UI
- **Likes**: toggle `liked_by_me` and `like_count` in Redux immediately on click, revert on API error
- **Post delete**: remove from `items[]` immediately, show undo toast (or just revert on error)

### Infinite Scroll
- `InfiniteScrollTrigger` div sits at bottom of post list
- `useInfiniteScroll` hook fires `fetchPosts({ page: currentPage + 1 })` when sentinel enters viewport
- Dispatch stops when `hasMore === false`

### UI Fidelity
- Import `bootstrap.min.css`, `common.css`, `main.css`, `responsive.css` in `main.jsx`
- All class names from the HTML templates copied exactly into JSX (`className`)
- Inline SVG icons copied from `feed.html` into components
- Dark mode toggle button rendered but non-functional (light mode only)
- `logo.svg` referenced in HTML — use `logo-copy.svg` from assets as the logo file

---

## 5. Key Constraints & Decisions

| Decision | Rationale |
|---|---|
| Sanctum SPA (cookies) over token auth | Simpler, more secure for same-origin SPA |
| Polymorphic `likes` table | One table for post/comment/reply likes, avoids 3 separate tables |
| Soft deletes on posts/comments/replies | Allows recovery, avoids FK issues |
| RTK slices over RTK Query | Simpler for custom pagination + optimistic UI patterns |
| `first_name` + `last_name` separate columns | Matches spec; display combined as full name |
| Image stored as path in DB, file in `storage/app/public/posts/` | Simple, no external service needed |
| Vite proxy for `/api` and `/sanctum` | Avoids CORS; Sanctum SPA requires same-origin |
