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,
|
||||
});
|
||||
Reference in New Issue
Block a user