last commit before trying out opencode on repo
This commit is contained in:
1
apps/expo/.cache/.prettiercache
Normal file
1
apps/expo/.cache/.prettiercache
Normal file
@@ -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"]
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1 +1 @@
|
||||
import "expo-router/entry";
|
||||
import 'expo-router/entry';
|
||||
|
||||
@@ -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'),
|
||||
}),
|
||||
];
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
module.exports = require("@acme/tailwind-config/postcss-config");
|
||||
module.exports = require('@acme/tailwind-config/postcss-config');
|
||||
|
||||
@@ -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() {
|
||||
<Stack
|
||||
screenOptions={{
|
||||
headerStyle: {
|
||||
backgroundColor: "#c03484",
|
||||
backgroundColor: '#c03484',
|
||||
},
|
||||
contentStyle: {
|
||||
backgroundColor: colorScheme == "dark" ? "#09090B" : "#FFFFFF",
|
||||
backgroundColor: colorScheme == 'dark' ? '#09090B' : '#FFFFFF',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -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: {
|
||||
<Link
|
||||
asChild
|
||||
href={{
|
||||
pathname: "/post/[id]",
|
||||
pathname: '/post/[id]',
|
||||
params: { id: props.post.id },
|
||||
}}
|
||||
>
|
||||
@@ -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() {
|
||||
>
|
||||
<Text className="text-foreground">Create</Text>
|
||||
</Pressable>
|
||||
{error?.data?.code === "UNAUTHORIZED" && (
|
||||
{error?.data?.code === 'UNAUTHORIZED' && (
|
||||
<Text className="text-destructive mt-2">
|
||||
You need to be logged in to create a post
|
||||
</Text>
|
||||
@@ -104,20 +104,20 @@ function MobileAuth() {
|
||||
return (
|
||||
<>
|
||||
<Text className="text-foreground pb-2 text-center text-xl font-semibold">
|
||||
{session?.user.name ? `Hello, ${session.user.name}` : "Not logged in"}
|
||||
{session?.user.name ? `Hello, ${session.user.name}` : 'Not logged in'}
|
||||
</Text>
|
||||
<Pressable
|
||||
onPress={() =>
|
||||
session
|
||||
? authClient.signOut()
|
||||
: authClient.signIn.social({
|
||||
provider: "discord",
|
||||
callbackURL: "/",
|
||||
provider: 'discord',
|
||||
callbackURL: '/',
|
||||
})
|
||||
}
|
||||
className="bg-primary flex items-center rounded-sm p-2"
|
||||
>
|
||||
<Text>{session ? "Sign Out" : "Sign In With Discord"}</Text>
|
||||
<Text>{session ? 'Sign Out' : 'Sign In With Discord'}</Text>
|
||||
</Pressable>
|
||||
</>
|
||||
);
|
||||
@@ -138,7 +138,7 @@ export default function Index() {
|
||||
return (
|
||||
<SafeAreaView className="bg-background">
|
||||
{/* Changes page title visible on the header */}
|
||||
<Stack.Screen options={{ title: "Home Page" }} />
|
||||
<Stack.Screen options={{ title: 'Home Page' }} />
|
||||
<View className="bg-background h-full w-full p-4">
|
||||
<Text className="text-foreground pb-2 text-center text-5xl font-bold">
|
||||
Create <Text className="text-primary">T3</Text> Turbo
|
||||
|
||||
@@ -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 }>();
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
@import "tailwindcss";
|
||||
@import "nativewind/theme";
|
||||
@import "@acme/tailwind-config/theme";
|
||||
@import 'tailwindcss';
|
||||
@import 'nativewind/theme';
|
||||
@import '@acme/tailwind-config/theme';
|
||||
|
||||
@@ -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<AppRouter>({
|
||||
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<string, string>();
|
||||
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<AppRouter>({
|
||||
queryClient,
|
||||
});
|
||||
|
||||
export type { RouterInputs, RouterOutputs } from "@acme/api";
|
||||
export type { RouterInputs, RouterOutputs } from '@acme/api';
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
],
|
||||
|
||||
@@ -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`;
|
||||
|
||||
@@ -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);
|
||||
|
||||
1
apps/next/.cache/.prettiercache
Normal file
1
apps/next/.cache/.prettiercache
Normal file
@@ -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"]
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "@gib/nextjs",
|
||||
"name": "@gib/next",
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
|
||||
@@ -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 (
|
||||
<form>
|
||||
<Button
|
||||
size="lg"
|
||||
formAction={async () => {
|
||||
"use server";
|
||||
const res = await auth.api.signInSocial({
|
||||
body: {
|
||||
provider: "discord",
|
||||
callbackURL: "/",
|
||||
},
|
||||
});
|
||||
if (!res.url) {
|
||||
throw new Error("No URL returned from signInSocial");
|
||||
}
|
||||
redirect(res.url);
|
||||
}}
|
||||
>
|
||||
Sign in with Discord
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center gap-4">
|
||||
<p className="text-center text-2xl">
|
||||
<span>Logged in as {session.user.name}</span>
|
||||
</p>
|
||||
|
||||
<form>
|
||||
<Button
|
||||
size="lg"
|
||||
formAction={async () => {
|
||||
"use server";
|
||||
await auth.api.signOut({
|
||||
headers: await headers(),
|
||||
});
|
||||
redirect("/");
|
||||
}}
|
||||
>
|
||||
Sign out
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<form
|
||||
className="w-full max-w-2xl"
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
void form.handleSubmit();
|
||||
}}
|
||||
>
|
||||
<FieldGroup>
|
||||
<form.Field
|
||||
name="title"
|
||||
children={(field) => {
|
||||
const isInvalid =
|
||||
field.state.meta.isTouched && !field.state.meta.isValid;
|
||||
return (
|
||||
<Field data-invalid={isInvalid}>
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor={field.name}>Bug Title</FieldLabel>
|
||||
</FieldContent>
|
||||
<Input
|
||||
id={field.name}
|
||||
name={field.name}
|
||||
value={field.state.value}
|
||||
onBlur={field.handleBlur}
|
||||
onChange={(e) => field.handleChange(e.target.value)}
|
||||
aria-invalid={isInvalid}
|
||||
placeholder="Title"
|
||||
/>
|
||||
{isInvalid && <FieldError errors={field.state.meta.errors} />}
|
||||
</Field>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<form.Field
|
||||
name="content"
|
||||
children={(field) => {
|
||||
const isInvalid =
|
||||
field.state.meta.isTouched && !field.state.meta.isValid;
|
||||
return (
|
||||
<Field data-invalid={isInvalid}>
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor={field.name}>Content</FieldLabel>
|
||||
</FieldContent>
|
||||
<Input
|
||||
id={field.name}
|
||||
name={field.name}
|
||||
value={field.state.value}
|
||||
onBlur={field.handleBlur}
|
||||
onChange={(e) => field.handleChange(e.target.value)}
|
||||
aria-invalid={isInvalid}
|
||||
placeholder="Content"
|
||||
/>
|
||||
{isInvalid && <FieldError errors={field.state.meta.errors} />}
|
||||
</Field>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</FieldGroup>
|
||||
<Button type="submit">Create</Button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
export function PostList() {
|
||||
const trpc = useTRPC();
|
||||
const { data: posts } = useSuspenseQuery(trpc.post.all.queryOptions());
|
||||
|
||||
if (posts.length === 0) {
|
||||
return (
|
||||
<div className="relative flex w-full flex-col gap-4">
|
||||
<PostCardSkeleton pulse={false} />
|
||||
<PostCardSkeleton pulse={false} />
|
||||
<PostCardSkeleton pulse={false} />
|
||||
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center bg-black/10">
|
||||
<p className="text-2xl font-bold text-white">No posts yet</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-col gap-4">
|
||||
{posts.map((p) => {
|
||||
return <PostCard key={p.id} post={p} />;
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className="bg-muted flex flex-row rounded-lg p-4">
|
||||
<div className="grow">
|
||||
<h2 className="text-primary text-2xl font-bold">{props.post.title}</h2>
|
||||
<p className="mt-2 text-sm">{props.post.content}</p>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="text-primary cursor-pointer text-sm font-bold uppercase hover:bg-transparent hover:text-white"
|
||||
onClick={() => deletePost.mutate(props.post.id)}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function PostCardSkeleton(props: { pulse?: boolean }) {
|
||||
const { pulse = true } = props;
|
||||
return (
|
||||
<div className="bg-muted flex flex-row rounded-lg p-4">
|
||||
<div className="grow">
|
||||
<h2
|
||||
className={cn(
|
||||
"bg-primary w-1/4 rounded-sm text-2xl font-bold",
|
||||
pulse && "animate-pulse",
|
||||
)}
|
||||
>
|
||||
|
||||
</h2>
|
||||
<p
|
||||
className={cn(
|
||||
"mt-2 w-1/3 rounded-sm bg-current text-sm",
|
||||
pulse && "animate-pulse",
|
||||
)}
|
||||
>
|
||||
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
import { auth } from "~/auth/server";
|
||||
|
||||
export const GET = auth.handler;
|
||||
export const POST = auth.handler;
|
||||
@@ -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 };
|
||||
65
apps/next/src/app/global-error.tsx
Normal file
65
apps/next/src/app/global-error.tsx
Normal file
@@ -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 (
|
||||
<PlausibleProvider
|
||||
domain="studybuddy.gbrown.org"
|
||||
customDomain="https://plausible.gbrown.org"
|
||||
>
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<body
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||
>
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="system"
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
>
|
||||
<ConvexClientProvider>
|
||||
<main className="flex min-h-[90vh] flex-col items-center">
|
||||
<NextError statusCode={0} />
|
||||
{reset !== undefined && (
|
||||
<Button onClick={() => reset()}>Try Again</Button>
|
||||
)}
|
||||
<Toaster />
|
||||
</main>
|
||||
</ConvexClientProvider>
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
</PlausibleProvider>
|
||||
);
|
||||
};
|
||||
export default GlobalError;
|
||||
@@ -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 (
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<body
|
||||
className={cn(
|
||||
"bg-background text-foreground min-h-screen font-sans antialiased",
|
||||
geistSans.variable,
|
||||
geistMono.variable,
|
||||
)}
|
||||
<ConvexAuthNextjsServerProvider>
|
||||
<PlausibleProvider
|
||||
domain='studybuddy.gbrown.org'
|
||||
customDomain='https://plausible.gbrown.org'
|
||||
>
|
||||
<ThemeProvider>
|
||||
<TRPCReactProvider>{props.children}</TRPCReactProvider>
|
||||
<div className="absolute right-4 bottom-4">
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
<Toaster />
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
<html lang='en'>
|
||||
<body
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||
>
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="system"
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
>
|
||||
<ConvexClientProvider>
|
||||
{children}
|
||||
<Toaster />
|
||||
</ConvexClientProvider>
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
</PlausibleProvider>
|
||||
</ConvexAuthNextjsServerProvider>
|
||||
);
|
||||
}
|
||||
};
|
||||
export default RootLayout;
|
||||
|
||||
@@ -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 (
|
||||
<HydrateClient>
|
||||
<main className="container h-screen py-16">
|
||||
<div className="flex flex-col items-center justify-center gap-4">
|
||||
<h1 className="text-5xl font-extrabold tracking-tight sm:text-[5rem]">
|
||||
Create <span className="text-primary">T3</span> Turbo
|
||||
</h1>
|
||||
<AuthShowcase />
|
||||
|
||||
<CreatePostForm />
|
||||
<div className="w-full max-w-2xl overflow-y-scroll">
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="flex w-full flex-col gap-4">
|
||||
<PostCardSkeleton />
|
||||
<PostCardSkeleton />
|
||||
<PostCardSkeleton />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<PostList />
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</HydrateClient>
|
||||
<main>
|
||||
<h1 className='w-full text-center items-center justify-center'>Welcome to the Home Page</h1>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
};
|
||||
export default Home;
|
||||
|
||||
@@ -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 *));
|
||||
|
||||
15
apps/next/src/components/providers/ConvexClientProvider.tsx
Normal file
15
apps/next/src/components/providers/ConvexClientProvider.tsx
Normal file
@@ -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 (
|
||||
<ConvexAuthNextjsProvider client={convex}>
|
||||
{children}
|
||||
</ConvexAuthNextjsProvider>
|
||||
);
|
||||
};
|
||||
70
apps/next/src/components/providers/ThemeProvider.tsx
Normal file
70
apps/next/src/components/providers/ThemeProvider.tsx
Normal file
@@ -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<typeof NextThemesProvider>) => {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
if (!mounted) return null;
|
||||
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
|
||||
};
|
||||
|
||||
type ThemeToggleProps = {
|
||||
size?: number;
|
||||
buttonProps?: Omit<ComponentProps<typeof Button>, 'onClick'>;
|
||||
};
|
||||
|
||||
const ThemeToggle = ({ size = 1, buttonProps }: ThemeToggleProps) => {
|
||||
const { setTheme, resolvedTheme } = useTheme();
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
if (!mounted) {
|
||||
return (
|
||||
<Button {...buttonProps}>
|
||||
<span style={{ height: `${size}rem`, width: `${size}rem` }} />
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
const toggleTheme = () => {
|
||||
if (resolvedTheme === 'dark') setTheme('light');
|
||||
else setTheme('dark');
|
||||
};
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
{...buttonProps}
|
||||
onClick={toggleTheme}
|
||||
className={cn('cursor-pointer', buttonProps?.className)}
|
||||
>
|
||||
<Sun
|
||||
style={{ height: `${size}rem`, width: `${size}rem` }}
|
||||
className="scale-100 rotate-0 transition-all dark:scale-0 dark:-rotate-90"
|
||||
/>
|
||||
<Moon
|
||||
style={{ height: `${size}rem`, width: `${size}rem` }}
|
||||
className="absolute scale-0 rotate-90 transition-all dark:scale-100 dark:rotate-0"
|
||||
/>
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export { ThemeProvider, ThemeToggle, type ThemeToggleProps };
|
||||
7
apps/next/src/components/providers/index.tsx
Normal file
7
apps/next/src/components/providers/index.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
export { ConvexClientProvider } from './ConvexClientProvider';
|
||||
//export { NotificationsPermission } from './notification-permission';
|
||||
export {
|
||||
ThemeProvider,
|
||||
ThemeToggle,
|
||||
type ThemeToggleProps,
|
||||
} from './ThemeProvider';
|
||||
@@ -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',
|
||||
});
|
||||
28
apps/next/src/instrumentation-client.ts
Normal file
28
apps/next/src/instrumentation-client.ts
Normal file
@@ -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;
|
||||
7
apps/next/src/instrumentation.ts
Normal file
7
apps/next/src/instrumentation.ts
Normal file
@@ -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);
|
||||
};
|
||||
369
apps/next/src/lib/metadata.ts
Normal file
369
apps/next/src/lib/metadata.ts
Normal file
@@ -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,
|
||||
},
|
||||
},
|
||||
*/
|
||||
};
|
||||
};
|
||||
10
apps/next/src/sentry.server.config.ts
Normal file
10
apps/next/src/sentry.server.config.ts
Normal file
@@ -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,
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "convex-turbo",
|
||||
"name": "studybuddy-turbo",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": "^22.20.0"
|
||||
|
||||
@@ -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"]
|
||||
[["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"]
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
|
||||
4
packages/backend/convex/_generated/api.d.ts
vendored
4
packages/backend/convex/_generated/api.d.ts
vendored
@@ -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;
|
||||
}>;
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,7 +2,7 @@ export default {
|
||||
providers: [
|
||||
{
|
||||
domain: process.env.CONVEX_SITE_URL,
|
||||
applicationID: "convex",
|
||||
applicationID: 'convex',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -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<Doc<'users'>> => {
|
||||
const user = await ctx.db.get(userId);
|
||||
if (!user) throw new ConvexError('User not found.');
|
||||
|
||||
@@ -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<DataModel>({
|
||||
profile(params, ctx) {
|
||||
return {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { httpRouter } from 'convex/server';
|
||||
|
||||
import { auth } from './auth';
|
||||
|
||||
const http = httpRouter();
|
||||
|
||||
@@ -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 = [];
|
||||
|
||||
236
packages/backend/convex/quiz.ts
Normal file
236
packages/backend/convex/quiz.ts
Normal file
@@ -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,
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -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({
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -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,
|
||||
|
||||
@@ -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({
|
||||
<AvatarPrimitive.Root
|
||||
data-slot="avatar"
|
||||
className={cn(
|
||||
"relative flex size-8 shrink-0 overflow-hidden rounded-full",
|
||||
'relative flex size-8 shrink-0 overflow-hidden rounded-full',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -27,7 +28,7 @@ function AvatarImage({
|
||||
return (
|
||||
<AvatarPrimitive.Image
|
||||
data-slot="avatar-image"
|
||||
className={cn("aspect-square size-full", className)}
|
||||
className={cn('aspect-square size-full', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
@@ -41,7 +42,7 @@ function AvatarFallback({
|
||||
<AvatarPrimitive.Fallback
|
||||
data-slot="avatar-fallback"
|
||||
className={cn(
|
||||
"bg-muted flex size-full items-center justify-center rounded-full",
|
||||
'bg-muted flex size-full items-center justify-center rounded-full',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
"use client";
|
||||
'use client';
|
||||
|
||||
import { type ComponentProps } from "react";
|
||||
import { AvatarImage } from "@/components/ui/avatar";
|
||||
import { cn } from "@/lib/utils";
|
||||
import * as AvatarPrimitive from "@radix-ui/react-avatar";
|
||||
import { User } from "lucide-react";
|
||||
import { type ComponentProps } from 'react';
|
||||
import * as AvatarPrimitive from '@radix-ui/react-avatar';
|
||||
import { User } from 'lucide-react';
|
||||
|
||||
import { AvatarImage, cn } from '@gib/ui';
|
||||
|
||||
type BasedAvatarProps = ComponentProps<typeof AvatarPrimitive.Root> & {
|
||||
src?: string | null;
|
||||
fullName?: string | null;
|
||||
imageProps?: Omit<ComponentProps<typeof AvatarImage>, "data-slot">;
|
||||
imageProps?: Omit<ComponentProps<typeof AvatarImage>, 'data-slot'>;
|
||||
fallbackProps?: ComponentProps<typeof AvatarPrimitive.Fallback>;
|
||||
userIconProps?: ComponentProps<typeof User>;
|
||||
};
|
||||
@@ -29,7 +29,7 @@ const BasedAvatar = ({
|
||||
<AvatarPrimitive.Root
|
||||
data-slot="avatar"
|
||||
className={cn(
|
||||
"relative flex size-8 shrink-0 cursor-pointer overflow-hidden rounded-full",
|
||||
'relative flex size-8 shrink-0 cursor-pointer overflow-hidden rounded-full',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -45,20 +45,20 @@ const BasedAvatar = ({
|
||||
{...fallbackProps}
|
||||
data-slot="avatar-fallback"
|
||||
className={cn(
|
||||
"bg-muted flex size-full items-center justify-center rounded-full",
|
||||
'bg-muted flex size-full items-center justify-center rounded-full',
|
||||
fallbackProps?.className,
|
||||
)}
|
||||
>
|
||||
{fullName ? (
|
||||
fullName
|
||||
.split(" ")
|
||||
.split(' ')
|
||||
.map((n) => n[0])
|
||||
.join("")
|
||||
.join('')
|
||||
.toUpperCase()
|
||||
) : (
|
||||
<User
|
||||
{...userIconProps}
|
||||
className={cn("", userIconProps?.className)}
|
||||
className={cn('', userIconProps?.className)}
|
||||
/>
|
||||
)}
|
||||
</AvatarPrimitive.Fallback>
|
||||
|
||||
@@ -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 = ({
|
||||
<ProgressPrimitive.Root
|
||||
data-slot="progress"
|
||||
className={cn(
|
||||
"bg-primary/20 relative h-2 w-full overflow-hidden rounded-full",
|
||||
'bg-primary/20 relative h-2 w-full overflow-hidden rounded-full',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import type { VariantProps } from "class-variance-authority";
|
||||
import * as React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { cva } from "class-variance-authority";
|
||||
import type { VariantProps } from 'class-variance-authority';
|
||||
import * as React from 'react';
|
||||
import { Slot } from '@radix-ui/react-slot';
|
||||
import { cva } from 'class-variance-authority';
|
||||
|
||||
import { cn } from '@gib/ui';
|
||||
|
||||
const buttonVariants = cva(
|
||||
"focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
@@ -10,27 +11,27 @@ const buttonVariants = cva(
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"bg-primary text-primary-foreground hover:bg-primary/90 shadow-xs",
|
||||
'bg-primary text-primary-foreground hover:bg-primary/90 shadow-xs',
|
||||
destructive:
|
||||
"bg-destructive hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60 text-white shadow-xs",
|
||||
'bg-destructive hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60 text-white shadow-xs',
|
||||
outline:
|
||||
"bg-background hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 border shadow-xs",
|
||||
'bg-background hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 border shadow-xs',
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80 shadow-xs",
|
||||
'bg-secondary text-secondary-foreground hover:bg-secondary/80 shadow-xs',
|
||||
ghost:
|
||||
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
|
||||
link: 'text-primary underline-offset-4 hover:underline',
|
||||
},
|
||||
size: {
|
||||
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",
|
||||
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<typeof buttonVariants> & {
|
||||
asChild?: boolean;
|
||||
}) {
|
||||
const Comp = asChild ? Slot : "button";
|
||||
const Comp = asChild ? Slot : 'button';
|
||||
|
||||
return (
|
||||
<Comp
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import * as React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import * as React from 'react';
|
||||
|
||||
function Card({ className, ...props }: React.ComponentProps<"div">) {
|
||||
import { cn } from '@gib/ui';
|
||||
|
||||
function Card({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card"
|
||||
className={cn(
|
||||
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
|
||||
'bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -14,12 +15,12 @@ function Card({ className, ...props }: React.ComponentProps<"div">) {
|
||||
);
|
||||
}
|
||||
|
||||
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
function CardHeader({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-header"
|
||||
className={cn(
|
||||
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
|
||||
'@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -27,32 +28,32 @@ function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
);
|
||||
}
|
||||
|
||||
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
function CardTitle({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-title"
|
||||
className={cn("leading-none font-semibold", className)}
|
||||
className={cn('leading-none font-semibold', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
||||
function CardDescription({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
className={cn('text-muted-foreground text-sm', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
||||
function CardAction({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-action"
|
||||
className={cn(
|
||||
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
||||
'col-start-2 row-span-2 row-start-1 self-start justify-self-end',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -60,21 +61,21 @@ function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
||||
);
|
||||
}
|
||||
|
||||
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
function CardContent({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-content"
|
||||
className={cn("px-6", className)}
|
||||
className={cn('px-6', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
function CardFooter({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-footer"
|
||||
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
|
||||
className={cn('flex items-center px-6 [.border-t]:pt-6', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -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({
|
||||
<CheckboxPrimitive.Root
|
||||
data-slot="checkbox"
|
||||
className={cn(
|
||||
"peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||
'peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
"use client";
|
||||
'use client';
|
||||
|
||||
import * as React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Drawer as DrawerPrimitive } from "vaul";
|
||||
import * as React from 'react';
|
||||
import { Drawer as DrawerPrimitive } from 'vaul';
|
||||
|
||||
import { cn } from '@gib/ui';
|
||||
|
||||
function Drawer({
|
||||
...props
|
||||
@@ -36,7 +37,7 @@ function DrawerOverlay({
|
||||
<DrawerPrimitive.Overlay
|
||||
data-slot="drawer-overlay"
|
||||
className={cn(
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -55,11 +56,11 @@ function DrawerContent({
|
||||
<DrawerPrimitive.Content
|
||||
data-slot="drawer-content"
|
||||
className={cn(
|
||||
"group/drawer-content bg-background fixed z-50 flex h-auto flex-col",
|
||||
"data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=top]:mb-24 data-[vaul-drawer-direction=top]:max-h-[80vh] data-[vaul-drawer-direction=top]:rounded-b-lg data-[vaul-drawer-direction=top]:border-b",
|
||||
"data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=bottom]:rounded-t-lg data-[vaul-drawer-direction=bottom]:border-t",
|
||||
"data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=right]:border-l data-[vaul-drawer-direction=right]:sm:max-w-sm",
|
||||
"data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=left]:border-r data-[vaul-drawer-direction=left]:sm:max-w-sm",
|
||||
'group/drawer-content bg-background fixed z-50 flex h-auto flex-col',
|
||||
'data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=top]:mb-24 data-[vaul-drawer-direction=top]:max-h-[80vh] data-[vaul-drawer-direction=top]:rounded-b-lg data-[vaul-drawer-direction=top]:border-b',
|
||||
'data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=bottom]:rounded-t-lg data-[vaul-drawer-direction=bottom]:border-t',
|
||||
'data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=right]:border-l data-[vaul-drawer-direction=right]:sm:max-w-sm',
|
||||
'data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=left]:border-r data-[vaul-drawer-direction=left]:sm:max-w-sm',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -71,12 +72,12 @@ function DrawerContent({
|
||||
);
|
||||
}
|
||||
|
||||
function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
function DrawerHeader({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="drawer-header"
|
||||
className={cn(
|
||||
"flex flex-col gap-0.5 p-4 group-data-[vaul-drawer-direction=bottom]/drawer-content:text-center group-data-[vaul-drawer-direction=top]/drawer-content:text-center md:gap-1.5 md:text-left",
|
||||
'flex flex-col gap-0.5 p-4 group-data-[vaul-drawer-direction=bottom]/drawer-content:text-center group-data-[vaul-drawer-direction=top]/drawer-content:text-center md:gap-1.5 md:text-left',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -84,11 +85,11 @@ function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
);
|
||||
}
|
||||
|
||||
function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
function DrawerFooter({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="drawer-footer"
|
||||
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
|
||||
className={cn('mt-auto flex flex-col gap-2 p-4', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
@@ -101,7 +102,7 @@ function DrawerTitle({
|
||||
return (
|
||||
<DrawerPrimitive.Title
|
||||
data-slot="drawer-title"
|
||||
className={cn("text-foreground font-semibold", className)}
|
||||
className={cn('text-foreground font-semibold', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
@@ -114,7 +115,7 @@ function DrawerDescription({
|
||||
return (
|
||||
<DrawerPrimitive.Description
|
||||
data-slot="drawer-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
className={cn('text-muted-foreground text-sm', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -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<typeof DropdownMenuPrimitive.Item> & {
|
||||
inset?: boolean;
|
||||
variant?: "default" | "destructive";
|
||||
variant?: 'default' | 'destructive';
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Item
|
||||
@@ -154,7 +155,7 @@ function DropdownMenuLabel({
|
||||
data-slot="dropdown-menu-label"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
|
||||
'px-2 py-1.5 text-sm font-medium data-[inset]:pl-8',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -169,7 +170,7 @@ function DropdownMenuSeparator({
|
||||
return (
|
||||
<DropdownMenuPrimitive.Separator
|
||||
data-slot="dropdown-menu-separator"
|
||||
className={cn("bg-border -mx-1 my-1 h-px", className)}
|
||||
className={cn('bg-border -mx-1 my-1 h-px', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
@@ -178,12 +179,12 @@ function DropdownMenuSeparator({
|
||||
function DropdownMenuShortcut({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) {
|
||||
}: React.ComponentProps<'span'>) {
|
||||
return (
|
||||
<span
|
||||
data-slot="dropdown-menu-shortcut"
|
||||
className={cn(
|
||||
"text-muted-foreground ml-auto text-xs tracking-widest",
|
||||
'text-muted-foreground ml-auto text-xs tracking-widest',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -210,7 +211,7 @@ function DropdownMenuSubTrigger({
|
||||
data-slot="dropdown-menu-sub-trigger"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8",
|
||||
'focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -229,7 +230,7 @@ function DropdownMenuSubContent({
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
data-slot="dropdown-menu-sub-content"
|
||||
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 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
|
||||
'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 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -1,23 +1,21 @@
|
||||
"use client";
|
||||
'use client';
|
||||
|
||||
import type { VariantProps } from "class-variance-authority";
|
||||
import { useMemo } from "react";
|
||||
import { cva } from "class-variance-authority";
|
||||
import type { VariantProps } from 'class-variance-authority';
|
||||
import { useMemo } from 'react';
|
||||
import { cva } from 'class-variance-authority';
|
||||
|
||||
import { cn } from "@acme/ui";
|
||||
import { Label } from "@acme/ui/label";
|
||||
import { Separator } from "@acme/ui/separator";
|
||||
import { cn, Label, Separator } from '@gib/ui';
|
||||
|
||||
export function FieldSet({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"fieldset">) {
|
||||
}: React.ComponentProps<'fieldset'>) {
|
||||
return (
|
||||
<fieldset
|
||||
data-slot="field-set"
|
||||
className={cn(
|
||||
"flex flex-col gap-6",
|
||||
"has-[>[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 (
|
||||
<legend
|
||||
data-slot="field-legend"
|
||||
data-variant={variant}
|
||||
className={cn(
|
||||
"mb-3 font-medium",
|
||||
"data-[variant=legend]:text-base",
|
||||
"data-[variant=label]:text-sm",
|
||||
'mb-3 font-medium',
|
||||
'data-[variant=legend]:text-base',
|
||||
'data-[variant=label]:text-sm',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -48,12 +46,12 @@ export function FieldLegend({
|
||||
export function FieldGroup({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div">) {
|
||||
}: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="field-group"
|
||||
className={cn(
|
||||
"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",
|
||||
'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<typeof fieldVariants>) {
|
||||
}: React.ComponentProps<'div'> & VariantProps<typeof fieldVariants>) {
|
||||
return (
|
||||
<div
|
||||
role="group"
|
||||
@@ -104,12 +102,12 @@ export function Field({
|
||||
export function FieldContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div">) {
|
||||
}: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="field-content"
|
||||
className={cn(
|
||||
"group/field-content flex flex-1 flex-col gap-1.5 leading-snug",
|
||||
'group/field-content flex flex-1 flex-col gap-1.5 leading-snug',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -125,9 +123,9 @@ export function FieldLabel({
|
||||
<Label
|
||||
data-slot="field-label"
|
||||
className={cn(
|
||||
"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",
|
||||
'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 (
|
||||
<div
|
||||
data-slot="field-label"
|
||||
className={cn(
|
||||
"flex w-fit items-center gap-2 text-sm leading-snug font-medium group-data-[disabled=true]/field:opacity-50",
|
||||
'flex w-fit items-center gap-2 text-sm leading-snug font-medium group-data-[disabled=true]/field:opacity-50',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -154,14 +152,14 @@ export function FieldTitle({
|
||||
export function FieldDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"p">) {
|
||||
}: React.ComponentProps<'p'>) {
|
||||
return (
|
||||
<p
|
||||
data-slot="field-description"
|
||||
className={cn(
|
||||
"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",
|
||||
'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({
|
||||
<div
|
||||
role="alert"
|
||||
data-slot="field-error"
|
||||
className={cn("text-destructive text-sm font-normal", className)}
|
||||
className={cn('text-destructive text-sm font-normal', className)}
|
||||
{...props}
|
||||
>
|
||||
{content}
|
||||
|
||||
@@ -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 <FormField>");
|
||||
throw new Error('useFormField should be used within <FormField>');
|
||||
}
|
||||
|
||||
const { id } = itemContext;
|
||||
@@ -70,14 +70,14 @@ const FormItemContext = React.createContext<FormItemContextValue>(
|
||||
{} as FormItemContextValue,
|
||||
);
|
||||
|
||||
function FormItem({ className, ...props }: React.ComponentProps<"div">) {
|
||||
function FormItem({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
const id = React.useId();
|
||||
|
||||
return (
|
||||
<FormItemContext.Provider value={{ id }}>
|
||||
<div
|
||||
data-slot="form-item"
|
||||
className={cn("grid gap-2", className)}
|
||||
className={cn('grid gap-2', className)}
|
||||
{...props}
|
||||
/>
|
||||
</FormItemContext.Provider>
|
||||
@@ -94,7 +94,7 @@ function FormLabel({
|
||||
<Label
|
||||
data-slot="form-label"
|
||||
data-error={!!error}
|
||||
className={cn("data-[error=true]:text-destructive", className)}
|
||||
className={cn('data-[error=true]:text-destructive', className)}
|
||||
htmlFor={formItemId}
|
||||
{...props}
|
||||
/>
|
||||
@@ -120,22 +120,22 @@ function FormControl({ ...props }: React.ComponentProps<typeof Slot>) {
|
||||
);
|
||||
}
|
||||
|
||||
function FormDescription({ className, ...props }: React.ComponentProps<"p">) {
|
||||
function FormDescription({ className, ...props }: React.ComponentProps<'p'>) {
|
||||
const { formDescriptionId } = useFormField();
|
||||
|
||||
return (
|
||||
<p
|
||||
data-slot="form-description"
|
||||
id={formDescriptionId}
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
className={cn('text-muted-foreground text-sm', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
|
||||
function FormMessage({ className, ...props }: React.ComponentProps<'p'>) {
|
||||
const { error, formMessageId } = useFormField();
|
||||
const body = error ? String(error?.message ?? "") : props.children;
|
||||
const body = error ? String(error?.message ?? '') : props.children;
|
||||
|
||||
if (!body) {
|
||||
return null;
|
||||
@@ -145,7 +145,7 @@ function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
|
||||
<p
|
||||
data-slot="form-message"
|
||||
id={formMessageId}
|
||||
className={cn("text-destructive text-sm", className)}
|
||||
className={cn('text-destructive text-sm', className)}
|
||||
{...props}
|
||||
>
|
||||
{body}
|
||||
|
||||
@@ -1,16 +1,109 @@
|
||||
import { cx } from "class-variance-authority";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { cx } from 'class-variance-authority';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export const cn = (...inputs: Parameters<typeof cx>) => twMerge(cx(inputs));
|
||||
|
||||
export const ccn = ({
|
||||
context,
|
||||
className,
|
||||
on = "",
|
||||
off = "",
|
||||
on = '',
|
||||
off = '',
|
||||
}: {
|
||||
context: boolean;
|
||||
className: string;
|
||||
on: string;
|
||||
off: string;
|
||||
}) => twMerge(className, context ? on : off);
|
||||
|
||||
export { Avatar, AvatarImage, AvatarFallback } from './avatar';
|
||||
export { BasedAvatar } from './based-avatar';
|
||||
export { BasedProgress } from './based-progress';
|
||||
export { Button, buttonVariants } from './button';
|
||||
export {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardFooter,
|
||||
CardTitle,
|
||||
CardAction,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
} from './card';
|
||||
export { Checkbox } from './checkbox';
|
||||
export {
|
||||
Drawer,
|
||||
DrawerPortal,
|
||||
DrawerOverlay,
|
||||
DrawerTrigger,
|
||||
DrawerClose,
|
||||
DrawerContent,
|
||||
DrawerHeader,
|
||||
DrawerFooter,
|
||||
DrawerTitle,
|
||||
DrawerDescription,
|
||||
} from './drawer';
|
||||
export {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from './dropdown-menu';
|
||||
export {
|
||||
useFormField,
|
||||
Form,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormMessage,
|
||||
FormField,
|
||||
} from './form';
|
||||
export {
|
||||
type ImageCropProps,
|
||||
type ImageCropApplyProps,
|
||||
type ImageCropContentProps,
|
||||
type ImageCropResetProps,
|
||||
type CropperProps,
|
||||
Cropper,
|
||||
ImageCrop,
|
||||
ImageCropApply,
|
||||
ImageCropContent,
|
||||
ImageCropReset,
|
||||
} from './shadcn-io/image-crop';
|
||||
export { Input } from './input';
|
||||
export {
|
||||
InputOTP,
|
||||
InputOTPGroup,
|
||||
InputOTPSlot,
|
||||
InputOTPSeparator,
|
||||
} from './input-otp';
|
||||
export { Label } from './label';
|
||||
export {
|
||||
Pagination,
|
||||
PaginationContent,
|
||||
PaginationLink,
|
||||
PaginationItem,
|
||||
PaginationPrevious,
|
||||
PaginationNext,
|
||||
PaginationEllipsis,
|
||||
} from './pagination';
|
||||
export { Progress } from './progress';
|
||||
export { ScrollArea, ScrollBar } from './scroll-area';
|
||||
export { Separator } from './separator';
|
||||
export { StatusMessage } from './status-message';
|
||||
export { SubmitButton } from './submit-button';
|
||||
export { Switch } from './switch';
|
||||
export {
|
||||
Table,
|
||||
TableHeader,
|
||||
TableBody,
|
||||
TableFooter,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableCaption,
|
||||
} from './table';
|
||||
export { Tabs, TabsList, TabsTrigger, TabsContent } from './tabs';
|
||||
export { Toaster } from './sonner';
|
||||
export { ThemeProvider, ThemeToggle, type ThemeToggleProps } from './theme';
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
"use client";
|
||||
'use client';
|
||||
|
||||
import * as React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { OTPInput, OTPInputContext } from "input-otp";
|
||||
import { MinusIcon } from "lucide-react";
|
||||
import * as React from 'react';
|
||||
import { OTPInput, OTPInputContext } from 'input-otp';
|
||||
import { MinusIcon } from 'lucide-react';
|
||||
|
||||
import { cn } from '@gib/ui';
|
||||
|
||||
function InputOTP({
|
||||
className,
|
||||
@@ -16,20 +17,20 @@ function InputOTP({
|
||||
<OTPInput
|
||||
data-slot="input-otp"
|
||||
containerClassName={cn(
|
||||
"flex items-center gap-2 has-disabled:opacity-50",
|
||||
'flex items-center gap-2 has-disabled:opacity-50',
|
||||
containerClassName,
|
||||
)}
|
||||
className={cn("disabled:cursor-not-allowed", className)}
|
||||
className={cn('disabled:cursor-not-allowed', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function InputOTPGroup({ className, ...props }: React.ComponentProps<"div">) {
|
||||
function InputOTPGroup({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="input-otp-group"
|
||||
className={cn("flex items-center", className)}
|
||||
className={cn('flex items-center', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
@@ -39,7 +40,7 @@ function InputOTPSlot({
|
||||
index,
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & {
|
||||
}: React.ComponentProps<'div'> & {
|
||||
index: number;
|
||||
}) {
|
||||
const inputOTPContext = React.useContext(OTPInputContext);
|
||||
@@ -50,7 +51,7 @@ function InputOTPSlot({
|
||||
data-slot="input-otp-slot"
|
||||
data-active={isActive}
|
||||
className={cn(
|
||||
"data-[active=true]:border-ring data-[active=true]:ring-ring/50 data-[active=true]:aria-invalid:ring-destructive/20 dark:data-[active=true]:aria-invalid:ring-destructive/40 aria-invalid:border-destructive data-[active=true]:aria-invalid:border-destructive dark:bg-input/30 border-input relative flex h-9 w-9 items-center justify-center border-y border-r text-sm shadow-xs transition-all outline-none first:rounded-l-md first:border-l last:rounded-r-md data-[active=true]:z-10 data-[active=true]:ring-[3px]",
|
||||
'data-[active=true]:border-ring data-[active=true]:ring-ring/50 data-[active=true]:aria-invalid:ring-destructive/20 dark:data-[active=true]:aria-invalid:ring-destructive/40 aria-invalid:border-destructive data-[active=true]:aria-invalid:border-destructive dark:bg-input/30 border-input relative flex h-9 w-9 items-center justify-center border-y border-r text-sm shadow-xs transition-all outline-none first:rounded-l-md first:border-l last:rounded-r-md data-[active=true]:z-10 data-[active=true]:ring-[3px]',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -65,7 +66,7 @@ function InputOTPSlot({
|
||||
);
|
||||
}
|
||||
|
||||
function InputOTPSeparator({ ...props }: React.ComponentProps<"div">) {
|
||||
function InputOTPSeparator({ ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div data-slot="input-otp-separator" role="separator" {...props}>
|
||||
<MinusIcon />
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import * as React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import * as React from 'react';
|
||||
|
||||
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
||||
import { cn } from '@gib/ui';
|
||||
|
||||
function Input({ className, type, ...props }: React.ComponentProps<'input'>) {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
data-slot="input"
|
||||
className={cn(
|
||||
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
||||
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
'file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
|
||||
'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',
|
||||
'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
"use client";
|
||||
'use client';
|
||||
|
||||
import * as React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import * as LabelPrimitive from "@radix-ui/react-label";
|
||||
import * as React from 'react';
|
||||
import * as LabelPrimitive from '@radix-ui/react-label';
|
||||
|
||||
import { cn } from '@gib/ui';
|
||||
|
||||
function Label({
|
||||
className,
|
||||
@@ -12,7 +13,7 @@ function Label({
|
||||
<LabelPrimitive.Root
|
||||
data-slot="label"
|
||||
className={cn(
|
||||
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
|
||||
'flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import * as React from "react";
|
||||
import { Button, buttonVariants } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
import * as React from 'react';
|
||||
import {
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
MoreHorizontalIcon,
|
||||
} from "lucide-react";
|
||||
} from 'lucide-react';
|
||||
|
||||
function Pagination({ className, ...props }: React.ComponentProps<"nav">) {
|
||||
import { Button, buttonVariants, cn } from '@gib/ui';
|
||||
|
||||
function Pagination({ className, ...props }: React.ComponentProps<'nav'>) {
|
||||
return (
|
||||
<nav
|
||||
role="navigation"
|
||||
aria-label="pagination"
|
||||
data-slot="pagination"
|
||||
className={cn("mx-auto flex w-full justify-center", className)}
|
||||
className={cn('mx-auto flex w-full justify-center', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
@@ -22,39 +22,39 @@ function Pagination({ className, ...props }: React.ComponentProps<"nav">) {
|
||||
function PaginationContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"ul">) {
|
||||
}: React.ComponentProps<'ul'>) {
|
||||
return (
|
||||
<ul
|
||||
data-slot="pagination-content"
|
||||
className={cn("flex flex-row items-center gap-1", className)}
|
||||
className={cn('flex flex-row items-center gap-1', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function PaginationItem({ ...props }: React.ComponentProps<"li">) {
|
||||
function PaginationItem({ ...props }: React.ComponentProps<'li'>) {
|
||||
return <li data-slot="pagination-item" {...props} />;
|
||||
}
|
||||
|
||||
type PaginationLinkProps = {
|
||||
isActive?: boolean;
|
||||
} & Pick<React.ComponentProps<typeof Button>, "size"> &
|
||||
React.ComponentProps<"a">;
|
||||
} & Pick<React.ComponentProps<typeof Button>, 'size'> &
|
||||
React.ComponentProps<'a'>;
|
||||
|
||||
function PaginationLink({
|
||||
className,
|
||||
isActive,
|
||||
size = "icon",
|
||||
size = 'icon',
|
||||
...props
|
||||
}: PaginationLinkProps) {
|
||||
return (
|
||||
<a
|
||||
aria-current={isActive ? "page" : undefined}
|
||||
aria-current={isActive ? 'page' : undefined}
|
||||
data-slot="pagination-link"
|
||||
data-active={isActive}
|
||||
className={cn(
|
||||
buttonVariants({
|
||||
variant: isActive ? "outline" : "ghost",
|
||||
variant: isActive ? 'outline' : 'ghost',
|
||||
size,
|
||||
}),
|
||||
className,
|
||||
@@ -72,7 +72,7 @@ function PaginationPrevious({
|
||||
<PaginationLink
|
||||
aria-label="Go to previous page"
|
||||
size="default"
|
||||
className={cn("gap-1 px-2.5 sm:pl-2.5", className)}
|
||||
className={cn('gap-1 px-2.5 sm:pl-2.5', className)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronLeftIcon />
|
||||
@@ -89,7 +89,7 @@ function PaginationNext({
|
||||
<PaginationLink
|
||||
aria-label="Go to next page"
|
||||
size="default"
|
||||
className={cn("gap-1 px-2.5 sm:pr-2.5", className)}
|
||||
className={cn('gap-1 px-2.5 sm:pr-2.5', className)}
|
||||
{...props}
|
||||
>
|
||||
<span className="hidden sm:block">Next</span>
|
||||
@@ -101,12 +101,12 @@ function PaginationNext({
|
||||
function PaginationEllipsis({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) {
|
||||
}: React.ComponentProps<'span'>) {
|
||||
return (
|
||||
<span
|
||||
aria-hidden
|
||||
data-slot="pagination-ellipsis"
|
||||
className={cn("flex size-9 items-center justify-center", className)}
|
||||
className={cn('flex size-9 items-center justify-center', className)}
|
||||
{...props}
|
||||
>
|
||||
<MoreHorizontalIcon className="size-4" />
|
||||
|
||||
@@ -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';
|
||||
|
||||
function Progress({
|
||||
className,
|
||||
@@ -13,7 +14,7 @@ function Progress({
|
||||
<ProgressPrimitive.Root
|
||||
data-slot="progress"
|
||||
className={cn(
|
||||
"bg-primary/20 relative h-2 w-full overflow-hidden rounded-full",
|
||||
'bg-primary/20 relative h-2 w-full overflow-hidden rounded-full',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
"use client";
|
||||
'use client';
|
||||
|
||||
import * as React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
|
||||
import * as React from 'react';
|
||||
import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area';
|
||||
|
||||
import { cn } from '@gib/ui';
|
||||
|
||||
function ScrollArea({
|
||||
className,
|
||||
@@ -12,7 +13,7 @@ function ScrollArea({
|
||||
return (
|
||||
<ScrollAreaPrimitive.Root
|
||||
data-slot="scroll-area"
|
||||
className={cn("relative", className)}
|
||||
className={cn('relative', className)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.Viewport
|
||||
@@ -29,7 +30,7 @@ function ScrollArea({
|
||||
|
||||
function ScrollBar({
|
||||
className,
|
||||
orientation = "vertical",
|
||||
orientation = 'vertical',
|
||||
...props
|
||||
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
|
||||
return (
|
||||
@@ -37,11 +38,11 @@ function ScrollBar({
|
||||
data-slot="scroll-area-scrollbar"
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"flex touch-none p-px transition-colors select-none",
|
||||
orientation === "vertical" &&
|
||||
"h-full w-2.5 border-l border-l-transparent",
|
||||
orientation === "horizontal" &&
|
||||
"h-2.5 flex-col border-t border-t-transparent",
|
||||
'flex touch-none p-px transition-colors select-none',
|
||||
orientation === 'vertical' &&
|
||||
'h-full w-2.5 border-l border-l-transparent',
|
||||
orientation === 'horizontal' &&
|
||||
'h-2.5 flex-col border-t border-t-transparent',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
"use client";
|
||||
'use client';
|
||||
|
||||
import * as React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import * as SeparatorPrimitive from "@radix-ui/react-separator";
|
||||
import * as React from 'react';
|
||||
import * as SeparatorPrimitive from '@radix-ui/react-separator';
|
||||
|
||||
import { cn } from '@gib/ui';
|
||||
|
||||
function Separator({
|
||||
className,
|
||||
orientation = "horizontal",
|
||||
orientation = 'horizontal',
|
||||
decorative = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
|
||||
@@ -16,7 +17,7 @@ function Separator({
|
||||
decorative={decorative}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
|
||||
'bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"use client";
|
||||
'use client';
|
||||
|
||||
import type {
|
||||
ComponentProps,
|
||||
@@ -7,8 +7,8 @@ import type {
|
||||
ReactNode,
|
||||
RefObject,
|
||||
SyntheticEvent,
|
||||
} from "react";
|
||||
import type { PercentCrop, PixelCrop, ReactCropProps } from "react-image-crop";
|
||||
} from 'react';
|
||||
import type { PercentCrop, PixelCrop, ReactCropProps } from 'react-image-crop';
|
||||
import {
|
||||
createContext,
|
||||
useCallback,
|
||||
@@ -16,14 +16,14 @@ import {
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { Button } from "@/components/ui";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { CropIcon, RotateCcwIcon } from "lucide-react";
|
||||
import { Slot } from "radix-ui";
|
||||
import ReactCrop, { centerCrop, makeAspectCrop } from "react-image-crop";
|
||||
} from 'react';
|
||||
import { CropIcon, RotateCcwIcon } from 'lucide-react';
|
||||
import { Slot } from 'radix-ui';
|
||||
import ReactCrop, { centerCrop, makeAspectCrop } from 'react-image-crop';
|
||||
|
||||
import "react-image-crop/dist/ReactCrop.css";
|
||||
import { Button, cn } from '@gib/ui';
|
||||
|
||||
import 'react-image-crop/dist/ReactCrop.css';
|
||||
|
||||
const centerAspectCrop = (
|
||||
mediaWidth: number,
|
||||
@@ -34,14 +34,14 @@ const centerAspectCrop = (
|
||||
aspect
|
||||
? makeAspectCrop(
|
||||
{
|
||||
unit: "%",
|
||||
unit: '%',
|
||||
width: 90,
|
||||
},
|
||||
aspect,
|
||||
mediaWidth,
|
||||
mediaHeight,
|
||||
)
|
||||
: { x: 0, y: 0, width: 90, height: 90, unit: "%" },
|
||||
: { x: 0, y: 0, width: 90, height: 90, unit: '%' },
|
||||
mediaWidth,
|
||||
mediaHeight,
|
||||
);
|
||||
@@ -52,11 +52,11 @@ const getCroppedPngImage = async (
|
||||
pixelCrop: PixelCrop,
|
||||
maxImageSize: number,
|
||||
): Promise<string> => {
|
||||
const canvas = document.createElement("canvas");
|
||||
const ctx = canvas.getContext("2d");
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
if (!ctx) {
|
||||
throw new Error("Context is null, this should never happen.");
|
||||
throw new Error('Context is null, this should never happen.');
|
||||
}
|
||||
|
||||
const scaleX = imageSrc.naturalWidth / imageSrc.width;
|
||||
@@ -78,7 +78,7 @@ const getCroppedPngImage = async (
|
||||
canvas.height,
|
||||
);
|
||||
|
||||
const croppedImageUrl = canvas.toDataURL("image/png");
|
||||
const croppedImageUrl = canvas.toDataURL('image/png');
|
||||
const response = await fetch(croppedImageUrl);
|
||||
const blob = await response.blob();
|
||||
|
||||
@@ -102,7 +102,7 @@ type ImageCropContextType = {
|
||||
completedCrop: PixelCrop | null;
|
||||
imgRef: RefObject<HTMLImageElement | null>;
|
||||
onCrop?: (croppedImage: string) => void;
|
||||
reactCropProps: Omit<ReactCropProps, "onChange" | "onComplete" | "children">;
|
||||
reactCropProps: Omit<ReactCropProps, 'onChange' | 'onComplete' | 'children'>;
|
||||
handleChange: (pixelCrop: PixelCrop, percentCrop: PercentCrop) => void;
|
||||
handleComplete: (
|
||||
pixelCrop: PixelCrop,
|
||||
@@ -118,7 +118,7 @@ const ImageCropContext = createContext<ImageCropContextType | null>(null);
|
||||
const useImageCrop = () => {
|
||||
const context = useContext(ImageCropContext);
|
||||
if (!context) {
|
||||
throw new Error("ImageCrop components must be used within ImageCrop");
|
||||
throw new Error('ImageCrop components must be used within ImageCrop');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
@@ -128,9 +128,9 @@ export type ImageCropProps = {
|
||||
maxImageSize?: number;
|
||||
onCrop?: (croppedImage: string) => void;
|
||||
children: ReactNode;
|
||||
onChange?: ReactCropProps["onChange"];
|
||||
onComplete?: ReactCropProps["onComplete"];
|
||||
} & Omit<ReactCropProps, "onChange" | "onComplete" | "children">;
|
||||
onChange?: ReactCropProps['onChange'];
|
||||
onComplete?: ReactCropProps['onComplete'];
|
||||
} & Omit<ReactCropProps, 'onChange' | 'onComplete' | 'children'>;
|
||||
|
||||
export const ImageCrop = ({
|
||||
file,
|
||||
@@ -142,15 +142,15 @@ export const ImageCrop = ({
|
||||
...reactCropProps
|
||||
}: ImageCropProps) => {
|
||||
const imgRef = useRef<HTMLImageElement | null>(null);
|
||||
const [imgSrc, setImgSrc] = useState<string>("");
|
||||
const [imgSrc, setImgSrc] = useState<string>('');
|
||||
const [crop, setCrop] = useState<PercentCrop>();
|
||||
const [completedCrop, setCompletedCrop] = useState<PixelCrop | null>(null);
|
||||
const [initialCrop, setInitialCrop] = useState<PercentCrop>();
|
||||
|
||||
useEffect(() => {
|
||||
const reader = new FileReader();
|
||||
reader.addEventListener("load", () =>
|
||||
setImgSrc(reader.result?.toString() || ""),
|
||||
reader.addEventListener('load', () =>
|
||||
setImgSrc(reader.result?.toString() || ''),
|
||||
);
|
||||
reader.readAsDataURL(file);
|
||||
}, [file]);
|
||||
@@ -244,13 +244,13 @@ export const ImageCropContent = ({
|
||||
} = useImageCrop();
|
||||
|
||||
const shadcnStyle = {
|
||||
"--rc-border-color": "var(--color-border)",
|
||||
"--rc-focus-color": "var(--color-primary)",
|
||||
'--rc-border-color': 'var(--color-border)',
|
||||
'--rc-focus-color': 'var(--color-primary)',
|
||||
} as CSSProperties;
|
||||
|
||||
return (
|
||||
<ReactCrop
|
||||
className={cn("max-h-[277px] max-w-full", className)}
|
||||
className={cn('max-h-[277px] max-w-full', className)}
|
||||
crop={crop}
|
||||
onChange={handleChange}
|
||||
onComplete={handleComplete}
|
||||
@@ -270,7 +270,7 @@ export const ImageCropContent = ({
|
||||
);
|
||||
};
|
||||
|
||||
export type ImageCropApplyProps = ComponentProps<"button"> & {
|
||||
export type ImageCropApplyProps = ComponentProps<'button'> & {
|
||||
asChild?: boolean;
|
||||
};
|
||||
|
||||
@@ -302,7 +302,7 @@ export const ImageCropApply = ({
|
||||
);
|
||||
};
|
||||
|
||||
export type ImageCropResetProps = ComponentProps<"button"> & {
|
||||
export type ImageCropResetProps = ComponentProps<'button'> & {
|
||||
asChild?: boolean;
|
||||
};
|
||||
|
||||
@@ -335,11 +335,11 @@ export const ImageCropReset = ({
|
||||
};
|
||||
|
||||
// Keep the original Cropper component for backward compatibility
|
||||
export type CropperProps = Omit<ReactCropProps, "onChange"> & {
|
||||
export type CropperProps = Omit<ReactCropProps, 'onChange'> & {
|
||||
file: File;
|
||||
maxImageSize?: number;
|
||||
onCrop?: (croppedImage: string) => void;
|
||||
onChange?: ReactCropProps["onChange"];
|
||||
onChange?: ReactCropProps['onChange'];
|
||||
};
|
||||
|
||||
export const Cropper = ({
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
"use client";
|
||||
'use client';
|
||||
|
||||
import { useTheme } from "next-themes";
|
||||
import { Toaster as Sonner, ToasterProps } from "sonner";
|
||||
import { useTheme } from 'next-themes';
|
||||
import { Toaster as Sonner, ToasterProps } from 'sonner';
|
||||
|
||||
const Toaster = ({ ...props }: ToasterProps) => {
|
||||
const { theme = "system" } = useTheme();
|
||||
const { theme = 'system' } = useTheme();
|
||||
|
||||
return (
|
||||
<Sonner
|
||||
theme={theme as ToasterProps["theme"]}
|
||||
theme={theme as ToasterProps['theme']}
|
||||
className="toaster group"
|
||||
style={
|
||||
{
|
||||
"--normal-bg": "var(--popover)",
|
||||
"--normal-text": "var(--popover-foreground)",
|
||||
"--normal-border": "var(--border)",
|
||||
'--normal-bg': 'var(--popover)',
|
||||
'--normal-text': 'var(--popover-foreground)',
|
||||
'--normal-border': 'var(--border)',
|
||||
} as React.CSSProperties
|
||||
}
|
||||
{...props}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { type ComponentProps } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { type ComponentProps } from 'react';
|
||||
|
||||
import { cn } from '@gib/ui';
|
||||
|
||||
type Message = { success: string } | { error: string } | { message: string };
|
||||
|
||||
type StatusMessageProps = {
|
||||
message: Message;
|
||||
containerProps?: ComponentProps<"div">;
|
||||
textProps?: ComponentProps<"div">;
|
||||
containerProps?: ComponentProps<'div'>;
|
||||
textProps?: ComponentProps<'div'>;
|
||||
};
|
||||
|
||||
export const StatusMessage = ({
|
||||
@@ -16,37 +17,37 @@ export const StatusMessage = ({
|
||||
}: StatusMessageProps) => {
|
||||
return (
|
||||
<div className="flex w-full flex-col items-center">
|
||||
{"success" in message && (
|
||||
{'success' in message && (
|
||||
<div
|
||||
{...containerProps}
|
||||
className={cn(
|
||||
"flex w-11/12 flex-col items-center rounded-md p-2",
|
||||
"border-2 bg-green-700/20 dark:bg-green-500/20",
|
||||
"border-green-700/50 dark:border-green-500/50",
|
||||
'flex w-11/12 flex-col items-center rounded-md p-2',
|
||||
'border-2 bg-green-700/20 dark:bg-green-500/20',
|
||||
'border-green-700/50 dark:border-green-500/50',
|
||||
containerProps?.className,
|
||||
)}
|
||||
>
|
||||
<p {...textProps}>{message.success}</p>
|
||||
</div>
|
||||
)}
|
||||
{"error" in message && (
|
||||
{'error' in message && (
|
||||
<div
|
||||
{...containerProps}
|
||||
className={cn(
|
||||
"flex w-11/12 flex-col items-center rounded-md p-2",
|
||||
"bg-destructive/20 border-destructive/80 border-2",
|
||||
'flex w-11/12 flex-col items-center rounded-md p-2',
|
||||
'bg-destructive/20 border-destructive/80 border-2',
|
||||
containerProps?.className,
|
||||
)}
|
||||
>
|
||||
<p {...textProps}>{message.error}</p>
|
||||
</div>
|
||||
)}
|
||||
{"message" in message && (
|
||||
{'message' in message && (
|
||||
<div
|
||||
{...containerProps}
|
||||
className={cn(
|
||||
"flex w-11/12 flex-col items-center rounded-md p-2",
|
||||
"bg-accent/20 border-primary/80 border-2",
|
||||
'flex w-11/12 flex-col items-center rounded-md p-2',
|
||||
'bg-accent/20 border-primary/80 border-2',
|
||||
containerProps?.className,
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
"use client";
|
||||
'use client';
|
||||
|
||||
import { type ComponentProps } from "react";
|
||||
import { Button } from "@/components/ui";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { useFormStatus } from "react-dom";
|
||||
import { type ComponentProps } from 'react';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import { useFormStatus } from 'react-dom';
|
||||
|
||||
import { Button, cn } from '@gib/ui';
|
||||
|
||||
export type SubmitButtonProps = Omit<
|
||||
ComponentProps<typeof Button>,
|
||||
"type" | "aria-disabled"
|
||||
'type' | 'aria-disabled'
|
||||
> & {
|
||||
pendingText?: string;
|
||||
pendingTextProps?: ComponentProps<"p">;
|
||||
pendingTextProps?: ComponentProps<'p'>;
|
||||
loaderProps?: ComponentProps<typeof Loader2>;
|
||||
};
|
||||
|
||||
export const SubmitButton = ({
|
||||
children,
|
||||
className,
|
||||
pendingText = "Submitting...",
|
||||
pendingText = 'Submitting...',
|
||||
pendingTextProps,
|
||||
loaderProps,
|
||||
...props
|
||||
@@ -29,17 +29,17 @@ export const SubmitButton = ({
|
||||
type="submit"
|
||||
aria-disabled={pending}
|
||||
{...props}
|
||||
className={cn("cursor-pointer", className)}
|
||||
className={cn('cursor-pointer', className)}
|
||||
>
|
||||
{pending || props.disabled ? (
|
||||
<>
|
||||
<Loader2
|
||||
{...loaderProps}
|
||||
className={cn("mr-2 h-4 w-4 animate-spin", loaderProps?.className)}
|
||||
className={cn('mr-2 h-4 w-4 animate-spin', loaderProps?.className)}
|
||||
/>
|
||||
<p
|
||||
{...pendingTextProps}
|
||||
className={cn("text-sm font-medium", pendingTextProps?.className)}
|
||||
className={cn('text-sm font-medium', pendingTextProps?.className)}
|
||||
>
|
||||
{pendingText}
|
||||
</p>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
"use client";
|
||||
'use client';
|
||||
|
||||
import * as React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import * as SwitchPrimitive from "@radix-ui/react-switch";
|
||||
import * as React from 'react';
|
||||
import * as SwitchPrimitive from '@radix-ui/react-switch';
|
||||
|
||||
import { cn } from '@gib/ui';
|
||||
|
||||
function Switch({
|
||||
className,
|
||||
@@ -12,7 +13,7 @@ function Switch({
|
||||
<SwitchPrimitive.Root
|
||||
data-slot="switch"
|
||||
className={cn(
|
||||
"peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||
'peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -20,7 +21,7 @@ function Switch({
|
||||
<SwitchPrimitive.Thumb
|
||||
data-slot="switch-thumb"
|
||||
className={cn(
|
||||
"bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0",
|
||||
'bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0',
|
||||
)}
|
||||
/>
|
||||
</SwitchPrimitive.Root>
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
"use client";
|
||||
'use client';
|
||||
|
||||
import * as React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import * as React from 'react';
|
||||
|
||||
function Table({ className, ...props }: React.ComponentProps<"table">) {
|
||||
import { cn } from '@gib/ui';
|
||||
|
||||
function Table({ className, ...props }: React.ComponentProps<'table'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="table-container"
|
||||
@@ -11,39 +12,39 @@ function Table({ className, ...props }: React.ComponentProps<"table">) {
|
||||
>
|
||||
<table
|
||||
data-slot="table"
|
||||
className={cn("w-full caption-bottom text-sm", className)}
|
||||
className={cn('w-full caption-bottom text-sm', className)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
|
||||
function TableHeader({ className, ...props }: React.ComponentProps<'thead'>) {
|
||||
return (
|
||||
<thead
|
||||
data-slot="table-header"
|
||||
className={cn("[&_tr]:border-b", className)}
|
||||
className={cn('[&_tr]:border-b', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
|
||||
function TableBody({ className, ...props }: React.ComponentProps<'tbody'>) {
|
||||
return (
|
||||
<tbody
|
||||
data-slot="table-body"
|
||||
className={cn("[&_tr:last-child]:border-0", className)}
|
||||
className={cn('[&_tr:last-child]:border-0', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
|
||||
function TableFooter({ className, ...props }: React.ComponentProps<'tfoot'>) {
|
||||
return (
|
||||
<tfoot
|
||||
data-slot="table-footer"
|
||||
className={cn(
|
||||
"bg-muted/50 border-t font-medium [&>tr]:last:border-b-0",
|
||||
'bg-muted/50 border-t font-medium [&>tr]:last:border-b-0',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -51,12 +52,12 @@ function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
|
||||
);
|
||||
}
|
||||
|
||||
function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
|
||||
function TableRow({ className, ...props }: React.ComponentProps<'tr'>) {
|
||||
return (
|
||||
<tr
|
||||
data-slot="table-row"
|
||||
className={cn(
|
||||
"hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors",
|
||||
'hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -64,12 +65,12 @@ function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
|
||||
);
|
||||
}
|
||||
|
||||
function TableHead({ className, ...props }: React.ComponentProps<"th">) {
|
||||
function TableHead({ className, ...props }: React.ComponentProps<'th'>) {
|
||||
return (
|
||||
<th
|
||||
data-slot="table-head"
|
||||
className={cn(
|
||||
"text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||
'text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -77,12 +78,12 @@ function TableHead({ className, ...props }: React.ComponentProps<"th">) {
|
||||
);
|
||||
}
|
||||
|
||||
function TableCell({ className, ...props }: React.ComponentProps<"td">) {
|
||||
function TableCell({ className, ...props }: React.ComponentProps<'td'>) {
|
||||
return (
|
||||
<td
|
||||
data-slot="table-cell"
|
||||
className={cn(
|
||||
"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||
'p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -93,11 +94,11 @@ function TableCell({ className, ...props }: React.ComponentProps<"td">) {
|
||||
function TableCaption({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"caption">) {
|
||||
}: React.ComponentProps<'caption'>) {
|
||||
return (
|
||||
<caption
|
||||
data-slot="table-caption"
|
||||
className={cn("text-muted-foreground mt-4 text-sm", className)}
|
||||
className={cn('text-muted-foreground mt-4 text-sm', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
"use client";
|
||||
'use client';
|
||||
|
||||
import * as React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import * as TabsPrimitive from "@radix-ui/react-tabs";
|
||||
import * as React from 'react';
|
||||
import * as TabsPrimitive from '@radix-ui/react-tabs';
|
||||
|
||||
import { cn } from '@gib/ui';
|
||||
|
||||
function Tabs({
|
||||
className,
|
||||
@@ -11,7 +12,7 @@ function Tabs({
|
||||
return (
|
||||
<TabsPrimitive.Root
|
||||
data-slot="tabs"
|
||||
className={cn("flex flex-col gap-2", className)}
|
||||
className={cn('flex flex-col gap-2', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
@@ -25,7 +26,7 @@ function TabsList({
|
||||
<TabsPrimitive.List
|
||||
data-slot="tabs-list"
|
||||
className={cn(
|
||||
"bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]",
|
||||
'bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -56,7 +57,7 @@ function TabsContent({
|
||||
return (
|
||||
<TabsPrimitive.Content
|
||||
data-slot="tabs-content"
|
||||
className={cn("flex-1 outline-none", className)}
|
||||
className={cn('flex-1 outline-none', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,184 +1,70 @@
|
||||
"use client";
|
||||
'use client';
|
||||
|
||||
import * as React from "react";
|
||||
import { DesktopIcon, MoonIcon, SunIcon } from "@radix-ui/react-icons";
|
||||
import * as z from "zod/v4";
|
||||
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 } from "./button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "./dropdown-menu";
|
||||
import { Button, cn } from '@gib/ui';
|
||||
|
||||
const ThemeModeSchema = z.enum(["light", "dark", "auto"]);
|
||||
const ThemeProvider = ({
|
||||
children,
|
||||
...props
|
||||
}: ComponentProps<typeof NextThemesProvider>) => {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
const themeKey = "theme-mode";
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
export type ThemeMode = z.output<typeof ThemeModeSchema>;
|
||||
export type ResolvedTheme = Exclude<ThemeMode, "auto">;
|
||||
if (!mounted) return null;
|
||||
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
|
||||
};
|
||||
|
||||
const getStoredThemeMode = (): ThemeMode => {
|
||||
if (typeof window === "undefined") return "auto";
|
||||
try {
|
||||
const storedTheme = localStorage.getItem(themeKey);
|
||||
return ThemeModeSchema.parse(storedTheme);
|
||||
} catch {
|
||||
return "auto";
|
||||
type ThemeToggleProps = {
|
||||
size?: number;
|
||||
buttonProps?: Omit<ComponentProps<typeof Button>, 'onClick'>;
|
||||
};
|
||||
|
||||
const ThemeToggle = ({ size = 1, buttonProps }: ThemeToggleProps) => {
|
||||
const { setTheme, resolvedTheme } = useTheme();
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
if (!mounted) {
|
||||
return (
|
||||
<Button {...buttonProps}>
|
||||
<span style={{ height: `${size}rem`, width: `${size}rem` }} />
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const setStoredThemeMode = (theme: ThemeMode) => {
|
||||
try {
|
||||
const parsedTheme = ThemeModeSchema.parse(theme);
|
||||
localStorage.setItem(themeKey, parsedTheme);
|
||||
} catch {
|
||||
// Silently fail if localStorage is unavailable
|
||||
}
|
||||
};
|
||||
|
||||
const getSystemTheme = () => {
|
||||
if (typeof window === "undefined") return "light";
|
||||
return window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
? "dark"
|
||||
: "light";
|
||||
};
|
||||
|
||||
const updateThemeClass = (themeMode: ThemeMode) => {
|
||||
const root = document.documentElement;
|
||||
root.classList.remove("light", "dark", "auto");
|
||||
const newTheme = themeMode === "auto" ? getSystemTheme() : themeMode;
|
||||
root.classList.add(newTheme);
|
||||
|
||||
if (themeMode === "auto") {
|
||||
root.classList.add("auto");
|
||||
}
|
||||
};
|
||||
|
||||
const setupPreferredListener = () => {
|
||||
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
const handler = () => updateThemeClass("auto");
|
||||
mediaQuery.addEventListener("change", handler);
|
||||
return () => mediaQuery.removeEventListener("change", handler);
|
||||
};
|
||||
|
||||
const getNextTheme = (current: ThemeMode): ThemeMode => {
|
||||
const themes: ThemeMode[] =
|
||||
getSystemTheme() === "dark"
|
||||
? ["auto", "light", "dark"]
|
||||
: ["auto", "dark", "light"];
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
return themes[(themes.indexOf(current) + 1) % themes.length]!;
|
||||
};
|
||||
|
||||
export const themeDetectorScript = (function () {
|
||||
function themeFn() {
|
||||
const isValidTheme = (theme: string): theme is ThemeMode => {
|
||||
const validThemes = ["light", "dark", "auto"] as const;
|
||||
return validThemes.includes(theme as ThemeMode);
|
||||
};
|
||||
|
||||
const storedTheme = localStorage.getItem("theme-mode") ?? "auto";
|
||||
const validTheme = isValidTheme(storedTheme) ? storedTheme : "auto";
|
||||
|
||||
if (validTheme === "auto") {
|
||||
const autoTheme = window.matchMedia("(prefers-color-scheme: dark)")
|
||||
.matches
|
||||
? "dark"
|
||||
: "light";
|
||||
document.documentElement.classList.add(autoTheme, "auto");
|
||||
} else {
|
||||
document.documentElement.classList.add(validTheme);
|
||||
}
|
||||
}
|
||||
return `(${themeFn.toString()})();`;
|
||||
})();
|
||||
|
||||
interface ThemeContextProps {
|
||||
themeMode: ThemeMode;
|
||||
resolvedTheme: ResolvedTheme;
|
||||
setTheme: (theme: ThemeMode) => void;
|
||||
toggleMode: () => void;
|
||||
}
|
||||
const ThemeContext = React.createContext<ThemeContextProps | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
export function ThemeProvider({ children }: React.PropsWithChildren) {
|
||||
const [themeMode, setThemeMode] = React.useState(getStoredThemeMode);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (themeMode !== "auto") return;
|
||||
return setupPreferredListener();
|
||||
}, [themeMode]);
|
||||
|
||||
const resolvedTheme = themeMode === "auto" ? getSystemTheme() : themeMode;
|
||||
|
||||
const setTheme = (newTheme: ThemeMode) => {
|
||||
setThemeMode(newTheme);
|
||||
setStoredThemeMode(newTheme);
|
||||
updateThemeClass(newTheme);
|
||||
};
|
||||
|
||||
const toggleMode = () => {
|
||||
setTheme(getNextTheme(themeMode));
|
||||
const toggleTheme = () => {
|
||||
if (resolvedTheme === 'dark') setTheme('light');
|
||||
else setTheme('dark');
|
||||
};
|
||||
|
||||
return (
|
||||
<ThemeContext
|
||||
value={{
|
||||
themeMode,
|
||||
resolvedTheme,
|
||||
setTheme,
|
||||
toggleMode,
|
||||
}}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
{...buttonProps}
|
||||
onClick={toggleTheme}
|
||||
className={cn('cursor-pointer', buttonProps?.className)}
|
||||
>
|
||||
<script
|
||||
dangerouslySetInnerHTML={{ __html: themeDetectorScript }}
|
||||
suppressHydrationWarning
|
||||
<Sun
|
||||
style={{ height: `${size}rem`, width: `${size}rem` }}
|
||||
className="scale-100 rotate-0 transition-all dark:scale-0 dark:-rotate-90"
|
||||
/>
|
||||
{children}
|
||||
</ThemeContext>
|
||||
<Moon
|
||||
style={{ height: `${size}rem`, width: `${size}rem` }}
|
||||
className="absolute scale-0 rotate-90 transition-all dark:scale-100 dark:rotate-0"
|
||||
/>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export function useTheme() {
|
||||
const context = React.use(ThemeContext);
|
||||
if (!context) {
|
||||
throw new Error("useTheme must be used within a ThemeProvider");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
export function ThemeToggle() {
|
||||
const { setTheme } = useTheme();
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="[&>svg]:absolute [&>svg]:size-5 [&>svg]:scale-0"
|
||||
>
|
||||
<SunIcon className="light:scale-100! auto:scale-0!" />
|
||||
<MoonIcon className="auto:scale-0! dark:scale-100!" />
|
||||
<DesktopIcon className="auto:scale-100!" />
|
||||
<span className="sr-only">Toggle theme</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={() => setTheme("light")}>
|
||||
Light
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setTheme("dark")}>
|
||||
Dark
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setTheme("auto")}>
|
||||
System
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
export { ThemeProvider, ThemeToggle, type ThemeToggleProps };
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import type { ToasterProps } from "sonner";
|
||||
import { Toaster as Sonner, toast } from "sonner";
|
||||
|
||||
import { useTheme } from "./theme";
|
||||
|
||||
export const Toaster = ({ ...props }: ToasterProps) => {
|
||||
const { themeMode } = useTheme();
|
||||
|
||||
return (
|
||||
<Sonner
|
||||
theme={themeMode === "auto" ? "system" : themeMode}
|
||||
className="toaster group"
|
||||
style={
|
||||
{
|
||||
"--normal-bg": "var(--popover)",
|
||||
"--normal-text": "var(--popover-foreground)",
|
||||
"--normal-border": "var(--border)",
|
||||
} as React.CSSProperties
|
||||
}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export { toast };
|
||||
1
tools/eslint/.cache/.prettiercache
Normal file
1
tools/eslint/.cache/.prettiercache
Normal file
@@ -0,0 +1 @@
|
||||
[["1","2","3","4","5"],{"key":"6","value":"7"},{"key":"8","value":"9"},{"key":"10","value":"11"},{"key":"12","value":"13"},{"key":"14","value":"15"},"/home/gib/Documents/Code/studybuddy/tools/eslint/base.ts",{"size":2511,"mtime":1768143609481,"data":"16"},"/home/gib/Documents/Code/studybuddy/tools/eslint/nextjs.ts",{"size":440,"mtime":1768143609505,"data":"17"},"/home/gib/Documents/Code/studybuddy/tools/eslint/package.json",{"size":979,"mtime":1767666484841,"data":"18"},"/home/gib/Documents/Code/studybuddy/tools/eslint/react.ts",{"size":592,"mtime":1768143609549,"data":"19"},"/home/gib/Documents/Code/studybuddy/tools/eslint/tsconfig.json",{"size":94,"mtime":1767666484841,"data":"20"},{"hashOfOptions":"21"},{"hashOfOptions":"22"},{"hashOfOptions":"23"},{"hashOfOptions":"24"},{"hashOfOptions":"25"},"1431902546","3979710663","3047921244","3279956730","3404194763"]
|
||||
@@ -1,33 +1,33 @@
|
||||
import * as path from "node:path";
|
||||
import { includeIgnoreFile } from "@eslint/compat";
|
||||
import eslint from "@eslint/js";
|
||||
import importPlugin from "eslint-plugin-import";
|
||||
import turboPlugin from "eslint-plugin-turbo";
|
||||
import { defineConfig } from "eslint/config";
|
||||
import tseslint from "typescript-eslint";
|
||||
import * as path from 'node:path';
|
||||
import { includeIgnoreFile } from '@eslint/compat';
|
||||
import eslint from '@eslint/js';
|
||||
import importPlugin from 'eslint-plugin-import';
|
||||
import turboPlugin from 'eslint-plugin-turbo';
|
||||
import { defineConfig } from 'eslint/config';
|
||||
import tseslint from 'typescript-eslint';
|
||||
|
||||
/**
|
||||
* All packages that leverage t3-env should use this rule
|
||||
*/
|
||||
export const restrictEnvAccess = defineConfig(
|
||||
{ ignores: ["**/env.ts"] },
|
||||
{ ignores: ['**/env.ts'] },
|
||||
{
|
||||
files: ["**/*.js", "**/*.ts", "**/*.tsx"],
|
||||
files: ['**/*.js', '**/*.ts', '**/*.tsx'],
|
||||
rules: {
|
||||
"no-restricted-properties": [
|
||||
"error",
|
||||
'no-restricted-properties': [
|
||||
'error',
|
||||
{
|
||||
object: "process",
|
||||
property: "env",
|
||||
object: 'process',
|
||||
property: 'env',
|
||||
message:
|
||||
"Use `import { env } from '@/env'` instead to ensure validated types.",
|
||||
},
|
||||
],
|
||||
"no-restricted-imports": [
|
||||
"error",
|
||||
'no-restricted-imports': [
|
||||
'error',
|
||||
{
|
||||
name: "process",
|
||||
importNames: ["env"],
|
||||
name: 'process',
|
||||
importNames: ['env'],
|
||||
message:
|
||||
"Use `import { env } from '@/env'` instead to ensure validated types.",
|
||||
},
|
||||
@@ -38,10 +38,10 @@ export const restrictEnvAccess = defineConfig(
|
||||
|
||||
export const baseConfig = defineConfig(
|
||||
// Ignore files not tracked by VCS and any config files
|
||||
includeIgnoreFile(path.join(import.meta.dirname, "../../.gitignore")),
|
||||
{ ignores: ["**/*.config.*"] },
|
||||
includeIgnoreFile(path.join(import.meta.dirname, '../../.gitignore')),
|
||||
{ ignores: ['**/*.config.*'] },
|
||||
{
|
||||
files: ["**/*.js", "**/*.ts", "**/*.tsx"],
|
||||
files: ['**/*.js', '**/*.ts', '**/*.tsx'],
|
||||
plugins: {
|
||||
import: importPlugin,
|
||||
turbo: turboPlugin,
|
||||
@@ -54,26 +54,26 @@ export const baseConfig = defineConfig(
|
||||
],
|
||||
rules: {
|
||||
...turboPlugin.configs.recommended.rules,
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"error",
|
||||
{ argsIgnorePattern: "^_", varsIgnorePattern: "^_" },
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'error',
|
||||
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_' },
|
||||
],
|
||||
"@typescript-eslint/consistent-type-imports": [
|
||||
"warn",
|
||||
{ prefer: "type-imports", fixStyle: "separate-type-imports" },
|
||||
'@typescript-eslint/consistent-type-imports': [
|
||||
'warn',
|
||||
{ prefer: 'type-imports', fixStyle: 'separate-type-imports' },
|
||||
],
|
||||
"@typescript-eslint/no-misused-promises": [
|
||||
'@typescript-eslint/no-misused-promises': [
|
||||
2,
|
||||
{ checksVoidReturn: { attributes: false } },
|
||||
],
|
||||
"@typescript-eslint/no-unnecessary-condition": [
|
||||
"error",
|
||||
'@typescript-eslint/no-unnecessary-condition': [
|
||||
'error',
|
||||
{
|
||||
allowConstantLoopConditions: true,
|
||||
},
|
||||
],
|
||||
"@typescript-eslint/no-non-null-assertion": "error",
|
||||
"import/consistent-type-specifier-style": ["error", "prefer-top-level"],
|
||||
'@typescript-eslint/no-non-null-assertion': 'error',
|
||||
'import/consistent-type-specifier-style': ['error', 'prefer-top-level'],
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import nextPlugin from "@next/eslint-plugin-next";
|
||||
import { defineConfig } from "eslint/config";
|
||||
import nextPlugin from '@next/eslint-plugin-next';
|
||||
import { defineConfig } from 'eslint/config';
|
||||
|
||||
export const nextjsConfig = defineConfig({
|
||||
files: ["**/*.ts", "**/*.tsx"],
|
||||
files: ['**/*.ts', '**/*.tsx'],
|
||||
plugins: {
|
||||
"@next/next": nextPlugin,
|
||||
'@next/next': nextPlugin,
|
||||
},
|
||||
rules: {
|
||||
...nextPlugin.configs.recommended.rules,
|
||||
...nextPlugin.configs["core-web-vitals"].rules,
|
||||
...nextPlugin.configs['core-web-vitals'].rules,
|
||||
// TypeError: context.getAncestors is not a function
|
||||
"@next/next/no-duplicate-head": "off",
|
||||
'@next/next/no-duplicate-head': 'off',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import reactPlugin from "eslint-plugin-react";
|
||||
import reactHooks from "eslint-plugin-react-hooks";
|
||||
import { defineConfig } from "eslint/config";
|
||||
import reactPlugin from 'eslint-plugin-react';
|
||||
import reactHooks from 'eslint-plugin-react-hooks';
|
||||
import { defineConfig } from 'eslint/config';
|
||||
|
||||
export const reactConfig = defineConfig(
|
||||
{
|
||||
files: ["**/*.ts", "**/*.tsx"],
|
||||
files: ['**/*.ts', '**/*.tsx'],
|
||||
...reactPlugin.configs.flat.recommended,
|
||||
...reactPlugin.configs.flat["jsx-runtime"],
|
||||
...reactPlugin.configs.flat['jsx-runtime'],
|
||||
languageOptions: {
|
||||
...reactPlugin.configs.flat.recommended?.languageOptions,
|
||||
...reactPlugin.configs.flat["jsx-runtime"]?.languageOptions,
|
||||
...reactPlugin.configs.flat['jsx-runtime']?.languageOptions,
|
||||
globals: {
|
||||
React: "writable",
|
||||
React: 'writable',
|
||||
},
|
||||
},
|
||||
},
|
||||
reactHooks.configs.flat["recommended-latest"]!,
|
||||
reactHooks.configs.flat['recommended-latest']!,
|
||||
);
|
||||
|
||||
1
tools/prettier/.cache/.prettiercache
Normal file
1
tools/prettier/.cache/.prettiercache
Normal file
@@ -0,0 +1 @@
|
||||
[["1","2","3"],{"key":"4","value":"5"},{"key":"6","value":"7"},{"key":"8","value":"9"},"/home/gib/Documents/Code/studybuddy/tools/prettier/index.js",{"size":1170,"mtime":1768143608718,"data":"10"},"/home/gib/Documents/Code/studybuddy/tools/prettier/package.json",{"size":607,"mtime":1767666484841,"data":"11"},"/home/gib/Documents/Code/studybuddy/tools/prettier/tsconfig.json",{"size":94,"mtime":1767666484841,"data":"12"},{"hashOfOptions":"13"},{"hashOfOptions":"14"},{"hashOfOptions":"15"},"1313973583","3244410010","3673900301"]
|
||||
@@ -5,42 +5,42 @@
|
||||
/** @type { PrettierConfig | SortImportsConfig | TailwindConfig } */
|
||||
const config = {
|
||||
singleQuote: true,
|
||||
trailingComma: "all",
|
||||
trailingComma: 'all',
|
||||
tabWidth: 2,
|
||||
printWidth: 80,
|
||||
plugins: [
|
||||
"@ianvs/prettier-plugin-sort-imports",
|
||||
"prettier-plugin-tailwindcss",
|
||||
'@ianvs/prettier-plugin-sort-imports',
|
||||
'prettier-plugin-tailwindcss',
|
||||
],
|
||||
tailwindFunctions: ["cn", "cva"],
|
||||
tailwindFunctions: ['cn', 'cva'],
|
||||
importOrder: [
|
||||
"<TYPES>",
|
||||
"^(react/(.*)$)|^(react$)|^(react-native(.*)$)",
|
||||
"^(next/(.*)$)|^(next$)",
|
||||
"^(expo(.*)$)|^(expo$)",
|
||||
"<THIRD_PARTY_MODULES>",
|
||||
"",
|
||||
"<TYPES>^@gib",
|
||||
"^@gib/(.*)$",
|
||||
"",
|
||||
"<TYPES>^[.|..|~]",
|
||||
"^~/",
|
||||
"^[../]",
|
||||
"^[./]",
|
||||
'<TYPES>',
|
||||
'^(react/(.*)$)|^(react$)|^(react-native(.*)$)',
|
||||
'^(next/(.*)$)|^(next$)',
|
||||
'^(expo(.*)$)|^(expo$)',
|
||||
'<THIRD_PARTY_MODULES>',
|
||||
'',
|
||||
'<TYPES>^@gib',
|
||||
'^@gib/(.*)$',
|
||||
'',
|
||||
'<TYPES>^[.|..|~]',
|
||||
'^~/',
|
||||
'^[../]',
|
||||
'^[./]',
|
||||
],
|
||||
importOrderParserPlugins: ["typescript", "jsx", "decorators-legacy"],
|
||||
importOrderTypeScriptVersion: "5.0.0",
|
||||
importOrderParserPlugins: ['typescript', 'jsx', 'decorators-legacy'],
|
||||
importOrderTypeScriptVersion: '5.0.0',
|
||||
overrides: [
|
||||
{
|
||||
files: "*.json.hbs",
|
||||
files: '*.json.hbs',
|
||||
options: {
|
||||
parser: "json",
|
||||
parser: 'json',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: "*.ts.hbs",
|
||||
files: '*.ts.hbs',
|
||||
options: {
|
||||
parser: "babel",
|
||||
parser: 'babel',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
1
tools/tailwind/.cache/.prettiercache
Normal file
1
tools/tailwind/.cache/.prettiercache
Normal file
@@ -0,0 +1 @@
|
||||
[["1","2","3","4","5"],{"key":"6","value":"7"},{"key":"8","value":"9"},{"key":"10","value":"11"},{"key":"12","value":"13"},{"key":"14","value":"15"},"/home/gib/Documents/Code/studybuddy/tools/tailwind/postcss-config.js",{"size":70,"mtime":1768143608435,"data":"16"},"/home/gib/Documents/Code/studybuddy/tools/tailwind/package.json",{"size":851,"mtime":1767666484842,"data":"17"},"/home/gib/Documents/Code/studybuddy/tools/tailwind/tsconfig.json",{"size":94,"mtime":1767666484842,"data":"18"},"/home/gib/Documents/Code/studybuddy/tools/tailwind/eslint.config.ts",{"size":143,"mtime":1768143608345,"data":"19"},"/home/gib/Documents/Code/studybuddy/tools/tailwind/theme.css",{"size":6741,"mtime":1767666484842,"data":"20"},{"hashOfOptions":"21"},{"hashOfOptions":"22"},{"hashOfOptions":"23"},{"hashOfOptions":"24"},{"hashOfOptions":"25"},"261655752","130635797","3253964882","957385747","2634406715"]
|
||||
@@ -1,5 +1,5 @@
|
||||
import { defineConfig } from "eslint/config";
|
||||
import { defineConfig } from 'eslint/config';
|
||||
|
||||
import { baseConfig } from "@gib/eslint-config/base";
|
||||
import { baseConfig } from '@gib/eslint-config/base';
|
||||
|
||||
export default defineConfig(baseConfig);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export default {
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
'@tailwindcss/postcss': {},
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user