Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions docs/database-schema-message.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Message Database Schema Relations

This document outlines the database schema relations for the `messages` table within IntelliCircle, as defined in `packages/shared/src/schema.ts`.

## The `messages` Table
The `messages` table stores the core chat content for the application.

### Columns
- `id`: Serial Primary Key
- `roomId`: Integer (Foreign Key to `chatRooms.id`)
- `userId`: Integer (Foreign Key to `users.id`)
- `content`: Text (Not Null)
- `createdAt`: Timestamp (Defaults to now)

### Relationships
1. **Belongs to a Room:**
Each message is tied to exactly one chat room via `roomId`.
2. **Belongs to a User:**
Each message is authored by exactly one user via `userId`.

### Indexes
- `roomHistoryIdx`: A composite index on `(roomId, createdAt)` exists to optimize the fetching of room message history in chronological order.

### Drizzle ORM Definition
```typescript
export const messages = pgTable("messages", {
id: serial("id").primaryKey(),
roomId: integer("room_id").notNull(),
userId: integer("user_id").notNull(),
content: text("content").notNull(),
createdAt: timestamp("created_at").notNull().defaultNow(),
}, (table) => {
return {
roomHistoryIdx: index("message_room_created_idx").on(table.roomId, table.createdAt),
};
});
```
82 changes: 82 additions & 0 deletions packages/client/src/app/(app)/dashboard/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { Activity, Compass, MessageSquare, Target } from "lucide-react";

export default function DashboardLoading() {
return (
<div className="flex-1 w-full bg-background flex flex-col animate-pulse">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-10 w-full flex-grow">
{/* Header Section Skeleton */}
<div className="mb-10">
<div className="h-10 w-64 bg-white/10 rounded mb-4"></div>
<div className="h-6 w-96 bg-white/5 rounded"></div>
</div>

{/* Main Dashboard Grid Skeleton */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">

{/* Left Column Skeleton */}
<div className="lg:col-span-2 flex flex-col gap-8">
{/* Action Cards Skeleton */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div className="p-6 rounded-2xl bg-[#111827] border border-[#27272a] h-48">
<Compass className="w-8 h-8 text-white/20 mb-4" />
<div className="h-6 w-32 bg-white/10 rounded mb-4"></div>
<div className="h-4 w-full bg-white/5 rounded mb-2"></div>
<div className="h-4 w-3/4 bg-white/5 rounded"></div>
</div>
<div className="p-6 rounded-2xl bg-[#111827] border border-[#27272a] h-48">
<MessageSquare className="w-8 h-8 text-white/20 mb-4" />
<div className="h-6 w-32 bg-white/10 rounded mb-4"></div>
<div className="h-4 w-full bg-white/5 rounded mb-2"></div>
<div className="h-4 w-3/4 bg-white/5 rounded"></div>
</div>
</div>

{/* Recent Activity/Feed Section Skeleton */}
<div className="bg-[#111827] rounded-2xl border border-[#27272a] p-6 flex flex-col flex-grow h-64">
<div className="flex items-center justify-between mb-6">
<div className="flex items-center">
<Activity className="w-5 h-5 mr-2 text-white/20" />
<div className="h-6 w-40 bg-white/10 rounded"></div>
</div>
<div className="h-4 w-16 bg-white/10 rounded"></div>
</div>
<div className="flex-1 border-2 border-dashed border-[#27272a] rounded-xl bg-background/50"></div>
</div>
</div>

{/* Right Column Skeleton */}
<div className="flex flex-col gap-6">
{/* Mini Profile Card Skeleton */}
<div className="bg-[#111827] rounded-2xl border border-[#27272a] p-6 text-center">
<div className="w-20 h-20 rounded-full bg-white/10 mx-auto mb-4"></div>
<div className="h-6 w-24 bg-white/10 rounded mx-auto mb-6"></div>

<div className="grid grid-cols-2 gap-2 border-t border-white/10 pt-4">
<div>
<div className="h-8 w-8 bg-white/10 rounded mx-auto mb-2"></div>
<div className="h-3 w-16 bg-white/5 rounded mx-auto"></div>
</div>
<div className="border-l border-white/10">
<div className="h-8 w-8 bg-white/10 rounded mx-auto mb-2"></div>
<div className="h-3 w-16 bg-white/5 rounded mx-auto"></div>
</div>
</div>
</div>

{/* Suggestions Block Skeleton */}
<div className="bg-[#111827] rounded-2xl border border-[#27272a] p-6 flex-grow">
<div className="flex items-center mb-4">
<Target className="w-5 h-5 mr-2 text-white/20" />
<div className="h-6 w-32 bg-white/10 rounded"></div>
</div>
<div className="space-y-4">
<div className="bg-background/80 p-4 rounded-xl h-24 border border-white/5"></div>
<div className="bg-background/80 p-4 rounded-xl h-24 border border-white/5"></div>
</div>
</div>
</div>
</div>
</div>
</div>
);
}
10 changes: 2 additions & 8 deletions packages/client/src/app/(app)/dashboard/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { useRouter } from "next/navigation";
import { useEffect } from "react";
import Link from "next/link";
import { Compass, MessageSquare, Users, Activity, ChevronRight, Zap, Target } from "lucide-react";
import DashboardLoading from "./loading";

export default function Dashboard() {
const { isAuthenticated, user } = useAuthStore();
Expand All @@ -17,14 +18,7 @@ export default function Dashboard() {
}, [isAuthenticated, router]);

if (!isAuthenticated || !user) {
return (
<div className="flex-1 flex items-center justify-center p-8">
<div className="animate-pulse flex flex-col items-center">
<div className="w-12 h-12 bg-white/10 rounded-full mb-4"></div>
<div className="h-4 w-32 bg-white/10 rounded"></div>
</div>
</div>
);
return <DashboardLoading />;
}

return (
Expand Down
57 changes: 57 additions & 0 deletions packages/client/src/app/(app)/discover/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { Compass, MapPin, PlusCircle } from "lucide-react";

export default function DiscoverLoading() {
return (
<div className="min-h-screen bg-black text-white relative animate-pulse">
<div className="absolute top-0 right-0 w-96 h-96 bg-indigo-500/5 rounded-full blur-[128px] pointer-events-none" />
<div className="absolute bottom-0 left-0 w-96 h-96 bg-emerald-500/5 rounded-full blur-[128px] pointer-events-none" />

<main className="max-w-5xl mx-auto px-4 py-12 relative z-10">
<header className="flex items-center justify-between mb-12">
<div>
<div className="h-10 w-48 bg-white/10 rounded mb-4"></div>
<div className="h-5 w-80 bg-white/5 rounded"></div>
</div>
<div className="h-10 w-36 bg-white/10 rounded-full"></div>
</header>

<div className="space-y-6">
<div className="flex items-center justify-between bg-white/5 border border-white/10 px-6 py-4 rounded-2xl">
<div className="flex items-center gap-3">
<div className="p-2 bg-white/10 rounded-full h-9 w-9"></div>
<div>
<div className="h-4 w-24 bg-white/10 rounded mb-2"></div>
<div className="h-3 w-32 bg-white/5 rounded"></div>
</div>
</div>
<div className="flex items-center gap-4">
<div className="h-4 w-20 bg-white/10 rounded"></div>
<div className="h-2 w-32 bg-white/5 rounded"></div>
</div>
</div>

<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{[1, 2, 3, 4, 5, 6].map((i) => (
<div key={i} className="bg-white/5 border border-white/10 rounded-2xl p-6 h-48 flex flex-col justify-between">
<div>
<div className="flex justify-between items-start mb-4">
<div className="h-6 w-32 bg-white/10 rounded"></div>
<div className="h-5 w-16 bg-white/10 rounded-md"></div>
</div>
<div className="space-y-2 mb-6">
<div className="h-3 w-full bg-white/5 rounded"></div>
<div className="h-3 w-2/3 bg-white/5 rounded"></div>
</div>
</div>
<div className="flex gap-2">
<div className="h-5 w-12 bg-white/10 rounded"></div>
<div className="h-5 w-16 bg-white/10 rounded"></div>
</div>
</div>
))}
</div>
</div>
</main>
</div>
);
}
3 changes: 2 additions & 1 deletion packages/client/src/app/(app)/discover/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { motion } from "framer-motion";
import { Loader2, MapPin, Compass, AlertCircle, PlusCircle } from "lucide-react";
import { CreateRoomModal } from "@/components/CreateRoomModal";
import { usePostHog } from 'posthog-js/react';
import DiscoverLoading from "./loading";

interface Room {
id: number;
Expand Down Expand Up @@ -85,7 +86,7 @@ export default function DiscoverPage() {
}
}, [coordinates, geoLoading, geoError, searchRadius]);

if (!isAuthenticated) return null; // Gate render until redirect loop catches
if (!isAuthenticated) return <DiscoverLoading />; // Gate render until redirect loop catches

return (
<div className="min-h-screen bg-black text-white relative">
Expand Down
38 changes: 38 additions & 0 deletions packages/client/src/components/LocationPin.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
'use client';

import React from 'react';
import { motion } from 'framer-motion';

interface LocationPinProps {
label?: string;
color?: string;
onClick?: () => void;
}

export function LocationPin({ label, color = '#6366F1', onClick }: LocationPinProps) {
return (
<motion.div
className="relative flex flex-col items-center group cursor-pointer"
whileHover={{ scale: 1.1, y: -5 }}
whileTap={{ scale: 0.95 }}
onClick={onClick}
>
<div className="absolute -top-8 bg-white dark:bg-gray-800 text-gray-800 dark:text-gray-100 text-xs font-semibold px-2 py-1 rounded shadow opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap pointer-events-none z-50">
{label || 'Location'}
</div>
<svg
width="32"
height="32"
viewBox="0 0 24 24"
fill={color}
xmlns="http://www.w3.org/2000/svg"
className="drop-shadow-md"
>
<path
d="M12 2C8.13 2 5 5.13 5 9C5 14.25 12 22 12 22C12 22 19 14.25 19 9C19 5.13 15.87 2 12 2ZM12 11.5C10.62 11.5 9.5 10.38 9.5 9C9.5 7.62 10.62 6.5 12 6.5C13.38 6.5 14.5 7.62 14.5 9C14.5 10.38 13.38 11.5 12 11.5Z"
/>
</svg>
<div className="w-4 h-1 bg-black/20 blur-[2px] rounded-full mt-1 group-hover:w-3 group-hover:bg-black/30 transition-all duration-300" />
</motion.div>
);
}
67 changes: 67 additions & 0 deletions packages/client/src/components/UserList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
'use client';

import React from 'react';
import { motion } from 'framer-motion';

export interface User {
id: string | number;
name: string;
avatarUrl?: string;
role?: string;
}

interface UserListProps {
users: User[];
onUserClick?: (user: User) => void;
}

export function UserList({ users, onUserClick }: UserListProps) {
if (!users || users.length === 0) {
return (
<div className="flex items-center justify-center p-8 text-gray-500">
No users found.
</div>
);
}

return (
<ul className="divide-y divide-gray-200 dark:divide-gray-800">
{users.map((user, index) => (
<motion.li
key={user.id}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.2, delay: index * 0.05 }}
className="flex items-center justify-between p-4 hover:bg-gray-50 dark:hover:bg-gray-900 transition-colors cursor-pointer"
onClick={() => onUserClick?.(user)}
>
<div className="flex items-center space-x-4">
<div className="relative h-10 w-10 flex-shrink-0">
{user.avatarUrl ? (
<img
className="h-10 w-10 rounded-full object-cover border border-gray-200 dark:border-gray-700"
src={user.avatarUrl}
alt={user.name}
/>
) : (
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-indigo-100 dark:bg-indigo-900/50 text-indigo-700 dark:text-indigo-300 font-semibold uppercase">
{user.name.charAt(0)}
</div>
)}
</div>
<div>
<p className="text-sm font-medium text-gray-900 dark:text-gray-100">
{user.name}
</p>
{user.role && (
<p className="text-sm text-gray-500 dark:text-gray-400">
{user.role}
</p>
)}
</div>
</div>
</motion.li>
))}
</ul>
);
}
2 changes: 1 addition & 1 deletion packages/client/src/components/auth-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ export const AuthModal = () => {
{mounted && createPortal(
<AnimatePresence>
{isOpen && (
<div className="fixed inset-0 z-[9999] flex items-center justify-center p-4">
<div className="fixed inset-0 z-[99999] flex items-center justify-center p-4">
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
Expand Down
21 changes: 21 additions & 0 deletions packages/client/src/store/themeStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { create } from 'zustand';
import { persist } from 'zustand/middleware';

type ThemeMode = 'light' | 'dark' | 'system';

interface ThemeState {
theme: ThemeMode;
setTheme: (theme: ThemeMode) => void;
}

export const useThemeStore = create<ThemeState>()(
persist(
(set) => ({
theme: 'system',
setTheme: (theme) => set({ theme }),
}),
{
name: 'intellicircle-theme-preferences',
}
)
);
11 changes: 9 additions & 2 deletions packages/shared/src/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ export const participants = pgTable(

// Waitlist Schemas
export const insertWaitlistSchema = z.object({
email: z.string().email(),
email: z.string().email().trim().toLowerCase(),
name: z.string().optional(),
profession: z.string().optional()
});
Expand All @@ -113,7 +113,7 @@ export const anonymousAuthSchema = z.object({
});

export const upgradeAuthSchema = z.object({
email: z.string().email(),
email: z.string().email().trim().toLowerCase(),
password: z.string()
.min(8, "Password must be at least 8 characters long")
.regex(/[A-Z]/, "Password must contain at least one uppercase letter")
Expand Down Expand Up @@ -153,3 +153,10 @@ export const nearbyRoomsQuerySchema = z.object({
interests: z.union([z.string(), z.array(z.string())]).optional()
.transform(val => Array.isArray(val) ? val : (val ? [val] : []))
});

// --- WebSocket Schemas ---
export const userJoinedPayloadSchema = z.object({
userId: z.number(),
roomId: z.number(),
timestamp: z.date().default(() => new Date()),
});