diff --git a/README.md b/README.md index e215bc4..a96e94d 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,14 @@ To learn more about Next.js, take a look at the following resources: - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. +## Install +- Jose +- Node.js +- Add emoji pakages +- Install postgres for databases +- Typescript + + You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! ## Deploy on Vercel diff --git a/database/migrations/003_chat.sql b/database/migrations/003_chat.sql new file mode 100644 index 0000000..9221490 --- /dev/null +++ b/database/migrations/003_chat.sql @@ -0,0 +1,80 @@ +-- ============================================================================ +-- Chat System: conversations, participants, and messages +-- ============================================================================ + +CREATE TABLE IF NOT EXISTS conversations ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE TABLE IF NOT EXISTS conversation_participants ( + conversation_id UUID NOT NULL REFERENCES conversations(id) ON DELETE CASCADE, + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + joined_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + PRIMARY KEY (conversation_id, user_id) +); + +CREATE TABLE IF NOT EXISTS messages ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + conversation_id UUID NOT NULL REFERENCES conversations(id) ON DELETE CASCADE, + sender_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + content TEXT NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ, + deleted_at TIMESTAMPTZ +); + +CREATE INDEX IF NOT EXISTS idx_messages_conversation_id ON messages(conversation_id); +CREATE INDEX IF NOT EXISTS idx_messages_created_at ON messages(created_at); +CREATE INDEX IF NOT EXISTS idx_conversation_participants_user_id ON conversation_participants(user_id); + +-- Seed conversations between superadmin and other users +INSERT INTO conversations (id, created_at) VALUES + ('c0000000-0000-0000-0000-000000000001', NOW() - INTERVAL '2 hours'), + ('c0000000-0000-0000-0000-000000000002', NOW() - INTERVAL '1 hour'), + ('c0000000-0000-0000-0000-000000000003', NOW() - INTERVAL '30 minutes') +ON CONFLICT DO NOTHING; + +-- Add participants (superadmin + each sales/dev user) +INSERT INTO conversation_participants (conversation_id, user_id) VALUES + ('c0000000-0000-0000-0000-000000000001', '00000000-0000-0000-0000-000000000001'), + ('c0000000-0000-0000-0000-000000000001', '00000000-0000-0000-0000-000000000003'), + ('c0000000-0000-0000-0000-000000000002', '00000000-0000-0000-0000-000000000001'), + ('c0000000-0000-0000-0000-000000000002', '00000000-0000-0000-0000-000000000004'), + ('c0000000-0000-0000-0000-000000000003', '00000000-0000-0000-0000-000000000001'), + ('c0000000-0000-0000-0000-000000000003', '00000000-0000-0000-0000-000000000002') +ON CONFLICT DO NOTHING; + +-- Seed some messages +INSERT INTO messages (id, conversation_id, sender_id, content, created_at) VALUES + ('00000000-0000-0000-0000-000000000101', 'c0000000-0000-0000-0000-000000000001', + '00000000-0000-0000-0000-000000000003', 'Hey! Just finalized the TechCorp deal.', + NOW() - INTERVAL '2 hours'), + ('00000000-0000-0000-0000-000000000102', 'c0000000-0000-0000-0000-000000000001', + '00000000-0000-0000-0000-000000000001', 'Great work! What were the final terms?', + NOW() - INTERVAL '105 minutes'), + ('00000000-0000-0000-0000-000000000103', 'c0000000-0000-0000-0000-000000000001', + '00000000-0000-0000-0000-000000000003', '3-year enterprise license, 15% discount. They signed this morning.', + NOW() - INTERVAL '100 minutes'), + ('00000000-0000-0000-0000-000000000104', 'c0000000-0000-0000-0000-000000000002', + '00000000-0000-0000-0000-000000000004', 'The API integration tests are passing on staging.', + NOW() - INTERVAL '50 minutes'), + ('00000000-0000-0000-0000-000000000105', 'c0000000-0000-0000-0000-000000000002', + '00000000-0000-0000-0000-000000000001', 'Great, when can we deploy to production?', + NOW() - INTERVAL '45 minutes'), + ('00000000-0000-0000-0000-000000000106', 'c0000000-0000-0000-0000-000000000002', + '00000000-0000-0000-0000-000000000004', 'Tomorrow morning after final review.', + NOW() - INTERVAL '40 minutes'), + ('00000000-0000-0000-0000-000000000107', 'c0000000-0000-0000-0000-000000000003', + '00000000-0000-0000-0000-000000000002', 'New report shows 23% increase in lead conversion this quarter.', + NOW() - INTERVAL '25 minutes'), + ('00000000-0000-0000-0000-000000000108', 'c0000000-0000-0000-0000-000000000003', + '00000000-0000-0000-0000-000000000001', 'That is excellent! Let us discuss in the next team meeting.', + NOW() - INTERVAL '20 minutes') +ON CONFLICT DO NOTHING; + +-- Update conversation timestamps +UPDATE conversations SET updated_at = NOW() - INTERVAL '20 minutes' WHERE id = 'c0000000-0000-0000-0000-000000000003'; +UPDATE conversations SET updated_at = NOW() - INTERVAL '40 minutes' WHERE id = 'c0000000-0000-0000-0000-000000000002'; +UPDATE conversations SET updated_at = NOW() - INTERVAL '100 minutes' WHERE id = 'c0000000-0000-0000-0000-000000000001'; diff --git a/database/migrations/004_avatar_url.sql b/database/migrations/004_avatar_url.sql new file mode 100644 index 0000000..dda99f4 --- /dev/null +++ b/database/migrations/004_avatar_url.sql @@ -0,0 +1 @@ +ALTER TABLE users ADD COLUMN IF NOT EXISTS avatar_url TEXT; \ No newline at end of file diff --git a/database/migrations/005_last_read_at.sql b/database/migrations/005_last_read_at.sql new file mode 100644 index 0000000..06510fb --- /dev/null +++ b/database/migrations/005_last_read_at.sql @@ -0,0 +1,2 @@ +ALTER TABLE conversation_participants ADD COLUMN IF NOT EXISTS last_read_at TIMESTAMPTZ; +UPDATE conversation_participants SET last_read_at = NOW() WHERE last_read_at IS NULL; \ No newline at end of file diff --git a/database/migrations/006_test_leads.sql b/database/migrations/006_test_leads.sql new file mode 100644 index 0000000..df557da --- /dev/null +++ b/database/migrations/006_test_leads.sql @@ -0,0 +1,87 @@ +๏ปฟ-- Test leads - 2026-06-18 +INSERT INTO leads (id, stage_id, assigned_to, company_name, contact_name, email, phone, created_at, updated_at) VALUES +('c0011001-0000-0000-0000-000000000001', 'e0000000-0000-0000-0000-000000000001', '00000000-0000-0000-0000-000000000003', 'Test Company 1', 'Contact 1', 'contact1@test.com', '555-2636', '2025-12-23T22:09:54Z', '2025-12-23T22:09:54Z'), +('c0011001-0000-0000-0000-000000000002', 'e0000000-0000-0000-0000-000000000001', '00000000-0000-0000-0000-000000000003', 'Test Company 2', 'Contact 2', 'contact2@test.com', '555-5691', '2025-12-29T22:09:54Z', '2025-12-29T22:09:54Z'), +('c0011001-0000-0000-0000-000000000003', 'e0000000-0000-0000-0000-000000000001', '00000000-0000-0000-0000-000000000003', 'Test Company 3', 'Contact 3', 'contact3@test.com', '555-6446', '2025-12-24T22:09:54Z', '2025-12-24T22:09:54Z'), +('c0011001-0000-0000-0000-000000000004', 'e0000000-0000-0000-0000-000000000001', '00000000-0000-0000-0000-000000000003', 'Test Company 4', 'Contact 4', 'contact4@test.com', '555-5969', '2026-01-02T22:09:54Z', '2026-01-02T22:09:54Z'), +('c0011001-0000-0000-0000-000000000005', 'e0000000-0000-0000-0000-000000000001', '00000000-0000-0000-0000-000000000003', 'Test Company 5', 'Contact 5', 'contact5@test.com', '555-4200', '2026-01-14T22:09:54Z', '2026-01-14T22:09:54Z'), +('c0011001-0000-0000-0000-000000000006', 'e0000000-0000-0000-0000-000000000001', '00000000-0000-0000-0000-000000000003', 'Test Company 6', 'Contact 6', 'contact6@test.com', '555-8324', '2026-01-03T22:09:54Z', '2026-01-03T22:09:54Z'), +('c0011001-0000-0000-0000-000000000007', 'e0000000-0000-0000-0000-000000000001', '00000000-0000-0000-0000-000000000003', 'Test Company 7', 'Contact 7', 'contact7@test.com', '555-1047', '2025-12-27T22:09:54Z', '2025-12-27T22:09:54Z'), +('c0011001-0000-0000-0000-000000000008', 'e0000000-0000-0000-0000-000000000001', '00000000-0000-0000-0000-000000000003', 'Test Company 8', 'Contact 8', 'contact8@test.com', '555-634', '2026-01-20T22:09:54Z', '2026-01-20T22:09:54Z'), +('c0011001-0000-0000-0000-000000000009', 'e0000000-0000-0000-0000-000000000001', '00000000-0000-0000-0000-000000000003', 'Test Company 9', 'Contact 9', 'contact9@test.com', '555-3245', '2026-01-15T22:09:54Z', '2026-01-15T22:09:54Z'), +('c0011001-0000-0000-0000-000000000010', 'e0000000-0000-0000-0000-000000000001', '00000000-0000-0000-0000-000000000003', 'Test Company 10', 'Contact 10', 'contact10@test.com', '555-3656', '2026-01-12T22:09:54Z', '2026-01-12T22:09:54Z'), +('c0011001-0000-0000-0000-000000000011', 'e0000000-0000-0000-0000-000000000001', '00000000-0000-0000-0000-000000000003', 'Test Company 11', 'Contact 11', 'contact11@test.com', '555-2993', '2026-01-23T22:09:54Z', '2026-01-23T22:09:54Z'), +('c0011001-0000-0000-0000-000000000012', 'e0000000-0000-0000-0000-000000000001', '00000000-0000-0000-0000-000000000003', 'Test Company 12', 'Contact 12', 'contact12@test.com', '555-1634', '2026-02-24T22:09:54Z', '2026-02-24T22:09:54Z'), +('c0011001-0000-0000-0000-000000000013', 'e0000000-0000-0000-0000-000000000001', '00000000-0000-0000-0000-000000000003', 'Test Company 13', 'Contact 13', 'contact13@test.com', '555-2032', '2026-02-10T22:09:54Z', '2026-02-10T22:09:54Z'), +('c0011001-0000-0000-0000-000000000014', 'e0000000-0000-0000-0000-000000000001', '00000000-0000-0000-0000-000000000003', 'Test Company 14', 'Contact 14', 'contact14@test.com', '555-867', '2026-01-31T22:09:54Z', '2026-01-31T22:09:54Z'), +('c0011001-0000-0000-0000-000000000015', 'e0000000-0000-0000-0000-000000000001', '00000000-0000-0000-0000-000000000003', 'Test Company 15', 'Contact 15', 'contact15@test.com', '555-2136', '2026-02-02T22:09:54Z', '2026-02-02T22:09:54Z'), +('c0011001-0000-0000-0000-000000000016', 'e0000000-0000-0000-0000-000000000001', '00000000-0000-0000-0000-000000000003', 'Test Company 16', 'Contact 16', 'contact16@test.com', '555-89', '2026-01-20T22:09:54Z', '2026-01-20T22:09:54Z'), +('c0011001-0000-0000-0000-000000000017', 'e0000000-0000-0000-0000-000000000001', '00000000-0000-0000-0000-000000000003', 'Test Company 17', 'Contact 17', 'contact17@test.com', '555-4274', '2026-01-25T22:09:54Z', '2026-01-25T22:09:54Z'), +('c0011001-0000-0000-0000-000000000018', 'e0000000-0000-0000-0000-000000000001', '00000000-0000-0000-0000-000000000003', 'Test Company 18', 'Contact 18', 'contact18@test.com', '555-4294', '2026-03-16T22:09:54Z', '2026-03-16T22:09:54Z'), +('c0011001-0000-0000-0000-000000000019', 'e0000000-0000-0000-0000-000000000001', '00000000-0000-0000-0000-000000000003', 'Test Company 19', 'Contact 19', 'contact19@test.com', '555-7998', '2025-12-30T22:09:54Z', '2025-12-30T22:09:54Z'), +('c0011001-0000-0000-0000-000000000020', 'e0000000-0000-0000-0000-000000000001', '00000000-0000-0000-0000-000000000003', 'Test Company 20', 'Contact 20', 'contact20@test.com', '555-8557', '2026-02-13T22:09:54Z', '2026-02-13T22:09:54Z'), +('c0011001-0000-0000-0000-000000000021', 'e0000000-0000-0000-0000-000000000001', '00000000-0000-0000-0000-000000000003', 'Test Company 21', 'Contact 21', 'contact21@test.com', '555-5170', '2026-04-11T22:09:54Z', '2026-04-11T22:09:54Z'), +('c0011001-0000-0000-0000-000000000022', 'e0000000-0000-0000-0000-000000000001', '00000000-0000-0000-0000-000000000003', 'Test Company 22', 'Contact 22', 'contact22@test.com', '555-6250', '2025-12-21T22:09:54Z', '2025-12-21T22:09:54Z'), +('c0011001-0000-0000-0000-000000000023', 'e0000000-0000-0000-0000-000000000001', '00000000-0000-0000-0000-000000000003', 'Test Company 23', 'Contact 23', 'contact23@test.com', '555-5611', '2026-01-06T22:09:54Z', '2026-01-06T22:09:54Z'), +('c0011001-0000-0000-0000-000000000024', 'e0000000-0000-0000-0000-000000000001', '00000000-0000-0000-0000-000000000003', 'Test Company 24', 'Contact 24', 'contact24@test.com', '555-3467', '2026-04-16T22:09:54Z', '2026-04-16T22:09:54Z'), +('c0011001-0000-0000-0000-000000000025', 'e0000000-0000-0000-0000-000000000001', '00000000-0000-0000-0000-000000000003', 'Test Company 25', 'Contact 25', 'contact25@test.com', '555-7087', '2026-05-07T22:09:54Z', '2026-05-07T22:09:54Z'), +('c0011001-0000-0000-0000-000000000026', 'e0000000-0000-0000-0000-000000000001', '00000000-0000-0000-0000-000000000003', 'Test Company 26', 'Contact 26', 'contact26@test.com', '555-2945', '2025-12-22T22:09:54Z', '2025-12-22T22:09:54Z'), +('c0011001-0000-0000-0000-000000000027', 'e0000000-0000-0000-0000-000000000001', '00000000-0000-0000-0000-000000000003', 'Test Company 27', 'Contact 27', 'contact27@test.com', '555-9502', '2026-01-09T22:09:54Z', '2026-01-09T22:09:54Z'), +('c0011001-0000-0000-0000-000000000028', 'e0000000-0000-0000-0000-000000000001', '00000000-0000-0000-0000-000000000003', 'Test Company 28', 'Contact 28', 'contact28@test.com', '555-8114', '2026-05-10T22:09:54Z', '2026-05-10T22:09:54Z'), +('c0011001-0000-0000-0000-000000000029', 'e0000000-0000-0000-0000-000000000001', '00000000-0000-0000-0000-000000000003', 'Test Company 29', 'Contact 29', 'contact29@test.com', '555-4926', '2026-03-02T22:09:54Z', '2026-03-02T22:09:54Z'), +('c0011001-0000-0000-0000-000000000030', 'e0000000-0000-0000-0000-000000000001', '00000000-0000-0000-0000-000000000003', 'Test Company 30', 'Contact 30', 'contact30@test.com', '555-2421', '2026-03-17T22:09:54Z', '2026-03-17T22:09:54Z'), +('c0011001-0000-0000-0000-000000000031', 'e0000000-0000-0000-0000-000000000002', '00000000-0000-0000-0000-000000000003', 'Test Company 31', 'Contact 31', 'contact31@test.com', '555-7506', '2025-12-21T22:09:54Z', '2025-12-21T22:09:54Z'), +('c0011001-0000-0000-0000-000000000032', 'e0000000-0000-0000-0000-000000000002', '00000000-0000-0000-0000-000000000003', 'Test Company 32', 'Contact 32', 'contact32@test.com', '555-6832', '2025-12-29T22:09:54Z', '2025-12-29T22:09:54Z'), +('c0011001-0000-0000-0000-000000000033', 'e0000000-0000-0000-0000-000000000002', '00000000-0000-0000-0000-000000000003', 'Test Company 33', 'Contact 33', 'contact33@test.com', '555-6548', '2026-01-07T22:09:54Z', '2026-01-07T22:09:54Z'), +('c0011001-0000-0000-0000-000000000034', 'e0000000-0000-0000-0000-000000000002', '00000000-0000-0000-0000-000000000003', 'Test Company 34', 'Contact 34', 'contact34@test.com', '555-8793', '2026-01-02T22:09:54Z', '2026-01-02T22:09:54Z'), +('c0011001-0000-0000-0000-000000000035', 'e0000000-0000-0000-0000-000000000002', '00000000-0000-0000-0000-000000000003', 'Test Company 35', 'Contact 35', 'contact35@test.com', '555-6217', '2026-01-29T22:09:54Z', '2026-01-29T22:09:54Z'), +('c0011001-0000-0000-0000-000000000036', 'e0000000-0000-0000-0000-000000000002', '00000000-0000-0000-0000-000000000003', 'Test Company 36', 'Contact 36', 'contact36@test.com', '555-9206', '2025-12-31T22:09:54Z', '2025-12-31T22:09:54Z'), +('c0011001-0000-0000-0000-000000000037', 'e0000000-0000-0000-0000-000000000002', '00000000-0000-0000-0000-000000000003', 'Test Company 37', 'Contact 37', 'contact37@test.com', '555-1599', '2025-12-26T22:09:54Z', '2025-12-26T22:09:54Z'), +('c0011001-0000-0000-0000-000000000038', 'e0000000-0000-0000-0000-000000000002', '00000000-0000-0000-0000-000000000003', 'Test Company 38', 'Contact 38', 'contact38@test.com', '555-3873', '2025-12-29T22:09:54Z', '2025-12-29T22:09:54Z'), +('c0011001-0000-0000-0000-000000000039', 'e0000000-0000-0000-0000-000000000002', '00000000-0000-0000-0000-000000000003', 'Test Company 39', 'Contact 39', 'contact39@test.com', '555-6122', '2026-01-09T22:09:54Z', '2026-01-09T22:09:54Z'), +('c0011001-0000-0000-0000-000000000040', 'e0000000-0000-0000-0000-000000000002', '00000000-0000-0000-0000-000000000003', 'Test Company 40', 'Contact 40', 'contact40@test.com', '555-832', '2026-01-13T22:09:54Z', '2026-01-13T22:09:54Z'), +('c0011001-0000-0000-0000-000000000041', 'e0000000-0000-0000-0000-000000000002', '00000000-0000-0000-0000-000000000003', 'Test Company 41', 'Contact 41', 'contact41@test.com', '555-5979', '2026-01-11T22:09:54Z', '2026-01-11T22:09:54Z'), +('c0011001-0000-0000-0000-000000000042', 'e0000000-0000-0000-0000-000000000002', '00000000-0000-0000-0000-000000000003', 'Test Company 42', 'Contact 42', 'contact42@test.com', '555-1823', '2025-12-26T22:09:54Z', '2025-12-26T22:09:54Z'), +('c0011001-0000-0000-0000-000000000043', 'e0000000-0000-0000-0000-000000000002', '00000000-0000-0000-0000-000000000003', 'Test Company 43', 'Contact 43', 'contact43@test.com', '555-7102', '2026-02-28T22:09:54Z', '2026-02-28T22:09:54Z'), +('c0011001-0000-0000-0000-000000000044', 'e0000000-0000-0000-0000-000000000002', '00000000-0000-0000-0000-000000000003', 'Test Company 44', 'Contact 44', 'contact44@test.com', '555-5106', '2026-01-22T22:09:54Z', '2026-01-22T22:09:54Z'), +('c0011001-0000-0000-0000-000000000045', 'e0000000-0000-0000-0000-000000000002', '00000000-0000-0000-0000-000000000003', 'Test Company 45', 'Contact 45', 'contact45@test.com', '555-3278', '2026-03-27T22:09:54Z', '2026-03-27T22:09:54Z'), +('c0011001-0000-0000-0000-000000000046', 'e0000000-0000-0000-0000-000000000002', '00000000-0000-0000-0000-000000000003', 'Test Company 46', 'Contact 46', 'contact46@test.com', '555-4973', '2026-04-28T22:09:54Z', '2026-04-28T22:09:54Z'), +('c0011001-0000-0000-0000-000000000047', 'e0000000-0000-0000-0000-000000000002', '00000000-0000-0000-0000-000000000003', 'Test Company 47', 'Contact 47', 'contact47@test.com', '555-3297', '2026-03-01T22:09:54Z', '2026-03-01T22:09:54Z'), +('c0011001-0000-0000-0000-000000000048', 'e0000000-0000-0000-0000-000000000002', '00000000-0000-0000-0000-000000000003', 'Test Company 48', 'Contact 48', 'contact48@test.com', '555-4584', '2026-03-18T22:09:54Z', '2026-03-18T22:09:54Z'), +('c0011001-0000-0000-0000-000000000049', 'e0000000-0000-0000-0000-000000000002', '00000000-0000-0000-0000-000000000003', 'Test Company 49', 'Contact 49', 'contact49@test.com', '555-6788', '2026-03-04T22:09:54Z', '2026-03-04T22:09:54Z'), +('c0011001-0000-0000-0000-000000000050', 'e0000000-0000-0000-0000-000000000002', '00000000-0000-0000-0000-000000000003', 'Test Company 50', 'Contact 50', 'contact50@test.com', '555-2734', '2026-01-03T22:09:54Z', '2026-01-03T22:09:54Z'), +('c0011001-0000-0000-0000-000000000051', 'e0000000-0000-0000-0000-000000000003', '00000000-0000-0000-0000-000000000003', 'Test Company 51', 'Contact 51', 'contact51@test.com', '555-2625', '2025-12-19T22:09:54Z', '2025-12-19T22:09:54Z'), +('c0011001-0000-0000-0000-000000000052', 'e0000000-0000-0000-0000-000000000003', '00000000-0000-0000-0000-000000000003', 'Test Company 52', 'Contact 52', 'contact52@test.com', '555-620', '2025-12-21T22:09:54Z', '2025-12-21T22:09:54Z'), +('c0011001-0000-0000-0000-000000000053', 'e0000000-0000-0000-0000-000000000003', '00000000-0000-0000-0000-000000000003', 'Test Company 53', 'Contact 53', 'contact53@test.com', '555-6691', '2025-12-22T22:09:54Z', '2025-12-22T22:09:54Z'), +('c0011001-0000-0000-0000-000000000054', 'e0000000-0000-0000-0000-000000000003', '00000000-0000-0000-0000-000000000003', 'Test Company 54', 'Contact 54', 'contact54@test.com', '555-8847', '2026-01-25T22:09:54Z', '2026-01-25T22:09:54Z'), +('c0011001-0000-0000-0000-000000000055', 'e0000000-0000-0000-0000-000000000003', '00000000-0000-0000-0000-000000000003', 'Test Company 55', 'Contact 55', 'contact55@test.com', '555-2187', '2026-02-02T22:09:54Z', '2026-02-02T22:09:54Z'), +('c0011001-0000-0000-0000-000000000056', 'e0000000-0000-0000-0000-000000000003', '00000000-0000-0000-0000-000000000003', 'Test Company 56', 'Contact 56', 'contact56@test.com', '555-3267', '2026-01-19T22:09:54Z', '2026-01-19T22:09:54Z'), +('c0011001-0000-0000-0000-000000000057', 'e0000000-0000-0000-0000-000000000003', '00000000-0000-0000-0000-000000000003', 'Test Company 57', 'Contact 57', 'contact57@test.com', '555-8119', '2026-03-05T22:09:54Z', '2026-03-05T22:09:54Z'), +('c0011001-0000-0000-0000-000000000058', 'e0000000-0000-0000-0000-000000000003', '00000000-0000-0000-0000-000000000003', 'Test Company 58', 'Contact 58', 'contact58@test.com', '555-3181', '2025-12-19T22:09:54Z', '2025-12-19T22:09:54Z'), +('c0011001-0000-0000-0000-000000000059', 'e0000000-0000-0000-0000-000000000003', '00000000-0000-0000-0000-000000000003', 'Test Company 59', 'Contact 59', 'contact59@test.com', '555-4403', '2026-02-23T22:09:54Z', '2026-02-23T22:09:54Z'), +('c0011001-0000-0000-0000-000000000060', 'e0000000-0000-0000-0000-000000000003', '00000000-0000-0000-0000-000000000003', 'Test Company 60', 'Contact 60', 'contact60@test.com', '555-1407', '2026-04-06T22:09:54Z', '2026-04-06T22:09:54Z'), +('c0011001-0000-0000-0000-000000000061', 'e0000000-0000-0000-0000-000000000003', '00000000-0000-0000-0000-000000000003', 'Test Company 61', 'Contact 61', 'contact61@test.com', '555-2333', '2026-02-27T22:09:54Z', '2026-02-27T22:09:54Z'), +('c0011001-0000-0000-0000-000000000062', 'e0000000-0000-0000-0000-000000000003', '00000000-0000-0000-0000-000000000003', 'Test Company 62', 'Contact 62', 'contact62@test.com', '555-1436', '2026-04-26T22:09:54Z', '2026-04-26T22:09:54Z'), +('c0011001-0000-0000-0000-000000000063', 'e0000000-0000-0000-0000-000000000003', '00000000-0000-0000-0000-000000000003', 'Test Company 63', 'Contact 63', 'contact63@test.com', '555-7135', '2026-04-19T22:09:54Z', '2026-04-19T22:09:54Z'), +('c0011001-0000-0000-0000-000000000064', 'e0000000-0000-0000-0000-000000000003', '00000000-0000-0000-0000-000000000003', 'Test Company 64', 'Contact 64', 'contact64@test.com', '555-9366', '2026-01-22T22:09:54Z', '2026-01-22T22:09:54Z'), +('c0011001-0000-0000-0000-000000000065', 'e0000000-0000-0000-0000-000000000003', '00000000-0000-0000-0000-000000000003', 'Test Company 65', 'Contact 65', 'contact65@test.com', '555-7135', '2026-02-18T22:09:54Z', '2026-02-18T22:09:54Z'), +('c0011001-0000-0000-0000-000000000066', 'e0000000-0000-0000-0000-000000000007', '00000000-0000-0000-0000-000000000003', 'Test Company 66', 'Contact 66', 'contact66@test.com', '555-6854', '2025-12-19T22:09:54Z', '2025-12-19T22:09:54Z'), +('c0011001-0000-0000-0000-000000000067', 'e0000000-0000-0000-0000-000000000007', '00000000-0000-0000-0000-000000000003', 'Test Company 67', 'Contact 67', 'contact67@test.com', '555-2322', '2025-12-24T22:09:54Z', '2025-12-24T22:09:54Z'), +('c0011001-0000-0000-0000-000000000068', 'e0000000-0000-0000-0000-000000000007', '00000000-0000-0000-0000-000000000003', 'Test Company 68', 'Contact 68', 'contact68@test.com', '555-2632', '2026-01-13T22:09:54Z', '2026-01-13T22:09:54Z'), +('c0011001-0000-0000-0000-000000000069', 'e0000000-0000-0000-0000-000000000007', '00000000-0000-0000-0000-000000000003', 'Test Company 69', 'Contact 69', 'contact69@test.com', '555-876', '2025-12-29T22:09:54Z', '2025-12-29T22:09:54Z'), +('c0011001-0000-0000-0000-000000000070', 'e0000000-0000-0000-0000-000000000007', '00000000-0000-0000-0000-000000000003', 'Test Company 70', 'Contact 70', 'contact70@test.com', '555-974', '2026-01-15T22:09:54Z', '2026-01-15T22:09:54Z'), +('c0011001-0000-0000-0000-000000000071', 'e0000000-0000-0000-0000-000000000007', '00000000-0000-0000-0000-000000000003', 'Test Company 71', 'Contact 71', 'contact71@test.com', '555-1919', '2026-01-18T22:09:54Z', '2026-01-18T22:09:54Z'), +('c0011001-0000-0000-0000-000000000072', 'e0000000-0000-0000-0000-000000000007', '00000000-0000-0000-0000-000000000003', 'Test Company 72', 'Contact 72', 'contact72@test.com', '555-3034', '2025-12-25T22:09:54Z', '2025-12-25T22:09:54Z'), +('c0011001-0000-0000-0000-000000000073', 'e0000000-0000-0000-0000-000000000007', '00000000-0000-0000-0000-000000000003', 'Test Company 73', 'Contact 73', 'contact73@test.com', '555-1795', '2026-01-27T22:09:54Z', '2026-01-27T22:09:54Z'), +('c0011001-0000-0000-0000-000000000074', 'e0000000-0000-0000-0000-000000000007', '00000000-0000-0000-0000-000000000003', 'Test Company 74', 'Contact 74', 'contact74@test.com', '555-727', '2026-01-08T22:09:54Z', '2026-01-08T22:09:54Z'), +('c0011001-0000-0000-0000-000000000075', 'e0000000-0000-0000-0000-000000000007', '00000000-0000-0000-0000-000000000003', 'Test Company 75', 'Contact 75', 'contact75@test.com', '555-3877', '2026-03-08T22:09:54Z', '2026-03-08T22:09:54Z'), +('c0011001-0000-0000-0000-000000000076', 'e0000000-0000-0000-0000-000000000007', '00000000-0000-0000-0000-000000000003', 'Test Company 76', 'Contact 76', 'contact76@test.com', '555-6384', '2026-01-13T22:09:54Z', '2026-01-13T22:09:54Z'), +('c0011001-0000-0000-0000-000000000077', 'e0000000-0000-0000-0000-000000000007', '00000000-0000-0000-0000-000000000003', 'Test Company 77', 'Contact 77', 'contact77@test.com', '555-5607', '2026-01-12T22:09:54Z', '2026-01-12T22:09:54Z'), +('c0011001-0000-0000-0000-000000000078', 'e0000000-0000-0000-0000-000000000007', '00000000-0000-0000-0000-000000000003', 'Test Company 78', 'Contact 78', 'contact78@test.com', '555-6225', '2026-01-12T22:09:54Z', '2026-01-12T22:09:54Z'), +('c0011001-0000-0000-0000-000000000079', 'e0000000-0000-0000-0000-000000000007', '00000000-0000-0000-0000-000000000003', 'Test Company 79', 'Contact 79', 'contact79@test.com', '555-9288', '2026-04-16T22:09:54Z', '2026-04-16T22:09:54Z'), +('c0011001-0000-0000-0000-000000000080', 'e0000000-0000-0000-0000-000000000007', '00000000-0000-0000-0000-000000000003', 'Test Company 80', 'Contact 80', 'contact80@test.com', '555-8660', '2026-02-16T22:09:54Z', '2026-02-16T22:09:54Z'), +('c0011001-0000-0000-0000-000000000081', 'e0000000-0000-0000-0000-000000000007', '00000000-0000-0000-0000-000000000003', 'Test Company 81', 'Contact 81', 'contact81@test.com', '555-826', '2025-12-28T22:09:54Z', '2025-12-28T22:09:54Z'), +('c0011001-0000-0000-0000-000000000082', 'e0000000-0000-0000-0000-000000000007', '00000000-0000-0000-0000-000000000003', 'Test Company 82', 'Contact 82', 'contact82@test.com', '555-1761', '2026-03-11T22:09:54Z', '2026-03-11T22:09:54Z'), +('c0011001-0000-0000-0000-000000000083', 'e0000000-0000-0000-0000-000000000007', '00000000-0000-0000-0000-000000000003', 'Test Company 83', 'Contact 83', 'contact83@test.com', '555-9860', '2026-05-18T22:09:54Z', '2026-05-18T22:09:54Z'), +('c0011001-0000-0000-0000-000000000084', 'e0000000-0000-0000-0000-000000000007', '00000000-0000-0000-0000-000000000003', 'Test Company 84', 'Contact 84', 'contact84@test.com', '555-1104', '2026-06-01T22:09:54Z', '2026-06-01T22:09:54Z'), +('c0011001-0000-0000-0000-000000000085', 'e0000000-0000-0000-0000-000000000007', '00000000-0000-0000-0000-000000000003', 'Test Company 85', 'Contact 85', 'contact85@test.com', '555-9528', '2026-06-14T22:09:54Z', '2026-06-14T22:09:54Z'); diff --git a/src/app/(dashboard)/chats/page.tsx b/src/app/(dashboard)/chats/page.tsx index 580b980..0af8154 100644 --- a/src/app/(dashboard)/chats/page.tsx +++ b/src/app/(dashboard)/chats/page.tsx @@ -14,7 +14,6 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" -import { conversations as conversationsData } from "@/data/chats" import { Search, Send, Phone, Video, MoreHorizontal, Paperclip, Smile, Flag, Ban, Trash2, Image, FileIcon, X, Mic, Square, Play, Pause, Check, CheckCheck, @@ -94,7 +93,7 @@ function VoiceMessagePlayer({ src, initialDuration }: { src: string; initialDura export default function ChatsPage() { const { theme } = useTheme() const { user } = useUser() - const [activeChat, setActiveChat] = useState(conversationsData[0]?.id ?? null) + const [activeChat, setActiveChat] = useState(null) const [messageInput, setMessageInput] = useState("") const [showEmojiPicker, setShowEmojiPicker] = useState(false) const [attachments, setAttachments] = useState([]) @@ -103,7 +102,7 @@ export default function ChatsPage() { const [reportDialogOpen, setReportDialogOpen] = useState(false) const [reportReason, setReportReason] = useState("") const [forwardDialogOpen, setForwardDialogOpen] = useState(false) - const [forwardMessage, setForwardMessage] = useState(null) + const [forwardMessage, setForwardMessage] = useState(null) const [forwardSearch, setForwardSearch] = useState("") const [searchQuery, setSearchQuery] = useState("") const [isRecording, setIsRecording] = useState(false) @@ -112,18 +111,17 @@ export default function ChatsPage() { const recordingDurationRef = useRef(0) const [voiceMessages, setVoiceMessages] = useState>(new Map()) const [messageAttachments, setMessageAttachments] = useState>(new Map()) - const [replyingTo, setReplyingTo] = useState(null) - const [editingMessage, setEditingMessage] = useState(null) + const [replyingTo, setReplyingTo] = useState(null) + const [editingMessage, setEditingMessage] = useState(null) const [replyMap, setReplyMap] = useState>(new Map()) - const [messageStatus, setMessageStatus] = useState>(() => { - const map = new Map() - conversationsData.forEach((conv) => - conv.messages.forEach((msg) => { - if (msg.senderId === "user1") map.set(msg.id, "read") - }) - ) - return map - }) + // message status tracking removed โ€” now using msg.read from API + const [conversations, setConversations] = useState([]) + const [conversationMessages, setConversationMessages] = useState>(new Map()) + const [loadingChats, setLoadingChats] = useState(true) + const [searchResults, setSearchResults] = useState([]) + const [searchingUsers, setSearchingUsers] = useState(false) + const [unreadMap, setUnreadMap] = useState>(new Map()) + const [previewAvatarUrl, setPreviewAvatarUrl] = useState(null) const fileInputRef = useRef(null) const textareaRef = useRef(null) const emojiPickerRef = useRef(null) @@ -141,15 +139,79 @@ export default function ChatsPage() { ta.style.height = `${ta.scrollHeight}px` }, []) - const [conversations, setConversations] = useState(conversationsData) if (!user) return null + + const messages = conversationMessages.get(activeChat || "") || [] const conversation = conversations.find((c) => c.id === activeChat) - const otherParticipant = (conv: typeof conversationsData[0]) => - conv.participants.find((p) => p.id !== "user1") ?? conv.participants[0] + const otherParticipant = (conv: any) => conv.otherUser const filteredConversations = searchQuery.trim() ? conversations.filter((conv) => otherParticipant(conv).name.toLowerCase().includes(searchQuery.toLowerCase())) : conversations + // Fetch conversations from API + useEffect(() => { + const fetchConvs = async () => { + try { + const res = await fetch("/api/conversations") + if (res.ok) { + const data = await res.json() + const convs = data.conversations || [] + setConversations(convs) + setUnreadMap(new Map(convs.map((c: any) => [c.id, c.unread]))) + if (convs.length > 0) setActiveChat(convs[0].id) + } + } catch { + // ignore + } finally { + setLoadingChats(false) + } + } + fetchConvs() + }, []) + + // Fetch messages when active chat changes, and mark as read + useEffect(() => { + if (!activeChat) return + const fetchMsgs = async () => { + try { + const res = await fetch(`/api/conversations/${activeChat}/messages`) + if (res.ok) { + const data = await res.json() + setConversationMessages((prev) => { + const next = new Map(prev) + next.set(activeChat, data.messages || []) + return next + }) + } + } catch { + // ignore + } + // Mark conversation as read + fetch(`/api/conversations/${activeChat}/read`, { method: "POST" }).catch(() => {}) + setUnreadMap((prev) => { const m = new Map(prev); m.set(activeChat, 0); return m }) + } + fetchMsgs() + }, [activeChat]) + + // Search users when search query changes + useEffect(() => { + const q = searchQuery.trim() + if (!q) { setSearchResults([]); return } + const timer = setTimeout(async () => { + setSearchingUsers(true) + try { + const res = await fetch(`/api/users/search?q=${encodeURIComponent(q)}`) + if (res.ok) { + const data = await res.json() + const existingIds = new Set(conversations.map((c) => otherParticipant(c).id)) + setSearchResults(data.users.filter((u: any) => !existingIds.has(u.id))) + } + } catch { /* ignore */ } + setSearchingUsers(false) + }, 300) + return () => clearTimeout(timer) + }, [searchQuery, conversations]) + useEffect(() => { const handleClickOutside = (e: MouseEvent) => { if (emojiPickerRef.current && !emojiPickerRef.current.contains(e.target as Node)) { @@ -202,79 +264,77 @@ export default function ChatsPage() { setAttachments((prev) => prev.filter((_, i) => i !== index)) } - const addMessageToChat = (content: string, voice?: { url: string; duration: number }, replyTo?: { senderName: string; content: string }, attachments?: { name: string; type: string; url: string }[]) => { - const id = crypto.randomUUID() - const newMessage = { - id, - conversationId: activeChat!, - senderId: "user1", - senderName: "Sarah Chen", - senderAvatar: "SC", - content, - timestamp: new Date().toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }), - } - setConversations((prev) => - prev.map((conv) => - conv.id === activeChat - ? { - ...conv, - messages: [...conv.messages, newMessage], - lastMessage: voice ? "๐ŸŽค Voice note" : (replyTo ? `โ†ช ${content}` : content), - lastMessageTime: "Just now", - unread: 0, - } - : conv - ) - ) - setMessageStatus((prev) => { - const next = new Map(prev) - next.set(id, "sent") - return next - }) - setTimeout(() => { - setMessageStatus((prev) => { + const addMessageToChat = async (content: string, voice?: { url: string; duration: number }, replyTo?: { senderName: string; content: string }, fileAttachments?: { name: string; type: string; url: string }[]) => { + if (!activeChat || !user) return + const payload = voice ? "[Voice message]" : content + try { + const res = await fetch(`/api/conversations/${activeChat}/messages`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ content: payload }), + }) + if (!res.ok) return + const data = await res.json() + const msg = data.message + setConversationMessages((prev) => { const next = new Map(prev) - if (next.get(id) === "sent") next.set(id, "delivered") + const existing = next.get(activeChat) || [] + next.set(activeChat, [...existing, msg]) return next }) - }, 1000) - if (voice) { - setVoiceMessages((prev) => { - const next = new Map(prev) - next.set(id, voice) - return next - }) - } - if (replyTo) { - setReplyMap((prev) => { - const next = new Map(prev) - next.set(id, { repliedToSender: replyTo.senderName, repliedToContent: replyTo.content }) - return next - }) - } - if (attachments && attachments.length > 0) { - setMessageAttachments((prev) => { - const next = new Map(prev) - next.set(id, attachments) - return next + setConversations((prev) => { + const updated = prev.map((conv) => + conv.id === activeChat + ? { ...conv, lastMessage: content, lastMessageTime: "Just now" } + : conv + ) + const idx = updated.findIndex((c) => c.id === activeChat) + if (idx > 0) { + const [item] = updated.splice(idx, 1) + updated.unshift(item) + } + return updated }) + setUnreadMap((prev) => { const m = new Map(prev); m.set(activeChat, 0); return m }) + await fetch(`/api/conversations/${activeChat}/read`, { method: "POST" }) + if (voice) { + setVoiceMessages((prev) => { + const next = new Map(prev) + next.set(msg.id, voice) + return next + }) + } + if (replyTo) { + setReplyMap((prev) => { + const next = new Map(prev) + next.set(msg.id, { repliedToSender: replyTo.senderName, repliedToContent: replyTo.content }) + return next + }) + } + if (fileAttachments && fileAttachments.length > 0) { + setMessageAttachments((prev) => { + const next = new Map(prev) + next.set(msg.id, fileAttachments) + return next + }) + } + } catch { + toast.error("Failed to send message") } } - const handleSend = (e: React.FormEvent) => { + const handleSend = async (e: React.FormEvent) => { e.preventDefault() if (!messageInput.trim() && attachments.length === 0) return if (editingMessage) { - setConversations((prev) => - prev.map((conv) => ({ - ...conv, - messages: conv.messages.map((m) => - m.id === editingMessage.id ? { ...m, content: messageInput.trim() } : m - ), - lastMessage: conv.id === activeChat && conv.messages.some((m) => m.id === editingMessage.id) - ? messageInput.trim() : conv.lastMessage, - })) - ) + setConversationMessages((prev) => { + const next = new Map(prev) + const msgs = (next.get(activeChat || "") || []).map((m) => + m.id === editingMessage.id ? { ...m, content: messageInput.trim() } : m + ) + next.set(activeChat || "", msgs) + return next + }) setEditingMessage(null) setMessageInput("") setTimeout(() => autoResizeTextarea(), 0) @@ -284,11 +344,11 @@ export default function ChatsPage() { ? attachments.map((f) => ({ name: f.name, type: f.type, url: URL.createObjectURL(f) })) : undefined if (replyingTo) { - const replySender = replyingTo.senderId === "user1" ? user.name : otherParticipant(conversation!).name - addMessageToChat(messageInput.trim(), undefined, { senderName: replySender, content: replyingTo.content }, fileAttachments) + const replySender = replyingTo.senderId === user.id ? user.name : otherParticipant(conversation!).name + await addMessageToChat(messageInput.trim(), undefined, { senderName: replySender, content: replyingTo.content }, fileAttachments) setReplyingTo(null) } else { - addMessageToChat(messageInput.trim(), undefined, undefined, fileAttachments) + await addMessageToChat(messageInput.trim(), undefined, undefined, fileAttachments) } setMessageInput("") setTimeout(() => autoResizeTextarea(), 0) @@ -296,12 +356,18 @@ export default function ChatsPage() { } const handleDeleteMessage = (messageId: string) => { + setConversationMessages((prev) => { + const next = new Map(prev) + const msgs = (next.get(activeChat || "") || []).filter((m) => m.id !== messageId) + next.set(activeChat || "", msgs) + return next + }) setConversations((prev) => - prev.map((conv) => ({ - ...conv, - messages: conv.messages.filter((m) => m.id !== messageId), - lastMessage: conv.messages.filter((m) => m.id !== messageId).at(-1)?.content ?? conv.lastMessage, - })) + prev.map((conv) => + conv.id === activeChat + ? { ...conv, lastMessage: conversationMessages.get(activeChat || "")?.filter((m) => m.id !== messageId).at(-1)?.content ?? conv.lastMessage } + : conv + ) ) toast.success("Message deleted") } @@ -405,16 +471,30 @@ export default function ChatsPage() { const person = otherParticipant(conv) const isActive = conv.id === activeChat return ( - ) })} - {filteredConversations.length === 0 && searchQuery && ( + {searchResults.length > 0 && ( + <> + {searchQuery &&
New conversation
} + {searchResults.map((person: any) => ( + + ))} + + )} + {filteredConversations.length === 0 && searchResults.length === 0 && searchQuery && !searchingUsers && (
No conversations found
)} @@ -451,8 +571,9 @@ export default function ChatsPage() { {/* Chat header */}
- - {otherParticipant(conversation).avatar} + setPreviewAvatarUrl(otherParticipant(conversation).avatar)}> + + {otherParticipant(conversation).name?.charAt(0) || "?"}

{otherParticipant(conversation).name}

@@ -493,15 +614,15 @@ export default function ChatsPage() { {/* Messages */}
- {conversation.messages.map((msg) => { - const isMe = msg.senderId === "user1" + {messages.map((msg) => { + const isMe = msg.senderId === user?.id const voiceUrl = voiceMessages.get(msg.id) return (
- - {isMe ? : null} + setPreviewAvatarUrl(msg.senderAvatar)}> + - {msg.senderAvatar} + {msg.senderName?.charAt(0) || "?"}
@@ -608,12 +729,9 @@ export default function ChatsPage() { {msg.timestamp} {isMe && ( - (() => { - const status = messageStatus.get(msg.id) - if (status === "read") return - if (status === "delivered") return - return - })() + msg.read + ? + : )}
@@ -649,7 +767,7 @@ export default function ChatsPage() {

- Replying to {replyingTo.senderId === "user1" ? "yourself" : otherParticipant(conversation).name} + Replying to {replyingTo.senderId === user?.id ? "yourself" : otherParticipant(conversation).name}

{replyingTo.content}

@@ -690,7 +808,7 @@ export default function ChatsPage() {
) : ( <> -