From 91682bd887f219b1b10294dc0461a2de1f4bb946 Mon Sep 17 00:00:00 2001 From: gib Date: Sun, 11 Jan 2026 10:17:30 -0500 Subject: [PATCH] last commit before trying out opencode on repo --- apps/expo/.cache/.prettiercache | 1 + apps/expo/app.config.ts | 46 +-- apps/expo/eslint.config.mts | 9 +- apps/expo/index.ts | 2 +- apps/expo/metro.config.js | 10 +- apps/expo/postcss.config.js | 2 +- apps/expo/src/app/_layout.tsx | 16 +- apps/expo/src/app/index.tsx | 42 +- apps/expo/src/app/post/[id].tsx | 8 +- apps/expo/src/styles.css | 6 +- apps/expo/src/utils/api.tsx | 27 +- apps/expo/src/utils/auth.ts | 12 +- apps/expo/src/utils/base-url.ts | 6 +- apps/expo/src/utils/session-store.ts | 4 +- apps/next/.cache/.prettiercache | 1 + apps/next/eslint.config.ts | 11 +- apps/next/next.config.js | 14 +- apps/next/package.json | 2 +- .../src/app/_components/auth-showcase.tsx | 58 --- apps/next/src/app/_components/posts.tsx | 210 ---------- apps/next/src/app/api/auth/[...all]/route.ts | 4 - apps/next/src/app/api/trpc/[trpc]/route.ts | 46 --- apps/next/src/app/global-error.tsx | 65 +++ apps/next/src/app/layout.tsx | 100 +++-- apps/next/src/app/page.tsx | 44 +-- apps/next/src/app/styles.css | 8 +- .../providers/ConvexClientProvider.tsx | 15 + .../components/providers/ThemeProvider.tsx | 70 ++++ apps/next/src/components/providers/index.tsx | 7 + apps/next/src/{env.ts => env.js} | 15 +- apps/next/src/instrumentation-client.ts | 28 ++ apps/next/src/instrumentation.ts | 7 + apps/next/src/lib/metadata.ts | 369 ++++++++++++++++++ apps/next/src/sentry.server.config.ts | 10 + package.json | 2 +- packages/backend/.cache/.prettiercache | 2 +- packages/backend/convex/README.md | 18 +- packages/backend/convex/_generated/api.d.ts | 4 +- packages/backend/convex/auth.config.ts | 2 +- packages/backend/convex/auth.ts | 25 +- .../convex/custom/auth/providers/password.ts | 5 +- .../convex/custom/auth/providers/usesend.ts | 2 +- packages/backend/convex/http.ts | 1 + packages/backend/convex/questions.ts | 35 +- packages/backend/convex/quiz.ts | 236 +++++++++++ packages/backend/convex/schema.ts | 10 +- packages/backend/convex/utils.ts | 16 - packages/ui/.cache/.prettiercache | 2 +- packages/ui/eslint.config.ts | 8 +- packages/ui/src/avatar.tsx | 15 +- packages/ui/src/based-avatar.tsx | 24 +- packages/ui/src/based-progress.tsx | 11 +- packages/ui/src/button.tsx | 39 +- packages/ui/src/card.tsx | 33 +- packages/ui/src/checkbox.tsx | 13 +- packages/ui/src/drawer.tsx | 33 +- packages/ui/src/dropdown-menu.tsx | 29 +- packages/ui/src/field.tsx | 84 ++-- packages/ui/src/form.tsx | 34 +- packages/ui/src/index.ts | 101 ++++- packages/ui/src/input-otp.tsx | 25 +- packages/ui/src/input.tsx | 13 +- packages/ui/src/label.tsx | 11 +- packages/ui/src/pagination.tsx | 36 +- packages/ui/src/progress.tsx | 11 +- packages/ui/src/scroll-area.tsx | 23 +- packages/ui/src/separator.tsx | 13 +- .../ui/src/shadcn-io/image-crop/index.tsx | 62 +-- packages/ui/src/sonner.tsx | 16 +- packages/ui/src/status-message.tsx | 29 +- packages/ui/src/submit-button.tsx | 24 +- packages/ui/src/switch.tsx | 13 +- packages/ui/src/table.tsx | 39 +- packages/ui/src/tabs.tsx | 15 +- packages/ui/src/theme.tsx | 224 +++-------- packages/ui/src/toast.tsx | 27 -- tools/eslint/.cache/.prettiercache | 1 + tools/eslint/base.ts | 62 +-- tools/eslint/nextjs.ts | 12 +- tools/eslint/react.ts | 16 +- tools/prettier/.cache/.prettiercache | 1 + tools/prettier/index.js | 46 +-- tools/tailwind/.cache/.prettiercache | 1 + tools/tailwind/eslint.config.ts | 4 +- tools/tailwind/postcss-config.js | 2 +- 85 files changed, 1589 insertions(+), 1196 deletions(-) create mode 100644 apps/expo/.cache/.prettiercache create mode 100644 apps/next/.cache/.prettiercache delete mode 100644 apps/next/src/app/_components/auth-showcase.tsx delete mode 100644 apps/next/src/app/_components/posts.tsx delete mode 100644 apps/next/src/app/api/auth/[...all]/route.ts delete mode 100644 apps/next/src/app/api/trpc/[trpc]/route.ts create mode 100644 apps/next/src/app/global-error.tsx create mode 100644 apps/next/src/components/providers/ConvexClientProvider.tsx create mode 100644 apps/next/src/components/providers/ThemeProvider.tsx create mode 100644 apps/next/src/components/providers/index.tsx rename apps/next/src/{env.ts => env.js} (77%) create mode 100644 apps/next/src/instrumentation-client.ts create mode 100644 apps/next/src/instrumentation.ts create mode 100644 apps/next/src/lib/metadata.ts create mode 100644 apps/next/src/sentry.server.config.ts create mode 100644 packages/backend/convex/quiz.ts delete mode 100644 packages/backend/convex/utils.ts delete mode 100644 packages/ui/src/toast.tsx create mode 100644 tools/eslint/.cache/.prettiercache create mode 100644 tools/prettier/.cache/.prettiercache create mode 100644 tools/tailwind/.cache/.prettiercache diff --git a/apps/expo/.cache/.prettiercache b/apps/expo/.cache/.prettiercache new file mode 100644 index 0000000..458dce1 --- /dev/null +++ b/apps/expo/.cache/.prettiercache @@ -0,0 +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/studybuddy/apps/expo/app.config.ts",{"size":1333,"mtime":1768143608432,"data":"64"},"/home/gib/Documents/Code/studybuddy/apps/expo/src/utils/session-store.ts",{"size":272,"mtime":1768143609298,"data":"65"},"/home/gib/Documents/Code/studybuddy/apps/expo/src/app/index.tsx",{"size":5019,"mtime":1768143609009,"data":"66"},"/home/gib/Documents/Code/studybuddy/apps/expo/assets/icon-dark.png",{"size":19633,"mtime":1767666484837},"/home/gib/Documents/Code/studybuddy/apps/expo/index.ts",{"size":28,"mtime":1768143608580,"data":"67"},"/home/gib/Documents/Code/studybuddy/apps/expo/postcss.config.js",{"size":66,"mtime":1768143608686,"data":"68"},"/home/gib/Documents/Code/studybuddy/apps/expo/src/app/post/[id].tsx",{"size":757,"mtime":1768143609067,"data":"69"},"/home/gib/Documents/Code/studybuddy/apps/expo/src/utils/api.tsx",{"size":1326,"mtime":1768143609180,"data":"70"},"/home/gib/Documents/Code/studybuddy/apps/expo/src/utils/auth.ts",{"size":398,"mtime":1768143609220,"data":"71"},"/home/gib/Documents/Code/studybuddy/apps/expo/eas.json",{"size":567,"mtime":1767666484838,"data":"72"},"/home/gib/Documents/Code/studybuddy/apps/expo/src/app/_layout.tsx",{"size":927,"mtime":1768143608751,"data":"73"},"/home/gib/Documents/Code/studybuddy/apps/expo/src/styles.css",{"size":90,"mtime":1768143609095,"data":"74"},"/home/gib/Documents/Code/studybuddy/apps/expo/assets/icon-light.png",{"size":19133,"mtime":1767666484838},"/home/gib/Documents/Code/studybuddy/apps/expo/package.json",{"size":2249,"mtime":1767666484838,"data":"75"},"/home/gib/Documents/Code/studybuddy/apps/expo/turbo.json",{"size":163,"mtime":1767666484838,"data":"76"},"/home/gib/Documents/Code/studybuddy/apps/expo/eslint.config.mts",{"size":275,"mtime":1768143608554,"data":"77"},"/home/gib/Documents/Code/studybuddy/apps/expo/tsconfig.json",{"size":387,"mtime":1767666484838,"data":"78"},"/home/gib/Documents/Code/studybuddy/apps/expo/metro.config.js",{"size":511,"mtime":1768143608634,"data":"79"},"/home/gib/Documents/Code/studybuddy/apps/expo/nativewind-env.d.ts",{"size":246,"mtime":1767666484838,"data":"80"},"/home/gib/Documents/Code/studybuddy/apps/expo/src/utils/base-url.ts",{"size":880,"mtime":1768143609265,"data":"81"},"/home/gib/Documents/Code/studybuddy/apps/expo/.expo-shared/assets.json",{"size":155,"mtime":1767666484837,"data":"82"},{"hashOfOptions":"83"},{"hashOfOptions":"84"},{"hashOfOptions":"85"},{"hashOfOptions":"86"},{"hashOfOptions":"87"},{"hashOfOptions":"88"},{"hashOfOptions":"89"},{"hashOfOptions":"90"},{"hashOfOptions":"91"},{"hashOfOptions":"92"},{"hashOfOptions":"93"},{"hashOfOptions":"94"},{"hashOfOptions":"95"},{"hashOfOptions":"96"},{"hashOfOptions":"97"},{"hashOfOptions":"98"},{"hashOfOptions":"99"},{"hashOfOptions":"100"},{"hashOfOptions":"101"},"3643443648","1553667980","1482524294","216717627","1655909324","4237219338","3866199870","3075608766","3190062399","2443944861","2945306946","4245091856","1453504504","896187967","3122163991","929633954","2918344858","3218409107","3873583862"] \ No newline at end of file diff --git a/apps/expo/app.config.ts b/apps/expo/app.config.ts index 1f42aaa..3689574 100644 --- a/apps/expo/app.config.ts +++ b/apps/expo/app.config.ts @@ -1,32 +1,32 @@ -import type { ConfigContext, ExpoConfig } from "expo/config"; +import type { ConfigContext, ExpoConfig } from 'expo/config'; export default ({ config }: ConfigContext): ExpoConfig => ({ ...config, - name: "expo", - slug: "expo", - scheme: "expo", - version: "0.1.0", - orientation: "portrait", - icon: "./assets/icon-light.png", - userInterfaceStyle: "automatic", + name: 'expo', + slug: 'expo', + scheme: 'expo', + version: '0.1.0', + orientation: 'portrait', + icon: './assets/icon-light.png', + userInterfaceStyle: 'automatic', updates: { fallbackToCacheTimeout: 0, }, newArchEnabled: true, - assetBundlePatterns: ["**/*"], + assetBundlePatterns: ['**/*'], ios: { - bundleIdentifier: "your.bundle.identifier", + bundleIdentifier: 'your.bundle.identifier', supportsTablet: true, icon: { - light: "./assets/icon-light.png", - dark: "./assets/icon-dark.png", + light: './assets/icon-light.png', + dark: './assets/icon-dark.png', }, }, android: { - package: "your.bundle.identifier", + package: 'your.bundle.identifier', adaptiveIcon: { - foregroundImage: "./assets/icon-light.png", - backgroundColor: "#1F104A", + foregroundImage: './assets/icon-light.png', + backgroundColor: '#1F104A', }, edgeToEdgeEnabled: true, }, @@ -42,17 +42,17 @@ export default ({ config }: ConfigContext): ExpoConfig => ({ reactCompiler: true, }, plugins: [ - "expo-router", - "expo-secure-store", - "expo-web-browser", + 'expo-router', + 'expo-secure-store', + 'expo-web-browser', [ - "expo-splash-screen", + 'expo-splash-screen', { - backgroundColor: "#E4E4E7", - image: "./assets/icon-light.png", + backgroundColor: '#E4E4E7', + image: './assets/icon-light.png', dark: { - backgroundColor: "#18181B", - image: "./assets/icon-dark.png", + backgroundColor: '#18181B', + image: './assets/icon-dark.png', }, }, ], diff --git a/apps/expo/eslint.config.mts b/apps/expo/eslint.config.mts index 84c3e9d..77d2f63 100644 --- a/apps/expo/eslint.config.mts +++ b/apps/expo/eslint.config.mts @@ -1,11 +1,10 @@ -import { defineConfig } from "eslint/config"; - -import { baseConfig } from "@acme/eslint-config/base"; -import { reactConfig } from "@acme/eslint-config/react"; +import { baseConfig } from '@acme/eslint-config/base'; +import { reactConfig } from '@acme/eslint-config/react'; +import { defineConfig } from 'eslint/config'; export default defineConfig( { - ignores: [".expo/**", "expo-plugins/**"], + ignores: ['.expo/**', 'expo-plugins/**'], }, baseConfig, reactConfig, diff --git a/apps/expo/index.ts b/apps/expo/index.ts index 80d3d99..5b83418 100644 --- a/apps/expo/index.ts +++ b/apps/expo/index.ts @@ -1 +1 @@ -import "expo-router/entry"; +import 'expo-router/entry'; diff --git a/apps/expo/metro.config.js b/apps/expo/metro.config.js index bc118a0..799d735 100644 --- a/apps/expo/metro.config.js +++ b/apps/expo/metro.config.js @@ -1,14 +1,14 @@ // Learn more: https://docs.expo.dev/guides/monorepos/ -const path = require("node:path"); -const { getDefaultConfig } = require("expo/metro-config"); -const { FileStore } = require("metro-cache"); -const { withNativewind } = require("nativewind/metro"); +const path = require('node:path'); +const { getDefaultConfig } = require('expo/metro-config'); +const { FileStore } = require('metro-cache'); +const { withNativewind } = require('nativewind/metro'); const config = getDefaultConfig(__dirname); config.cacheStores = [ new FileStore({ - root: path.join(__dirname, "node_modules", ".cache", "metro"), + root: path.join(__dirname, 'node_modules', '.cache', 'metro'), }), ]; diff --git a/apps/expo/postcss.config.js b/apps/expo/postcss.config.js index 2e9905f..0e1e999 100644 --- a/apps/expo/postcss.config.js +++ b/apps/expo/postcss.config.js @@ -1 +1 @@ -module.exports = require("@acme/tailwind-config/postcss-config"); +module.exports = require('@acme/tailwind-config/postcss-config'); diff --git a/apps/expo/src/app/_layout.tsx b/apps/expo/src/app/_layout.tsx index bfab22c..352c3a0 100644 --- a/apps/expo/src/app/_layout.tsx +++ b/apps/expo/src/app/_layout.tsx @@ -1,11 +1,11 @@ -import { useColorScheme } from "react-native"; -import { Stack } from "expo-router"; -import { StatusBar } from "expo-status-bar"; -import { QueryClientProvider } from "@tanstack/react-query"; +import { useColorScheme } from 'react-native'; +import { Stack } from 'expo-router'; +import { StatusBar } from 'expo-status-bar'; +import { QueryClientProvider } from '@tanstack/react-query'; -import { queryClient } from "~/utils/api"; +import { queryClient } from '~/utils/api'; -import "../styles.css"; +import '../styles.css'; // This is the main layout of the app // It wraps your pages with the providers they need @@ -20,10 +20,10 @@ export default function RootLayout() { diff --git a/apps/expo/src/app/index.tsx b/apps/expo/src/app/index.tsx index 72fb1c3..87f5c88 100644 --- a/apps/expo/src/app/index.tsx +++ b/apps/expo/src/app/index.tsx @@ -1,16 +1,16 @@ -import { useState } from "react"; -import { Pressable, Text, TextInput, View } from "react-native"; -import { SafeAreaView } from "react-native-safe-area-context"; -import { Link, Stack } from "expo-router"; -import { LegendList } from "@legendapp/list"; -import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { useState } from 'react'; +import { Pressable, Text, TextInput, View } from 'react-native'; +import { SafeAreaView } from 'react-native-safe-area-context'; +import { Link, Stack } from 'expo-router'; +import { LegendList } from '@legendapp/list'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; -import type { RouterOutputs } from "~/utils/api"; -import { trpc } from "~/utils/api"; -import { authClient } from "~/utils/auth"; +import type { RouterOutputs } from '~/utils/api'; +import { trpc } from '~/utils/api'; +import { authClient } from '~/utils/auth'; function PostCard(props: { - post: RouterOutputs["post"]["all"][number]; + post: RouterOutputs['post']['all'][number]; onDelete: () => void; }) { return ( @@ -19,7 +19,7 @@ function PostCard(props: { @@ -41,14 +41,14 @@ function PostCard(props: { function CreatePost() { const queryClient = useQueryClient(); - const [title, setTitle] = useState(""); - const [content, setContent] = useState(""); + const [title, setTitle] = useState(''); + const [content, setContent] = useState(''); const { mutate, error } = useMutation( trpc.post.create.mutationOptions({ async onSuccess() { - setTitle(""); - setContent(""); + setTitle(''); + setContent(''); await queryClient.invalidateQueries(trpc.post.all.queryFilter()); }, }), @@ -89,7 +89,7 @@ function CreatePost() { > Create - {error?.data?.code === "UNAUTHORIZED" && ( + {error?.data?.code === 'UNAUTHORIZED' && ( You need to be logged in to create a post @@ -104,20 +104,20 @@ function MobileAuth() { return ( <> - {session?.user.name ? `Hello, ${session.user.name}` : "Not logged in"} + {session?.user.name ? `Hello, ${session.user.name}` : 'Not logged in'} session ? authClient.signOut() : authClient.signIn.social({ - provider: "discord", - callbackURL: "/", + provider: 'discord', + callbackURL: '/', }) } className="bg-primary flex items-center rounded-sm p-2" > - {session ? "Sign Out" : "Sign In With Discord"} + {session ? 'Sign Out' : 'Sign In With Discord'} ); @@ -138,7 +138,7 @@ export default function Index() { return ( {/* Changes page title visible on the header */} - + Create T3 Turbo diff --git a/apps/expo/src/app/post/[id].tsx b/apps/expo/src/app/post/[id].tsx index 5470017..27c4119 100644 --- a/apps/expo/src/app/post/[id].tsx +++ b/apps/expo/src/app/post/[id].tsx @@ -1,8 +1,8 @@ -import { SafeAreaView, Text, View } from "react-native"; -import { Stack, useGlobalSearchParams } from "expo-router"; -import { useQuery } from "@tanstack/react-query"; +import { SafeAreaView, Text, View } from 'react-native'; +import { Stack, useGlobalSearchParams } from 'expo-router'; +import { useQuery } from '@tanstack/react-query'; -import { trpc } from "~/utils/api"; +import { trpc } from '~/utils/api'; export default function Post() { const { id } = useGlobalSearchParams<{ id: string }>(); diff --git a/apps/expo/src/styles.css b/apps/expo/src/styles.css index 759cbd9..6672a6b 100644 --- a/apps/expo/src/styles.css +++ b/apps/expo/src/styles.css @@ -1,3 +1,3 @@ -@import "tailwindcss"; -@import "nativewind/theme"; -@import "@acme/tailwind-config/theme"; +@import 'tailwindcss'; +@import 'nativewind/theme'; +@import '@acme/tailwind-config/theme'; diff --git a/apps/expo/src/utils/api.tsx b/apps/expo/src/utils/api.tsx index 308e2ee..c859951 100644 --- a/apps/expo/src/utils/api.tsx +++ b/apps/expo/src/utils/api.tsx @@ -1,12 +1,11 @@ -import { QueryClient } from "@tanstack/react-query"; -import { createTRPCClient, httpBatchLink, loggerLink } from "@trpc/client"; -import { createTRPCOptionsProxy } from "@trpc/tanstack-react-query"; -import superjson from "superjson"; +import type { AppRouter } from '@acme/api'; +import { QueryClient } from '@tanstack/react-query'; +import { createTRPCClient, httpBatchLink, loggerLink } from '@trpc/client'; +import { createTRPCOptionsProxy } from '@trpc/tanstack-react-query'; +import superjson from 'superjson'; -import type { AppRouter } from "@acme/api"; - -import { authClient } from "./auth"; -import { getBaseUrl } from "./base-url"; +import { authClient } from './auth'; +import { getBaseUrl } from './base-url'; export const queryClient = new QueryClient({ defaultOptions: { @@ -24,20 +23,20 @@ export const trpc = createTRPCOptionsProxy({ links: [ loggerLink({ enabled: (opts) => - process.env.NODE_ENV === "development" || - (opts.direction === "down" && opts.result instanceof Error), - colorMode: "ansi", + process.env.NODE_ENV === 'development' || + (opts.direction === 'down' && opts.result instanceof Error), + colorMode: 'ansi', }), httpBatchLink({ transformer: superjson, url: `${getBaseUrl()}/api/trpc`, headers() { const headers = new Map(); - headers.set("x-trpc-source", "expo-react"); + headers.set('x-trpc-source', 'expo-react'); const cookies = authClient.getCookie(); if (cookies) { - headers.set("Cookie", cookies); + headers.set('Cookie', cookies); } return headers; }, @@ -47,4 +46,4 @@ export const trpc = createTRPCOptionsProxy({ queryClient, }); -export type { RouterInputs, RouterOutputs } from "@acme/api"; +export type { RouterInputs, RouterOutputs } from '@acme/api'; diff --git a/apps/expo/src/utils/auth.ts b/apps/expo/src/utils/auth.ts index 7c64135..a216b59 100644 --- a/apps/expo/src/utils/auth.ts +++ b/apps/expo/src/utils/auth.ts @@ -1,15 +1,15 @@ -import * as SecureStore from "expo-secure-store"; -import { expoClient } from "@better-auth/expo/client"; -import { createAuthClient } from "better-auth/react"; +import * as SecureStore from 'expo-secure-store'; +import { expoClient } from '@better-auth/expo/client'; +import { createAuthClient } from 'better-auth/react'; -import { getBaseUrl } from "./base-url"; +import { getBaseUrl } from './base-url'; export const authClient = createAuthClient({ baseURL: getBaseUrl(), plugins: [ expoClient({ - scheme: "expo", - storagePrefix: "expo", + scheme: 'expo', + storagePrefix: 'expo', storage: SecureStore, }), ], diff --git a/apps/expo/src/utils/base-url.ts b/apps/expo/src/utils/base-url.ts index 94dfbbc..58d0697 100644 --- a/apps/expo/src/utils/base-url.ts +++ b/apps/expo/src/utils/base-url.ts @@ -1,4 +1,4 @@ -import Constants from "expo-constants"; +import Constants from 'expo-constants'; /** * Extend this function when going to production by @@ -14,12 +14,12 @@ export const getBaseUrl = () => { * baseUrl to your production API URL. */ const debuggerHost = Constants.expoConfig?.hostUri; - const localhost = debuggerHost?.split(":")[0]; + const localhost = debuggerHost?.split(':')[0]; if (!localhost) { // return "https://turbo.t3.gg"; throw new Error( - "Failed to get localhost. Please point to your production server.", + 'Failed to get localhost. Please point to your production server.', ); } return `http://${localhost}:3000`; diff --git a/apps/expo/src/utils/session-store.ts b/apps/expo/src/utils/session-store.ts index 8f55284..7d4964f 100644 --- a/apps/expo/src/utils/session-store.ts +++ b/apps/expo/src/utils/session-store.ts @@ -1,6 +1,6 @@ -import * as SecureStore from "expo-secure-store"; +import * as SecureStore from 'expo-secure-store'; -const key = "session_token"; +const key = 'session_token'; export const getToken = () => SecureStore.getItem(key); export const deleteToken = () => SecureStore.deleteItemAsync(key); diff --git a/apps/next/.cache/.prettiercache b/apps/next/.cache/.prettiercache new file mode 100644 index 0000000..3ca83c9 --- /dev/null +++ b/apps/next/.cache/.prettiercache @@ -0,0 +1 @@ +[["1","2","3","4","5","6","7","8","9","10","11","12","13","14","15","16","17","18","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"},{"key":"46","value":"47"},{"key":"48","value":"49"},{"key":"50","value":"51"},{"key":"52","value":"53"},{"key":"54","value":"55"},{"key":"56","value":"57"},"/home/gib/Documents/Code/studybuddy/apps/next/eslint.config.ts",{"size":369,"mtime":1768143608395,"data":"58"},"/home/gib/Documents/Code/studybuddy/apps/next/public/t3-icon.svg",{"size":923,"mtime":1767666484838},"/home/gib/Documents/Code/studybuddy/apps/next/src/sentry.server.config.ts",{"size":188,"mtime":1768143609244,"data":"59"},"/home/gib/Documents/Code/studybuddy/apps/next/src/env.js",{"size":1676,"mtime":1768143609028,"data":"60"},"/home/gib/Documents/Code/studybuddy/apps/next/src/app/global-error.tsx",{"size":1920,"mtime":1768143608737,"data":"61"},"/home/gib/Documents/Code/studybuddy/apps/next/src/instrumentation-client.ts",{"size":986,"mtime":1768143609110,"data":"62"},"/home/gib/Documents/Code/studybuddy/apps/next/public/favicon.ico",{"size":103027,"mtime":1767666484838},"/home/gib/Documents/Code/studybuddy/apps/next/next.config.js",{"size":2099,"mtime":1768143608490,"data":"63"},"/home/gib/Documents/Code/studybuddy/apps/next/postcss.config.js",{"size":63,"mtime":1767666484838,"data":"64"},"/home/gib/Documents/Code/studybuddy/apps/next/src/components/providers/index.tsx",{"size":226,"mtime":1768078215060,"data":"65"},"/home/gib/Documents/Code/studybuddy/apps/next/src/instrumentation.ts",{"size":283,"mtime":1768143609138,"data":"66"},"/home/gib/Documents/Code/studybuddy/apps/next/src/app/page.tsx",{"size":396,"mtime":1768143608801,"data":"67"},"/home/gib/Documents/Code/studybuddy/apps/next/src/app/layout.tsx",{"size":1713,"mtime":1768143608782,"data":"68"},"/home/gib/Documents/Code/studybuddy/apps/next/src/app/styles.css",{"size":595,"mtime":1768143608846,"data":"69"},"/home/gib/Documents/Code/studybuddy/apps/next/src/components/providers/ConvexClientProvider.tsx",{"size":446,"mtime":1768143608874,"data":"70"},"/home/gib/Documents/Code/studybuddy/apps/next/src/lib/metadata.ts",{"size":9226,"mtime":1768077898562,"data":"71"},"/home/gib/Documents/Code/studybuddy/apps/next/package.json",{"size":1505,"mtime":1768079670617,"data":"72"},"/home/gib/Documents/Code/studybuddy/apps/next/src/components/providers/ThemeProvider.tsx",{"size":1794,"mtime":1768143608966,"data":"73"},"/home/gib/Documents/Code/studybuddy/apps/next/tsconfig.json",{"size":297,"mtime":1767666484839,"data":"74"},{"hashOfOptions":"75"},{"hashOfOptions":"76"},{"hashOfOptions":"77"},{"hashOfOptions":"78"},{"hashOfOptions":"79"},{"hashOfOptions":"80"},{"hashOfOptions":"81"},{"hashOfOptions":"82"},{"hashOfOptions":"83"},{"hashOfOptions":"84"},{"hashOfOptions":"85"},{"hashOfOptions":"86"},{"hashOfOptions":"87"},{"hashOfOptions":"88"},{"hashOfOptions":"89"},{"hashOfOptions":"90"},{"hashOfOptions":"91"},"2910562861","275615143","999282740","2825906753","2416185851","1794979929","3137312877","1333455489","4076447305","4134612210","1315814573","1413471855","2292959436","2762996876","1252708399","2576318537","2945064632"] \ No newline at end of file diff --git a/apps/next/eslint.config.ts b/apps/next/eslint.config.ts index e0e2cbe..533512f 100644 --- a/apps/next/eslint.config.ts +++ b/apps/next/eslint.config.ts @@ -1,16 +1,15 @@ -import { defineConfig } from "eslint/config"; +import { defineConfig } from 'eslint/config'; -import { baseConfig, restrictEnvAccess } from "@gib/eslint-config/base"; -import { nextjsConfig } from "@gib/eslint-config/nextjs"; -import { reactConfig } from "@gib/eslint-config/react"; +import { baseConfig, restrictEnvAccess } from '@gib/eslint-config/base'; +import { nextjsConfig } from '@gib/eslint-config/nextjs'; +import { reactConfig } from '@gib/eslint-config/react'; export default defineConfig( { - ignores: [".next/**"], + ignores: ['.next/**'], }, baseConfig, reactConfig, nextjsConfig, restrictEnvAccess, ); - diff --git a/apps/next/next.config.js b/apps/next/next.config.js index 624af3a..036cc76 100644 --- a/apps/next/next.config.js +++ b/apps/next/next.config.js @@ -1,12 +1,13 @@ -import { env } from './src/env.ts'; import { withSentryConfig } from '@sentry/nextjs'; +import { createJiti } from 'jiti'; import { withPlausibleProxy } from 'next-plausible'; -import { createJiti } from "jiti"; + +import { env } from './src/env.js'; const jiti = createJiti(import.meta.url); // Import env files to validate at build time. Use jiti so we can load .ts files in here. -await jiti.import("./src/env"); +await jiti.import('./src/env'); /** @type {import("next").NextConfig} */ const config = withPlausibleProxy({ @@ -28,15 +29,10 @@ const config = withPlausibleProxy({ }, }, /** Enables hot reloading for local packages without a build step */ - transpilePackages: [ - "@gib/backend", - "@gib/ui", - ], + transpilePackages: ['@gib/backend', '@gib/ui'], typescript: { ignoreBuildErrors: true }, }); - - const sentryConfig = { // For all available options, see: // https://www.npmjs.com/package/@sentry/webpack-plugin#options diff --git a/apps/next/package.json b/apps/next/package.json index 96b1594..00dfbbe 100644 --- a/apps/next/package.json +++ b/apps/next/package.json @@ -1,5 +1,5 @@ { - "name": "@gib/nextjs", + "name": "@gib/next", "version": "0.1.0", "type": "module", "private": true, diff --git a/apps/next/src/app/_components/auth-showcase.tsx b/apps/next/src/app/_components/auth-showcase.tsx deleted file mode 100644 index 685a3f2..0000000 --- a/apps/next/src/app/_components/auth-showcase.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { headers } from "next/headers"; -import { redirect } from "next/navigation"; - -import { Button } from "@acme/ui/button"; - -import { auth, getSession } from "~/auth/server"; - -export async function AuthShowcase() { - const session = await getSession(); - - if (!session) { - return ( -
- -
- ); - } - - return ( -
-

- Logged in as {session.user.name} -

- -
- -
-
- ); -} diff --git a/apps/next/src/app/_components/posts.tsx b/apps/next/src/app/_components/posts.tsx deleted file mode 100644 index 5a4bbeb..0000000 --- a/apps/next/src/app/_components/posts.tsx +++ /dev/null @@ -1,210 +0,0 @@ -"use client"; - -import { useForm } from "@tanstack/react-form"; -import { - useMutation, - useQueryClient, - useSuspenseQuery, -} from "@tanstack/react-query"; - -import type { RouterOutputs } from "@acme/api"; -import { CreatePostSchema } from "@acme/db/schema"; -import { cn } from "@acme/ui"; -import { Button } from "@acme/ui/button"; -import { - Field, - FieldContent, - FieldError, - FieldGroup, - FieldLabel, -} from "@acme/ui/field"; -import { Input } from "@acme/ui/input"; -import { toast } from "@acme/ui/toast"; - -import { useTRPC } from "~/trpc/react"; - -export function CreatePostForm() { - const trpc = useTRPC(); - - const queryClient = useQueryClient(); - const createPost = useMutation( - trpc.post.create.mutationOptions({ - onSuccess: async () => { - form.reset(); - await queryClient.invalidateQueries(trpc.post.pathFilter()); - }, - onError: (err) => { - toast.error( - err.data?.code === "UNAUTHORIZED" - ? "You must be logged in to post" - : "Failed to create post", - ); - }, - }), - ); - - const form = useForm({ - defaultValues: { - content: "", - title: "", - }, - validators: { - onSubmit: CreatePostSchema, - }, - onSubmit: (data) => createPost.mutate(data.value), - }); - - return ( -
{ - event.preventDefault(); - void form.handleSubmit(); - }} - > - - { - const isInvalid = - field.state.meta.isTouched && !field.state.meta.isValid; - return ( - - - Bug Title - - field.handleChange(e.target.value)} - aria-invalid={isInvalid} - placeholder="Title" - /> - {isInvalid && } - - ); - }} - /> - { - const isInvalid = - field.state.meta.isTouched && !field.state.meta.isValid; - return ( - - - Content - - field.handleChange(e.target.value)} - aria-invalid={isInvalid} - placeholder="Content" - /> - {isInvalid && } - - ); - }} - /> - - -
- ); -} - -export function PostList() { - const trpc = useTRPC(); - const { data: posts } = useSuspenseQuery(trpc.post.all.queryOptions()); - - if (posts.length === 0) { - return ( -
- - - - -
-

No posts yet

-
-
- ); - } - - return ( -
- {posts.map((p) => { - return ; - })} -
- ); -} - -export function PostCard(props: { - post: RouterOutputs["post"]["all"][number]; -}) { - const trpc = useTRPC(); - const queryClient = useQueryClient(); - const deletePost = useMutation( - trpc.post.delete.mutationOptions({ - onSuccess: async () => { - await queryClient.invalidateQueries(trpc.post.pathFilter()); - }, - onError: (err) => { - toast.error( - err.data?.code === "UNAUTHORIZED" - ? "You must be logged in to delete a post" - : "Failed to delete post", - ); - }, - }), - ); - - return ( -
-
-

{props.post.title}

-

{props.post.content}

-
-
- -
-
- ); -} - -export function PostCardSkeleton(props: { pulse?: boolean }) { - const { pulse = true } = props; - return ( -
-
-

-   -

-

-   -

-
-
- ); -} diff --git a/apps/next/src/app/api/auth/[...all]/route.ts b/apps/next/src/app/api/auth/[...all]/route.ts deleted file mode 100644 index 71c7ff7..0000000 --- a/apps/next/src/app/api/auth/[...all]/route.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { auth } from "~/auth/server"; - -export const GET = auth.handler; -export const POST = auth.handler; diff --git a/apps/next/src/app/api/trpc/[trpc]/route.ts b/apps/next/src/app/api/trpc/[trpc]/route.ts deleted file mode 100644 index c5548f9..0000000 --- a/apps/next/src/app/api/trpc/[trpc]/route.ts +++ /dev/null @@ -1,46 +0,0 @@ -import type { NextRequest } from "next/server"; -import { fetchRequestHandler } from "@trpc/server/adapters/fetch"; - -import { appRouter, createTRPCContext } from "@acme/api"; - -import { auth } from "~/auth/server"; - -/** - * Configure basic CORS headers - * You should extend this to match your needs - */ -const setCorsHeaders = (res: Response) => { - res.headers.set("Access-Control-Allow-Origin", "*"); - res.headers.set("Access-Control-Request-Method", "*"); - res.headers.set("Access-Control-Allow-Methods", "OPTIONS, GET, POST"); - res.headers.set("Access-Control-Allow-Headers", "*"); -}; - -export const OPTIONS = () => { - const response = new Response(null, { - status: 204, - }); - setCorsHeaders(response); - return response; -}; - -const handler = async (req: NextRequest) => { - const response = await fetchRequestHandler({ - endpoint: "/api/trpc", - router: appRouter, - req, - createContext: () => - createTRPCContext({ - auth: auth, - headers: req.headers, - }), - onError({ error, path }) { - console.error(`>>> tRPC Error on '${path}'`, error); - }, - }); - - setCorsHeaders(response); - return response; -}; - -export { handler as GET, handler as POST }; diff --git a/apps/next/src/app/global-error.tsx b/apps/next/src/app/global-error.tsx new file mode 100644 index 0000000..4d66a09 --- /dev/null +++ b/apps/next/src/app/global-error.tsx @@ -0,0 +1,65 @@ +'use client'; + +import type { Metadata } from 'next'; +import NextError from 'next/error'; +import { Geist, Geist_Mono } from 'next/font/google'; +import '@/app/styles.css'; +import { useEffect } from 'react'; +import { ConvexClientProvider, ThemeProvider } from '@/components/providers'; +import { generateMetadata } from '@/lib/metadata'; +import * as Sentry from '@sentry/nextjs'; +import PlausibleProvider from 'next-plausible'; +import { Button, Toaster } from '@gib/ui'; + +const geistSans = Geist({ + variable: '--font-geist-sans', + subsets: ['latin'], +}); +const geistMono = Geist_Mono({ + variable: '--font-geist-mono', + subsets: ['latin'], +}); +const metadata: Metadata = generateMetadata(); +metadata.title = `Error | Study Buddy`; +export { metadata }; + +interface GlobalErrorProps { + error: Error & { digest?: string }; + reset?: () => void; +}; + +const GlobalError = ({ error, reset = undefined }: GlobalErrorProps) => { + useEffect(() => { + Sentry.captureException(error); + }, [error]); + return ( + + + + + +
+ + {reset !== undefined && ( + + )} + +
+
+
+ + +
+ ); +}; +export default GlobalError; diff --git a/apps/next/src/app/layout.tsx b/apps/next/src/app/layout.tsx index 8985f5d..2e69943 100644 --- a/apps/next/src/app/layout.tsx +++ b/apps/next/src/app/layout.tsx @@ -1,70 +1,62 @@ -import type { Metadata, Viewport } from "next"; -import { Geist, Geist_Mono } from "next/font/google"; +import type { Metadata, Viewport } from 'next'; +import { Geist, Geist_Mono } from 'next/font/google'; +import '@/app/styles.css'; +import { ConvexAuthNextjsServerProvider } from '@convex-dev/auth/nextjs/server'; +import { ConvexClientProvider } from '@/components/providers'; +import { generateMetadata } from '@/lib/metadata'; +import PlausibleProvider from 'next-plausible'; +import { ThemeProvider, Toaster } from '@gib/ui'; +import { env } from '@/env'; -import { cn } from "@acme/ui"; -import { ThemeProvider, ThemeToggle } from "@acme/ui/theme"; -import { Toaster } from "@acme/ui/toast"; -import { env } from "~/env"; -import { TRPCReactProvider } from "~/trpc/react"; - -import "~/app/styles.css"; - -export const metadata: Metadata = { - metadataBase: new URL( - env.VERCEL_ENV === "production" - ? "https://turbo.t3.gg" - : "http://localhost:3000", - ), - title: "Create T3 Turbo", - description: "Simple monorepo with shared backend for web & mobile apps", - openGraph: { - title: "Create T3 Turbo", - description: "Simple monorepo with shared backend for web & mobile apps", - url: "https://create-t3-turbo.vercel.app", - siteName: "Create T3 Turbo", - }, - twitter: { - card: "summary_large_image", - site: "@jullerino", - creator: "@jullerino", - }, -}; +export const metadata: Metadata = generateMetadata(); export const viewport: Viewport = { themeColor: [ - { media: "(prefers-color-scheme: light)", color: "white" }, - { media: "(prefers-color-scheme: dark)", color: "black" }, + { media: '(prefers-color-scheme: light)', color: 'white' }, + { media: '(prefers-color-scheme: dark)', color: 'black' }, ], }; const geistSans = Geist({ - subsets: ["latin"], - variable: "--font-geist-sans", + subsets: ['latin'], + variable: '--font-geist-sans', }); const geistMono = Geist_Mono({ - subsets: ["latin"], - variable: "--font-geist-mono", + subsets: ['latin'], + variable: '--font-geist-mono', }); -export default function RootLayout(props: { children: React.ReactNode }) { +const RootLayout = ({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) => { return ( - - + - - {props.children} -
- -
- -
- - + + + + + {children} + + + + + +
+ ); -} +}; +export default RootLayout; diff --git a/apps/next/src/app/page.tsx b/apps/next/src/app/page.tsx index 28a5d24..7cad448 100644 --- a/apps/next/src/app/page.tsx +++ b/apps/next/src/app/page.tsx @@ -1,41 +1,9 @@ -import { Suspense } from "react"; - -import { HydrateClient, prefetch, trpc } from "~/trpc/server"; -import { AuthShowcase } from "./_components/auth-showcase"; -import { - CreatePostForm, - PostCardSkeleton, - PostList, -} from "./_components/posts"; - -export default function HomePage() { - prefetch(trpc.post.all.queryOptions()); +const Home = () => { return ( - -
-
-

- Create T3 Turbo -

- - - -
- - - - -
- } - > - - -
- -
-
+
+

Welcome to the Home Page

+
); -} +}; +export default Home; diff --git a/apps/next/src/app/styles.css b/apps/next/src/app/styles.css index 25d002f..552d3f4 100644 --- a/apps/next/src/app/styles.css +++ b/apps/next/src/app/styles.css @@ -1,8 +1,8 @@ -@import "tailwindcss"; -@import "tw-animate-css"; -@import "@acme/tailwind-config/theme"; +@import 'tailwindcss'; +@import 'tw-animate-css'; +@import '@gib/tailwind-config/theme'; -@source "../../../../packages/ui/src/*.{ts,tsx}"; +@source '../../../../packages/ui/src/*.{ts,tsx}'; @custom-variant dark (&:where(.dark, .dark *)); @custom-variant light (&:where(.light, .light *)); diff --git a/apps/next/src/components/providers/ConvexClientProvider.tsx b/apps/next/src/components/providers/ConvexClientProvider.tsx new file mode 100644 index 0000000..121cb3e --- /dev/null +++ b/apps/next/src/components/providers/ConvexClientProvider.tsx @@ -0,0 +1,15 @@ +'use client'; + +import { type ReactNode } from 'react'; +import { ConvexAuthNextjsProvider } from '@convex-dev/auth/nextjs'; +import { ConvexReactClient } from 'convex/react'; + +const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!); + +export const ConvexClientProvider = ({ children }: { children: ReactNode }) => { + return ( + + {children} + + ); +}; diff --git a/apps/next/src/components/providers/ThemeProvider.tsx b/apps/next/src/components/providers/ThemeProvider.tsx new file mode 100644 index 0000000..e02825d --- /dev/null +++ b/apps/next/src/components/providers/ThemeProvider.tsx @@ -0,0 +1,70 @@ +'use client'; + +import type { ComponentProps } from 'react'; +import { useEffect, useState } from 'react'; +import { Moon, Sun } from 'lucide-react'; +import { ThemeProvider as NextThemesProvider, useTheme } from 'next-themes'; + +import { Button, cn } from '@gib/ui'; + +const ThemeProvider = ({ + children, + ...props +}: ComponentProps) => { + const [mounted, setMounted] = useState(false); + + useEffect(() => { + setMounted(true); + }, []); + + if (!mounted) return null; + return {children}; +}; + +type ThemeToggleProps = { + size?: number; + buttonProps?: Omit, 'onClick'>; +}; + +const ThemeToggle = ({ size = 1, buttonProps }: ThemeToggleProps) => { + const { setTheme, resolvedTheme } = useTheme(); + const [mounted, setMounted] = useState(false); + + useEffect(() => { + setMounted(true); + }, []); + + if (!mounted) { + return ( + + ); + } + + const toggleTheme = () => { + if (resolvedTheme === 'dark') setTheme('light'); + else setTheme('dark'); + }; + + return ( + + ); +}; + +export { ThemeProvider, ThemeToggle, type ThemeToggleProps }; diff --git a/apps/next/src/components/providers/index.tsx b/apps/next/src/components/providers/index.tsx new file mode 100644 index 0000000..e2e1e9b --- /dev/null +++ b/apps/next/src/components/providers/index.tsx @@ -0,0 +1,7 @@ +export { ConvexClientProvider } from './ConvexClientProvider'; +//export { NotificationsPermission } from './notification-permission'; +export { + ThemeProvider, + ThemeToggle, + type ThemeToggleProps, +} from './ThemeProvider'; diff --git a/apps/next/src/env.ts b/apps/next/src/env.js similarity index 77% rename from apps/next/src/env.ts rename to apps/next/src/env.js index 67dfede..84dd5ab 100644 --- a/apps/next/src/env.ts +++ b/apps/next/src/env.js @@ -1,13 +1,13 @@ -import { createEnv } from "@t3-oss/env-nextjs"; -import { z } from "zod/v4"; +import { createEnv } from '@t3-oss/env-nextjs'; +import { z } from 'zod/v4'; export const env = createEnv({ shared: { NODE_ENV: z - .enum(["development", "production", "test"]) - .default("development"), + .enum(['development', 'production', 'test']) + .default('development'), CI: z.boolean().default(false), - SITE_URL: z.string().default("http://localhost:3000"), + SITE_URL: z.string().default('http://localhost:3000'), }, /** * Specify your server-side environment variables schema here. @@ -41,8 +41,9 @@ export const env = createEnv({ NEXT_PUBLIC_SENTRY_DSN: process.env.NEXT_PUBLIC_SENTRY_DSN, NEXT_PUBLIC_SENTRY_URL: process.env.NEXT_PUBLIC_SENTRY_URL, NEXT_PUBLIC_SENTRY_ORG: process.env.NEXT_PUBLIC_SENTRY_ORG, - NEXT_PUBLIC_SENTRY_PROJECT_NAME: process.env.NEXT_PUBLIC_SENTRY_PROJECT_NAME, + NEXT_PUBLIC_SENTRY_PROJECT_NAME: + process.env.NEXT_PUBLIC_SENTRY_PROJECT_NAME, }, skipValidation: - !!process.env.CI || process.env.npm_lifecycle_event === "lint", + !!process.env.CI || process.env.npm_lifecycle_event === 'lint', }); diff --git a/apps/next/src/instrumentation-client.ts b/apps/next/src/instrumentation-client.ts new file mode 100644 index 0000000..4abcdfd --- /dev/null +++ b/apps/next/src/instrumentation-client.ts @@ -0,0 +1,28 @@ +// https://docs.sentry.io/platforms/javascript/guides/nextjs/ +import * as Sentry from '@sentry/nextjs'; + +import { env } from './env.js'; + +Sentry.init({ + dsn: env.NEXT_PUBLIC_SENTRY_DSN, + integrations: [ + Sentry.replayIntegration({ + maskAllText: false, + blockAllMedia: false, + }), + Sentry.feedbackIntegration({ + colorScheme: 'system', + }), + ], + // https://docs.sentry.io/platforms/javascript/guides/nextjs/configuration/options/#sendDefaultPii + sendDefaultPii: true, + // https://docs.sentry.io/platforms/javascript/configuration/options/#traces-sample-rate + tracesSampleRate: 1, + enableLogs: true, + // https://docs.sentry.io/platforms/javascript/session-replay/configuration/#general-integration-configuration + replaysSessionSampleRate: 0.5, + replaysOnErrorSampleRate: 1.0, + debug: false, +}); +// `captureRouterTransitionStart` is available from SDK version 9.12.0 onwards +export const onRouterTransitionStart = Sentry.captureRouterTransitionStart; diff --git a/apps/next/src/instrumentation.ts b/apps/next/src/instrumentation.ts new file mode 100644 index 0000000..ff8b9ef --- /dev/null +++ b/apps/next/src/instrumentation.ts @@ -0,0 +1,7 @@ +import type { Instrumentation } from 'next'; +import * as Sentry from '@sentry/nextjs'; + +export const register = async () => await import('./sentry.server.config'); +export const onRequestError: Instrumentation.onRequestError = (...args) => { + Sentry.captureRequestError(...args); +}; diff --git a/apps/next/src/lib/metadata.ts b/apps/next/src/lib/metadata.ts new file mode 100644 index 0000000..ea5998b --- /dev/null +++ b/apps/next/src/lib/metadata.ts @@ -0,0 +1,369 @@ +import type { Metadata } from 'next'; +import * as Sentry from '@sentry/nextjs'; + +export const generateMetadata = (): Metadata => { + return { + title: { + template: '%s | Study Buddy', + default: 'Study Buddy', + }, + description: + 'App used by COG IT employees to \ + update their status throughout the day.', + applicationName: 'Study Buddy', + keywords: + 'Study Buddy,T3 Template, Nextjs, ' + + 'Tailwind, TypeScript, React, T3, Gib', + authors: [{ name: 'Gib', url: 'https://gbrown.org' }], + creator: 'Gib Brown', + publisher: 'Gib Brown', + formatDetection: { + email: false, + address: false, + telephone: false, + }, + robots: { + index: true, + follow: true, + nocache: false, + googleBot: { + index: true, + follow: true, + noimageindex: false, + 'max-video-preview': -1, + 'max-image-preview': 'large', + 'max-snippet': -1, + }, + }, + icons: { + icon: [ + { url: '/favicon.ico', type: 'image/x-icon', sizes: 'any' }, + { + url: '/favicon-16.png', + type: 'image/png', + sizes: '16x16', + }, + { + url: '/favicon-32.png', + type: 'image/png', + sizes: '32x32', + }, + { url: '/favicon.png', type: 'image/png', sizes: '96x96' }, + { + url: '/favicon.ico', + type: 'image/x-icon', + sizes: 'any', + media: '(prefers-color-scheme: dark)', + }, + { + url: '/favicon-16.png', + type: 'image/png', + sizes: '16x16', + media: '(prefers-color-scheme: dark)', + }, + { + url: '/favicon-32.png', + type: 'image/png', + sizes: '32x32', + media: '(prefers-color-scheme: dark)', + }, + { + url: '/favicon.png', + type: 'image/png', + sizes: '96x96', + media: '(prefers-color-scheme: dark)', + }, + { + url: '/appicon/icon-36.png', + type: 'image/png', + sizes: '36x36', + }, + { + url: '/appicon/icon-48.png', + type: 'image/png', + sizes: '48x48', + }, + { + url: '/appicon/icon-72.png', + type: 'image/png', + sizes: '72x72', + }, + { + url: '/appicon/icon-96.png', + type: 'image/png', + sizes: '96x96', + }, + { + url: '/appicon/icon-144.png', + type: 'image/png', + sizes: '144x144', + }, + { + url: '/appicon/icon.png', + type: 'image/png', + sizes: '192x192', + }, + { + url: '/appicon/icon-36.png', + type: 'image/png', + sizes: '36x36', + media: '(prefers-color-scheme: dark)', + }, + { + url: '/appicon/icon-48.png', + type: 'image/png', + sizes: '48x48', + media: '(prefers-color-scheme: dark)', + }, + { + url: '/appicon/icon-72.png', + type: 'image/png', + sizes: '72x72', + media: '(prefers-color-scheme: dark)', + }, + { + url: '/appicon/icon-96.png', + type: 'image/png', + sizes: '96x96', + media: '(prefers-color-scheme: dark)', + }, + { + url: '/appicon/icon-144.png', + type: 'image/png', + sizes: '144x144', + media: '(prefers-color-scheme: dark)', + }, + { + url: '/appicon/icon.png', + type: 'image/png', + sizes: '192x192', + media: '(prefers-color-scheme: dark)', + }, + ], + shortcut: [ + { + url: '/appicon/icon-36.png', + type: 'image/png', + sizes: '36x36', + }, + { + url: '/appicon/icon-48.png', + type: 'image/png', + sizes: '48x48', + }, + { + url: '/appicon/icon-72.png', + type: 'image/png', + sizes: '72x72', + }, + { + url: '/appicon/icon-96.png', + type: 'image/png', + sizes: '96x96', + }, + { + url: '/appicon/icon-144.png', + type: 'image/png', + sizes: '144x144', + }, + { + url: '/appicon/icon.png', + type: 'image/png', + sizes: '192x192', + }, + { + url: '/appicon/icon-36.png', + type: 'image/png', + sizes: '36x36', + media: '(prefers-color-scheme: dark)', + }, + { + url: '/appicon/icon-48.png', + type: 'image/png', + sizes: '48x48', + media: '(prefers-color-scheme: dark)', + }, + { + url: '/appicon/icon-72.png', + type: 'image/png', + sizes: '72x72', + media: '(prefers-color-scheme: dark)', + }, + { + url: '/appicon/icon-96.png', + type: 'image/png', + sizes: '96x96', + media: '(prefers-color-scheme: dark)', + }, + { + url: '/appicon/icon-144.png', + type: 'image/png', + sizes: '144x144', + media: '(prefers-color-scheme: dark)', + }, + { + url: '/appicon/icon.png', + type: 'image/png', + sizes: '192x192', + media: '(prefers-color-scheme: dark)', + }, + ], + apple: [ + { + url: 'appicon/icon-57.png', + type: 'image/png', + sizes: '57x57', + }, + { + url: 'appicon/icon-60.png', + type: 'image/png', + sizes: '60x60', + }, + { + url: 'appicon/icon-72.png', + type: 'image/png', + sizes: '72x72', + }, + { + url: 'appicon/icon-76.png', + type: 'image/png', + sizes: '76x76', + }, + { + url: 'appicon/icon-114.png', + type: 'image/png', + sizes: '114x114', + }, + { + url: 'appicon/icon-120.png', + type: 'image/png', + sizes: '120x120', + }, + { + url: 'appicon/icon-144.png', + type: 'image/png', + sizes: '144x144', + }, + { + url: 'appicon/icon-152.png', + type: 'image/png', + sizes: '152x152', + }, + { + url: 'appicon/icon-180.png', + type: 'image/png', + sizes: '180x180', + }, + { + url: 'appicon/icon.png', + type: 'image/png', + sizes: '192x192', + }, + { + url: 'appicon/icon-57.png', + type: 'image/png', + sizes: '57x57', + media: '(prefers-color-scheme: dark)', + }, + { + url: 'appicon/icon-60.png', + type: 'image/png', + sizes: '60x60', + media: '(prefers-color-scheme: dark)', + }, + { + url: 'appicon/icon-72.png', + type: 'image/png', + sizes: '72x72', + media: '(prefers-color-scheme: dark)', + }, + { + url: 'appicon/icon-76.png', + type: 'image/png', + sizes: '76x76', + media: '(prefers-color-scheme: dark)', + }, + { + url: 'appicon/icon-114.png', + type: 'image/png', + sizes: '114x114', + media: '(prefers-color-scheme: dark)', + }, + { + url: 'appicon/icon-120.png', + type: 'image/png', + sizes: '120x120', + media: '(prefers-color-scheme: dark)', + }, + { + url: 'appicon/icon-144.png', + type: 'image/png', + sizes: '144x144', + media: '(prefers-color-scheme: dark)', + }, + { + url: 'appicon/icon-152.png', + type: 'image/png', + sizes: '152x152', + media: '(prefers-color-scheme: dark)', + }, + { + url: 'appicon/icon-180.png', + type: 'image/png', + sizes: '180x180', + media: '(prefers-color-scheme: dark)', + }, + { + 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', + }, + ], + }, + other: { + ...Sentry.getTraceData(), + }, + appleWebApp: { + title: 'Study Buddy', + 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', + yandex: 'yandex', + yahoo: 'yahoo', + }, + category: 'technology', + /* + appLinks: { + ios: { + url: 'https://techtracker.gbrown.org/ios', + app_store_id: 'com.gbrown.techtracker', + }, + android: { + package: 'https://techtracker.gbrown.org/android', + app_name: 'app_t3_template', + }, + web: { + url: 'https://techtracker.gbrown.org', + should_fallback: true, + }, + }, + */ + }; +}; diff --git a/apps/next/src/sentry.server.config.ts b/apps/next/src/sentry.server.config.ts new file mode 100644 index 0000000..12dc187 --- /dev/null +++ b/apps/next/src/sentry.server.config.ts @@ -0,0 +1,10 @@ +import * as Sentry from '@sentry/nextjs'; + +import { env } from './env.js'; + +Sentry.init({ + dsn: env.NEXT_PUBLIC_SENTRY_DSN, + tracesSampleRate: 1, + enableLogs: true, + debug: false, +}); diff --git a/package.json b/package.json index 411307d..b78cf75 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "convex-turbo", + "name": "studybuddy-turbo", "private": true, "engines": { "node": "^22.20.0" diff --git a/packages/backend/.cache/.prettiercache b/packages/backend/.cache/.prettiercache index 2d424bf..ad9fd2d 100644 --- a/packages/backend/.cache/.prettiercache +++ b/packages/backend/.cache/.prettiercache @@ -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/monorepo/convex-monorepo/packages/backend/.gitignore",{"size":16,"mtime":1753933693000,"hash":"46"},"/home/gib/Documents/Code/monorepo/convex-monorepo/packages/backend/convex/_generated/server.d.ts",{"size":5540,"mtime":1761665487097,"hash":"47","data":"48"},"/home/gib/Documents/Code/monorepo/convex-monorepo/packages/backend/convex/auth.config.js",{"size":128,"mtime":1753933693000,"hash":"49","data":"50"},"/home/gib/Documents/Code/monorepo/convex-monorepo/packages/backend/package.json",{"size":1135,"mtime":1761665487290,"hash":"51","data":"52"},"/home/gib/Documents/Code/monorepo/convex-monorepo/packages/backend/convex/openai.ts",{"size":2036,"mtime":1761665487195,"hash":"53","data":"54"},"/home/gib/Documents/Code/monorepo/convex-monorepo/packages/backend/convex/README.md",{"size":2525,"mtime":1761665487249,"hash":"55","data":"56"},"/home/gib/Documents/Code/monorepo/convex-monorepo/packages/backend/convex/_generated/api.js",{"size":414,"mtime":1753933693000,"hash":"57","data":"58"},"/home/gib/Documents/Code/monorepo/convex-monorepo/packages/backend/convex/tsconfig.json",{"size":732,"mtime":1753933693000,"hash":"59","data":"60"},"/home/gib/Documents/Code/monorepo/convex-monorepo/packages/backend/convex/_generated/server.js",{"size":3453,"mtime":1761665487114,"hash":"61","data":"62"},"/home/gib/Documents/Code/monorepo/convex-monorepo/packages/backend/convex/notes.ts",{"size":1632,"mtime":1761665487164,"hash":"63","data":"64"},"/home/gib/Documents/Code/monorepo/convex-monorepo/packages/backend/env.ts",{"size":219,"mtime":1761665487285,"hash":"65","data":"66"},"/home/gib/Documents/Code/monorepo/convex-monorepo/packages/backend/convex/_generated/dataModel.d.ts",{"size":1726,"mtime":1761665487061,"hash":"67","data":"68"},"/home/gib/Documents/Code/monorepo/convex-monorepo/packages/backend/convex/schema.ts",{"size":267,"mtime":1753933693000,"hash":"69","data":"70"},"/home/gib/Documents/Code/monorepo/convex-monorepo/packages/backend/convex/utils.ts",{"size":635,"mtime":1753933693000,"hash":"71","data":"72"},"/home/gib/Documents/Code/monorepo/convex-monorepo/packages/backend/convex/_generated/api.d.ts",{"size":845,"mtime":1761665486984,"hash":"73","data":"74"},"175b1a771387d8b7e26145c3d04e0475","da19b68e90524b894bbf1985bcec4d72",{"hashOfOptions":"75"},"a87d36cd21882ed8968ea6138f23acd9",{"hashOfOptions":"76"},"80903421dc266560a15314b96f727e40",{"hashOfOptions":"77"},"2cf34d0dd087d9749396d5f686419f4f",{"hashOfOptions":"78"},"7beaec3443b6db9c1a6b8c1668c98992",{"hashOfOptions":"79"},"c5dea56b7ddd47516cbd4038de020e53",{"hashOfOptions":"80"},"cfa98923457caed911ec68b626ef4234",{"hashOfOptions":"81"},"4a51b371d17b0a9dcc8d3a3d16790eec",{"hashOfOptions":"82"},"99403ed881c0f3caf6c13ad183e06049",{"hashOfOptions":"83"},"d78af64438f2e86a438ce045e0910a1d",{"hashOfOptions":"84"},"1a6eccbe2f9771c5b418adc40d8532c4",{"hashOfOptions":"85"},"9b1b5b82ff06ae9856df10ae91c15946",{"hashOfOptions":"86"},"0a1b4f144561ba1da064a40a2e089f10",{"hashOfOptions":"87"},"26351eae8dc2fa74ce0e3e6f6f0d4a49",{"hashOfOptions":"88"},"2545271053","1384390439","3159514942","514606721","3861339677","511151554","2611213275","1927515469","2174687556","3247610094","3411100989","2670794290","530123092","4075214594"] \ No newline at end of file +[["1","2","3","4","5","6","7","8","9","10","11","12","13","14","15","16","17","18","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"},{"key":"46","value":"47"},{"key":"48","value":"49"},{"key":"50","value":"51"},{"key":"52","value":"53"},{"key":"54","value":"55"},{"key":"56","value":"57"},"/home/gib/Documents/Code/studybuddy/packages/backend/convex/_generated/api.js",{"size":480,"mtime":1768143608462,"data":"58"},"/home/gib/Documents/Code/studybuddy/packages/backend/package.json",{"size":1218,"mtime":1768064103051,"data":"59"},"/home/gib/Documents/Code/studybuddy/packages/backend/.gitignore",{"size":16,"mtime":1767666484840},"/home/gib/Documents/Code/studybuddy/packages/backend/convex/custom/auth/providers/usesend.ts",{"size":3130,"mtime":1768143608925,"data":"60"},"/home/gib/Documents/Code/studybuddy/packages/backend/convex/http.ts",{"size":153,"mtime":1768143608966,"data":"61"},"/home/gib/Documents/Code/studybuddy/packages/backend/convex/tsconfig.json",{"size":732,"mtime":1767666484841,"data":"62"},"/home/gib/Documents/Code/studybuddy/packages/backend/convex/schema.ts",{"size":1136,"mtime":1768143609448,"data":"63"},"/home/gib/Documents/Code/studybuddy/packages/backend/convex/auth.ts",{"size":3630,"mtime":1768143608790,"data":"64"},"/home/gib/Documents/Code/studybuddy/packages/backend/convex/custom/auth/index.ts",{"size":142,"mtime":1767721415917,"data":"65"},"/home/gib/Documents/Code/studybuddy/packages/backend/convex/_generated/server.d.ts",{"size":5601,"mtime":1768143608577,"data":"66"},"/home/gib/Documents/Code/studybuddy/packages/backend/convex/auth.config.ts",{"size":127,"mtime":1768143608673,"data":"67"},"/home/gib/Documents/Code/studybuddy/packages/backend/convex/README.md",{"size":2525,"mtime":1768143609372,"data":"68"},"/home/gib/Documents/Code/studybuddy/packages/backend/convex/_generated/api.d.ts",{"size":1578,"mtime":1768143608406,"data":"69"},"/home/gib/Documents/Code/studybuddy/packages/backend/convex/quiz.ts",{"size":6633,"mtime":1768143609143,"data":"70"},"/home/gib/Documents/Code/studybuddy/packages/backend/convex/_generated/dataModel.d.ts",{"size":1737,"mtime":1768143608519,"data":"71"},"/home/gib/Documents/Code/studybuddy/packages/backend/convex/_generated/server.js",{"size":3696,"mtime":1768143608637,"data":"72"},"/home/gib/Documents/Code/studybuddy/packages/backend/.cache/.prettiercache",{"size":3699,"mtime":1767666484840},"/home/gib/Documents/Code/studybuddy/packages/backend/convex/custom/auth/providers/password.ts",{"size":900,"mtime":1768143608859,"data":"73"},"/home/gib/Documents/Code/studybuddy/packages/backend/convex/questions.ts",{"size":3362,"mtime":1768143609034,"data":"74"},{"hashOfOptions":"75"},{"hashOfOptions":"76"},{"hashOfOptions":"77"},{"hashOfOptions":"78"},{"hashOfOptions":"79"},{"hashOfOptions":"80"},{"hashOfOptions":"81"},{"hashOfOptions":"82"},{"hashOfOptions":"83"},{"hashOfOptions":"84"},{"hashOfOptions":"85"},{"hashOfOptions":"86"},{"hashOfOptions":"87"},{"hashOfOptions":"88"},{"hashOfOptions":"89"},{"hashOfOptions":"90"},{"hashOfOptions":"91"},"147160179","641208047","2537039807","1274207356","2687637644","1677824739","1873703484","2785340751","922120512","973677520","2103926734","2106123827","1114972079","2442144494","3732209152","1611314387","2049149899"] \ No newline at end of file diff --git a/packages/backend/convex/README.md b/packages/backend/convex/README.md index 97f0913..311398b 100644 --- a/packages/backend/convex/README.md +++ b/packages/backend/convex/README.md @@ -7,9 +7,9 @@ A query function that takes two arguments looks like: ```ts // functions.js -import { v } from "convex/values"; +import { v } from 'convex/values'; -import { query } from "./_generated/server"; +import { query } from './_generated/server'; export const myQueryFunction = query({ // Validators for arguments. @@ -22,7 +22,7 @@ export const myQueryFunction = query({ handler: async (ctx, args) => { // Read the database as many times as you need here. // See https://docs.convex.dev/database/reading-data. - const documents = await ctx.db.query("tablename").collect(); + const documents = await ctx.db.query('tablename').collect(); // Arguments passed from the client are properties of the args object. console.log(args.first, args.second); @@ -39,7 +39,7 @@ Using this query function in a React component looks like: ```ts const data = useQuery(api.functions.myQueryFunction, { first: 10, - second: "hello", + second: 'hello', }); ``` @@ -47,9 +47,9 @@ A mutation function looks like: ```ts // functions.js -import { v } from "convex/values"; +import { v } from 'convex/values'; -import { mutation } from "./_generated/server"; +import { mutation } from './_generated/server'; export const myMutationFunction = mutation({ // Validators for arguments. @@ -64,7 +64,7 @@ export const myMutationFunction = mutation({ // Mutations can also read from the database like queries. // See https://docs.convex.dev/database/writing-data. const message = { body: args.first, author: args.second }; - const id = await ctx.db.insert("messages", message); + const id = await ctx.db.insert('messages', message); // Optionally, return a value from your mutation. return await ctx.db.get(id); @@ -78,10 +78,10 @@ Using this mutation function in a React component looks like: const mutation = useMutation(api.functions.myMutationFunction); function handleButtonPress() { // fire and forget, the most common way to use mutations - mutation({ first: "Hello!", second: "me" }); + mutation({ first: 'Hello!', second: 'me' }); // OR // use the result once the mutation has completed - mutation({ first: "Hello!", second: "me" }).then((result) => + mutation({ first: 'Hello!', second: 'me' }).then((result) => console.log(result), ); } diff --git a/packages/backend/convex/_generated/api.d.ts b/packages/backend/convex/_generated/api.d.ts index 9851434..9f5c06e 100644 --- a/packages/backend/convex/_generated/api.d.ts +++ b/packages/backend/convex/_generated/api.d.ts @@ -14,7 +14,7 @@ import type * as custom_auth_providers_password from "../custom/auth/providers/p import type * as custom_auth_providers_usesend from "../custom/auth/providers/usesend.js"; import type * as http from "../http.js"; import type * as questions from "../questions.js"; -import type * as utils from "../utils.js"; +import type * as quiz from "../quiz.js"; import type { ApiFromModules, @@ -29,7 +29,7 @@ declare const fullApi: ApiFromModules<{ "custom/auth/providers/usesend": typeof custom_auth_providers_usesend; http: typeof http; questions: typeof questions; - utils: typeof utils; + quiz: typeof quiz; }>; /** diff --git a/packages/backend/convex/auth.config.ts b/packages/backend/convex/auth.config.ts index f4eb564..40b63c7 100644 --- a/packages/backend/convex/auth.config.ts +++ b/packages/backend/convex/auth.config.ts @@ -2,7 +2,7 @@ export default { providers: [ { domain: process.env.CONVEX_SITE_URL, - applicationID: "convex", + applicationID: 'convex', }, ], }; diff --git a/packages/backend/convex/auth.ts b/packages/backend/convex/auth.ts index ce84bae..788905d 100644 --- a/packages/backend/convex/auth.ts +++ b/packages/backend/convex/auth.ts @@ -1,32 +1,25 @@ -import { ConvexError, v } from 'convex/values'; +import Authentik from '@auth/core/providers/authentik'; import { convexAuth, getAuthUserId, - retrieveAccount, modifyAccountCredentials, + retrieveAccount, } from '@convex-dev/auth/server'; -import { api } from './_generated/api'; +import { ConvexError, v } from 'convex/values'; + import type { Doc, Id } from './_generated/dataModel'; -import { - action, - mutation, - query, - type MutationCtx, - type QueryCtx, -} from './_generated/server'; -import Authentik from '@auth/core/providers/authentik'; +import type { MutationCtx, QueryCtx } from './_generated/server'; +import { api } from './_generated/api'; +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 ( ctx: QueryCtx, - userId: Id<'users'> + userId: Id<'users'>, ): Promise> => { const user = await ctx.db.get(userId); if (!user) throw new ConvexError('User not found.'); diff --git a/packages/backend/convex/custom/auth/providers/password.ts b/packages/backend/convex/custom/auth/providers/password.ts index 26e12d7..7ef8ed2 100644 --- a/packages/backend/convex/custom/auth/providers/password.ts +++ b/packages/backend/convex/custom/auth/providers/password.ts @@ -1,8 +1,9 @@ import { Password as DefaultPassword } from '@convex-dev/auth/providers/Password'; -import { DataModel } from '../../../_generated/dataModel'; -import { UseSendOTP, UseSendOTPPasswordReset } from '..'; import { ConvexError } from 'convex/values'; +import { UseSendOTP, UseSendOTPPasswordReset } from '..'; +import { DataModel } from '../../../_generated/dataModel'; + export const Password = DefaultPassword({ profile(params, ctx) { return { diff --git a/packages/backend/convex/custom/auth/providers/usesend.ts b/packages/backend/convex/custom/auth/providers/usesend.ts index 3778d59..e99451c 100644 --- a/packages/backend/convex/custom/auth/providers/usesend.ts +++ b/packages/backend/convex/custom/auth/providers/usesend.ts @@ -1,6 +1,6 @@ import type { EmailConfig, EmailUserConfig } from '@auth/core/providers/email'; -import { alphabet } from 'oslo/crypto'; import { generateRandomString, RandomReader } from '@oslojs/crypto/random'; +import { alphabet } from 'oslo/crypto'; import { UseSend } from 'usesend-js'; export default function UseSendProvider(config: EmailUserConfig): EmailConfig { diff --git a/packages/backend/convex/http.ts b/packages/backend/convex/http.ts index 64c297e..3aa3437 100644 --- a/packages/backend/convex/http.ts +++ b/packages/backend/convex/http.ts @@ -1,4 +1,5 @@ import { httpRouter } from 'convex/server'; + import { auth } from './auth'; const http = httpRouter(); diff --git a/packages/backend/convex/questions.ts b/packages/backend/convex/questions.ts index 2bf7af0..64d8754 100644 --- a/packages/backend/convex/questions.ts +++ b/packages/backend/convex/questions.ts @@ -1,8 +1,9 @@ -import { v } from "convex/values"; -import { query, mutation, action } from "./_generated/server"; -import { getAuthUserId } from "@convex-dev/auth/server"; -import { api } from "./_generated/api"; -import OpenAI from "openai"; +import { getAuthUserId } from '@convex-dev/auth/server'; +import { v } from 'convex/values'; +import OpenAI from 'openai'; + +import { api } from './_generated/api'; +import { action, mutation, query } from './_generated/server'; const openai = new OpenAI({ baseURL: process.env.OPENAI_BASE_URL, @@ -13,8 +14,8 @@ export const getAllQuestions = query({ args: {}, handler: async (ctx) => { const userId = await getAuthUserId(ctx); - if (!userId) throw new Error("Not authenticated"); - return await ctx.db.query("questions").collect(); + if (!userId) throw new Error('Not authenticated'); + return await ctx.db.query('questions').collect(); }, }); @@ -22,10 +23,10 @@ export const getQuestionsByTopic = query({ args: { topic: v.string() }, handler: async (ctx, args) => { const userId = await getAuthUserId(ctx); - if (!userId) throw new Error("Not authenticated"); + if (!userId) throw new Error('Not authenticated'); return await ctx.db - .query("questions") - .withIndex("by_topic", (q) => q.eq("topic", args.topic)) + .query('questions') + .withIndex('by_topic', (q) => q.eq('topic', args.topic)) .collect(); }, }); @@ -41,8 +42,8 @@ export const addQuestion = mutation({ }, handler: async (ctx, args) => { const userId = await getAuthUserId(ctx); - if (!userId) throw new Error("Not authenticated"); - return await ctx.db.insert("questions", { + if (!userId) throw new Error('Not authenticated'); + return await ctx.db.insert('questions', { question: args.question, options: args.options, correctAnswer: args.correctAnswer, @@ -60,7 +61,7 @@ export const generateQuestions = action({ }, handler: async (ctx, args) => { const userId = await getAuthUserId(ctx); - if (!userId) throw new Error("Not authenticated"); + if (!userId) throw new Error('Not authenticated'); const prompt = `Generate ${args.count} multiple choice questions for the CompTIA Network+ exam on the topic: "${args.topic}". Return ONLY a valid JSON array with this exact structure: @@ -83,19 +84,19 @@ Rules: - Return ONLY the JSON array, no other text`; const response = await openai.chat.completions.create({ - model: "gpt-4o-mini", - messages: [{ role: "user", content: prompt }], + model: 'gpt-4o-mini', + messages: [{ role: 'user', content: prompt }], temperature: 0.8, }); const content = response.choices[0].message.content; - if (!content) throw new Error("No response from AI"); + if (!content) throw new Error('No response from AI'); let questions; try { questions = JSON.parse(content); } catch (e) { - throw new Error("Failed to parse AI response as JSON"); + throw new Error('Failed to parse AI response as JSON'); } const questionIds = []; diff --git a/packages/backend/convex/quiz.ts b/packages/backend/convex/quiz.ts new file mode 100644 index 0000000..94df2ad --- /dev/null +++ b/packages/backend/convex/quiz.ts @@ -0,0 +1,236 @@ +import { getAuthUserId } from '@convex-dev/auth/server'; +import { v } from 'convex/values'; + +import { mutation, query } from './_generated/server'; + +export const startQuizSession = mutation({ + args: { + questionIds: v.optional(v.array(v.id('questions'))), + }, + handler: async (ctx, args) => { + const userId = await getAuthUserId(ctx); + if (!userId) throw new Error('Not authenticated'); + + // End any active sessions + const activeSessions = await ctx.db + .query('quizSessions') + .withIndex('by_user_and_active', (q) => + q.eq('userId', userId).eq('isActive', true), + ) + .collect(); + + for (const session of activeSessions) { + await ctx.db.patch(session._id, { isActive: false }); + } + + // Get questions + let questionIds = args.questionIds; + if (!questionIds || questionIds.length === 0) { + const allQuestions = await ctx.db.query('questions').collect(); + questionIds = allQuestions.map((q) => q._id); + } + + // Shuffle questions + const shuffled = [...questionIds].sort(() => Math.random() - 0.5); + + return await ctx.db.insert('quizSessions', { + userId, + activeQuestions: shuffled, + missedQuestions: [], + completedQuestions: [], + currentQuestionIndex: 0, + isActive: true, + }); + }, +}); + +export const getActiveSession = query({ + args: {}, + handler: async (ctx) => { + const userId = await getAuthUserId(ctx); + if (!userId) throw new Error('Not authenticated'); + + const session = await ctx.db + .query('quizSessions') + .withIndex('by_user_and_active', (q) => + q.eq('userId', userId).eq('isActive', true), + ) + .first(); + + return session; + }, +}); + +export const getCurrentQuestion = query({ + args: {}, + handler: async (ctx) => { + const userId = await getAuthUserId(ctx); + if (!userId) throw new Error('Not authenticated'); + + const session = await ctx.db + .query('quizSessions') + .withIndex('by_user_and_active', (q) => + q.eq('userId', userId).eq('isActive', true), + ) + .first(); + + if (!session) return null; + + const allQuestions = [ + ...session.activeQuestions, + ...session.missedQuestions, + ]; + + if (allQuestions.length === 0) return null; + + const currentQuestionId = allQuestions[session.currentQuestionIndex]; + const question = await ctx.db.get(currentQuestionId); + + return { + question, + sessionId: session._id, + progress: { + current: session.currentQuestionIndex + 1, + total: allQuestions.length, + completed: session.completedQuestions.length, + }, + }; + }, +}); + +export const answerQuestion = mutation({ + args: { + sessionId: v.id('quizSessions'), + questionId: v.id('questions'), + selectedAnswer: v.number(), + }, + handler: async (ctx, args) => { + const userId = await getAuthUserId(ctx); + if (!userId) throw new Error('Not authenticated'); + + const session = await ctx.db.get(args.sessionId); + if (!session || session.userId !== userId) { + throw new Error('Invalid session'); + } + + const question = await ctx.db.get(args.questionId); + if (!question) throw new Error('Question not found'); + + const isCorrect = args.selectedAnswer === question.correctAnswer; + + // Update user progress + const existingProgress = await ctx.db + .query('userProgress') + .withIndex('by_user_and_question', (q) => + q.eq('userId', userId).eq('questionId', args.questionId), + ) + .first(); + + if (existingProgress) { + await ctx.db.patch(existingProgress._id, { + correctCount: existingProgress.correctCount + (isCorrect ? 1 : 0), + incorrectCount: existingProgress.incorrectCount + (isCorrect ? 0 : 1), + lastAttempted: Date.now(), + }); + } else { + await ctx.db.insert('userProgress', { + userId, + questionId: args.questionId, + correctCount: isCorrect ? 1 : 0, + incorrectCount: isCorrect ? 0 : 1, + lastAttempted: Date.now(), + }); + } + + // Update session + const allQuestions = [ + ...session.activeQuestions, + ...session.missedQuestions, + ]; + + let newActiveQuestions = [...session.activeQuestions]; + let newMissedQuestions = [...session.missedQuestions]; + let newCompletedQuestions = [...session.completedQuestions]; + + // Remove current question from active list + const currentIndex = newActiveQuestions.indexOf(args.questionId); + if (currentIndex !== -1) { + newActiveQuestions.splice(currentIndex, 1); + } else { + const missedIndex = newMissedQuestions.indexOf(args.questionId); + if (missedIndex !== -1) { + newMissedQuestions.splice(missedIndex, 1); + } + } + + if (isCorrect) { + newCompletedQuestions.push(args.questionId); + } else { + newMissedQuestions.push(args.questionId); + } + + const newAllQuestions = [...newActiveQuestions, ...newMissedQuestions]; + const newIndex = + newAllQuestions.length > 0 ? 0 : session.currentQuestionIndex; + + await ctx.db.patch(args.sessionId, { + activeQuestions: newActiveQuestions, + missedQuestions: newMissedQuestions, + completedQuestions: newCompletedQuestions, + currentQuestionIndex: newIndex, + }); + + return { + isCorrect, + correctAnswer: question.correctAnswer, + explanation: question.explanation, + }; + }, +}); + +export const endQuizSession = mutation({ + args: { + sessionId: v.id('quizSessions'), + }, + handler: async (ctx, args) => { + const userId = await getAuthUserId(ctx); + if (!userId) throw new Error('Not authenticated'); + + const session = await ctx.db.get(args.sessionId); + if (!session || session.userId !== userId) { + throw new Error('Invalid session'); + } + + await ctx.db.patch(args.sessionId, { isActive: false }); + return null; + }, +}); + +export const getUserStats = query({ + args: {}, + handler: async (ctx) => { + const userId = await getAuthUserId(ctx); + if (!userId) throw new Error('Not authenticated'); + + const progress = await ctx.db + .query('userProgress') + .withIndex('by_user', (q) => q.eq('userId', userId)) + .collect(); + + const totalQuestions = progress.length; + const totalCorrect = progress.reduce((sum, p) => sum + p.correctCount, 0); + const totalIncorrect = progress.reduce( + (sum, p) => sum + p.incorrectCount, + 0, + ); + const totalAttempts = totalCorrect + totalIncorrect; + + return { + totalQuestions, + totalAttempts, + totalCorrect, + totalIncorrect, + accuracy: totalAttempts > 0 ? (totalCorrect / totalAttempts) * 100 : 0, + }; + }, +}); diff --git a/packages/backend/convex/schema.ts b/packages/backend/convex/schema.ts index c0c17b5..8553e57 100644 --- a/packages/backend/convex/schema.ts +++ b/packages/backend/convex/schema.ts @@ -1,6 +1,6 @@ -import { defineSchema, defineTable } from "convex/server"; -import { v, VId } from "convex/values"; import { authTables } from '@convex-dev/auth/server'; +import { defineSchema, defineTable } from 'convex/server'; +import { v, VId } from 'convex/values'; const applicationTables = { questions: defineTable({ @@ -10,8 +10,7 @@ const applicationTables = { topic: v.string(), difficulty: v.optional(v.string()), explanation: v.optional(v.string()), - }) - .index('by_topic', ['topic']), + }).index('by_topic', ['topic']), userProgress: defineTable({ userId: v.id('users'), @@ -30,8 +29,7 @@ const applicationTables = { completedQuestions: v.array(v.id('questions')), currentQuestionIndex: v.number(), isActive: v.boolean(), - }) - .index('by_user_and_active', ['userId', 'isActive']), + }).index('by_user_and_active', ['userId', 'isActive']), }; export default defineSchema({ diff --git a/packages/backend/convex/utils.ts b/packages/backend/convex/utils.ts deleted file mode 100644 index 24d17c2..0000000 --- a/packages/backend/convex/utils.ts +++ /dev/null @@ -1,16 +0,0 @@ -export function missingEnvVariableUrl(envVarName: string, whereToGet: string) { - const deployment = deploymentName(); - if (!deployment) return `Missing ${envVarName} in environment variables.`; - return ( - `\n Missing ${envVarName} in environment variables.\n\n` + - ` Get it from ${whereToGet} .\n Paste it on the Convex dashboard:\n` + - ` https://dashboard.convex.dev/d/${deployment}/settings?var=${envVarName}` - ); -} - -export function deploymentName() { - const url = process.env.CONVEX_CLOUD_URL; - if (!url) return undefined; - const regex = new RegExp("https://(.+).convex.cloud"); - return regex.exec(url)?.[1]; -} diff --git a/packages/ui/.cache/.prettiercache b/packages/ui/.cache/.prettiercache index 689ef31..66da0ef 100644 --- a/packages/ui/.cache/.prettiercache +++ b/packages/ui/.cache/.prettiercache @@ -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","33","34","35","36","37","38","39","40","41","42","43","44","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"},{"key":"94","value":"95"},{"key":"96","value":"97"},{"key":"98","value":"99"},{"key":"100","value":"101"},{"key":"102","value":"103"},{"key":"104","value":"105"},{"key":"106","value":"107"},{"key":"108","value":"109"},{"key":"110","value":"111"},{"key":"112","value":"113"},{"key":"114","value":"115"},{"key":"116","value":"117"},{"key":"118","value":"119"},{"key":"120","value":"121"},{"key":"122","value":"123"},{"key":"124","value":"125"},{"key":"126","value":"127"},{"key":"128","value":"129"},{"key":"130","value":"131"},{"key":"132","value":"133"},{"key":"134","value":"135"},"/home/gib/Documents/Code/monorepo/convex-monorepo/packages/ui/src/separator.tsx",{"size":706,"mtime":1761658333099,"hash":"136","data":"137"},"/home/gib/Documents/Code/monorepo/techtracker/packages/ui/src/scroll-area.tsx",{"size":1652,"mtime":1761674665017,"hash":"138","data":"139"},"/home/gib/Documents/Code/monorepo/convex-monorepo/packages/ui/src/input.tsx",{"size":972,"mtime":1761658333099,"hash":"140","data":"141"},"/home/gib/Documents/Code/monorepo/convex-monorepo/packages/ui/src/label.tsx",{"size":618,"mtime":1761658333099,"hash":"142","data":"143"},"/home/gib/Documents/Code/monorepo/techtracker/packages/ui/src/avatar.tsx",{"size":1106,"mtime":1761674664567,"hash":"144","data":"145"},"/home/gib/Documents/Code/monorepo/techtracker/packages/ui/src/tabs.tsx",{"size":1979,"mtime":1761674665139,"hash":"146","data":"147"},"/home/gib/Documents/Code/monorepo/techtracker/packages/ui/src/checkbox.tsx",{"size":1233,"mtime":1761674664724,"hash":"148","data":"149"},"/home/gib/Documents/Code/monorepo/techtracker/packages/ui/src/drawer.tsx",{"size":4272,"mtime":1761674664758,"hash":"150","data":"151"},"/home/gib/Documents/Code/monorepo/techtracker/packages/ui/src/status-message.tsx",{"size":1620,"mtime":1761674665103,"hash":"152","data":"153"},"/home/gib/Documents/Code/monorepo/techtracker/packages/ui/.cache/.prettiercache",{"size":3120,"mtime":1761674052425,"hash":"154"},"/home/gib/Documents/Code/monorepo/techtracker/packages/ui/src/switch.tsx",{"size":1184,"mtime":1761674665118,"hash":"155","data":"156"},"/home/gib/Documents/Code/monorepo/techtracker/packages/ui/tsconfig.json",{"size":213,"mtime":1761674052424,"hash":"157","data":"158"},"/home/gib/Documents/Code/monorepo/convex-monorepo/packages/ui/components.json",{"size":308,"mtime":1761443987000,"hash":"159","data":"160"},"/home/gib/Documents/Code/monorepo/techtracker/packages/ui/package.json",{"size":2866,"mtime":1761674052425,"hash":"161","data":"162"},"/home/gib/Documents/Code/monorepo/convex-monorepo/packages/ui/src/index.ts",{"size":358,"mtime":1761673214180,"hash":"163","data":"164"},"/home/gib/Documents/Code/monorepo/techtracker/packages/ui/components.json",{"size":308,"mtime":1761674052424,"hash":"159","data":"165"},"/home/gib/Documents/Code/monorepo/techtracker/packages/ui/src/card.tsx",{"size":2001,"mtime":1761674664709,"hash":"166","data":"167"},"/home/gib/Documents/Code/monorepo/techtracker/packages/ui/src/form.tsx",{"size":3820,"mtime":1761674664909,"hash":"168","data":"169"},"/home/gib/Documents/Code/monorepo/techtracker/packages/ui/src/field.tsx",{"size":6144,"mtime":1761674052424,"hash":"170","data":"171"},"/home/gib/Documents/Code/monorepo/techtracker/packages/ui/src/pagination.tsx",{"size":2726,"mtime":1761674664998,"hash":"172","data":"173"},"/home/gib/Documents/Code/monorepo/techtracker/packages/ui/src/separator.tsx",{"size":705,"mtime":1761674665027,"hash":"174","data":"175"},"/home/gib/Documents/Code/monorepo/techtracker/packages/ui/eslint.config.ts",{"size":256,"mtime":1761674052424,"hash":"176","data":"177"},"/home/gib/Documents/Code/monorepo/convex-monorepo/packages/ui/eslint.config.ts",{"size":256,"mtime":1761443987000,"hash":"176","data":"178"},"/home/gib/Documents/Code/monorepo/convex-monorepo/packages/ui/src/button.tsx",{"size":2133,"mtime":1761658333099,"hash":"179","data":"180"},"/home/gib/Documents/Code/monorepo/convex-monorepo/packages/ui/src/dropdown-menu.tsx",{"size":8317,"mtime":1761658333099,"hash":"181","data":"182"},"/home/gib/Documents/Code/monorepo/convex-monorepo/packages/ui/src/theme.tsx",{"size":5136,"mtime":1761443987000,"hash":"183","data":"184"},"/home/gib/Documents/Code/monorepo/techtracker/packages/ui/src/label.tsx",{"size":617,"mtime":1761674664981,"hash":"185","data":"186"},"/home/gib/Documents/Code/monorepo/convex-monorepo/packages/ui/tsconfig.json",{"size":213,"mtime":1761443987000,"hash":"157","data":"187"},"/home/gib/Documents/Code/monorepo/techtracker/packages/ui/src/input-otp.tsx",{"size":2269,"mtime":1761674664947,"hash":"188","data":"189"},"/home/gib/Documents/Code/monorepo/techtracker/packages/ui/src/progress.tsx",{"size":746,"mtime":1761674665007,"hash":"190","data":"191"},"/home/gib/Documents/Code/monorepo/convex-monorepo/packages/ui/src/toast.tsx",{"size":616,"mtime":1761443987000,"hash":"192","data":"193"},"/home/gib/Documents/Code/monorepo/techtracker/packages/ui/src/based-avatar.tsx",{"size":1783,"mtime":1761674664606,"hash":"194","data":"195"},"/home/gib/Documents/Code/monorepo/techtracker/packages/ui/src/dropdown-menu.tsx",{"size":8316,"mtime":1761674664802,"hash":"196","data":"197"},"/home/gib/Documents/Code/monorepo/techtracker/packages/ui/src/button.tsx",{"size":2175,"mtime":1761674664685,"hash":"198","data":"199"},"/home/gib/Documents/Code/monorepo/techtracker/packages/ui/src/input.tsx",{"size":971,"mtime":1761674664969,"hash":"200","data":"201"},"/home/gib/Documents/Code/monorepo/convex-monorepo/packages/ui/src/field.tsx",{"size":6144,"mtime":1761443987000,"hash":"170","data":"202"},"/home/gib/Documents/Code/monorepo/techtracker/packages/ui/src/based-progress.tsx",{"size":1375,"mtime":1761674664644,"hash":"203","data":"204"},"/home/gib/Documents/Code/monorepo/techtracker/packages/ui/src/sonner.tsx",{"size":571,"mtime":1761674665092,"hash":"205","data":"206"},"/home/gib/Documents/Code/monorepo/techtracker/packages/ui/src/index.ts",{"size":358,"mtime":1761674664925,"hash":"207","data":"208"},"/home/gib/Documents/Code/monorepo/techtracker/packages/ui/src/toast.tsx",{"size":616,"mtime":1761674052424,"hash":"192","data":"209"},"/home/gib/Documents/Code/monorepo/convex-monorepo/packages/ui/package.json",{"size":2866,"mtime":1761673551701,"hash":"161","data":"210"},"/home/gib/Documents/Code/monorepo/techtracker/packages/ui/src/submit-button.tsx",{"size":1229,"mtime":1761674665112,"hash":"211","data":"212"},"/home/gib/Documents/Code/monorepo/techtracker/packages/ui/src/table.tsx",{"size":2463,"mtime":1761674665128,"hash":"213","data":"214"},"/home/gib/Documents/Code/monorepo/techtracker/packages/ui/src/shadcn-io/image-crop/index.tsx",{"size":8265,"mtime":1761674665081,"hash":"215","data":"216"},"/home/gib/Documents/Code/monorepo/techtracker/packages/ui/src/theme.tsx",{"size":5136,"mtime":1761674052424,"hash":"183","data":"217"},"731b649d937269f3b503aaca17934aaa",{"hashOfOptions":"218"},"273d3801b3d47a7dd4d31aa54e7645b4",{"hashOfOptions":"219"},"ec768e64b0da9f38330d2b7726c13c17",{"hashOfOptions":"220"},"e94441ac6bcb9b0570535168c392397e",{"hashOfOptions":"221"},"387654428ac345b74aa00853940a9b11",{"hashOfOptions":"222"},"a923fbff6ce24854cc1e463250408190",{"hashOfOptions":"223"},"d6694599f738f2aa5b03161cfc116124",{"hashOfOptions":"224"},"cc5fe21b6e9d947f0f76e2de63fb8ef0",{"hashOfOptions":"225"},"198320d03ec10aa170486ea3dc840691",{"hashOfOptions":"226"},"acadf0e1f17d0efc7956a80c11cdaa2d","9dea06743cb29fc72f797e5ff08abf7a",{"hashOfOptions":"227"},"a6fc50dd2d117cd2ab5544870b6eb4fb",{"hashOfOptions":"228"},"9c7797b58e9124116b585c64e6c0b829",{"hashOfOptions":"229"},"18826dc64db4eeab3e7cf31987234b12",{"hashOfOptions":"230"},"d316842b0028a3181f96441e0c96df50",{"hashOfOptions":"231"},{"hashOfOptions":"232"},"bcc428e61be66cd876aa91ab16fd6a6e",{"hashOfOptions":"233"},"5643583506d0d0304d34c0c858cf9ada",{"hashOfOptions":"234"},"a47a38dceeddc2f58e8f06c2a2c47a96",{"hashOfOptions":"235"},"23ec6ca5e03490abe988d89dd531a8e2",{"hashOfOptions":"236"},"04ec1a30bd3b2223989c683bcf349d9b",{"hashOfOptions":"237"},"420f5beb8cfa59be129b4697f434770b",{"hashOfOptions":"238"},{"hashOfOptions":"239"},"461c40f0b297acf5185ee382c9cfc1cb",{"hashOfOptions":"240"},"3bd4ec699ff17ce2b746a8c3943b47a5",{"hashOfOptions":"241"},"b62cf6fa9377430923c4b0137ae2d414",{"hashOfOptions":"242"},"cd7c04c07ef8678c7700754847833ed2",{"hashOfOptions":"243"},{"hashOfOptions":"244"},"542f873a5b0cd2d9853f01953e55cb29",{"hashOfOptions":"245"},"53c22d47b4d34e513e33b00335f97039",{"hashOfOptions":"246"},"3f99d212c8aceb0666e2a52d42f73af1",{"hashOfOptions":"247"},"5711c8ed7ef241a8cdfbe70830bb7fba",{"hashOfOptions":"248"},"1d1b440c7869a8cc678231ac9190a736",{"hashOfOptions":"249"},"56b6fdc1484e01685bd1c2131ae8da69",{"hashOfOptions":"250"},"d3f6606da7f44a46ea37d05b1b21969d",{"hashOfOptions":"251"},{"hashOfOptions":"252"},"932bc9fe6218c65ffda5c419f19242eb",{"hashOfOptions":"253"},"ace3a6465858e1f9a441724008f2d921",{"hashOfOptions":"254"},"524bc6f1a8f04cffc598190f799e96dc",{"hashOfOptions":"255"},{"hashOfOptions":"256"},{"hashOfOptions":"257"},"2c632d90ad9775d38f613c0dca696b71",{"hashOfOptions":"258"},"fc48e1886a58f146c06688b033eff1bf",{"hashOfOptions":"259"},"79347453bbcd04d3550727a9ac9a3b38",{"hashOfOptions":"260"},{"hashOfOptions":"261"},"2094140157","2888048876","671104514","1777924684","3408101102","981627891","3130371192","1161469478","2360178049","3202854857","3110581844","3986582840","3132369135","634744684","1203621759","3988801829","2026963065","2075641401","2870669263","475445700","253824749","2132860788","4287561166","2299778291","1203426049","750047251","4138459277","3819667015","918333282","3973557695","1281540968","2582142138","3247413703","3938194377","3103518834","3232583388","459794942","258258917","2945680262","3508854902","1294568070","4273303501","2957904921","175548616"] \ No newline at end of file +[["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/studybuddy/packages/ui/components.json",{"size":307,"mtime":1767666484841,"data":"94"},"/home/gib/Documents/Code/studybuddy/packages/ui/src/card.tsx",{"size":1998,"mtime":1768143609158,"data":"95"},"/home/gib/Documents/Code/studybuddy/packages/ui/src/pagination.tsx",{"size":2682,"mtime":1768143609593,"data":"96"},"/home/gib/Documents/Code/studybuddy/packages/ui/src/tabs.tsx",{"size":1976,"mtime":1768143609816,"data":"97"},"/home/gib/Documents/Code/studybuddy/packages/ui/src/based-avatar.tsx",{"size":1739,"mtime":1768143608866,"data":"98"},"/home/gib/Documents/Code/studybuddy/packages/ui/src/button.tsx",{"size":2172,"mtime":1768143609117,"data":"99"},"/home/gib/Documents/Code/studybuddy/packages/ui/src/submit-button.tsx",{"size":1192,"mtime":1768143609769,"data":"100"},"/home/gib/Documents/Code/studybuddy/packages/ui/.cache/.prettiercache",{"size":10630,"mtime":1767666484841},"/home/gib/Documents/Code/studybuddy/packages/ui/src/avatar.tsx",{"size":1103,"mtime":1768143608735,"data":"101"},"/home/gib/Documents/Code/studybuddy/packages/ui/src/dropdown-menu.tsx",{"size":8313,"mtime":1768143609317,"data":"102"},"/home/gib/Documents/Code/studybuddy/packages/ui/package.json",{"size":2861,"mtime":1767666484841,"data":"103"},"/home/gib/Documents/Code/studybuddy/packages/ui/src/index.ts",{"size":2381,"mtime":1768143609492,"data":"104"},"/home/gib/Documents/Code/studybuddy/packages/ui/src/input-otp.tsx",{"size":2266,"mtime":1768143609524,"data":"105"},"/home/gib/Documents/Code/studybuddy/packages/ui/src/input.tsx",{"size":968,"mtime":1768143609542,"data":"106"},"/home/gib/Documents/Code/studybuddy/packages/ui/src/label.tsx",{"size":614,"mtime":1768143609563,"data":"107"},"/home/gib/Documents/Code/studybuddy/packages/ui/src/shadcn-io/image-crop/index.tsx",{"size":8228,"mtime":1768143609704,"data":"108"},"/home/gib/Documents/Code/studybuddy/packages/ui/src/based-progress.tsx",{"size":1372,"mtime":1768143609026,"data":"109"},"/home/gib/Documents/Code/studybuddy/packages/ui/src/separator.tsx",{"size":702,"mtime":1768143609635,"data":"110"},"/home/gib/Documents/Code/studybuddy/packages/ui/tsconfig.json",{"size":212,"mtime":1767666484841,"data":"111"},"/home/gib/Documents/Code/studybuddy/packages/ui/src/switch.tsx",{"size":1181,"mtime":1768143609782,"data":"112"},"/home/gib/Documents/Code/studybuddy/packages/ui/src/theme.tsx",{"size":1794,"mtime":1768143609836,"data":"113"},"/home/gib/Documents/Code/studybuddy/packages/ui/eslint.config.ts",{"size":254,"mtime":1768143608527,"data":"114"},"/home/gib/Documents/Code/studybuddy/packages/ui/src/field.tsx",{"size":6073,"mtime":1768143609399,"data":"115"},"/home/gib/Documents/Code/studybuddy/packages/ui/src/form.tsx",{"size":3777,"mtime":1768143609457,"data":"116"},"/home/gib/Documents/Code/studybuddy/packages/ui/src/drawer.tsx",{"size":4269,"mtime":1768143609229,"data":"117"},"/home/gib/Documents/Code/studybuddy/packages/ui/src/progress.tsx",{"size":743,"mtime":1768143609607,"data":"118"},"/home/gib/Documents/Code/studybuddy/packages/ui/src/scroll-area.tsx",{"size":1649,"mtime":1768143609625,"data":"119"},"/home/gib/Documents/Code/studybuddy/packages/ui/src/sonner.tsx",{"size":571,"mtime":1768143609728,"data":"120"},"/home/gib/Documents/Code/studybuddy/packages/ui/src/status-message.tsx",{"size":1617,"mtime":1768143609746,"data":"121"},"/home/gib/Documents/Code/studybuddy/packages/ui/src/checkbox.tsx",{"size":1230,"mtime":1768143609182,"data":"122"},"/home/gib/Documents/Code/studybuddy/packages/ui/src/table.tsx",{"size":2460,"mtime":1768143609800,"data":"123"},{"hashOfOptions":"124"},{"hashOfOptions":"125"},{"hashOfOptions":"126"},{"hashOfOptions":"127"},{"hashOfOptions":"128"},{"hashOfOptions":"129"},{"hashOfOptions":"130"},{"hashOfOptions":"131"},{"hashOfOptions":"132"},{"hashOfOptions":"133"},{"hashOfOptions":"134"},{"hashOfOptions":"135"},{"hashOfOptions":"136"},{"hashOfOptions":"137"},{"hashOfOptions":"138"},{"hashOfOptions":"139"},{"hashOfOptions":"140"},{"hashOfOptions":"141"},{"hashOfOptions":"142"},{"hashOfOptions":"143"},{"hashOfOptions":"144"},{"hashOfOptions":"145"},{"hashOfOptions":"146"},{"hashOfOptions":"147"},{"hashOfOptions":"148"},{"hashOfOptions":"149"},{"hashOfOptions":"150"},{"hashOfOptions":"151"},{"hashOfOptions":"152"},{"hashOfOptions":"153"},"1394942569","1930594143","3404310537","3283502125","2715049634","3387326977","1507952","3574034472","326885540","1205452073","711210527","2510609713","2864572595","3918887677","2174286419","2642829846","553495470","3943723070","2625451523","1262038706","4178795815","2234133283","2478713523","477157216","138219676","1528348374","3556100920","2144695227","4188266418","3741614263"] \ No newline at end of file diff --git a/packages/ui/eslint.config.ts b/packages/ui/eslint.config.ts index d9d6fe0..93f5f7a 100644 --- a/packages/ui/eslint.config.ts +++ b/packages/ui/eslint.config.ts @@ -1,11 +1,11 @@ -import { defineConfig } from "eslint/config"; +import { defineConfig } from 'eslint/config'; -import { baseConfig } from "@gib/eslint-config/base"; -import { reactConfig } from "@gib/eslint-config/react"; +import { baseConfig } from '@gib/eslint-config/base'; +import { reactConfig } from '@gib/eslint-config/react'; export default defineConfig( { - ignores: ["dist/**"], + ignores: ['dist/**'], }, baseConfig, reactConfig, diff --git a/packages/ui/src/avatar.tsx b/packages/ui/src/avatar.tsx index f4359b8..d9aa985 100644 --- a/packages/ui/src/avatar.tsx +++ b/packages/ui/src/avatar.tsx @@ -1,8 +1,9 @@ -"use client"; +'use client'; -import * as React from "react"; -import { cn } from "@/lib/utils"; -import * as AvatarPrimitive from "@radix-ui/react-avatar"; +import * as React from 'react'; +import * as AvatarPrimitive from '@radix-ui/react-avatar'; + +import { cn } from '@gib/ui'; function Avatar({ className, @@ -12,7 +13,7 @@ function Avatar({ ); @@ -41,7 +42,7 @@ function AvatarFallback({ & { src?: string | null; fullName?: string | null; - imageProps?: Omit, "data-slot">; + imageProps?: Omit, 'data-slot'>; fallbackProps?: ComponentProps; userIconProps?: ComponentProps; }; @@ -29,7 +29,7 @@ const BasedAvatar = ({ {fullName ? ( fullName - .split(" ") + .split(' ') .map((n) => n[0]) - .join("") + .join('') .toUpperCase() ) : ( )} diff --git a/packages/ui/src/based-progress.tsx b/packages/ui/src/based-progress.tsx index 0eebbc0..a82de57 100644 --- a/packages/ui/src/based-progress.tsx +++ b/packages/ui/src/based-progress.tsx @@ -1,8 +1,9 @@ -"use client"; +'use client'; -import * as React from "react"; -import { cn } from "@/lib/utils"; -import * as ProgressPrimitive from "@radix-ui/react-progress"; +import * as React from 'react'; +import * as ProgressPrimitive from '@radix-ui/react-progress'; + +import { cn } from '@gib/ui'; type BasedProgressProps = React.ComponentProps< typeof ProgressPrimitive.Root @@ -36,7 +37,7 @@ const BasedProgress = ({ svg]:px-3", - sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5", - lg: "h-10 rounded-md px-6 has-[>svg]:px-4", - icon: "size-9", + default: 'h-9 px-4 py-2 has-[>svg]:px-3', + sm: 'h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5', + lg: 'h-10 rounded-md px-6 has-[>svg]:px-4', + icon: 'size-9', }, }, defaultVariants: { - variant: "default", - size: "default", + variant: 'default', + size: 'default', }, }, ); @@ -41,11 +42,11 @@ function Button({ size, asChild = false, ...props -}: React.ComponentProps<"button"> & +}: React.ComponentProps<'button'> & VariantProps & { asChild?: boolean; }) { - const Comp = asChild ? Slot : "button"; + const Comp = asChild ? Slot : 'button'; return ( ) { +import { cn } from '@gib/ui'; + +function Card({ className, ...props }: React.ComponentProps<'div'>) { return (
) { ); } -function CardHeader({ className, ...props }: React.ComponentProps<"div">) { +function CardHeader({ className, ...props }: React.ComponentProps<'div'>) { return (
) { ); } -function CardTitle({ className, ...props }: React.ComponentProps<"div">) { +function CardTitle({ className, ...props }: React.ComponentProps<'div'>) { return (
); } -function CardDescription({ className, ...props }: React.ComponentProps<"div">) { +function CardDescription({ className, ...props }: React.ComponentProps<'div'>) { return (
); } -function CardAction({ className, ...props }: React.ComponentProps<"div">) { +function CardAction({ className, ...props }: React.ComponentProps<'div'>) { return (
) { ); } -function CardContent({ className, ...props }: React.ComponentProps<"div">) { +function CardContent({ className, ...props }: React.ComponentProps<'div'>) { return (
); } -function CardFooter({ className, ...props }: React.ComponentProps<"div">) { +function CardFooter({ className, ...props }: React.ComponentProps<'div'>) { return (
); diff --git a/packages/ui/src/checkbox.tsx b/packages/ui/src/checkbox.tsx index 24c99fc..49c8a33 100644 --- a/packages/ui/src/checkbox.tsx +++ b/packages/ui/src/checkbox.tsx @@ -1,9 +1,10 @@ -"use client"; +'use client'; -import * as React from "react"; -import { cn } from "@/lib/utils"; -import * as CheckboxPrimitive from "@radix-ui/react-checkbox"; -import { CheckIcon } from "lucide-react"; +import * as React from 'react'; +import * as CheckboxPrimitive from '@radix-ui/react-checkbox'; +import { CheckIcon } from 'lucide-react'; + +import { cn } from '@gib/ui'; function Checkbox({ className, @@ -13,7 +14,7 @@ function Checkbox({ ) { +function DrawerHeader({ className, ...props }: React.ComponentProps<'div'>) { return (
) { ); } -function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) { +function DrawerFooter({ className, ...props }: React.ComponentProps<'div'>) { return (
); @@ -101,7 +102,7 @@ function DrawerTitle({ return ( ); @@ -114,7 +115,7 @@ function DrawerDescription({ return ( ); diff --git a/packages/ui/src/dropdown-menu.tsx b/packages/ui/src/dropdown-menu.tsx index c0630e2..477a17e 100644 --- a/packages/ui/src/dropdown-menu.tsx +++ b/packages/ui/src/dropdown-menu.tsx @@ -1,9 +1,10 @@ -"use client"; +'use client'; -import * as React from "react"; -import { cn } from "@/lib/utils"; -import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"; -import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"; +import * as React from 'react'; +import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'; +import { CheckIcon, ChevronRightIcon, CircleIcon } from 'lucide-react'; + +import { cn } from '@gib/ui'; function DropdownMenu({ ...props @@ -41,7 +42,7 @@ function DropdownMenuContent({ data-slot="dropdown-menu-content" sideOffset={sideOffset} className={cn( - "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md", + 'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md', className, )} {...props} @@ -61,11 +62,11 @@ function DropdownMenuGroup({ function DropdownMenuItem({ className, inset, - variant = "default", + variant = 'default', ...props }: React.ComponentProps & { inset?: boolean; - variant?: "default" | "destructive"; + variant?: 'default' | 'destructive'; }) { return ( ); @@ -178,12 +179,12 @@ function DropdownMenuSeparator({ function DropdownMenuShortcut({ className, ...props -}: React.ComponentProps<"span">) { +}: React.ComponentProps<'span'>) { return ( ) { +}: React.ComponentProps<'fieldset'>) { return (
[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3", + 'flex flex-col gap-6', + 'has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3', className, )} {...props} @@ -27,17 +25,17 @@ export function FieldSet({ export function FieldLegend({ className, - variant = "legend", + variant = 'legend', ...props -}: React.ComponentProps<"legend"> & { variant?: "legend" | "label" }) { +}: React.ComponentProps<'legend'> & { variant?: 'legend' | 'label' }) { return ( ) { +}: React.ComponentProps<'div'>) { return (
[data-slot=field-group]]:gap-4", + 'group/field-group @container/field-group flex w-full flex-col gap-7 data-[slot=checkbox-group]:gap-3 [&>[data-slot=field-group]]:gap-4', className, )} {...props} @@ -62,34 +60,34 @@ export function FieldGroup({ } const fieldVariants = cva( - "group/field data-[invalid=true]:text-destructive flex w-full gap-3", + 'group/field data-[invalid=true]:text-destructive flex w-full gap-3', { variants: { orientation: { - vertical: ["flex-col [&>*]:w-full [&>.sr-only]:w-auto"], + vertical: ['flex-col [&>*]:w-full [&>.sr-only]:w-auto'], horizontal: [ - "flex-row items-center", - "[&>[data-slot=field-label]]:flex-auto", - "has-[>[data-slot=field-content]]:items-start has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px", + 'flex-row items-center', + '[&>[data-slot=field-label]]:flex-auto', + 'has-[>[data-slot=field-content]]:items-start has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px', ], responsive: [ - "flex-col @md/field-group:flex-row @md/field-group:items-center [&>*]:w-full @md/field-group:[&>*]:w-auto [&>.sr-only]:w-auto", - "@md/field-group:[&>[data-slot=field-label]]:flex-auto", - "@md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px", + 'flex-col @md/field-group:flex-row @md/field-group:items-center [&>*]:w-full @md/field-group:[&>*]:w-auto [&>.sr-only]:w-auto', + '@md/field-group:[&>[data-slot=field-label]]:flex-auto', + '@md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px', ], }, }, defaultVariants: { - orientation: "vertical", + orientation: 'vertical', }, }, ); export function Field({ className, - orientation = "vertical", + orientation = 'vertical', ...props -}: React.ComponentProps<"div"> & VariantProps) { +}: React.ComponentProps<'div'> & VariantProps) { return (
) { +}: React.ComponentProps<'div'>) { return (
[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col has-[>[data-slot=field]]:rounded-md has-[>[data-slot=field]]:border [&>*]:data-[slot=field]:p-4", - "has-data-[state=checked]:bg-primary/5 has-data-[state=checked]:border-primary dark:has-data-[state=checked]:bg-primary/10", + 'group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50', + 'has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col has-[>[data-slot=field]]:rounded-md has-[>[data-slot=field]]:border [&>*]:data-[slot=field]:p-4', + 'has-data-[state=checked]:bg-primary/5 has-data-[state=checked]:border-primary dark:has-data-[state=checked]:bg-primary/10', className, )} {...props} @@ -138,12 +136,12 @@ export function FieldLabel({ export function FieldTitle({ className, ...props -}: React.ComponentProps<"div">) { +}: React.ComponentProps<'div'>) { return (
) { +}: React.ComponentProps<'p'>) { return (

a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4", + 'text-muted-foreground text-sm leading-normal font-normal group-has-[[data-orientation=horizontal]]/field:text-balance', + 'last:mt-0 nth-last-2:-mt-1 [[data-variant=legend]+&]:-mt-1.5', + '[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4', className, )} {...props} @@ -173,7 +171,7 @@ export function FieldSeparator({ children, className, ...props -}: React.ComponentProps<"div"> & { +}: React.ComponentProps<'div'> & { children?: React.ReactNode; }) { return ( @@ -181,7 +179,7 @@ export function FieldSeparator({ data-slot="field-separator" data-content={!!children} className={cn( - "relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2", + 'relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2', className, )} {...props} @@ -204,7 +202,7 @@ export function FieldError({ children, errors: maybeErrors, ...props -}: React.ComponentProps<"div"> & { +}: React.ComponentProps<'div'> & { errors?: ({ message?: string } | undefined)[]; }) { const content = useMemo(() => { @@ -240,7 +238,7 @@ export function FieldError({

{content} diff --git a/packages/ui/src/form.tsx b/packages/ui/src/form.tsx index 65447e4..0a7a38e 100644 --- a/packages/ui/src/form.tsx +++ b/packages/ui/src/form.tsx @@ -1,17 +1,17 @@ -"use client"; +'use client'; -import type { ControllerProps, FieldPath, FieldValues } from "react-hook-form"; -import * as React from "react"; -import { Label } from "@/components/ui/label"; -import { cn } from "@/lib/utils"; -import * as LabelPrimitive from "@radix-ui/react-label"; -import { Slot } from "@radix-ui/react-slot"; +import type { ControllerProps, FieldPath, FieldValues } from 'react-hook-form'; +import * as React from 'react'; +import * as LabelPrimitive from '@radix-ui/react-label'; +import { Slot } from '@radix-ui/react-slot'; import { Controller, FormProvider, useFormContext, useFormState, -} from "react-hook-form"; +} from 'react-hook-form'; + +import { cn, Label } from '@gib/ui'; const Form = FormProvider; @@ -47,7 +47,7 @@ const useFormField = () => { const fieldState = getFieldState(fieldContext.name, formState); if (!fieldContext) { - throw new Error("useFormField should be used within "); + throw new Error('useFormField should be used within '); } const { id } = itemContext; @@ -70,14 +70,14 @@ const FormItemContext = React.createContext( {} as FormItemContextValue, ); -function FormItem({ className, ...props }: React.ComponentProps<"div">) { +function FormItem({ className, ...props }: React.ComponentProps<'div'>) { const id = React.useId(); return (
@@ -94,7 +94,7 @@ function FormLabel({