diff --git a/bun.lock b/bun.lock
index b0ec268..6526a1e 100644
--- a/bun.lock
+++ b/bun.lock
@@ -7,8 +7,10 @@
"@convex-dev/auth": "^0.0.81",
"@hookform/resolvers": "^5.2.1",
"@radix-ui/react-avatar": "^1.1.10",
+ "@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-label": "^2.1.7",
+ "@radix-ui/react-progress": "^1.1.7",
"@radix-ui/react-separator": "^1.1.7",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-tabs": "^1.1.13",
@@ -29,12 +31,13 @@
"sonner": "^2.0.7",
"tailwind-merge": "^3.3.1",
"typescript-eslint": "^8.42.0",
+ "vaul": "^1.1.2",
"zod": "^4.1.5",
},
"devDependencies": {
"@eslint/eslintrc": "^3.3.1",
"@tailwindcss/postcss": "^4.1.12",
- "@types/node": "^20.19.12",
+ "@types/node": "^20.19.13",
"@types/react": "^19.1.12",
"@types/react-dom": "^19.1.9",
"dotenv": "^16.6.1",
@@ -555,6 +558,8 @@
"@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
+ "@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw=="],
+
"@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw=="],
"@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg=="],
@@ -579,6 +584,8 @@
"@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
+ "@radix-ui/react-progress": ["@radix-ui/react-progress@1.1.7", "", { "dependencies": { "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg=="],
+
"@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA=="],
"@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA=="],
@@ -1705,6 +1712,8 @@
"validate-npm-package-license": ["validate-npm-package-license@3.0.4", "", { "dependencies": { "spdx-correct": "^3.0.0", "spdx-expression-parse": "^3.0.0" } }, "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew=="],
+ "vaul": ["vaul@1.1.2", "", { "dependencies": { "@radix-ui/react-dialog": "^1.1.1" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA=="],
+
"vscode-languageserver-textdocument": ["vscode-languageserver-textdocument@1.0.12", "", {}, "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA=="],
"vscode-uri": ["vscode-uri@3.1.0", "", {}, "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ=="],
diff --git a/convex/auth.ts b/convex/auth.ts
index 34d65cf..12e0daa 100644
--- a/convex/auth.ts
+++ b/convex/auth.ts
@@ -21,8 +21,8 @@ export const getUser = query(async (ctx) => {
if (!user) throw new ConvexError('User not found.');
const image: Id<'_storage'> | null =
typeof user.image === 'string' && user.image.length > 0
- ? user.image as Id<'_storage'>
- : null
+ ? (user.image as Id<'_storage'>)
+ : null;
return {
id: user._id,
email: user.email ?? null,
@@ -56,7 +56,7 @@ export const updateUserEmail = mutation({
if (!user) throw new ConvexError('User not found.');
await ctx.db.patch(userId, { email });
return { success: true };
- }
+ },
});
export const updateUserImage = mutation({
@@ -70,8 +70,7 @@ export const updateUserImage = mutation({
if (!user) throw new ConvexError('User not found.');
const oldImage = user.image as Id<'_storage'> | undefined;
await ctx.db.patch(userId, { image: storageId });
- if (oldImage && oldImage !== storageId)
- await ctx.storage.delete(oldImage);
+ if (oldImage && oldImage !== storageId) await ctx.storage.delete(oldImage);
return { success: true };
},
});
@@ -94,14 +93,14 @@ export const updateUserPassword = action({
currentPassword: v.string(),
newPassword: v.string(),
},
- handler: async (ctx, {currentPassword, newPassword}) => {
+ handler: async (ctx, { currentPassword, newPassword }) => {
const userId = await getAuthUserId(ctx);
if (!userId) throw new ConvexError('Not authenticated.');
const user = await ctx.runQuery(api.auth.getUser);
if (!user?.email) throw new ConvexError('User not found.');
const verified = await retrieveAccount(ctx, {
provider: 'password',
- account: { id: user.email, secret: currentPassword }
+ account: { id: user.email, secret: currentPassword },
});
if (!verified) throw new ConvexError('Current password is incorrect.');
@@ -110,9 +109,9 @@ export const updateUserPassword = action({
await modifyAccountCredentials(ctx, {
provider: 'password',
- account: { id: user.email, secret: newPassword }
+ account: { id: user.email, secret: newPassword },
});
return { success: true };
},
-})
+});
diff --git a/convex/schema.ts b/convex/schema.ts
index 7e31b7a..fb6ae8f 100644
--- a/convex/schema.ts
+++ b/convex/schema.ts
@@ -17,14 +17,14 @@ export default defineSchema({
phoneVerificationTime: v.optional(v.number()),
isAnonymous: v.optional(v.boolean()),
})
- .index("email", ["email"])
- .index("phone", ["phone"]),
+ .index('email', ['email'])
+ .index('phone', ['phone']),
statuses: defineTable({
userId: v.id('users'),
message: v.string(),
updatedAt: v.number(),
updatedBy: v.id('users'),
})
- .index('by_user', ['userId'])
- .index('by_user_updatedAt', ['userId', 'updatedAt']),
+ .index('by_user', ['userId'])
+ .index('by_user_updatedAt', ['userId', 'updatedAt']),
});
diff --git a/convex/statuses.ts b/convex/statuses.ts
index 308f652..b4bfa98 100644
--- a/convex/statuses.ts
+++ b/convex/statuses.ts
@@ -11,22 +11,16 @@ import type { MutationCtx, QueryCtx } from './_generated/server';
type RWCtx = MutationCtx | QueryCtx;
// CHANGED: typed helpers
-const ensureUser = async (
- ctx: RWCtx,
- userId: Id<'users'>,
-) => {
+const ensureUser = async (ctx: RWCtx, userId: Id<'users'>) => {
const user = await ctx.db.get(userId);
if (!user) throw new ConvexError('User not found.');
return user;
};
-const latestStatusForOwner = async (
- ctx: RWCtx,
- ownerId: Id<'users'>,
-) => {
+const latestStatusForOwner = async (ctx: RWCtx, ownerId: Id<'users'>) => {
const [latest] = await ctx.db
.query('statuses')
- .withIndex('by_user_updatedAt', q => q.eq('userId', ownerId))
+ .withIndex('by_user_updatedAt', (q) => q.eq('userId', ownerId))
.order('desc')
.take(1);
return latest as Doc<'statuses'> | null;
@@ -180,7 +174,7 @@ export const listHistoryByUser = query({
return await ctx.db
.query('statuses')
- .withIndex('by_user_updatedAt', q => q.eq('userId', userId))
+ .withIndex('by_user_updatedAt', (q) => q.eq('userId', userId))
.order('desc')
.paginate(paginationOpts);
},
diff --git a/package.json b/package.json
index 53e38ff..a8452f3 100644
--- a/package.json
+++ b/package.json
@@ -21,8 +21,10 @@
"@convex-dev/auth": "^0.0.81",
"@hookform/resolvers": "^5.2.1",
"@radix-ui/react-avatar": "^1.1.10",
+ "@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-label": "^2.1.7",
+ "@radix-ui/react-progress": "^1.1.7",
"@radix-ui/react-separator": "^1.1.7",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-tabs": "^1.1.13",
@@ -43,6 +45,7 @@
"sonner": "^2.0.7",
"tailwind-merge": "^3.3.1",
"typescript-eslint": "^8.42.0",
+ "vaul": "^1.1.2",
"zod": "^4.1.5"
},
"devDependencies": {
diff --git a/src/app/(auth)/profile/page.tsx b/src/app/(auth)/profile/page.tsx
index dc89e1c..28655a7 100644
--- a/src/app/(auth)/profile/page.tsx
+++ b/src/app/(auth)/profile/page.tsx
@@ -7,7 +7,7 @@ import {
ProfileHeader,
ResetPasswordForm,
SignOutForm,
- UserInfoForm
+ UserInfoForm,
} from '@/components/layout/profile';
const Profile = async () => {
diff --git a/src/app/(auth)/signin/page.tsx b/src/app/(auth)/signin/page.tsx
index 016c6c3..ad60329 100644
--- a/src/app/(auth)/signin/page.tsx
+++ b/src/app/(auth)/signin/page.tsx
@@ -29,40 +29,46 @@ const signInFormSchema = z.object({
email: z.email({
message: 'Please enter a valid email address.',
}),
- password: z.string()
- .regex(/^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[@#$%^&+=])(?=\S+$).{8,100}$/, {
- message: 'Incorrect password. Does not meet requirements.'
- })
+ password: z
+ .string()
+ .regex(
+ /^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[@#$%^&+=])(?=\S+$).{8,100}$/,
+ {
+ message: 'Incorrect password. Does not meet requirements.',
+ },
+ ),
});
-const signUpFormSchema = z.object({
- name: z.string().min(2, {
- message: 'Name must be at least 2 characters.',
- }),
- email: z.email({
- message: 'Please enter a valid email address.',
- }),
- password: z.string()
- .min(8, { message: 'Password must be at least 8 characters.' })
- .max(100, { message: 'Password must be no more than 100 characters.' })
- .regex(/[0-9]/, { message: 'Password must contain at least one digit.' })
- .regex(/[a-z]/, {
- message: 'Password must contain at least one lowercase letter.'
- })
- .regex(/[A-Z]/, {
- message: 'Password must contain at least one uppercase letter.'
- })
- .regex(/[@#$%^&+=]/, {
- message: 'Password must contain at least one special character.'
+const signUpFormSchema = z
+ .object({
+ name: z.string().min(2, {
+ message: 'Name must be at least 2 characters.',
}),
- confirmPassword: z.string().min(8, {
- message: 'Password must be at least 8 characters.',
- }),
-})
-.refine((data) => data.password === data.confirmPassword, {
- message: 'Passwords do not match!',
- path: ['confirmPassword'],
-});
+ email: z.email({
+ message: 'Please enter a valid email address.',
+ }),
+ password: z
+ .string()
+ .min(8, { message: 'Password must be at least 8 characters.' })
+ .max(100, { message: 'Password must be no more than 100 characters.' })
+ .regex(/[0-9]/, { message: 'Password must contain at least one digit.' })
+ .regex(/[a-z]/, {
+ message: 'Password must contain at least one lowercase letter.',
+ })
+ .regex(/[A-Z]/, {
+ message: 'Password must contain at least one uppercase letter.',
+ })
+ .regex(/[@#$%^&+=]/, {
+ message: 'Password must contain at least one special character.',
+ }),
+ confirmPassword: z.string().min(8, {
+ message: 'Password must be at least 8 characters.',
+ }),
+ })
+ .refine((data) => data.password === data.confirmPassword, {
+ message: 'Passwords do not match!',
+ path: ['confirmPassword'],
+ });
export default function SignIn() {
const { signIn } = useAuthActions();
diff --git a/src/app/page.tsx b/src/app/page.tsx
index 160f74e..8e858bd 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -1,12 +1,21 @@
-'use client';
-
-import { useConvexAuth, useMutation, useQuery } from 'convex/react';
+'use server';
+import { preloadQuery } from 'convex/nextjs';
import { api } from '~/convex/_generated/api';
+import { StatusList } from '@/components/layout/status/list'
+import { useConvexAuth, useMutation, useQuery } from 'convex/react';
import Link from 'next/link';
import { useAuthActions } from '@convex-dev/auth/react';
-import { useRouter } from 'next/navigation';
-const Home = () => {
- return ;
+const Home = async () => {
+ const preloadedUser = await preloadQuery(api.auth.getUser);
+ const preloadedStatuses = await preloadQuery(api.statuses.getCurrentForAll)
+ return (
+