Template
Made great progress on monorepo & auth for next. Very happy with work!
This commit is contained in:
1 parent
72f11f0b02
commit
321fecb5e1
58 files changed
+1266
-222
No files matched your search
@@ -1 +1 @@
|
||||
[["1","2","3","4","5","6","7","8","9","10","11","12","13","14","15","16","17","18","19","20","21"],{"key":"22","value":"23"},{"key":"24","value":"25"},{"key":"26","value":"27"},{"key":"28","value":"29"},{"key":"30","value":"31"},{"key":"32","value":"33"},{"key":"34","value":"35"},{"key":"36","value":"37"},{"key":"38","value":"39"},{"key":"40","value":"41"},{"key":"42","value":"43"},{"key":"44","value":"45"},{"key":"46","value":"47"},{"key":"48","value":"49"},{"key":"50","value":"51"},{"key":"52","value":"53"},{"key":"54","value":"55"},{"key":"56","value":"57"},{"key":"58","value":"59"},{"key":"60","value":"61"},{"key":"62","value":"63"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/package.json",{"size":2249,"mtime":1766222924000,"hash":"64","data":"65"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/index.ts",{"size":28,"mtime":1768155639384,"hash":"66","data":"67"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/metro.config.js",{"size":511,"mtime":1768155639442,"hash":"68","data":"69"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/src/app/index.tsx",{"size":5019,"mtime":1768155639734,"hash":"70","data":"71"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/turbo.json",{"size":163,"mtime":1766222924000,"hash":"72","data":"73"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/postcss.config.js",{"size":66,"mtime":1768155639501,"hash":"74","data":"75"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/eas.json",{"size":567,"mtime":1766222924000,"hash":"76","data":"77"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/.expo-shared/assets.json",{"size":155,"mtime":1766222924000,"hash":"78","data":"79"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/src/app/_layout.tsx",{"size":927,"mtime":1768155639581,"hash":"80","data":"81"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/nativewind-env.d.ts",{"size":246,"mtime":1766222924000,"hash":"82","data":"83"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/src/utils/auth.ts",{"size":398,"mtime":1768155639911,"hash":"84","data":"85"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/src/app/post/[id].tsx",{"size":757,"mtime":1768155639785,"hash":"86","data":"87"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/src/styles.css",{"size":90,"mtime":1768155639819,"hash":"88","data":"89"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/src/utils/session-store.ts",{"size":272,"mtime":1768155639965,"hash":"90","data":"91"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/assets/icon-dark.png",{"size":19633,"mtime":1766222924000,"hash":"92"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/eslint.config.mts",{"size":275,"mtime":1768155639360,"hash":"93","data":"94"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/src/utils/api.tsx",{"size":1326,"mtime":1768155639881,"hash":"95","data":"96"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/src/utils/base-url.ts",{"size":880,"mtime":1768155639943,"hash":"97","data":"98"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/app.config.ts",{"size":1333,"mtime":1768155639275,"hash":"99","data":"100"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/assets/icon-light.png",{"size":19133,"mtime":1766222924000,"hash":"101"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/tsconfig.json",{"size":387,"mtime":1766228480000,"hash":"102","data":"103"},"d8763702c14cdc382dcfb84f6f9a068f",{"hashOfOptions":"104"},"11cdbef6afa001cd39bc187041ca6865",{"hashOfOptions":"105"},"dbe97bcde588a81538bbcd6a9befdddd",{"hashOfOptions":"106"},"1c10eb388cf5dcbc87d2d63770a227f1",{"hashOfOptions":"107"},"c7d4dcf839dfeaa02e0407adfd5e47a6",{"hashOfOptions":"108"},"b7edffce093c4c84092cc93f3dc208ef",{"hashOfOptions":"109"},"a3c1487f8318513ae7c156acc857fde2",{"hashOfOptions":"110"},"0f7f54c7161b8403d3bc42d91f59cd91",{"hashOfOptions":"111"},"8e407b4b1b0c0bd9c862a00243344be3",{"hashOfOptions":"112"},"d4d589c153ac8b5e7bf0fb130a5b5a7d",{"hashOfOptions":"113"},"cecbed1604a530a7cc099fecddddd76c",{"hashOfOptions":"114"},"4fcefde979d34a7339f7a266d3ec931b",{"hashOfOptions":"115"},"52a1d72379b952dd802f47e1865bd0da",{"hashOfOptions":"116"},"1bc3e15a40c117eecc51294886ea9b38",{"hashOfOptions":"117"},"1e8ac0d261e95efb19d290ffcf70ce36","1c1710ce3de3ce02e8054cc3787c8579",{"hashOfOptions":"118"},"5ff899a601102659dcbd2900e415ce8b",{"hashOfOptions":"119"},"dd2007a211e323deabb3f7fa7d16313f",{"hashOfOptions":"120"},"4f49c6df7733f874fbe72b4e20b3092b",{"hashOfOptions":"121"},"863da15dbd856008b7c24077ca746d91","6937fb7370f1e17491df649888d6ecc9",{"hashOfOptions":"122"},"2742538293","3367041120","3963086909","3173464737","3850474653","3346849767","2045418596","3048291867","3525769784","4000169781","471581913","1427279653","2305419495","818733105","2587128410","1262173017","408469422","2836740315","2315460658"]
|
||||
[["1","2","3","4","5","6","7","8","9","10","11","12","13","14","15","16","17","18","19","20","21","22"],{"key":"23","value":"24"},{"key":"25","value":"26"},{"key":"27","value":"28"},{"key":"29","value":"30"},{"key":"31","value":"32"},{"key":"33","value":"34"},{"key":"35","value":"36"},{"key":"37","value":"38"},{"key":"39","value":"40"},{"key":"41","value":"42"},{"key":"43","value":"44"},{"key":"45","value":"46"},{"key":"47","value":"48"},{"key":"49","value":"50"},{"key":"51","value":"52"},{"key":"53","value":"54"},{"key":"55","value":"56"},{"key":"57","value":"58"},{"key":"59","value":"60"},{"key":"61","value":"62"},{"key":"63","value":"64"},{"key":"65","value":"66"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/package.json",{"size":2249,"mtime":1766222924000,"hash":"67","data":"68"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/index.ts",{"size":28,"mtime":1768155639000,"hash":"69","data":"70"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/metro.config.js",{"size":511,"mtime":1768155639000,"hash":"71","data":"72"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/src/app/index.tsx",{"size":5019,"mtime":1768155639000,"hash":"73","data":"74"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/turbo.json",{"size":163,"mtime":1766222924000,"hash":"75","data":"76"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/postcss.config.js",{"size":66,"mtime":1768155639000,"hash":"77","data":"78"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/.cache/.prettiercache",{"size":4787,"mtime":1768171236840,"hash":"79"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/eas.json",{"size":567,"mtime":1766222924000,"hash":"80","data":"81"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/.expo-shared/assets.json",{"size":155,"mtime":1766222924000,"hash":"82","data":"83"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/src/app/_layout.tsx",{"size":927,"mtime":1768155639000,"hash":"84","data":"85"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/nativewind-env.d.ts",{"size":246,"mtime":1766222924000,"hash":"86","data":"87"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/src/utils/auth.ts",{"size":398,"mtime":1768155639000,"hash":"88","data":"89"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/src/app/post/[id].tsx",{"size":757,"mtime":1768155639000,"hash":"90","data":"91"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/src/styles.css",{"size":90,"mtime":1768155639000,"hash":"92","data":"93"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/src/utils/session-store.ts",{"size":272,"mtime":1768155639000,"hash":"94","data":"95"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/assets/icon-dark.png",{"size":19633,"mtime":1766222924000,"hash":"96"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/eslint.config.mts",{"size":275,"mtime":1768155639000,"hash":"97","data":"98"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/src/utils/api.tsx",{"size":1326,"mtime":1768155639000,"hash":"99","data":"100"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/src/utils/base-url.ts",{"size":880,"mtime":1768155639000,"hash":"101","data":"102"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/app.config.ts",{"size":1333,"mtime":1768155639000,"hash":"103","data":"104"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/assets/icon-light.png",{"size":19133,"mtime":1766222924000,"hash":"105"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/tsconfig.json",{"size":387,"mtime":1766228480000,"hash":"106","data":"107"},"d8763702c14cdc382dcfb84f6f9a068f",{"hashOfOptions":"108"},"11cdbef6afa001cd39bc187041ca6865",{"hashOfOptions":"109"},"dbe97bcde588a81538bbcd6a9befdddd",{"hashOfOptions":"110"},"1c10eb388cf5dcbc87d2d63770a227f1",{"hashOfOptions":"111"},"c7d4dcf839dfeaa02e0407adfd5e47a6",{"hashOfOptions":"112"},"b7edffce093c4c84092cc93f3dc208ef",{"hashOfOptions":"113"},"75e4e9158c89e7a7bc04e94fd2366743","a3c1487f8318513ae7c156acc857fde2",{"hashOfOptions":"114"},"0f7f54c7161b8403d3bc42d91f59cd91",{"hashOfOptions":"115"},"8e407b4b1b0c0bd9c862a00243344be3",{"hashOfOptions":"116"},"d4d589c153ac8b5e7bf0fb130a5b5a7d",{"hashOfOptions":"117"},"cecbed1604a530a7cc099fecddddd76c",{"hashOfOptions":"118"},"4fcefde979d34a7339f7a266d3ec931b",{"hashOfOptions":"119"},"52a1d72379b952dd802f47e1865bd0da",{"hashOfOptions":"120"},"1bc3e15a40c117eecc51294886ea9b38",{"hashOfOptions":"121"},"1e8ac0d261e95efb19d290ffcf70ce36","1c1710ce3de3ce02e8054cc3787c8579",{"hashOfOptions":"122"},"5ff899a601102659dcbd2900e415ce8b",{"hashOfOptions":"123"},"dd2007a211e323deabb3f7fa7d16313f",{"hashOfOptions":"124"},"4f49c6df7733f874fbe72b4e20b3092b",{"hashOfOptions":"125"},"863da15dbd856008b7c24077ca746d91","6937fb7370f1e17491df649888d6ecc9",{"hashOfOptions":"126"},"2742538293","3367041120","3963086909","3173464737","3850474653","3346849767","2045418596","3048291867","3525769784","4000169781","471581913","1427279653","2305419495","818733105","2587128410","1262173017","408469422","2836740315","2315460658"]
|
||||
@@ -1 +1 @@
|
||||
[["1","2","3","4","5","6","7","8","9","10","11"],{"key":"12","value":"13"},{"key":"14","value":"15"},{"key":"16","value":"17"},{"key":"18","value":"19"},{"key":"20","value":"21"},{"key":"22","value":"23"},{"key":"24","value":"25"},{"key":"26","value":"27"},{"key":"28","value":"29"},{"key":"30","value":"31"},{"key":"32","value":"33"},"/home/gib/Documents/Code/convex-monorepo/apps/next/src/env.ts",{"size":1676,"mtime":1768155639474,"hash":"34","data":"35"},"/home/gib/Documents/Code/convex-monorepo/apps/next/postcss.config.js",{"size":63,"mtime":1766225879000,"hash":"36","data":"37"},"/home/gib/Documents/Code/convex-monorepo/apps/next/public/favicon.ico",{"size":103027,"mtime":1766225172000,"hash":"38"},"/home/gib/Documents/Code/convex-monorepo/apps/next/src/app/layout.tsx",{"size":1885,"mtime":1768155639311,"hash":"39","data":"40"},"/home/gib/Documents/Code/convex-monorepo/apps/next/public/t3-icon.svg",{"size":923,"mtime":1766225172000,"hash":"41"},"/home/gib/Documents/Code/convex-monorepo/apps/next/next.config.js",{"size":2062,"mtime":1768155639139,"hash":"42","data":"43"},"/home/gib/Documents/Code/convex-monorepo/apps/next/package.json",{"size":1506,"mtime":1768154671185,"hash":"44","data":"45"},"/home/gib/Documents/Code/convex-monorepo/apps/next/tsconfig.json",{"size":297,"mtime":1766225936000,"hash":"46","data":"47"},"/home/gib/Documents/Code/convex-monorepo/apps/next/src/app/styles.css",{"size":596,"mtime":1768155639429,"hash":"48","data":"49"},"/home/gib/Documents/Code/convex-monorepo/apps/next/eslint.config.ts",{"size":369,"mtime":1768155639048,"hash":"50","data":"51"},"/home/gib/Documents/Code/convex-monorepo/apps/next/src/app/page.tsx",{"size":1169,"mtime":1768155639357,"hash":"52","data":"53"},"08dcc17ca04632a7262728b699450d5d",{"hashOfOptions":"54"},"d9fa362e836306677416d4e30dfb1528",{"hashOfOptions":"55"},"36aa513994f9883f30ff408fe777cec6","f819b8d99aba405d3975e6e5d3adf796",{"hashOfOptions":"56"},"44418c5550aee51cfdf0a321ad70ae47","1f2ba5c5e807955994fd4dfca4c709be",{"hashOfOptions":"57"},"6dba01a7cb29904291cb3c9930fc1ac3",{"hashOfOptions":"58"},"785dd21512a5bdf43635be77f6a483ae",{"hashOfOptions":"59"},"48e377494d0046d38b923af54e109529",{"hashOfOptions":"60"},"a9eecac327a9c09dd26e82c515134edc",{"hashOfOptions":"61"},"3847ea1421b6ea4079a526d7e44c946d",{"hashOfOptions":"62"},"1049592143","533286024","3492548434","1155092478","4045122132","2138361299","3590205716","4261166930","1190248983"]
|
||||
[["1","2","3","4","5","6","7","8","9","10","11","12","13","14","15","16","17","18","19","20","21","22","23","24","25","26","27","28","29","30","31","32"],{"key":"33","value":"34"},{"key":"35","value":"36"},{"key":"37","value":"38"},{"key":"39","value":"40"},{"key":"41","value":"42"},{"key":"43","value":"44"},{"key":"45","value":"46"},{"key":"47","value":"48"},{"key":"49","value":"50"},{"key":"51","value":"52"},{"key":"53","value":"54"},{"key":"55","value":"56"},{"key":"57","value":"58"},{"key":"59","value":"60"},{"key":"61","value":"62"},{"key":"63","value":"64"},{"key":"65","value":"66"},{"key":"67","value":"68"},{"key":"69","value":"70"},{"key":"71","value":"72"},{"key":"73","value":"74"},{"key":"75","value":"76"},{"key":"77","value":"78"},{"key":"79","value":"80"},{"key":"81","value":"82"},{"key":"83","value":"84"},{"key":"85","value":"86"},{"key":"87","value":"88"},{"key":"89","value":"90"},{"key":"91","value":"92"},{"key":"93","value":"94"},{"key":"95","value":"96"},"/home/gib/Documents/Code/convex-monorepo/apps/next/public/misc/auth/gibs_auth_wide_header.png",{"size":74049,"mtime":1768226576453,"hash":"97"},"/home/gib/Documents/Code/convex-monorepo/apps/next/src/app/(auth)/sign-in/page.tsx",{"size":16469,"mtime":1768239263791,"hash":"98","data":"99"},"/home/gib/Documents/Code/convex-monorepo/apps/next/src/app/(auth)/profile/layout.tsx",{"size":276,"mtime":1768236979857,"hash":"100","data":"101"},"/home/gib/Documents/Code/convex-monorepo/apps/next/src/app/(auth)/forgot-password/page.tsx",{"size":10607,"mtime":1768239263673,"hash":"102","data":"103"},"/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/layout/auth/profile/avatar-upload.tsx",{"size":6845,"mtime":1768239263940,"hash":"104","data":"105"},"/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/layout/auth/profile/header.tsx",{"size":775,"mtime":1768239263951,"hash":"106","data":"107"},"/home/gib/Documents/Code/convex-monorepo/apps/next/postcss.config.js",{"size":63,"mtime":1766225879000,"hash":"108","data":"109"},"/home/gib/Documents/Code/convex-monorepo/apps/next/public/favicon.ico",{"size":103027,"mtime":1766225172000,"hash":"110"},"/home/gib/Documents/Code/convex-monorepo/apps/next/src/app/layout.tsx",{"size":1711,"mtime":1768186401942,"hash":"111","data":"112"},"/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/providers/ConvexClientProvider.tsx",{"size":462,"mtime":1768239264017,"hash":"113","data":"114"},"/home/gib/Documents/Code/convex-monorepo/apps/next/src/app/(auth)/sign-in/layout.tsx",{"size":274,"mtime":1768236849421,"hash":"115","data":"116"},"/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/layout/auth/profile/user-info.tsx",{"size":4103,"mtime":1768239264007,"hash":"117","data":"118"},"/home/gib/Documents/Code/convex-monorepo/apps/next/public/t3-icon.svg",{"size":923,"mtime":1766225172000,"hash":"119"},"/home/gib/Documents/Code/convex-monorepo/apps/next/.cache/.prettiercache",{"size":2439,"mtime":1768171236840,"hash":"120"},"/home/gib/Documents/Code/convex-monorepo/apps/next/src/app/(auth)/forgot-password/layout.tsx",{"size":298,"mtime":1768236967188,"hash":"121","data":"122"},"/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/layout/auth/profile/index.tsx",{"size":230,"mtime":1768238412936,"hash":"123","data":"124"},"/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/layout/auth/profile/sign-out.tsx",{"size":613,"mtime":1768239263988,"hash":"125","data":"126"},"/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/providers/index.tsx",{"size":63,"mtime":1768171856108,"hash":"127","data":"128"},"/home/gib/Documents/Code/convex-monorepo/apps/next/next.config.js",{"size":1898,"mtime":1768239263537,"hash":"129","data":"130"},"/home/gib/Documents/Code/convex-monorepo/apps/next/src/lib/metadata.ts",{"size":3088,"mtime":1768239264052,"hash":"131","data":"132"},"/home/gib/Documents/Code/convex-monorepo/apps/next/src/lib/middleware/ban-sus-ips.ts",{"size":4894,"mtime":1768226323845,"hash":"133","data":"134"},"/home/gib/Documents/Code/convex-monorepo/apps/next/package.json",{"size":1484,"mtime":1768173526007,"hash":"135","data":"136"},"/home/gib/Documents/Code/convex-monorepo/apps/next/src/middleware.ts",{"size":1124,"mtime":1768239264082,"hash":"137","data":"138"},"/home/gib/Documents/Code/convex-monorepo/apps/next/tsconfig.json",{"size":297,"mtime":1766225936000,"hash":"139","data":"140"},"/home/gib/Documents/Code/convex-monorepo/apps/next/src/app/(auth)/profile/page.tsx",{"size":797,"mtime":1768239263701,"hash":"141","data":"142"},"/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/layout/auth/buttons/gibs-auth.tsx",{"size":1074,"mtime":1768239263891,"hash":"143","data":"144"},"/home/gib/Documents/Code/convex-monorepo/apps/next/src/app/styles.css",{"size":595,"mtime":1768186094852,"hash":"145","data":"146"},"/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/layout/auth/buttons/index.tsx",{"size":52,"mLine truncated
|
||||
@@ -1,5 +1,6 @@
|
||||
import { withSentryConfig } from '@sentry/nextjs';
|
||||
import { withPlausibleProxy } from 'next-plausible';
|
||||
|
||||
import { env } from './src/env.js';
|
||||
|
||||
/** @type {import("next").NextConfig} */
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import type { Metadata } from 'next';
|
||||
|
||||
export const generateMetadata = (): Metadata => {
|
||||
return {
|
||||
title: 'Forgot Password',
|
||||
};
|
||||
};
|
||||
|
||||
const ForgotPasswordLayout = ({
|
||||
children,
|
||||
}: Readonly<{ children: React.ReactNode }>) => {
|
||||
return <>{children}</>;
|
||||
};
|
||||
export default ForgotPasswordLayout;
|
||||
@@ -0,0 +1,300 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useAuthActions } from '@convex-dev/auth/react';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { toast } from 'sonner';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { PASSWORD_MAX, PASSWORD_MIN } from '@gib/backend/types';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
Input,
|
||||
InputOTP,
|
||||
InputOTPGroup,
|
||||
InputOTPSeparator,
|
||||
InputOTPSlot,
|
||||
SubmitButton,
|
||||
} from '@gib/ui';
|
||||
|
||||
const forgotPasswordSchema = z.object({
|
||||
email: z.email({ message: 'Invalid email.' }),
|
||||
});
|
||||
|
||||
const resetVerificationSchema = z
|
||||
.object({
|
||||
code: z.string({ message: 'Invalid code.' }),
|
||||
newPassword: z
|
||||
.string()
|
||||
.min(PASSWORD_MIN, {
|
||||
message: `Password must be at least ${PASSWORD_MIN} characters.`,
|
||||
})
|
||||
.max(PASSWORD_MAX, {
|
||||
message: `Password must be no more than ${PASSWORD_MAX} characters.`,
|
||||
})
|
||||
.regex(/^\S+$/, {
|
||||
message: 'Password must not contain whitespace.',
|
||||
})
|
||||
.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(/[\p{P}\p{S}]/u, {
|
||||
message: 'Password must contain at least one symbol.',
|
||||
}),
|
||||
confirmPassword: z.string().min(PASSWORD_MIN, {
|
||||
message: `Password must be at least ${PASSWORD_MIN} characters.`,
|
||||
}),
|
||||
})
|
||||
.refine((data) => data.newPassword === data.confirmPassword, {
|
||||
message: 'Passwords do not match!',
|
||||
path: ['confirmPassword'],
|
||||
});
|
||||
|
||||
const ForgotPassword = () => {
|
||||
const { signIn } = useAuthActions();
|
||||
const [flow, setFlow] = useState<'reset' | 'reset-verification'>('reset');
|
||||
const [email, setEmail] = useState<string>('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [code, setCode] = useState<string>('');
|
||||
const router = useRouter();
|
||||
|
||||
const forgotPasswordForm = useForm<z.infer<typeof forgotPasswordSchema>>({
|
||||
resolver: zodResolver(forgotPasswordSchema),
|
||||
defaultValues: { email },
|
||||
});
|
||||
|
||||
const resetVerificationForm = useForm<
|
||||
z.infer<typeof resetVerificationSchema>
|
||||
>({
|
||||
resolver: zodResolver(resetVerificationSchema),
|
||||
defaultValues: { code, newPassword: '', confirmPassword: '' },
|
||||
});
|
||||
|
||||
const handleForgotPasswordSubmit = async (
|
||||
values: z.infer<typeof forgotPasswordSchema>,
|
||||
) => {
|
||||
const formData = new FormData();
|
||||
formData.append('email', values.email);
|
||||
formData.append('flow', flow);
|
||||
setLoading(true);
|
||||
try {
|
||||
await signIn('password', formData).then(() => {
|
||||
setEmail(values.email);
|
||||
setFlow('reset-verification');
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error resetting password: ', error);
|
||||
toast.error('Error resetting password.');
|
||||
} finally {
|
||||
forgotPasswordForm.reset();
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleResetVerificationSubmit = async (
|
||||
values: z.infer<typeof resetVerificationSchema>,
|
||||
) => {
|
||||
const formData = new FormData();
|
||||
formData.append('code', code);
|
||||
formData.append('newPassword', values.newPassword);
|
||||
formData.append('email', email);
|
||||
formData.append('flow', flow);
|
||||
setLoading(true);
|
||||
try {
|
||||
await signIn('password', formData);
|
||||
router.push('/');
|
||||
} catch (error) {
|
||||
console.error('Error resetting password: ', error);
|
||||
toast.error('Error resetting password.');
|
||||
} finally {
|
||||
resetVerificationForm.reset();
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center">
|
||||
<Card className="bg-card/25 min-h-[400px] w-sm p-4 lg:w-md">
|
||||
<CardHeader className="flex flex-col items-center gap-4">
|
||||
{flow === 'reset' ? (
|
||||
<>
|
||||
<CardTitle className="text-2xl font-bold">
|
||||
Forgot Password
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Enter your email address and we will send you a link to reset
|
||||
your password.
|
||||
</CardDescription>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<CardTitle className="text-2xl font-bold">
|
||||
Reset Password
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Enter your code and new password and we will reset your
|
||||
password.
|
||||
</CardDescription>
|
||||
</>
|
||||
)}
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Card className="bg-card/50">
|
||||
<CardContent>
|
||||
{flow === 'reset' ? (
|
||||
<Form {...forgotPasswordForm}>
|
||||
<form
|
||||
onSubmit={forgotPasswordForm.handleSubmit(
|
||||
handleForgotPasswordSubmit,
|
||||
)}
|
||||
className="flex flex-col space-y-4"
|
||||
>
|
||||
<FormField
|
||||
control={forgotPasswordForm.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="text-xl">Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="email"
|
||||
placeholder="[email protected]"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<div className="flex w-full flex-col items-center">
|
||||
<FormMessage className="w-5/6 text-center" />
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<SubmitButton
|
||||
disabled={loading}
|
||||
pendingText="Sending Email..."
|
||||
className="mx-auto w-2/3 text-xl font-semibold"
|
||||
>
|
||||
Send Email
|
||||
</SubmitButton>
|
||||
</form>
|
||||
</Form>
|
||||
) : (
|
||||
<Form {...resetVerificationForm}>
|
||||
<form
|
||||
onSubmit={resetVerificationForm.handleSubmit(
|
||||
handleResetVerificationSubmit,
|
||||
)}
|
||||
className="flex flex-col space-y-4"
|
||||
>
|
||||
<FormField
|
||||
control={resetVerificationForm.control}
|
||||
name="code"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="text-xl">Code</FormLabel>
|
||||
<FormControl>
|
||||
<InputOTP
|
||||
maxLength={6}
|
||||
{...field}
|
||||
value={code}
|
||||
onChange={(value) => setCode(value)}
|
||||
>
|
||||
<InputOTPGroup>
|
||||
<InputOTPSlot index={0} />
|
||||
<InputOTPSlot index={1} />
|
||||
<InputOTPSlot index={2} />
|
||||
<InputOTPSeparator />
|
||||
<InputOTPSlot index={3} />
|
||||
<InputOTPSlot index={4} />
|
||||
<InputOTPSlot index={5} />
|
||||
</InputOTPGroup>
|
||||
</InputOTP>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Please enter the one-time password sent to your
|
||||
phone.
|
||||
</FormDescription>
|
||||
<div className="flex w-full flex-col items-center">
|
||||
<FormMessage className="w-5/6 text-center" />
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={resetVerificationForm.control}
|
||||
name="newPassword"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="text-xl">
|
||||
New Password
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="Your password"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<div className="flex w-full flex-col items-center">
|
||||
<FormMessage className="w-5/6 text-center" />
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={resetVerificationForm.control}
|
||||
name="confirmPassword"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="text-xl">
|
||||
Confirm Passsword
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="Confirm your password"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<div className="flex w-full flex-col items-center">
|
||||
<FormMessage className="w-5/6 text-center" />
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<SubmitButton
|
||||
disabled={loading}
|
||||
pendingText="Resetting Password..."
|
||||
className="mx-auto w-2/3 text-xl font-semibold"
|
||||
>
|
||||
Reset Password
|
||||
</SubmitButton>
|
||||
</form>
|
||||
</Form>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default ForgotPassword;
|
||||
@@ -0,0 +1,14 @@
|
||||
import type { Metadata } from 'next';
|
||||
|
||||
export const generateMetadata = (): Metadata => {
|
||||
return {
|
||||
title: 'Profile',
|
||||
};
|
||||
};
|
||||
|
||||
const ProfileLayout = ({
|
||||
children,
|
||||
}: Readonly<{ children: React.ReactNode }>) => {
|
||||
return <>{children}</>;
|
||||
};
|
||||
export default ProfileLayout;
|
||||
@@ -0,0 +1,29 @@
|
||||
'use server';
|
||||
|
||||
import {
|
||||
AvatarUpload,
|
||||
ProfileHeader,
|
||||
ResetPasswordForm,
|
||||
SignOutForm,
|
||||
UserInfoForm,
|
||||
} from '@/components/layout/auth/profile';
|
||||
import { preloadQuery } from 'convex/nextjs';
|
||||
|
||||
import { api } from '@gib/backend/convex/_generated/api.js';
|
||||
import { Card, Separator } from '@gib/ui';
|
||||
|
||||
const Profile = async () => {
|
||||
const preloadedUser = await preloadQuery(api.auth.getUser);
|
||||
return (
|
||||
<Card className="mx-auto mb-8 max-w-xl min-w-xs sm:min-w-md">
|
||||
<ProfileHeader preloadedUser={preloadedUser} />
|
||||
<AvatarUpload preloadedUser={preloadedUser} />
|
||||
<Separator />
|
||||
<UserInfoForm preloadedUser={preloadedUser} />
|
||||
<ResetPasswordForm preloadedUser={preloadedUser} />
|
||||
<Separator />
|
||||
<SignOutForm />
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
export default Profile;
|
||||
@@ -0,0 +1,14 @@
|
||||
import type { Metadata } from 'next';
|
||||
|
||||
export const generateMetadata = (): Metadata => {
|
||||
return {
|
||||
title: 'Sign In',
|
||||
};
|
||||
};
|
||||
|
||||
const SignInLayout = ({
|
||||
children,
|
||||
}: Readonly<{ children: React.ReactNode }>) => {
|
||||
return <>{children}</>;
|
||||
};
|
||||
export default SignInLayout;
|
||||
@@ -1,12 +1,17 @@
|
||||
'use client';
|
||||
import { z } from 'zod';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import Link from 'next/link';
|
||||
import { useAuthActions } from '@convex-dev/auth/react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { ConvexError } from 'convex/values';
|
||||
|
||||
import { useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { GibsAuthSignInButton } from '@/components/layout/auth/buttons';
|
||||
import { useAuthActions } from '@convex-dev/auth/react';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { ConvexError } from 'convex/values';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { toast } from 'sonner';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { PASSWORD_MAX, PASSWORD_MIN, PASSWORD_REGEX } from '@gib/backend/types';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
@@ -20,8 +25,8 @@ import {
|
||||
Input,
|
||||
InputOTP,
|
||||
InputOTPGroup,
|
||||
InputOTPSlot,
|
||||
InputOTPSeparator,
|
||||
InputOTPSlot,
|
||||
Separator,
|
||||
SubmitButton,
|
||||
Tabs,
|
||||
@@ -29,11 +34,6 @@ import {
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
} from '@gib/ui';
|
||||
import { toast } from 'sonner';
|
||||
import {
|
||||
GibsAuthSignInButton,
|
||||
} from '@/components/layout/auth/buttons';
|
||||
import { PASSWORD_MIN, PASSWORD_MAX, PASSWORD_REGEX } from '@gib/backend/types';
|
||||
|
||||
const signInFormSchema = z.object({
|
||||
email: z.email({
|
||||
@@ -179,24 +179,24 @@ const SignIn = () => {
|
||||
|
||||
if (flow === 'email-verification') {
|
||||
return (
|
||||
<div className='flex flex-col items-center'>
|
||||
<Card className='p-4 bg-card/25 min-h-[720px] w-md'>
|
||||
<div className="flex flex-col items-center">
|
||||
<Card className="bg-card/25 min-h-[720px] w-md p-4">
|
||||
<CardContent>
|
||||
<div className='text-center mb-6'>
|
||||
<h2 className='text-2xl font-bold'>Verify Your Email</h2>
|
||||
<p className='text-muted-foreground'>We sent a code to {email}</p>
|
||||
<div className="mb-6 text-center">
|
||||
<h2 className="text-2xl font-bold">Verify Your Email</h2>
|
||||
<p className="text-muted-foreground">We sent a code to {email}</p>
|
||||
</div>
|
||||
<Form {...verifyEmailForm}>
|
||||
<form
|
||||
onSubmit={verifyEmailForm.handleSubmit(handleVerifyEmail)}
|
||||
className='flex flex-col space-y-8'
|
||||
className="flex flex-col space-y-8"
|
||||
>
|
||||
<FormField
|
||||
control={verifyEmailForm.control}
|
||||
name='code'
|
||||
name="code"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className='text-xl'>Code</FormLabel>
|
||||
<FormLabel className="text-xl">Code</FormLabel>
|
||||
<FormControl>
|
||||
<InputOTP
|
||||
maxLength={6}
|
||||
@@ -217,25 +217,25 @@ const SignIn = () => {
|
||||
<FormDescription>
|
||||
Please enter the one-time password sent to your email.
|
||||
</FormDescription>
|
||||
<div className='flex flex-col w-full items-center'>
|
||||
<FormMessage className='w-5/6 text-center' />
|
||||
<div className="flex w-full flex-col items-center">
|
||||
<FormMessage className="w-5/6 text-center" />
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<SubmitButton
|
||||
disabled={loading}
|
||||
pendingText='Signing Up...'
|
||||
className='text-xl font-semibold w-2/3 mx-auto'
|
||||
pendingText="Signing Up..."
|
||||
className="mx-auto w-2/3 text-xl font-semibold"
|
||||
>
|
||||
Verify Email
|
||||
</SubmitButton>
|
||||
</form>
|
||||
</Form>
|
||||
<div className='text-center mt-4'>
|
||||
<div className="mt-4 text-center">
|
||||
<button
|
||||
onClick={() => setFlow('signUp')}
|
||||
className='text-sm text-muted-foreground hover:underline'
|
||||
className="text-muted-foreground text-sm hover:underline"
|
||||
>
|
||||
Back to Sign Up
|
||||
</button>
|
||||
@@ -247,207 +247,204 @@ const SignIn = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='flex flex-col items-center'>
|
||||
<Card className='p-4 bg-card/25 min-h-[720px] w-md'>
|
||||
<div className="flex flex-col items-center">
|
||||
<Card className="bg-card/25 min-h-[720px] w-md p-4">
|
||||
<Tabs
|
||||
defaultValue={flow}
|
||||
onValueChange={(value) => setFlow(value as 'signIn' | 'signUp')}
|
||||
className='items-center'
|
||||
className="items-center"
|
||||
>
|
||||
<TabsList className='py-6'>
|
||||
<TabsList className="py-6">
|
||||
<TabsTrigger
|
||||
value='signIn'
|
||||
className='p-6 text-2xl font-bold cursor-pointer'
|
||||
value="signIn"
|
||||
className="cursor-pointer p-6 text-2xl font-bold"
|
||||
>
|
||||
Sign In
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value='signUp'
|
||||
className='p-6 text-2xl font-bold cursor-pointer'
|
||||
value="signUp"
|
||||
className="cursor-pointer p-6 text-2xl font-bold"
|
||||
>
|
||||
Sign Up
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value='signIn'>
|
||||
<Card className='min-w-xs sm:min-w-sm bg-card/50'>
|
||||
<TabsContent value="signIn">
|
||||
<Card className="bg-card/50 min-w-xs sm:min-w-sm">
|
||||
<CardContent>
|
||||
<Form {...signInForm}>
|
||||
<form
|
||||
onSubmit={signInForm.handleSubmit(handleSignIn)}
|
||||
className='flex flex-col space-y-8'
|
||||
className="flex flex-col space-y-8"
|
||||
>
|
||||
<FormField
|
||||
control={signInForm.control}
|
||||
name='email'
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className='text-xl'>Email</FormLabel>
|
||||
<FormLabel className="text-xl">Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type='email'
|
||||
placeholder='[email protected]'
|
||||
type="email"
|
||||
placeholder="[email protected]"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<div className='flex flex-col w-full items-center'>
|
||||
<FormMessage className='w-5/6 text-center' />
|
||||
<div className="flex w-full flex-col items-center">
|
||||
<FormMessage className="w-5/6 text-center" />
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={signInForm.control}
|
||||
name='password'
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<div className='flex justify-between'>
|
||||
<FormLabel className='text-xl'>Password</FormLabel>
|
||||
<Link href='/forgot-password'>
|
||||
<div className="flex justify-between">
|
||||
<FormLabel className="text-xl">Password</FormLabel>
|
||||
<Link href="/forgot-password">
|
||||
Forgot Password?
|
||||
</Link>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Input
|
||||
type='password'
|
||||
placeholder='Your password'
|
||||
type="password"
|
||||
placeholder="Your password"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<div className='flex flex-col w-full items-center'>
|
||||
<FormMessage className='w-5/6 text-center' />
|
||||
<div className="flex w-full flex-col items-center">
|
||||
<FormMessage className="w-5/6 text-center" />
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<SubmitButton
|
||||
disabled={loading}
|
||||
pendingText='Signing in...'
|
||||
className='text-xl font-semibold w-2/3 mx-auto'
|
||||
pendingText="Signing in..."
|
||||
className="mx-auto w-2/3 text-xl font-semibold"
|
||||
>
|
||||
Sign In
|
||||
</SubmitButton>
|
||||
</form>
|
||||
</Form>
|
||||
<div className='flex justify-center'>
|
||||
<div
|
||||
className='flex flex-row items-center
|
||||
my-2.5 mx-auto justify-center w-1/4'
|
||||
>
|
||||
<Separator className='py-0.5 mr-3' />
|
||||
<span className='font-semibold text-lg'>or</span>
|
||||
<Separator className='py-0.5 ml-3' />
|
||||
<div className="flex justify-center">
|
||||
<div className="mx-auto my-2.5 flex w-1/4 flex-row items-center justify-center">
|
||||
<Separator className="mr-3 py-0.5" />
|
||||
<span className="text-lg font-semibold">or</span>
|
||||
<Separator className="ml-3 py-0.5" />
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex justify-center mt-3'>
|
||||
<div className="mt-3 flex justify-center">
|
||||
<GibsAuthSignInButton />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
<TabsContent value='signUp'>
|
||||
<Card className='min-w-xs sm:min-w-sm bg-card/50'>
|
||||
<TabsContent value="signUp">
|
||||
<Card className="bg-card/50 min-w-xs sm:min-w-sm">
|
||||
<CardContent>
|
||||
<Form {...signUpForm}>
|
||||
<form
|
||||
onSubmit={signUpForm.handleSubmit(handleSignUp)}
|
||||
className='flex flex-col space-y-8'
|
||||
className="flex flex-col space-y-8"
|
||||
>
|
||||
<FormField
|
||||
control={signUpForm.control}
|
||||
name='name'
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className='text-xl'>Name</FormLabel>
|
||||
<FormLabel className="text-xl">Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type='text'
|
||||
placeholder='Full Name'
|
||||
type="text"
|
||||
placeholder="Full Name"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<div className='flex flex-col w-full items-center'>
|
||||
<FormMessage className='w-5/6 text-center' />
|
||||
<div className="flex w-full flex-col items-center">
|
||||
<FormMessage className="w-5/6 text-center" />
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={signUpForm.control}
|
||||
name='email'
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className='text-xl'>Email</FormLabel>
|
||||
<FormLabel className="text-xl">Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type='email'
|
||||
placeholder='[email protected]'
|
||||
type="email"
|
||||
placeholder="[email protected]"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<div className='flex flex-col w-full items-center'>
|
||||
<FormMessage className='w-5/6 text-center' />
|
||||
<div className="flex w-full flex-col items-center">
|
||||
<FormMessage className="w-5/6 text-center" />
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={signUpForm.control}
|
||||
name='password'
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className='text-xl'>Password</FormLabel>
|
||||
<FormLabel className="text-xl">Password</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type='password'
|
||||
placeholder='Your password'
|
||||
type="password"
|
||||
placeholder="Your password"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<div className='flex flex-col w-full items-center'>
|
||||
<FormMessage className='w-5/6 text-center' />
|
||||
<div className="flex w-full flex-col items-center">
|
||||
<FormMessage className="w-5/6 text-center" />
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={signUpForm.control}
|
||||
name='confirmPassword'
|
||||
name="confirmPassword"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className='text-xl'>
|
||||
<FormLabel className="text-xl">
|
||||
Confirm Passsword
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type='password'
|
||||
placeholder='Confirm your password'
|
||||
type="password"
|
||||
placeholder="Confirm your password"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<div className='flex flex-col w-full items-center'>
|
||||
<FormMessage className='w-5/6 text-center' />
|
||||
<div className="flex w-full flex-col items-center">
|
||||
<FormMessage className="w-5/6 text-center" />
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<SubmitButton
|
||||
disabled={loading}
|
||||
pendingText='Signing Up...'
|
||||
className='text-xl font-semibold w-2/3 mx-auto'
|
||||
pendingText="Signing Up..."
|
||||
className="mx-auto w-2/3 text-xl font-semibold"
|
||||
>
|
||||
Sign Up
|
||||
</SubmitButton>
|
||||
</form>
|
||||
</Form>
|
||||
<div className='flex my-auto justify-center w-2/3'>
|
||||
<div className='flex flex-row w-1/3 items-center my-2.5'>
|
||||
<Separator className='py-0.5 mr-3' />
|
||||
<span className='font-semibold text-lg'>or</span>
|
||||
<Separator className='py-0.5 ml-3' />
|
||||
<div className="my-auto flex w-2/3 justify-center">
|
||||
<div className="my-2.5 flex w-1/3 flex-row items-center">
|
||||
<Separator className="mr-3 py-0.5" />
|
||||
<span className="text-lg font-semibold">or</span>
|
||||
<Separator className="ml-3 py-0.5" />
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex justify-center mt-3'>
|
||||
<GibsAuthSignInButton type='signUp' />
|
||||
<div className="mt-3 flex justify-center">
|
||||
<GibsAuthSignInButton type="signUp" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
'use client';
|
||||
'use server';
|
||||
|
||||
const Home = () => {
|
||||
const Home = async () => {
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center">
|
||||
<main
|
||||
className='flex min-h-screen items-center justify-center'
|
||||
>
|
||||
Hello!
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import type { VariantProps } from 'class-variance-authority';
|
||||
import type { ComponentProps } from 'react';
|
||||
import Image from 'next/image';
|
||||
import { useAuthActions } from '@convex-dev/auth/react';
|
||||
|
||||
import type { buttonVariants } from '@gib/ui';
|
||||
import { Button } from '@gib/ui';
|
||||
import type { ComponentProps } from 'react';
|
||||
import type { VariantProps } from 'class-variance-authority';
|
||||
|
||||
interface Props {
|
||||
buttonProps?: Omit<ComponentProps<'button'>, 'onClick'> &
|
||||
@@ -11,7 +12,7 @@ interface Props {
|
||||
asChild?: boolean;
|
||||
};
|
||||
type?: 'signIn' | 'signUp';
|
||||
};
|
||||
}
|
||||
|
||||
export const GibsAuthSignInButton = ({
|
||||
buttonProps,
|
||||
@@ -20,22 +21,16 @@ export const GibsAuthSignInButton = ({
|
||||
const { signIn } = useAuthActions();
|
||||
return (
|
||||
<Button
|
||||
size='lg'
|
||||
size="lg"
|
||||
onClick={() => signIn('authentik')}
|
||||
className='text-lg font-semibold'
|
||||
className="text-lg font-semibold"
|
||||
{...buttonProps}
|
||||
>
|
||||
<div className='flex flex-col my-auto space-x-1'>
|
||||
<p>
|
||||
{
|
||||
type === 'signIn'
|
||||
? 'Sign In'
|
||||
: 'Sign Up'
|
||||
} with
|
||||
</p>
|
||||
<div className="my-auto flex flex-col space-x-1">
|
||||
<p>{type === 'signIn' ? 'Sign In' : 'Sign Up'} with</p>
|
||||
<Image
|
||||
src={'/misc/auth/gibs_auth_wide_header.png'}
|
||||
className=''
|
||||
className=""
|
||||
alt="Gib's Auth"
|
||||
width={100}
|
||||
height={100}
|
||||
|
||||
@@ -0,0 +1,221 @@
|
||||
'use client';
|
||||
|
||||
import type { Preloaded } from 'convex/react';
|
||||
import type { ChangeEvent } from 'react';
|
||||
import { useRef, useState } from 'react';
|
||||
import { useMutation, usePreloadedQuery, useQuery } from 'convex/react';
|
||||
import { Loader2, Pencil, Upload, XIcon } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import type { Id } from '@gib/backend/convex/_generated/dataModel.js';
|
||||
import { api } from '@gib/backend/convex/_generated/api.js';
|
||||
import {
|
||||
Avatar,
|
||||
AvatarImage,
|
||||
BasedAvatar,
|
||||
Button,
|
||||
CardContent,
|
||||
ImageCrop,
|
||||
ImageCropApply,
|
||||
ImageCropContent,
|
||||
Input,
|
||||
} from '@gib/ui';
|
||||
|
||||
interface AvatarUploadProps {
|
||||
preloadedUser: Preloaded<typeof api.auth.getUser>;
|
||||
}
|
||||
|
||||
const dataUrlToBlob = async (
|
||||
dataUrl: string,
|
||||
): Promise<{ blob: Blob; type: string }> => {
|
||||
const re = /^data:([^;,]+)[;,]/;
|
||||
const m = re.exec(dataUrl);
|
||||
const type = m?.[1] ?? 'image/png';
|
||||
|
||||
const res = await fetch(dataUrl);
|
||||
const blob = await res.blob();
|
||||
return { blob, type };
|
||||
};
|
||||
|
||||
export const AvatarUpload = ({ preloadedUser }: AvatarUploadProps) => {
|
||||
const user = usePreloadedQuery(preloadedUser);
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
const [selectedFile, setSelectedFile] = useState<File | null>(null);
|
||||
const [croppedImage, setCroppedImage] = useState<string | null>(null);
|
||||
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const generateUploadUrl = useMutation(api.files.generateUploadUrl);
|
||||
const updateUser = useMutation(api.auth.updateUser);
|
||||
|
||||
const currentImageUrl = useQuery(
|
||||
api.files.getImageUrl,
|
||||
user?.image ? { storageId: user.image } : 'skip',
|
||||
);
|
||||
|
||||
const handleFileChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0] ?? null;
|
||||
if (!file) return;
|
||||
if (!file.type.startsWith('image/')) {
|
||||
toast.error('Please select an image file.');
|
||||
if (inputRef.current) inputRef.current.value = '';
|
||||
return;
|
||||
}
|
||||
setSelectedFile(file);
|
||||
setCroppedImage(null);
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setSelectedFile(null);
|
||||
setCroppedImage(null);
|
||||
if (inputRef.current) inputRef.current.value = '';
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!croppedImage) {
|
||||
toast.error('Please apply a crop first.');
|
||||
return;
|
||||
}
|
||||
setIsUploading(true);
|
||||
try {
|
||||
const { blob, type } = await dataUrlToBlob(croppedImage);
|
||||
const postUrl = await generateUploadUrl();
|
||||
|
||||
const result = await fetch(postUrl, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': type },
|
||||
body: blob,
|
||||
});
|
||||
if (!result.ok) {
|
||||
const msg = await result.text().catch(() => 'Upload failed.');
|
||||
throw new Error(msg);
|
||||
}
|
||||
|
||||
const uploadResponse = (await result.json()) as {
|
||||
storageId: Id<'_storage'>;
|
||||
};
|
||||
|
||||
await updateUser({ image: uploadResponse.storageId });
|
||||
|
||||
toast.success('Profile picture updated.');
|
||||
handleReset();
|
||||
} catch (error) {
|
||||
console.error('Upload failed:', error);
|
||||
toast.error('Upload failed. Please try again.');
|
||||
} finally {
|
||||
setIsUploading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<CardContent>
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
{/* Current avatar + trigger (hidden when cropping) */}
|
||||
{!selectedFile && (
|
||||
<div
|
||||
className="group relative cursor-pointer"
|
||||
onClick={() => inputRef.current?.click()}
|
||||
>
|
||||
<BasedAvatar
|
||||
src={currentImageUrl ?? undefined}
|
||||
fullName={user?.name}
|
||||
className="h-42 w-42 text-6xl font-semibold"
|
||||
userIconProps={{ size: 100 }}
|
||||
/>
|
||||
<div className="absolute inset-0 flex items-center justify-center rounded-full bg-black/0 transition-all group-hover:bg-black/50">
|
||||
<Upload
|
||||
className="text-white opacity-0 transition-opacity group-hover:opacity-100"
|
||||
size={24}
|
||||
/>
|
||||
</div>
|
||||
<div className="absolute inset-1 flex items-end justify-end transition-all">
|
||||
<Pencil
|
||||
className="text-white opacity-100 transition-opacity group-hover:opacity-0"
|
||||
size={24}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* File input (hidden) */}
|
||||
<Input
|
||||
ref={inputRef}
|
||||
id="avatar-upload"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
className="hidden"
|
||||
onChange={handleFileChange}
|
||||
disabled={isUploading}
|
||||
/>
|
||||
|
||||
{/* Crop UI */}
|
||||
{selectedFile && !croppedImage && (
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<ImageCrop
|
||||
aspect={1}
|
||||
circularCrop
|
||||
file={selectedFile}
|
||||
maxImageSize={3 * 1024 * 1024} // 3MB guard
|
||||
onCrop={setCroppedImage}
|
||||
>
|
||||
<ImageCropContent className="max-w-sm" />
|
||||
<div className="flex items-center gap-2">
|
||||
<ImageCropApply />
|
||||
<Button
|
||||
onClick={handleReset}
|
||||
size="icon"
|
||||
type="button"
|
||||
variant="ghost"
|
||||
>
|
||||
<XIcon className="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</ImageCrop>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Cropped preview + actions */}
|
||||
{croppedImage && (
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<Avatar className="h-42 w-42">
|
||||
<AvatarImage alt="Cropped preview" src={croppedImage} />
|
||||
</Avatar>
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
disabled={isUploading}
|
||||
className="px-4"
|
||||
>
|
||||
{isUploading ? (
|
||||
<span className="inline-flex items-center gap-2">
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
Saving...
|
||||
</span>
|
||||
) : (
|
||||
'Save Avatar'
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleReset}
|
||||
size="icon"
|
||||
type="button"
|
||||
className="hover:dark:bg-accent bg-red-400/80 hover:text-red-800/80 dark:bg-red-500/30 hover:dark:text-red-300/60"
|
||||
variant="secondary"
|
||||
>
|
||||
<XIcon className="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Uploading indicator */}
|
||||
{isUploading && !croppedImage && (
|
||||
<div className="mt-2 flex items-center text-sm text-gray-500">
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Uploading...
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
'use client';
|
||||
|
||||
import type { Preloaded } from 'convex/react';
|
||||
import { usePreloadedQuery } from 'convex/react';
|
||||
|
||||
import type { api } from '@gib/backend/convex/_generated/api.js';
|
||||
import { CardDescription, CardHeader, CardTitle } from '@gib/ui';
|
||||
|
||||
interface ProfileCardProps {
|
||||
preloadedUser: Preloaded<typeof api.auth.getUser>;
|
||||
}
|
||||
|
||||
const ProfileHeader = ({ preloadedUser }: ProfileCardProps) => {
|
||||
const user = usePreloadedQuery(preloadedUser);
|
||||
return (
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-2xl">
|
||||
{user?.name ?? user?.email ?? 'Your Profile'}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Manage your personal information & how it appears to others.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
);
|
||||
};
|
||||
|
||||
export { ProfileHeader };
|
||||
@@ -0,0 +1,5 @@
|
||||
export { AvatarUpload } from './avatar-upload';
|
||||
export { ProfileHeader } from './header';
|
||||
export { ResetPasswordForm } from './reset-password';
|
||||
export { SignOutForm } from './sign-out';
|
||||
export { UserInfoForm } from './user-info';
|
||||
@@ -0,0 +1,189 @@
|
||||
'use client';
|
||||
|
||||
import type { Preloaded } from 'convex/react';
|
||||
import { useState } from 'react';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useAction, usePreloadedQuery } from 'convex/react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { toast } from 'sonner';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { api } from '@gib/backend/convex/_generated/api.js';
|
||||
import { PASSWORD_MAX, PASSWORD_MIN, PASSWORD_REGEX } from '@gib/backend/types';
|
||||
import {
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
Input,
|
||||
Separator,
|
||||
SubmitButton,
|
||||
} from '@gib/ui';
|
||||
|
||||
const formSchema = z
|
||||
.object({
|
||||
currentPassword: z.string().regex(PASSWORD_REGEX, {
|
||||
message: 'Incorrect current password. Does not meet requirements.',
|
||||
}),
|
||||
newPassword: z
|
||||
.string()
|
||||
.min(PASSWORD_MIN, {
|
||||
message: 'New password must be at least 8 characters.',
|
||||
})
|
||||
.max(PASSWORD_MAX, {
|
||||
message: 'New password must be less than 100 characters.',
|
||||
})
|
||||
.regex(/^\S+$/, {
|
||||
message: 'Password must not contain whitespace.',
|
||||
})
|
||||
.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(/[\p{P}\p{S}]/u, {
|
||||
message: 'Password must contain at least one symbol.',
|
||||
}),
|
||||
confirmPassword: z.string(),
|
||||
})
|
||||
.refine((data) => data.currentPassword !== data.newPassword, {
|
||||
message: 'New password must be different from current password.',
|
||||
path: ['newPassword'],
|
||||
})
|
||||
.refine((data) => data.newPassword === data.confirmPassword, {
|
||||
message: 'Passwords do not match.',
|
||||
path: ['confirmPassword'],
|
||||
});
|
||||
|
||||
type ResetFormProps = {
|
||||
preloadedUser: Preloaded<typeof api.auth.getUser>;
|
||||
};
|
||||
|
||||
export const ResetPasswordForm = ({ preloadedUser }: ResetFormProps) => {
|
||||
const user = usePreloadedQuery(preloadedUser);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const changePassword = useAction(api.auth.updateUserPassword);
|
||||
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
currentPassword: '',
|
||||
newPassword: '',
|
||||
confirmPassword: '',
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit = async (values: z.infer<typeof formSchema>) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const result = await changePassword({
|
||||
currentPassword: values.currentPassword,
|
||||
newPassword: values.newPassword,
|
||||
});
|
||||
if (result.success) {
|
||||
form.reset();
|
||||
toast.success('Password updated successfully.');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error updating password:', error);
|
||||
toast.error('Error updating password.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
// TO DO: Make a function to get provider type from user.
|
||||
return (
|
||||
//user.provider !== 'password'
|
||||
!user?.email
|
||||
) ? (
|
||||
<div />
|
||||
) : (
|
||||
<>
|
||||
<Separator />
|
||||
<CardHeader>
|
||||
<CardTitle className="text-2xl">Change Password</CardTitle>
|
||||
<CardDescription>
|
||||
Update your password to keep your account secure
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(handleSubmit)}
|
||||
className="space-y-6"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="currentPassword"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Current Password</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="password" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Enter your current password.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="newPassword"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>New Password</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="password" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Enter your new password. Must be at least 8 characters.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="confirmPassword"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Confirm Password</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="password" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Please re-enter your new password to confirm.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<div className="flex justify-center">
|
||||
<SubmitButton
|
||||
className="w-2/3 text-[1.0rem] lg:w-1/3"
|
||||
disabled={loading}
|
||||
pendingText="Updating Password..."
|
||||
>
|
||||
Update Password
|
||||
</SubmitButton>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</CardContent>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
'use client';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useAuthActions } from '@convex-dev/auth/react';
|
||||
|
||||
import { SubmitButton } from '@gib/ui';
|
||||
|
||||
export const SignOutForm = () => {
|
||||
const { signOut } = useAuthActions();
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<div className="flex justify-center">
|
||||
<SubmitButton
|
||||
className="w-5/6 cursor-pointer text-[1.0rem] font-semibold hover:bg-red-700/60 lg:w-2/3 dark:hover:bg-red-300/80"
|
||||
onClick={() => void signOut().then(() => router.push('/sign-in'))}
|
||||
>
|
||||
Sign Out
|
||||
</SubmitButton>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,154 @@
|
||||
'use client';
|
||||
|
||||
import type { Preloaded } from 'convex/react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useMutation, usePreloadedQuery } from 'convex/react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { toast } from 'sonner';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { api } from '@gib/backend/convex/_generated/api.js';
|
||||
import {
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
Input,
|
||||
SubmitButton,
|
||||
} from '@gib/ui';
|
||||
|
||||
const formSchema = z.object({
|
||||
name: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(5, {
|
||||
message: 'Full name is required & must be at least 5 characters.',
|
||||
})
|
||||
.max(50, {
|
||||
message: 'Full name must be less than 50 characters.',
|
||||
}),
|
||||
email: z.email({
|
||||
message: 'Please enter a valid email address.',
|
||||
}),
|
||||
});
|
||||
|
||||
type UserInfoFormProps = {
|
||||
preloadedUser: Preloaded<typeof api.auth.getUser>;
|
||||
};
|
||||
|
||||
export const UserInfoForm = ({ preloadedUser }: UserInfoFormProps) => {
|
||||
const user = usePreloadedQuery(preloadedUser);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const updateUser = useMutation(api.auth.updateUser);
|
||||
|
||||
const initialValues = useMemo<z.infer<typeof formSchema>>(
|
||||
() => ({
|
||||
name: user?.name ?? '',
|
||||
email: user?.email ?? '',
|
||||
}),
|
||||
[user?.name, user?.email],
|
||||
);
|
||||
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
values: initialValues,
|
||||
});
|
||||
|
||||
const handleSubmit = async (values: z.infer<typeof formSchema>) => {
|
||||
const name = values.name.trim();
|
||||
const email = values.email.trim().toLowerCase();
|
||||
const patch: Partial<{
|
||||
name: string;
|
||||
email: string;
|
||||
lunchTime: string;
|
||||
automaticLunch: boolean;
|
||||
}> = {};
|
||||
if (name !== (user.name ?? '')) patch.name = name;
|
||||
if (email !== (user.email ?? '')) patch.email = email;
|
||||
if (Object.keys(patch).length === 0) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
await updateUser(patch);
|
||||
form.reset(patch);
|
||||
toast.success('Profile updated successfully.');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
toast.error('Error updating profile.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-2xl">Account Information</CardTitle>
|
||||
<CardDescription>Update your account information here.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(handleSubmit)}
|
||||
className="space-y-6"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Full Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>Your public display name.</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
disabled={
|
||||
//user.provider !== 'password'
|
||||
!user?.email
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Your email address associated with your account.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<div className="mt-5 flex justify-center">
|
||||
<SubmitButton
|
||||
className="w-2/3 text-[1.0rem] lg:w-1/3"
|
||||
disabled={loading}
|
||||
pendingText="Saving..."
|
||||
>
|
||||
Save Changes
|
||||
</SubmitButton>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</CardContent>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,9 +1,9 @@
|
||||
"use client";
|
||||
'use client';
|
||||
|
||||
import { ConvexAuthNextjsProvider } from "@convex-dev/auth/nextjs";
|
||||
import { ConvexReactClient } from "convex/react";
|
||||
import type { ReactNode } from "react";
|
||||
import type { ReactNode } from 'react';
|
||||
import { env } from '@/env';
|
||||
import { ConvexAuthNextjsProvider } from '@convex-dev/auth/nextjs';
|
||||
import { ConvexReactClient } from 'convex/react';
|
||||
|
||||
const convex = new ConvexReactClient(env.NEXT_PUBLIC_CONVEX_URL);
|
||||
|
||||
|
||||
@@ -43,65 +43,65 @@ export const generateMetadata = (): Metadata => {
|
||||
media: '(prefers-color-scheme: dark)',
|
||||
},
|
||||
//{
|
||||
//url: '/appicon/icon.png',
|
||||
//type: 'image/png',
|
||||
//sizes: '192x192',
|
||||
//url: '/appicon/icon.png',
|
||||
//type: 'image/png',
|
||||
//sizes: '192x192',
|
||||
//},
|
||||
//{
|
||||
//url: '/appicon/icon.png',
|
||||
//type: 'image/png',
|
||||
//sizes: '192x192',
|
||||
//media: '(prefers-color-scheme: dark)',
|
||||
//url: '/appicon/icon.png',
|
||||
//type: 'image/png',
|
||||
//sizes: '192x192',
|
||||
//media: '(prefers-color-scheme: dark)',
|
||||
//},
|
||||
],
|
||||
//shortcut: [
|
||||
//{
|
||||
//url: '/appicon/icon.png',
|
||||
//type: 'image/png',
|
||||
//sizes: '192x192',
|
||||
//},
|
||||
//{
|
||||
//url: '/appicon/icon.png',
|
||||
//type: 'image/png',
|
||||
//sizes: '192x192',
|
||||
//media: '(prefers-color-scheme: dark)',
|
||||
//},
|
||||
//{
|
||||
//url: '/appicon/icon.png',
|
||||
//type: 'image/png',
|
||||
//sizes: '192x192',
|
||||
//},
|
||||
//{
|
||||
//url: '/appicon/icon.png',
|
||||
//type: 'image/png',
|
||||
//sizes: '192x192',
|
||||
//media: '(prefers-color-scheme: dark)',
|
||||
//},
|
||||
//],
|
||||
//apple: [
|
||||
//{
|
||||
//url: 'appicon/icon.png',
|
||||
//type: 'image/png',
|
||||
//sizes: '192x192',
|
||||
//},
|
||||
//{
|
||||
//url: 'appicon/icon.png',
|
||||
//type: 'image/png',
|
||||
//sizes: '192x192',
|
||||
//media: '(prefers-color-scheme: dark)',
|
||||
//},
|
||||
//{
|
||||
//url: 'appicon/icon.png',
|
||||
//type: 'image/png',
|
||||
//sizes: '192x192',
|
||||
//},
|
||||
//{
|
||||
//url: 'appicon/icon.png',
|
||||
//type: 'image/png',
|
||||
//sizes: '192x192',
|
||||
//media: '(prefers-color-scheme: dark)',
|
||||
//},
|
||||
//],
|
||||
//other: [
|
||||
//{
|
||||
//rel: 'apple-touch-icon-precomposed',
|
||||
//url: '/appicon/icon-precomposed.png',
|
||||
//type: 'image/png',
|
||||
//sizes: '180x180',
|
||||
//},
|
||||
//{
|
||||
//rel: 'apple-touch-icon-precomposed',
|
||||
//url: '/appicon/icon-precomposed.png',
|
||||
//type: 'image/png',
|
||||
//sizes: '180x180',
|
||||
//},
|
||||
//],
|
||||
},
|
||||
other: {
|
||||
...Sentry.getTraceData(),
|
||||
},
|
||||
//appleWebApp: {
|
||||
//title: 'Convex Monorepo',
|
||||
//statusBarStyle: 'black-translucent',
|
||||
//startupImage: [
|
||||
//'/icons/apple/splash-768x1004.png',
|
||||
//{
|
||||
//url: '/icons/apple/splash-1536x2008.png',
|
||||
//media: '(device-width: 768px) and (device-height: 1024px)',
|
||||
//},
|
||||
//],
|
||||
//title: 'Convex Monorepo',
|
||||
//statusBarStyle: 'black-translucent',
|
||||
//startupImage: [
|
||||
//'/icons/apple/splash-768x1004.png',
|
||||
//{
|
||||
//url: '/icons/apple/splash-1536x2008.png',
|
||||
//media: '(device-width: 768px) and (device-height: 1024px)',
|
||||
//},
|
||||
//],
|
||||
//},
|
||||
verification: {
|
||||
google: 'google',
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { banSuspiciousIPs } from '@/lib/middleware/ban-sus-ips';
|
||||
import {
|
||||
convexAuthNextjsMiddleware,
|
||||
createRouteMatcher,
|
||||
nextjsMiddlewareRedirect,
|
||||
} from '@convex-dev/auth/nextjs/server';
|
||||
import { banSuspiciousIPs } from '@/lib/middleware/ban-sus-ips';
|
||||
|
||||
const isSignInPage = createRouteMatcher(['/sign-in']);
|
||||
const isProtectedRoute = createRouteMatcher(['/', '/profile']);
|
||||
|
||||
@@ -1 +1 @@
|
||||
[["1","2","3","4","5","6","7","8","9","10","11","12","13","14","15"],{"key":"16","value":"17"},{"key":"18","value":"19"},{"key":"20","value":"21"},{"key":"22","value":"23"},{"key":"24","value":"25"},{"key":"26","value":"27"},{"key":"28","value":"29"},{"key":"30","value":"31"},{"key":"32","value":"33"},{"key":"34","value":"35"},{"key":"36","value":"37"},{"key":"38","value":"39"},{"key":"40","value":"41"},{"key":"42","value":"43"},{"key":"44","value":"45"},"/home/gib/Documents/Code/convex-monorepo/packages/backend/.cache/.prettiercache",{"size":3699,"mtime":1766222924000,"hash":"46"},"/home/gib/Documents/Code/convex-monorepo/packages/backend/convex/_generated/dataModel.d.ts",{"size":1726,"mtime":1768155639236,"hash":"47","data":"48"},"/home/gib/Documents/Code/convex-monorepo/packages/backend/.gitignore",{"size":16,"mtime":1766222924000,"hash":"49"},"/home/gib/Documents/Code/convex-monorepo/packages/backend/convex/utils.ts",{"size":635,"mtime":1768155639993,"hash":"50","data":"51"},"/home/gib/Documents/Code/convex-monorepo/packages/backend/convex/tsconfig.json",{"size":732,"mtime":1766222924000,"hash":"52","data":"53"},"/home/gib/Documents/Code/convex-monorepo/packages/backend/package.json",{"size":1120,"mtime":1766223702000,"hash":"54","data":"55"},"/home/gib/Documents/Code/convex-monorepo/packages/backend/convex/_generated/server.js",{"size":3453,"mtime":1768155639358,"hash":"56","data":"57"},"/home/gib/Documents/Code/convex-monorepo/packages/backend/convex/auth.config.js",{"size":128,"mtime":1768155639410,"hash":"58","data":"59"},"/home/gib/Documents/Code/convex-monorepo/packages/backend/convex/notes.ts",{"size":1632,"mtime":1768155639618,"hash":"60","data":"61"},"/home/gib/Documents/Code/convex-monorepo/packages/backend/convex/_generated/server.d.ts",{"size":5540,"mtime":1768155639312,"hash":"62","data":"63"},"/home/gib/Documents/Code/convex-monorepo/packages/backend/convex/_generated/api.d.ts",{"size":845,"mtime":1768155639122,"hash":"64","data":"65"},"/home/gib/Documents/Code/convex-monorepo/packages/backend/convex/_generated/api.js",{"size":414,"mtime":1768155639185,"hash":"66","data":"67"},"/home/gib/Documents/Code/convex-monorepo/packages/backend/convex/README.md",{"size":2525,"mtime":1768155639908,"hash":"68","data":"69"},"/home/gib/Documents/Code/convex-monorepo/packages/backend/convex/openai.ts",{"size":2036,"mtime":1768155639718,"hash":"70","data":"71"},"/home/gib/Documents/Code/convex-monorepo/packages/backend/convex/schema.ts",{"size":267,"mtime":1768155639933,"hash":"72","data":"73"},"bf09c6d61dd6a010bca24cb05271176d","ce3275ccdd28673d7e9280b33792057c",{"hashOfOptions":"74"},"175b1a771387d8b7e26145c3d04e0475","6a3198db454396c63e99ee08065726bd",{"hashOfOptions":"75"},"cfa98923457caed911ec68b626ef4234",{"hashOfOptions":"76"},"355d413e63d71e7e2107534f0d82fa31",{"hashOfOptions":"77"},"a8efc7f01f2b2dcb19115187a6f5f935",{"hashOfOptions":"78"},"449225bb5e883c1319e2ade335cdf0ad",{"hashOfOptions":"79"},"4b3c533672165e2af8d66b7149f85e1b",{"hashOfOptions":"80"},"896efcfb7466854312acff830348fec5",{"hashOfOptions":"81"},"8effb22a4ee737ecaeb923db430289f9",{"hashOfOptions":"82"},"0f5f819cfc410629d8ba1209956a037c",{"hashOfOptions":"83"},"f5aa8269478f140adb4ccf0470dd1244",{"hashOfOptions":"84"},"e06978cd8943c0adfa4d2a855c8f3627",{"hashOfOptions":"85"},"0d3d6e28d2429a34e666323218320d74",{"hashOfOptions":"86"},"1690245769","3139400620","2702670119","1723032970","1018051749","1593187711","1215337372","3786452325","2623319630","3362284046","3941219177","30277581","3515117182"]
|
||||
[["1","2","3","4","5","6","7","8","9","10","11","12","13","14","15","16","17","18","19","20","21","22","23"],{"key":"24","value":"25"},{"key":"26","value":"27"},{"key":"28","value":"29"},{"key":"30","value":"31"},{"key":"32","value":"33"},{"key":"34","value":"35"},{"key":"36","value":"37"},{"key":"38","value":"39"},{"key":"40","value":"41"},{"key":"42","value":"43"},{"key":"44","value":"45"},{"key":"46","value":"47"},{"key":"48","value":"49"},{"key":"50","value":"51"},{"key":"52","value":"53"},{"key":"54","value":"55"},{"key":"56","value":"57"},{"key":"58","value":"59"},{"key":"60","value":"61"},{"key":"62","value":"63"},{"key":"64","value":"65"},{"key":"66","value":"67"},{"key":"68","value":"69"},"/home/gib/Documents/Code/convex-monorepo/packages/backend/.cache/.prettiercache",{"size":3533,"mtime":1768171236843,"hash":"70"},"/home/gib/Documents/Code/convex-monorepo/packages/backend/convex/crons.ts",{"size":528,"mtime":1768239263662,"hash":"71","data":"72"},"/home/gib/Documents/Code/convex-monorepo/packages/backend/convex/auth.ts",{"size":3630,"mtime":1768239263654,"hash":"73","data":"74"},"/home/gib/Documents/Code/convex-monorepo/packages/backend/convex/custom/auth/providers/password.ts",{"size":900,"mtime":1768239263701,"hash":"75","data":"76"},"/home/gib/Documents/Code/convex-monorepo/packages/backend/convex/_generated/dataModel.d.ts",{"size":1726,"mtime":1768239263554,"hash":"77","data":"78"},"/home/gib/Documents/Code/convex-monorepo/packages/backend/.gitignore",{"size":16,"mtime":1766222924000,"hash":"79"},"/home/gib/Documents/Code/convex-monorepo/packages/backend/convex/utils.ts",{"size":635,"mtime":1768155639000,"hash":"80","data":"81"},"/home/gib/Documents/Code/convex-monorepo/packages/backend/convex/auth.config.ts",{"size":127,"mtime":1768156477000,"hash":"82","data":"83"},"/home/gib/Documents/Code/convex-monorepo/packages/backend/convex/tsconfig.json",{"size":732,"mtime":1766222924000,"hash":"84","data":"85"},"/home/gib/Documents/Code/convex-monorepo/packages/backend/package.json",{"size":1300,"mtime":1768239263823,"hash":"86","data":"87"},"/home/gib/Documents/Code/convex-monorepo/packages/backend/convex/custom/auth/providers/usesend.ts",{"size":3115,"mtime":1768164053000,"hash":"88","data":"89"},"/home/gib/Documents/Code/convex-monorepo/packages/backend/convex/_generated/server.js",{"size":3474,"mtime":1768239263584,"hash":"90","data":"91"},"/home/gib/Documents/Code/convex-monorepo/packages/backend/convex/_generated/server.d.ts",{"size":5695,"mtime":1768239263572,"hash":"92","data":"93"},"/home/gib/Documents/Code/convex-monorepo/packages/backend/types/auth.ts",{"size":174,"mtime":1768188156942,"hash":"94","data":"95"},"/home/gib/Documents/Code/convex-monorepo/packages/backend/convex/files.ts",{"size":582,"mtime":1768239263763,"hash":"96","data":"97"},"/home/gib/Documents/Code/convex-monorepo/packages/backend/scripts/generateKeys.mjs",{"size":520,"mtime":1768239263846,"hash":"98","data":"99"},"/home/gib/Documents/Code/convex-monorepo/packages/backend/convex/_generated/api.d.ts",{"size":1521,"mtime":1768239263527,"hash":"100","data":"101"},"/home/gib/Documents/Code/convex-monorepo/packages/backend/types/index.ts",{"size":69,"mtime":1768239263861,"hash":"102","data":"103"},"/home/gib/Documents/Code/convex-monorepo/packages/backend/convex/_generated/api.js",{"size":480,"mtime":1768239263542,"hash":"104","data":"105"},"/home/gib/Documents/Code/convex-monorepo/packages/backend/convex/README.md",{"size":2525,"mtime":1768155639000,"hash":"106","data":"107"},"/home/gib/Documents/Code/convex-monorepo/packages/backend/convex/http.ts",{"size":153,"mtime":1768157491000,"hash":"108","data":"109"},"/home/gib/Documents/Code/convex-monorepo/packages/backend/convex/schema.ts",{"size":1197,"mtime":1768239263796,"hash":"110","data":"111"},"/home/gib/Documents/Code/convex-monorepo/packages/backend/convex/custom/auth/index.ts",{"size":142,"mtime":1768157708000,"hash":"112","data":"113"},"e6b51ffddcfeaaeaaf7d1a0453024eff","0d76c3867783aec5435b0c716f1bfc65",{"hashOfOptions":"114"},"bd827d1529ceec8f4804864db994747b",{"hashOfOptions":"115"},"55bcf7d546da727edf2c1b7e52db6c7f",{"hashOfOptions":"116"},"ce3275ccdd28673d7e9280b33792057c",{"hashOfOptions":"117"},"175b1a771387d8b7e26145c3d04e0475","6a3198db454396c63e99ee08065726bd",{"hashOfOptions":"118"},"2d4f4ccfc784af9d668d8c64726c2f6b",{"hashOfOptions":"119"},"cfa98923457caed911ec68b626ef4234",{"hashOfOptions":"120"},"5d96f2526115241c4a37250b0f45ac4d",{"hashOfOptions":"121"},"14f5e2d18e21f65282ac744b382f2d5f",{"hashOfOptions":"122"},"f42d53bd1ba24bfee1407d2098f3b9ff",{"hashOfOptions":"123"},"6719b019702fc173de16b1bdc8e8ed47",{"hashOfOptions":"124"},"f0f9ef72dc6fef350f3521d3fd8adfc7",{"hashOfOptions":"125"},"5ff3856f1a61b946df899f7c9880b72a",{"hashOfOptions":"126"},"ccf5b9105ad4ff66fce0a7133fa3db4b",{"hashOfOptions":"127"},"7c522302bfb45e9cf5f1c271b48fa247",{"hashOfOptions":"128"},"57577de5aadfa618e91739aea3630342",{"hashOfOptions":"129"},"9d0d3b8d30fb1a1207df0584bf0f4c2e",{Line truncated
|
||||
+4
@@ -9,9 +9,11 @@
|
||||
*/
|
||||
|
||||
import type * as auth from "../auth.js";
|
||||
import type * as crons from "../crons.js";
|
||||
import type * as custom_auth_index from "../custom/auth/index.js";
|
||||
import type * as custom_auth_providers_password from "../custom/auth/providers/password.js";
|
||||
import type * as custom_auth_providers_usesend from "../custom/auth/providers/usesend.js";
|
||||
import type * as files from "../files.js";
|
||||
import type * as http from "../http.js";
|
||||
import type * as utils from "../utils.js";
|
||||
|
||||
@@ -31,9 +33,11 @@ import type {
|
||||
*/
|
||||
declare const fullApi: ApiFromModules<{
|
||||
auth: typeof auth;
|
||||
crons: typeof crons;
|
||||
"custom/auth/index": typeof custom_auth_index;
|
||||
"custom/auth/providers/password": typeof custom_auth_providers_password;
|
||||
"custom/auth/providers/usesend": typeof custom_auth_providers_usesend;
|
||||
files: typeof files;
|
||||
http: typeof http;
|
||||
utils: typeof utils;
|
||||
}>;
|
||||
|
||||
@@ -14,10 +14,7 @@ import { action, mutation, query } from './_generated/server';
|
||||
import { Password, validatePassword } from './custom/auth';
|
||||
|
||||
export const { auth, signIn, signOut, store, isAuthenticated } = convexAuth({
|
||||
providers: [
|
||||
Authentik({ allowDangerousEmailAccountLinking: true }),
|
||||
Password,
|
||||
],
|
||||
providers: [Authentik({ allowDangerousEmailAccountLinking: true }), Password],
|
||||
});
|
||||
|
||||
const getUserById = async (
|
||||
@@ -39,9 +36,8 @@ const isSignedIn = async (ctx: QueryCtx): Promise<Doc<'users'> | null> => {
|
||||
export const getUser = query({
|
||||
args: { userId: v.optional(v.id('users')) },
|
||||
handler: async (ctx, args) => {
|
||||
const user = await isSignedIn(ctx);
|
||||
const userId = args.userId ?? user?._id;
|
||||
if (!userId) throw new ConvexError('Not authenticated or no ID provided.');
|
||||
const userId = args.userId ?? (await getAuthUserId(ctx));
|
||||
if (!userId) return null;
|
||||
return getUserById(ctx, userId);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { cronJobs } from 'convex/server';
|
||||
|
||||
import { api } from './_generated/api';
|
||||
|
||||
// Cron order: Minute Hour DayOfMonth Month DayOfWeek
|
||||
const crons = cronJobs();
|
||||
/* Example cron jobs
|
||||
crons.cron(
|
||||
// Run at 7:00 AM CST / 8:00 AM CDT
|
||||
// Only on weekdays
|
||||
'Schedule Automatic Lunches',
|
||||
'0 13 * * 1-5',
|
||||
api.statuses.automaticLunch,
|
||||
);
|
||||
crons.cron(
|
||||
// Run at 4:00 PM CST / 5:00 PM CDT
|
||||
// Only on weekdays
|
||||
'End of shift (weekdays 5pm CT)',
|
||||
'0 22 * * 1-5',
|
||||
api.statuses.endOfShiftUpdate,
|
||||
);
|
||||
*/
|
||||
export default crons;
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Password as DefaultPassword } from '@convex-dev/auth/providers/Password';
|
||||
import { ConvexError } from 'convex/values';
|
||||
|
||||
import { UseSendOTP, UseSendOTPPasswordReset } from '..';
|
||||
import { DataModel } from '../../../_generated/dataModel';
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { getAuthUserId } from '@convex-dev/auth/server';
|
||||
import { ConvexError, v } from 'convex/values';
|
||||
|
||||
import { mutation, query } from './_generated/server';
|
||||
|
||||
export const generateUploadUrl = mutation(async (ctx) => {
|
||||
const userId = await getAuthUserId(ctx);
|
||||
if (!userId) throw new ConvexError('Not authenticated.');
|
||||
return await ctx.storage.generateUploadUrl();
|
||||
});
|
||||
|
||||
export const getImageUrl = query({
|
||||
args: { storageId: v.id('_storage') },
|
||||
handler: async (ctx, { storageId }) => {
|
||||
const url = await ctx.storage.getUrl(storageId);
|
||||
return url ?? null;
|
||||
},
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
import { authTables } from '@convex-dev/auth/server';
|
||||
import { defineSchema, defineTable } from 'convex/server';
|
||||
import { v } from 'convex/values';
|
||||
import { authTables } from '@convex-dev/auth/server';
|
||||
|
||||
const applicationTables = {
|
||||
// Users contains name image & email.
|
||||
@@ -10,8 +10,7 @@ const applicationTables = {
|
||||
profiles: defineTable({
|
||||
userId: v.id('users'),
|
||||
theme_preference: v.optional(v.string()),
|
||||
})
|
||||
.index('userId', ['userId'])
|
||||
}).index('userId', ['userId']),
|
||||
};
|
||||
|
||||
export default defineSchema({
|
||||
@@ -29,8 +28,8 @@ export default defineSchema({
|
||||
phoneVerificationTime: v.optional(v.number()),
|
||||
isAnonymous: v.optional(v.boolean()),
|
||||
})
|
||||
.index("email", ["email"])
|
||||
.index('email', ['email'])
|
||||
.index('name', ['name'])
|
||||
.index("phone", ["phone"]),
|
||||
.index('phone', ['phone']),
|
||||
...applicationTables,
|
||||
});
|
||||
@@ -7,7 +7,9 @@
|
||||
"author": "Gib",
|
||||
"license": "MIT",
|
||||
"exports": {
|
||||
"./types" : "./types/index.ts"
|
||||
"./convex": "./convex/",
|
||||
"./convex/*": "./convex/*",
|
||||
"./types": "./types/index.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "bun with-env convex dev",
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
#!/usr/bin/env node
|
||||
import { exportJWK, exportPKCS8, generateKeyPair } from 'jose';
|
||||
|
||||
import { exportJWK, exportPKCS8, generateKeyPair } from "jose";
|
||||
|
||||
const keys = await generateKeyPair("RS256", {
|
||||
const keys = await generateKeyPair('RS256', {
|
||||
extractable: true,
|
||||
});
|
||||
const privateKey = await exportPKCS8(keys.privateKey);
|
||||
const publicKey = await exportJWK(keys.publicKey);
|
||||
const jwks = JSON.stringify({ keys: [{ use: "sig", ...publicKey }] });
|
||||
const jwks = JSON.stringify({ keys: [{ use: 'sig', ...publicKey }] });
|
||||
|
||||
process.stdout.write(
|
||||
`JWT_PRIVATE_KEY="${privateKey.trimEnd().replace(/\n/g, " ")}"`,
|
||||
`JWT_PRIVATE_KEY="${privateKey.trimEnd().replace(/\n/g, ' ')}"`,
|
||||
);
|
||||
process.stdout.write("\n");
|
||||
process.stdout.write('\n');
|
||||
process.stdout.write(`JWKS=${jwks}`);
|
||||
process.stdout.write("\n");
|
||||
process.stdout.write('\n');
|
||||
@@ -1,5 +1 @@
|
||||
export {
|
||||
PASSWORD_MIN,
|
||||
PASSWORD_MAX,
|
||||
PASSWORD_REGEX,
|
||||
} from './auth';
|
||||
export { PASSWORD_MIN, PASSWORD_MAX, PASSWORD_REGEX } from './auth';
|
||||
@@ -1 +1 @@
|
||||
[["1","2","3","4","5","6","7","8","9","10","11","12","13","14","15","16","17","18","19","20","21","22","23","24","25","26","27","28","29","30","31","32"],{"key":"33","value":"34"},{"key":"35","value":"36"},{"key":"37","value":"38"},{"key":"39","value":"40"},{"key":"41","value":"42"},{"key":"43","value":"44"},{"key":"45","value":"46"},{"key":"47","value":"48"},{"key":"49","value":"50"},{"key":"51","value":"52"},{"key":"53","value":"54"},{"key":"55","value":"56"},{"key":"57","value":"58"},{"key":"59","value":"60"},{"key":"61","value":"62"},{"key":"63","value":"64"},{"key":"65","value":"66"},{"key":"67","value":"68"},{"key":"69","value":"70"},{"key":"71","value":"72"},{"key":"73","value":"74"},{"key":"75","value":"76"},{"key":"77","value":"78"},{"key":"79","value":"80"},{"key":"81","value":"82"},{"key":"83","value":"84"},{"key":"85","value":"86"},{"key":"87","value":"88"},{"key":"89","value":"90"},{"key":"91","value":"92"},{"key":"93","value":"94"},{"key":"95","value":"96"},"/home/gib/Documents/Code/convex-monorepo/packages/ui/src/based-avatar.tsx",{"size":1783,"mtime":1768155640183,"hash":"97","data":"98"},"/home/gib/Documents/Code/convex-monorepo/packages/ui/src/switch.tsx",{"size":1184,"mtime":1768155641096,"hash":"99","data":"100"},"/home/gib/Documents/Code/convex-monorepo/packages/ui/src/card.tsx",{"size":2001,"mtime":1768155640347,"hash":"101","data":"102"},"/home/gib/Documents/Code/convex-monorepo/packages/ui/src/table.tsx",{"size":2463,"mtime":1768155641128,"hash":"103","data":"104"},"/home/gib/Documents/Code/convex-monorepo/packages/ui/src/theme.tsx",{"size":5136,"mtime":1768155641207,"hash":"105","data":"106"},"/home/gib/Documents/Code/convex-monorepo/packages/ui/src/status-message.tsx",{"size":1620,"mtime":1768155641049,"hash":"107","data":"108"},"/home/gib/Documents/Code/convex-monorepo/packages/ui/src/drawer.tsx",{"size":4272,"mtime":1768155640435,"hash":"109","data":"110"},"/home/gib/Documents/Code/convex-monorepo/packages/ui/src/field.tsx",{"size":6143,"mtime":1768155640581,"hash":"111","data":"112"},"/home/gib/Documents/Code/convex-monorepo/packages/ui/src/input-otp.tsx",{"size":2269,"mtime":1768155640716,"hash":"113","data":"114"},"/home/gib/Documents/Code/convex-monorepo/packages/ui/src/separator.tsx",{"size":705,"mtime":1768155640895,"hash":"115","data":"116"},"/home/gib/Documents/Code/convex-monorepo/packages/ui/src/shadcn-io/image-crop/index.tsx",{"size":8265,"mtime":1768155640999,"hash":"117","data":"118"},"/home/gib/Documents/Code/convex-monorepo/packages/ui/src/avatar.tsx",{"size":1103,"mtime":1768155640108,"hash":"119","data":"120"},"/home/gib/Documents/Code/convex-monorepo/packages/ui/src/checkbox.tsx",{"size":1233,"mtime":1768155640375,"hash":"121","data":"122"},"/home/gib/Documents/Code/convex-monorepo/packages/ui/src/submit-button.tsx",{"size":1229,"mtime":1768155641074,"hash":"123","data":"124"},"/home/gib/Documents/Code/convex-monorepo/packages/ui/eslint.config.ts",{"size":254,"mtime":1768155640007,"hash":"125","data":"126"},"/home/gib/Documents/Code/convex-monorepo/packages/ui/src/scroll-area.tsx",{"size":1652,"mtime":1768155640872,"hash":"127","data":"128"},"/home/gib/Documents/Code/convex-monorepo/packages/ui/src/toast.tsx",{"size":616,"mtime":1768155641226,"hash":"129","data":"130"},"/home/gib/Documents/Code/convex-monorepo/packages/ui/src/input.tsx",{"size":971,"mtime":1768155640742,"hash":"131","data":"132"},"/home/gib/Documents/Code/convex-monorepo/packages/ui/src/progress.tsx",{"size":746,"mtime":1768155640843,"hash":"133","data":"134"},"/home/gib/Documents/Code/convex-monorepo/packages/ui/src/sonner.tsx",{"size":571,"mtime":1768155641017,"hash":"135","data":"136"},"/home/gib/Documents/Code/convex-monorepo/packages/ui/components.json",{"size":334,"mtime":1768154488750,"hash":"137","data":"138"},"/home/gib/Documents/Code/convex-monorepo/packages/ui/package.json",{"size":2863,"mtime":1768154592304,"hash":"139","data":"140"},"/home/gib/Documents/Code/convex-monorepo/packages/ui/src/pagination.tsx",{"size":2726,"mtime":1768155640818,"hash":"141","data":"142"},"/home/gib/Documents/Code/convex-monorepo/packages/ui/src/dropdown-menu.tsx",{"size":8316,"mtime":1768155640501,"hash":"143","data":"144"},"/home/gib/Documents/Code/convex-monorepo/packages/ui/src/form.tsx",{"size":3820,"mtime":1768155640641,"hash":"145","data":"146"},"/home/gib/Documents/Code/convex-monorepo/packages/ui/src/index.ts",{"size":358,"mtime":1768155640668,"hash":"147","data":"148"},"/home/gib/Documents/Code/convex-monorepo/packages/ui/src/label.tsx",{"size":617,"mtime":1768155640773,"hash":"149","data":"150"},"/home/gib/Documents/Code/convex-monorepo/packages/ui/src/button.tsx",{"size":2175,"mtime":1768155640302,"hash":"151","data":"152"},"/home/gib/Documents/Code/convex-monorepo/packages/ui/src/tabs.tsx",{"size":1979,"mtime":1768155641150,"hash":"153","data":"154"},"/home/gib/Documents/Code/convex-monorepo/packages/ui/.cache/.prettiercache",{"size":10630,"mtime":1766222924000,"hash":"155"},"/home/gib/DocumLine truncated
|
||||
[["1","2","3","4","5","6","7","8","9","10","11","12","13","14","15","16","17","18","19","20","21","22","23","24","25","26","27","28","29","30","31"],{"key":"32","value":"33"},{"key":"34","value":"35"},{"key":"36","value":"37"},{"key":"38","value":"39"},{"key":"40","value":"41"},{"key":"42","value":"43"},{"key":"44","value":"45"},{"key":"46","value":"47"},{"key":"48","value":"49"},{"key":"50","value":"51"},{"key":"52","value":"53"},{"key":"54","value":"55"},{"key":"56","value":"57"},{"key":"58","value":"59"},{"key":"60","value":"61"},{"key":"62","value":"63"},{"key":"64","value":"65"},{"key":"66","value":"67"},{"key":"68","value":"69"},{"key":"70","value":"71"},{"key":"72","value":"73"},{"key":"74","value":"75"},{"key":"76","value":"77"},{"key":"78","value":"79"},{"key":"80","value":"81"},{"key":"82","value":"83"},{"key":"84","value":"85"},{"key":"86","value":"87"},{"key":"88","value":"89"},{"key":"90","value":"91"},{"key":"92","value":"93"},"/home/gib/Documents/Code/convex-monorepo/packages/ui/src/based-avatar.tsx",{"size":1738,"mtime":1768239263550,"hash":"94","data":"95"},"/home/gib/Documents/Code/convex-monorepo/packages/ui/src/switch.tsx",{"size":1186,"mtime":1768239263972,"hash":"96","data":"97"},"/home/gib/Documents/Code/convex-monorepo/packages/ui/src/card.tsx",{"size":1998,"mtime":1768239263622,"hash":"98","data":"99"},"/home/gib/Documents/Code/convex-monorepo/packages/ui/src/table.tsx",{"size":2465,"mtime":1768239263980,"hash":"100","data":"101"},"/home/gib/Documents/Code/convex-monorepo/packages/ui/src/theme.tsx",{"size":1796,"mtime":1768239264008,"hash":"102","data":"103"},"/home/gib/Documents/Code/convex-monorepo/packages/ui/src/status-message.tsx",{"size":1619,"mtime":1768239263956,"hash":"104","data":"105"},"/home/gib/Documents/Code/convex-monorepo/packages/ui/src/drawer.tsx",{"size":4269,"mtime":1768239263651,"hash":"106","data":"107"},"/home/gib/Documents/Code/convex-monorepo/packages/ui/src/field.tsx",{"size":6073,"mtime":1768239263711,"hash":"108","data":"109"},"/home/gib/Documents/Code/convex-monorepo/packages/ui/src/input-otp.tsx",{"size":2266,"mtime":1768239263796,"hash":"110","data":"111"},"/home/gib/Documents/Code/convex-monorepo/packages/ui/src/separator.tsx",{"size":702,"mtime":1768239263893,"hash":"112","data":"113"},"/home/gib/Documents/Code/convex-monorepo/packages/ui/src/shadcn-io/image-crop/index.tsx",{"size":8230,"mtime":1768239263934,"hash":"114","data":"115"},"/home/gib/Documents/Code/convex-monorepo/packages/ui/src/avatar.tsx",{"size":1102,"mtime":1768155815000,"hash":"116","data":"117"},"/home/gib/Documents/Code/convex-monorepo/packages/ui/src/checkbox.tsx",{"size":1230,"mtime":1768239263633,"hash":"118","data":"119"},"/home/gib/Documents/Code/convex-monorepo/packages/ui/src/submit-button.tsx",{"size":1192,"mtime":1768239263964,"hash":"120","data":"121"},"/home/gib/Documents/Code/convex-monorepo/packages/ui/eslint.config.ts",{"size":254,"mtime":1768155640000,"hash":"122","data":"123"},"/home/gib/Documents/Code/convex-monorepo/packages/ui/src/scroll-area.tsx",{"size":1654,"mtime":1768239263883,"hash":"124","data":"125"},"/home/gib/Documents/Code/convex-monorepo/packages/ui/src/input.tsx",{"size":968,"mtime":1768239263824,"hash":"126","data":"127"},"/home/gib/Documents/Code/convex-monorepo/packages/ui/src/progress.tsx",{"size":748,"mtime":1768239263868,"hash":"128","data":"129"},"/home/gib/Documents/Code/convex-monorepo/packages/ui/src/sonner.tsx",{"size":601,"mtime":1768239263943,"hash":"130","data":"131"},"/home/gib/Documents/Code/convex-monorepo/packages/ui/components.json",{"size":334,"mtime":1768154488000,"hash":"132","data":"133"},"/home/gib/Documents/Code/convex-monorepo/packages/ui/package.json",{"size":2864,"mtime":1768155775000,"hash":"134","data":"135"},"/home/gib/Documents/Code/convex-monorepo/packages/ui/src/pagination.tsx",{"size":2718,"mtime":1768239263856,"hash":"136","data":"137"},"/home/gib/Documents/Code/convex-monorepo/packages/ui/src/index.tsx",{"size":2381,"mtime":1768155754000,"hash":"138","data":"139"},"/home/gib/Documents/Code/convex-monorepo/packages/ui/src/dropdown-menu.tsx",{"size":8313,"mtime":1768239263677,"hash":"140","data":"141"},"/home/gib/Documents/Code/convex-monorepo/packages/ui/src/form.tsx",{"size":3782,"mtime":1768239263749,"hash":"142","data":"143"},"/home/gib/Documents/Code/convex-monorepo/packages/ui/src/label.tsx",{"size":614,"mtime":1768239263842,"hash":"144","data":"145"},"/home/gib/Documents/Code/convex-monorepo/packages/ui/src/button.tsx",{"size":2172,"mtime":1768239263600,"hash":"146","data":"147"},"/home/gib/Documents/Code/convex-monorepo/packages/ui/src/tabs.tsx",{"size":1981,"mtime":1768239263989,"hash":"148","data":"149"},"/home/gib/Documents/Code/convex-monorepo/packages/ui/.cache/.prettiercache",{"size":7506,"mtime":1768171236843,"hash":"150"},"/home/gib/Documents/Code/convex-monorepo/packages/ui/src/based-progress.tsx",{"size":1372,"mtime":1768239263575,"hash":"151","data":"152"},"/home/gib/Documents/Code/convex-monoreLine truncated
|
||||
@@ -1,10 +1,11 @@
|
||||
'use client';
|
||||
|
||||
import type { ComponentProps } from 'react';
|
||||
import { cn, AvatarImage } from '@gib/ui';
|
||||
import * as AvatarPrimitive from '@radix-ui/react-avatar';
|
||||
import { User } from 'lucide-react';
|
||||
|
||||
import { AvatarImage, cn } from '@gib/ui';
|
||||
|
||||
type BasedAvatarProps = ComponentProps<typeof AvatarPrimitive.Root> & {
|
||||
src?: string | null;
|
||||
fullName?: string | null;
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { cn } from '@gib/ui';
|
||||
import * as ProgressPrimitive from '@radix-ui/react-progress';
|
||||
|
||||
import { cn } from '@gib/ui';
|
||||
|
||||
type BasedProgressProps = React.ComponentProps<
|
||||
typeof ProgressPrimitive.Root
|
||||
> & {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import type { VariantProps } from 'class-variance-authority';
|
||||
import * as React from 'react';
|
||||
import { cn } from '@gib/ui';
|
||||
import { Slot } from '@radix-ui/react-slot';
|
||||
import { cva } from 'class-variance-authority';
|
||||
|
||||
import { cn } from '@gib/ui';
|
||||
|
||||
const buttonVariants = cva(
|
||||
"focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { cn } from '@gib/ui';
|
||||
|
||||
function Card({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { cn } from '@gib/ui';
|
||||
import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
|
||||
import { CheckIcon } from 'lucide-react';
|
||||
|
||||
import { cn } from '@gib/ui';
|
||||
|
||||
function Checkbox({
|
||||
className,
|
||||
...props
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { cn } from '@gib/ui';
|
||||
import { Drawer as DrawerPrimitive } from 'vaul';
|
||||
|
||||
import { cn } from '@gib/ui';
|
||||
|
||||
function Drawer({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Root>) {
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { cn } from '@gib/ui';
|
||||
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
|
||||
import { CheckIcon, ChevronRightIcon, CircleIcon } from 'lucide-react';
|
||||
|
||||
import { cn } from '@gib/ui';
|
||||
|
||||
function DropdownMenu({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
|
||||
import type { VariantProps } from 'class-variance-authority';
|
||||
import { useMemo } from 'react';
|
||||
import { cn, Label, Separator } from '@gib/ui';
|
||||
import { cva } from 'class-variance-authority';
|
||||
|
||||
import { cn, Label, Separator } from '@gib/ui';
|
||||
|
||||
export function FieldSet({
|
||||
className,
|
||||
...props
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import type * as LabelPrimitive from '@radix-ui/react-label';
|
||||
import type { ControllerProps, FieldPath, FieldValues } from 'react-hook-form';
|
||||
import * as React from 'react';
|
||||
import { cn, Label } from '@gib/ui';
|
||||
import type * as LabelPrimitive from '@radix-ui/react-label';
|
||||
import { Slot } from '@radix-ui/react-slot';
|
||||
import {
|
||||
Controller,
|
||||
@@ -12,6 +11,8 @@ import {
|
||||
useFormState,
|
||||
} from 'react-hook-form';
|
||||
|
||||
import { cn, Label } from '@gib/ui';
|
||||
|
||||
const Form = FormProvider;
|
||||
|
||||
type FormFieldContextValue<
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { cn } from '@gib/ui';
|
||||
import { OTPInput, OTPInputContext } from 'input-otp';
|
||||
import { MinusIcon } from 'lucide-react';
|
||||
|
||||
import { cn } from '@gib/ui';
|
||||
|
||||
function InputOTP({
|
||||
className,
|
||||
containerClassName,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { cn } from '@gib/ui';
|
||||
|
||||
function Input({ className, type, ...props }: React.ComponentProps<'input'>) {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { cn } from '@gib/ui';
|
||||
import * as LabelPrimitive from '@radix-ui/react-label';
|
||||
|
||||
import { cn } from '@gib/ui';
|
||||
|
||||
function Label({
|
||||
className,
|
||||
...props
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import type * as React from 'react';
|
||||
import type { Button } from '@gib/ui';
|
||||
import { cn, buttonVariants } from '@gib/ui';
|
||||
import {
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
MoreHorizontalIcon,
|
||||
} from 'lucide-react';
|
||||
|
||||
import type { Button } from '@gib/ui';
|
||||
import { buttonVariants, cn } from '@gib/ui';
|
||||
|
||||
function Pagination({ className, ...props }: React.ComponentProps<'nav'>) {
|
||||
return (
|
||||
<nav
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
'use client';
|
||||
|
||||
import type * as React from 'react';
|
||||
import { cn } from '@gib/ui';
|
||||
import * as ProgressPrimitive from '@radix-ui/react-progress';
|
||||
|
||||
import { cn } from '@gib/ui';
|
||||
|
||||
function Progress({
|
||||
className,
|
||||
value,
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
'use client';
|
||||
|
||||
import type * as React from 'react';
|
||||
import { cn } from '@gib/ui';
|
||||
import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area';
|
||||
|
||||
import { cn } from '@gib/ui';
|
||||
|
||||
function ScrollArea({
|
||||
className,
|
||||
children,
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { cn } from '@gib/ui';
|
||||
import * as SeparatorPrimitive from '@radix-ui/react-separator';
|
||||
|
||||
import { cn } from '@gib/ui';
|
||||
|
||||
function Separator({
|
||||
className,
|
||||
orientation = 'horizontal',
|
||||
|
||||
@@ -17,11 +17,12 @@ import {
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { cn, Button } from '@gib/ui';
|
||||
import { CropIcon, RotateCcwIcon } from 'lucide-react';
|
||||
import { Slot } from 'radix-ui';
|
||||
import ReactCrop, { centerCrop, makeAspectCrop } from 'react-image-crop';
|
||||
|
||||
import { Button, cn } from '@gib/ui';
|
||||
|
||||
import 'react-image-crop/dist/ReactCrop.css';
|
||||
|
||||
const centerAspectCrop = (
|
||||
@@ -110,7 +111,7 @@ interface ImageCropContextType {
|
||||
onImageLoad: (e: SyntheticEvent<HTMLImageElement>) => void;
|
||||
applyCrop: () => Promise<void>;
|
||||
resetCrop: () => void;
|
||||
};
|
||||
}
|
||||
|
||||
const ImageCropContext = createContext<ImageCropContextType | null>(null);
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { useTheme } from 'next-themes';
|
||||
import type { ToasterProps } from 'sonner';
|
||||
import { useTheme } from 'next-themes';
|
||||
import { Toaster as Sonner } from 'sonner';
|
||||
|
||||
const Toaster = ({ ...props }: ToasterProps) => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { ComponentProps } from 'react';
|
||||
|
||||
import { cn } from '@gib/ui';
|
||||
|
||||
type Message = { success: string } | { error: string } | { message: string };
|
||||
@@ -7,7 +8,7 @@ interface StatusMessageProps {
|
||||
message: Message;
|
||||
containerProps?: ComponentProps<'div'>;
|
||||
textProps?: ComponentProps<'div'>;
|
||||
};
|
||||
}
|
||||
|
||||
export const StatusMessage = ({
|
||||
message,
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
'use client';
|
||||
|
||||
import type { ComponentProps } from 'react';
|
||||
import { cn, Button } from '@gib/ui';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import { useFormStatus } from 'react-dom';
|
||||
|
||||
import { Button, cn } from '@gib/ui';
|
||||
|
||||
export type SubmitButtonProps = Omit<
|
||||
ComponentProps<typeof Button>,
|
||||
'type' | 'aria-disabled'
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
'use client';
|
||||
|
||||
import type * as React from 'react';
|
||||
import { cn } from '@gib/ui';
|
||||
import * as SwitchPrimitive from '@radix-ui/react-switch';
|
||||
|
||||
import { cn } from '@gib/ui';
|
||||
|
||||
function Switch({
|
||||
className,
|
||||
...props
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import type * as React from 'react';
|
||||
|
||||
import { cn } from '@gib/ui';
|
||||
|
||||
function Table({ className, ...props }: React.ComponentProps<'table'>) {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
'use client';
|
||||
|
||||
import type * as React from 'react';
|
||||
import { cn } from '@gib/ui';
|
||||
import * as TabsPrimitive from '@radix-ui/react-tabs';
|
||||
|
||||
import { cn } from '@gib/ui';
|
||||
|
||||
function Tabs({
|
||||
className,
|
||||
...props
|
||||
|
||||
@@ -24,7 +24,7 @@ const ThemeProvider = ({
|
||||
interface ThemeToggleProps {
|
||||
size?: number;
|
||||
buttonProps?: Omit<ComponentProps<typeof Button>, 'onClick'>;
|
||||
};
|
||||
}
|
||||
|
||||
const ThemeToggle = ({ size = 1, buttonProps }: ThemeToggleProps) => {
|
||||
const { setTheme, resolvedTheme } = useTheme();
|
||||
|
||||
@@ -1 +1 @@
|
||||
[["1","2","3","4","5"],{"key":"6","value":"7"},{"key":"8","value":"9"},{"key":"10","value":"11"},{"key":"12","value":"13"},{"key":"14","value":"15"},"/home/gib/Documents/Code/convex-monorepo/tools/eslint/package.json",{"size":979,"mtime":1766222924000,"hash":"16","data":"17"},"/home/gib/Documents/Code/convex-monorepo/tools/eslint/nextjs.ts",{"size":440,"mtime":1768155639188,"hash":"18","data":"19"},"/home/gib/Documents/Code/convex-monorepo/tools/eslint/react.ts",{"size":592,"mtime":1768155639239,"hash":"20","data":"21"},"/home/gib/Documents/Code/convex-monorepo/tools/eslint/base.ts",{"size":2511,"mtime":1768155639128,"hash":"22","data":"23"},"/home/gib/Documents/Code/convex-monorepo/tools/eslint/tsconfig.json",{"size":94,"mtime":1766222924000,"hash":"24","data":"25"},"a5326aca75246da261fd2e354257b45a",{"hashOfOptions":"26"},"25c52c46972131dcc296288599ff108d",{"hashOfOptions":"27"},"2292935ede6baf909f6a0c61486e15da",{"hashOfOptions":"28"},"9e432daa1849e911eb3c69df3d068c6d",{"hashOfOptions":"29"},"b3c77d33a30318d89c9c2cafcbe00bbe",{"hashOfOptions":"30"},"1786406903","2477157100","3372054421","3828872695","459831536"]
|
||||
[["1","2","3","4","5","6"],{"key":"7","value":"8"},{"key":"9","value":"10"},{"key":"11","value":"12"},{"key":"13","value":"14"},{"key":"15","value":"16"},{"key":"17","value":"18"},"/home/gib/Documents/Code/convex-monorepo/tools/eslint/package.json",{"size":979,"mtime":1768166330000,"hash":"19","data":"20"},"/home/gib/Documents/Code/convex-monorepo/tools/eslint/nextjs.ts",{"size":440,"mtime":1768155639000,"hash":"21","data":"22"},"/home/gib/Documents/Code/convex-monorepo/tools/eslint/.cache/.prettiercache",{"size":1132,"mtime":1768171236844,"hash":"23"},"/home/gib/Documents/Code/convex-monorepo/tools/eslint/react.ts",{"size":592,"mtime":1768155639000,"hash":"24","data":"25"},"/home/gib/Documents/Code/convex-monorepo/tools/eslint/base.ts",{"size":2511,"mtime":1768155639000,"hash":"26","data":"27"},"/home/gib/Documents/Code/convex-monorepo/tools/eslint/tsconfig.json",{"size":94,"mtime":1766222924000,"hash":"28","data":"29"},"a5326aca75246da261fd2e354257b45a",{"hashOfOptions":"30"},"25c52c46972131dcc296288599ff108d",{"hashOfOptions":"31"},"26c40ae02d206ffdd279df170ea85cae","2292935ede6baf909f6a0c61486e15da",{"hashOfOptions":"32"},"9e432daa1849e911eb3c69df3d068c6d",{"hashOfOptions":"33"},"b3c77d33a30318d89c9c2cafcbe00bbe",{"hashOfOptions":"34"},"1786406903","2477157100","3372054421","3828872695","459831536"]
|
||||
@@ -1 +1 @@
|
||||
[["1","2","3"],{"key":"4","value":"5"},{"key":"6","value":"7"},{"key":"8","value":"9"},"/home/gib/Documents/Code/convex-monorepo/tools/prettier/tsconfig.json",{"size":94,"mtime":1766222924000,"hash":"10","data":"11"},"/home/gib/Documents/Code/convex-monorepo/tools/prettier/package.json",{"size":607,"mtime":1766222924000,"hash":"12","data":"13"},"/home/gib/Documents/Code/convex-monorepo/tools/prettier/index.js",{"size":1170,"mtime":1768155639114,"hash":"14","data":"15"},"b3c77d33a30318d89c9c2cafcbe00bbe",{"hashOfOptions":"16"},"11b634ce56ac720ac9a2860d77fbd2cc",{"hashOfOptions":"17"},"d6f2202018912e47fdd2016f04a4b6eb",{"hashOfOptions":"18"},"1555666866","640383157","507270250"]
|
||||
[["1","2","3","4"],{"key":"5","value":"6"},{"key":"7","value":"8"},{"key":"9","value":"10"},{"key":"11","value":"12"},"/home/gib/Documents/Code/convex-monorepo/tools/prettier/tsconfig.json",{"size":94,"mtime":1766222924000,"hash":"13","data":"14"},"/home/gib/Documents/Code/convex-monorepo/tools/prettier/package.json",{"size":607,"mtime":1766222924000,"hash":"15","data":"16"},"/home/gib/Documents/Code/convex-monorepo/tools/prettier/.cache/.prettiercache",{"size":685,"mtime":1768171236844,"hash":"17"},"/home/gib/Documents/Code/convex-monorepo/tools/prettier/index.js",{"size":1170,"mtime":1768155639000,"hash":"18","data":"19"},"b3c77d33a30318d89c9c2cafcbe00bbe",{"hashOfOptions":"20"},"11b634ce56ac720ac9a2860d77fbd2cc",{"hashOfOptions":"21"},"bd15cce9406aeef5526bb5681494acce","d6f2202018912e47fdd2016f04a4b6eb",{"hashOfOptions":"22"},"1555666866","640383157","507270250"]
|
||||
@@ -1 +1 @@
|
||||
[["1","2","3","4","5"],{"key":"6","value":"7"},{"key":"8","value":"9"},{"key":"10","value":"11"},{"key":"12","value":"13"},{"key":"14","value":"15"},"/home/gib/Documents/Code/convex-monorepo/tools/tailwind/package.json",{"size":851,"mtime":1766222924000,"hash":"16","data":"17"},"/home/gib/Documents/Code/convex-monorepo/tools/tailwind/postcss-config.js",{"size":70,"mtime":1768155639454,"hash":"18","data":"19"},"/home/gib/Documents/Code/convex-monorepo/tools/tailwind/eslint.config.ts",{"size":143,"mtime":1768155639344,"hash":"20","data":"21"},"/home/gib/Documents/Code/convex-monorepo/tools/tailwind/theme.css",{"size":6741,"mtime":1766222924000,"hash":"22","data":"23"},"/home/gib/Documents/Code/convex-monorepo/tools/tailwind/tsconfig.json",{"size":94,"mtime":1766222924000,"hash":"24","data":"25"},"0d22e47f57739db9de04c6f8420d6fb5",{"hashOfOptions":"26"},"9a944fbda06979be39571bd9bd00b0d9",{"hashOfOptions":"27"},"b8fec960cb32340eea62ca1485093e68",{"hashOfOptions":"28"},"5dd421d25d104c47e1ab36df41ed0f7d",{"hashOfOptions":"29"},"b3c77d33a30318d89c9c2cafcbe00bbe",{"hashOfOptions":"30"},"1821576240","2434669165","2442413358","1994519264","1135731447"]
|
||||
[["1","2","3","4","5","6"],{"key":"7","value":"8"},{"key":"9","value":"10"},{"key":"11","value":"12"},{"key":"13","value":"14"},{"key":"15","value":"16"},{"key":"17","value":"18"},"/home/gib/Documents/Code/convex-monorepo/tools/tailwind/package.json",{"size":851,"mtime":1766222924000,"hash":"19","data":"20"},"/home/gib/Documents/Code/convex-monorepo/tools/tailwind/postcss-config.js",{"size":70,"mtime":1768155639000,"hash":"21","data":"22"},"/home/gib/Documents/Code/convex-monorepo/tools/tailwind/eslint.config.ts",{"size":143,"mtime":1768155639000,"hash":"23","data":"24"},"/home/gib/Documents/Code/convex-monorepo/tools/tailwind/theme.css",{"size":6741,"mtime":1766222924000,"hash":"25","data":"26"},"/home/gib/Documents/Code/convex-monorepo/tools/tailwind/tsconfig.json",{"size":94,"mtime":1766222924000,"hash":"27","data":"28"},"/home/gib/Documents/Code/convex-monorepo/tools/tailwind/.cache/.prettiercache",{"size":1160,"mtime":1768171236844,"hash":"29"},"0d22e47f57739db9de04c6f8420d6fb5",{"hashOfOptions":"30"},"9a944fbda06979be39571bd9bd00b0d9",{"hashOfOptions":"31"},"b8fec960cb32340eea62ca1485093e68",{"hashOfOptions":"32"},"5dd421d25d104c47e1ab36df41ed0f7d",{"hashOfOptions":"33"},"b3c77d33a30318d89c9c2cafcbe00bbe",{"hashOfOptions":"34"},"e96b5ac7b7aca2012f7ddbe4daef9e4c","1821576240","2434669165","2442413358","1994519264","1135731447"]
|
||||
Reference in new issue
Block a user