Skip to content

hoangngo-sudo/purpleit

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

26 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

purpleit project

A Reddit-style community post board built with React 18 + Vite. Features Google OAuth authentication, optimistic upvoting, threaded comments, user profiles, and image uploads via Supabase backend.

Live site: https://hoangngo-sudo.github.io/purpleit/

Demo

video.mp4

Architecture

flowchart TB
    USER([User]) --> HOME["HomePage<br/>Post feed with search/sort"]
    HOME --> |Click post| DETAIL["DetailPage<br/>Full post + comments"]
    HOME --> |Click Create| CREATE["CreatePage<br/>(Protected)"]
    HOME --> |Click avatar| PROFILE["ProfilePage<br/>User activity tabs"]

    DETAIL --> |Upvote| RPC[("Supabase RPC<br/>toggle_upvote")]
    DETAIL --> |Comment| COMMENTS[("comments table")]
    DETAIL --> |Edit| EDIT["EditPage<br/>(Protected, Owner only)"]
    DETAIL --> |Delete| DELETE["Confirm Modal"]
    DELETE --> |Confirm| SUPA_DEL[("DELETE posts")]

    CREATE --> |Submit| SUPA_INS[("INSERT posts")]
    CREATE --> |Upload image| STORAGE[("Supabase Storage<br/>post-images bucket")]
    EDIT --> |Submit| SUPA_UPD[("UPDATE posts")]
    EDIT --> |Upload image| STORAGE

    USER --> |Not logged in| LOGIN["LoginPage"]
    LOGIN --> |Google OAuth| AUTH[("Supabase Auth")]
    AUTH --> |Success| HOME

    PROFILE --> |Overview| POSTS_TAB["User's Posts"]
    PROFILE --> |Comments| COMMENTS_TAB["User's Comments"]
    PROFILE --> |Upvoted| UPVOTED_TAB["Upvoted Posts"]
Loading

Features

  • Google OAuth authentication Sign in with Google, user profiles, avatar display in navbar and post cards
  • Community post board Create posts with title, content, and images (URL or file upload); browse with infinite scroll pagination
  • Server-side search & sort Debounced search by title (300ms), sort by date or upvotes, all executed on Supabase
  • Toggle upvotes Upvote/un-upvote posts with optimistic UI; server-authoritative state via Supabase RPC
  • Threaded comments Nested replies up to 5 levels deep with collapsible threads, visual connector lines, inline reply forms, and OP badges for post author comments
  • User profiles Tabbed activity view showing posts, comments, and upvoted content
  • Author-based ownership Only post owners can edit/delete; multi-layer auth guards (route, component, action, server)
  • Protected routes Create and Edit pages require authentication; automatic redirect with toast notification
  • Image uploads Drag-and-drop zone with preview (50MB max), or enter external URL; stored in Supabase Storage
  • Toast notifications Context-based system with animated entry/exit via Motion (success, error, warning, info types)
  • URL-based search Search state lives in URL params via useSearchParams — shareable, bookmarkable, and browser back/forward aware
  • Relative timestamps Self-adjusting RelativeTime component with tiered update intervals (10s -> 30s -> 60s -> 5min as timestamps age)
  • Resilient data fetching fetchWithRetry wraps all Supabase reads with exponential-backoff retry (2 retries, 1s / 2s / 4s)
  • Profile tab caching Stale-while-revalidate cache (profileCache.js) with LRU eviction — instant tab switches, background refresh
  • Server-side comment pagination Two-query pattern: paginated root comments + all descendants fetched once on initial load; full tree built client-side via buildCommentTree
  • Error boundaries Per-route ErrorBoundary keeps the navbar visible on render errors
  • Animations (Motion) Micro-interactions via Motion for React with CSS easing curves — hover/tap feedback on post cards, animated toast enter/exit, smooth comment collapse/expand, drag-over feedback on image dropzone. Custom Spinner component uses Motion rotate. All animations respect prefers-reduced-motion
  • Responsive design Bootstrap 5 with custom indigo color scheme and Inter font

Tech Stack

graph TD
    subgraph External
        SUPA["Supabase<br/>PostgreSQL + Auth + Storage"]
        GOOGLE["Google OAuth<br/>Authentication provider"]
        BOOTSTRAP["Bootstrap 5.3<br/>CSS framework"]
        ICONS["Bootstrap Icons 1.13<br/>Icon font"]
        MOTION["Motion<br/>Animation library"]
    end

    subgraph "Build Tools"
        VITE["Vite 7<br/>Dev server + bundler"]
        REACT_PLUGIN["@vitejs/plugin-react<br/>Fast refresh"]
        ESLINT["ESLint 9<br/>Linting"]
    end

    subgraph "React App"
        MAIN["main.jsx<br/>Entry point"]
        PROVIDERS["AuthProvider → ToastProvider<br/>→ BrowserRouter"]
        APP["App.jsx<br/>Layout + navbar"]
        ROUTES["Route Components<br/>HomePage, DetailPage, etc."]
        COMPONENTS["Shared Components<br/>Post, CommentThread, Spinner,<br/>ImageDropZone, ProtectedRoute"]
    end

    subgraph "Utilities"
        CLIENT["client.js<br/>Supabase singleton"]
        HELPERS["helpers.js<br/>fetchWithRetry, formatTime,<br/>uploadImage, buildCommentTree,<br/>isEdited, isPostOwner"]
        CACHE["profileCache.js<br/>LRU tab cache"]
    end

    MAIN --> PROVIDERS
    PROVIDERS --> APP
    APP --> ROUTES
    ROUTES --> COMPONENTS
    ROUTES --> HELPERS
    HELPERS --> CLIENT
    CLIENT --> SUPA
    APP --> SUPA
    PROVIDERS --> GOOGLE
Loading
Dependency Purpose
React 18 UI framework with StrictMode
React Router 6 Client-side routing with URL search params
@supabase/supabase-js Database, auth, and storage client
Bootstrap 5.3 CSS/JS UI kit
Bootstrap Icons Icon font
Motion Animation library (formerly Framer Motion)
Vite 7 Build tool with HMR
gh-pages GitHub Pages deployment

Build

npm install
npm run dev

Before running, create a .env file from .env.example and set:

  • VITE_SUPABASE_URL
  • VITE_SUPABASE_ANON_KEY

For production build:

npm run build
npm run preview

For GitHub Pages deployment:

npm run deploy

Project Structure

.
├── src/
│   ├── main.jsx              # Entry point, provider setup
│   ├── App.jsx               # Root layout, navbar, outlet
│   ├── index.css             # Global styles, indigo theme
│   ├── routes/
│   │   ├── HomePage.jsx      # Post feed with infinite scroll
│   │   ├── DetailPage.jsx    # Single post view + comments
│   │   ├── CreatePage.jsx    # New post form (protected)
│   │   ├── EditPage.jsx      # Edit post form (protected)
│   │   ├── LoginPage.jsx     # Google OAuth login
│   │   └── ProfilePage.jsx   # User profile with tabs
│   ├── components/
│   │   ├── Post.jsx          # Post card with hover/tap micro-interactions
│   │   ├── CommentThread.jsx # Recursive threaded comments
│   │   ├── Spinner.jsx       # Motion-based loading spinner
│   │   ├── ProtectedRoute.jsx# Auth guard wrapper
│   │   ├── ImageDropZone.jsx # Drag-drop upload zone
│   │   ├── ErrorBoundary.jsx # Per-route error boundary
│   │   └── RelativeTime.jsx  # Self-adjusting relative timestamp
│   ├── contexts/
│   │   ├── AuthContext.jsx   # Google OAuth provider
│   │   ├── authContextValue.js # createContext export
│   │   ├── useAuth.js        # Auth hook
│   │   ├── ToastContext.jsx  # Toast notification provider
│   │   ├── toastContextValue.js # createContext export
│   │   └── useToast.js       # Toast hook
│   └── utils/
│       ├── client.js         # Supabase client singleton
│       ├── helpers.js        # fetchWithRetry, formatTime, uploadImage, buildCommentTree, isEdited, isPostOwner
│       └── profileCache.js   # LRU profile tab cache (stale-while-revalidate)
├── public/
│   └── 404.html              # SPA redirect for GitHub Pages
└── vite.config.js            # Base path: /purpleit/

Component & Data Flow

graph LR
    main["main.jsx"] --> AuthProvider
    AuthProvider --> ToastProvider
    ToastProvider --> Router["BrowserRouter"]
    Router --> App

    App -->|"URL search params"| HomePage
    App -->|"useAuth()"| AuthCtx["AuthContext"]

    HomePage -->|"props"| Post["Post.jsx"]
    HomePage -->|"SELECT posts<br/>+ profiles join"| DB[(Supabase)]

    DetailPage -->|"buildCommentTree()"| CommentThread["CommentThread.jsx<br/>(recursive)"]
    DetailPage -->|"RPC toggle_upvote"| DB
    DetailPage -->|"comments CRUD"| DB

    CommentThread -->|"Spinner"| Spinner["Spinner.jsx<br/>Motion rotate"]
    Post -->|"motion.div"| MotionLib["Motion for React<br/>hover/tap animations"]

    CreatePage -->|"INSERT posts"| DB
    CreatePage -->|"upload"| Storage[(Storage)]

    EditPage -->|"UPDATE posts"| DB
    EditPage -->|"upload"| Storage

    ProfilePage -->|"SELECT posts,<br/>comments, upvotes"| DB

    LoginPage -->|"signInWithOAuth"| Auth[(Supabase Auth)]
Loading

Database Schema

erDiagram
    profiles ||--o{ posts : "creates"
    profiles ||--o{ comments : "writes"
    profiles ||--o{ upvotes : "gives"
    posts ||--o{ comments : "has"
    posts ||--o{ upvotes : "receives"
    profiles {
        uuid id PK "User ID from Supabase Auth"
        text username "Display name from Google"
        text avatar_url "Profile picture URL"
        timestamptz created_at "Account creation timestamp"
    }
    posts {
        text slug PK "Random generated post ID"
        text title "Post title (required)"
        text content "Post body text (optional)"
        text imageUrl "Image URL or Storage path"
        int upvotes "Upvote count (default 0)"
        uuid author_id FK "References profiles.id (nullable)"
        timestamptz created_at "Post creation timestamp"
        timestamptz updated_at "Last edit timestamp (nullable)"
    }
    comments {
        int id PK "Auto-increment comment ID"
        text post_id FK "References posts.slug"
        text comment "Comment text content"
        uuid author_id FK "References profiles.id (nullable)"
        int parent_id FK "References comments.id for threading"
        bool is_deleted "Soft delete preserves thread structure"
        timestamptz created_at "Comment timestamp"
    }
    upvotes {
        uuid user_id FK "References profiles.id"
        text post_id FK "References posts.slug"
        timestamptz created_at "Upvote timestamp"
    }
Loading

Key Tables:

Table Purpose Notes
profiles User profile data Populated via Supabase Auth trigger on Google sign-in
posts Community posts author_id is nullable for legacy anonymous posts
comments Threaded comments parent_id enables nested replies; is_deleted preserves thread structure
upvotes User upvote tracking Composite key on (user_id, post_id) prevents duplicate upvotes

Supabase Storage:

  • Bucket: post-images (public)
  • Purpose: Store uploaded post images
  • Max size: 50MB per image

Supabase RPC Functions:

  • toggle_upvote(p_post_id text) Atomically toggles upvote state and returns authoritative count

Threaded Comments

flowchart TD
    subgraph "Data Layer"
        DB[(comments table)]
        DB -->|"SELECT with parent_id"| FLAT["Flat comment array"]
    end
    subgraph "Transformation"
        FLAT -->|"buildCommentTree()"| TREE["Nested tree structure"]
        TREE -->|"Each node has"| NODE["{ ...comment, children[], depth }"]
    end
    subgraph "Rendering"
        NODE --> CT1["CommentThread depth 0"]
        CT1 -->|"recursive"| CT2["CommentThread depth 1"]
        CT2 -->|"recursive"| CT3["...up to depth 5"]
    end
Loading

Features:

  • Recursive CommentThread component renders nested replies
  • Visual thread lines connect parent-child comments
  • Collapsible threads with reply count
  • Inline reply forms with auth guard
  • OP badge for post author comments
  • Soft-deleted comments show [Comment Deleted] preserving thread structure

Auth Flow

sequenceDiagram
    participant U as User
    participant LP as LoginPage
    participant AP as AuthProvider
    participant SA as Supabase Auth
    participant SP as profiles table

    U->>LP: Click "Sign in with Google"
    LP->>AP: signInWithGoogle()
    AP->>SA: signInWithOAuth({ provider: 'google' })
    SA-->>U: Redirect to Google OAuth
    U-->>SA: Authorize & redirect back
    SA->>AP: onAuthStateChange(session)
    AP->>SP: SELECT profile WHERE id = user.id
    SP-->>AP: { username, avatar_url }
    AP-->>LP: Redirect to HomePage
Loading

License

MIT

About

A Reddit inspired web application that empowers users to create posts, browse through a dynamic feed, leave comments, and upvote posts.

Resources

Stars

Watchers

Forks

Contributors