last commit before trying out opencode on repo

This commit is contained in:
2026-01-11 10:17:30 -05:00
parent 843b264c61
commit 91682bd887
85 changed files with 1589 additions and 1196 deletions

View File

@@ -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>
);
}

View File

@@ -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",
)}
>
&nbsp;
</h2>
<p
className={cn(
"mt-2 w-1/3 rounded-sm bg-current text-sm",
pulse && "animate-pulse",
)}
>
&nbsp;
</p>
</div>
</div>
);
}

View File

@@ -1,4 +0,0 @@
import { auth } from "~/auth/server";
export const GET = auth.handler;
export const POST = auth.handler;

View File

@@ -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 };

View 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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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 *));

View 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>
);
};

View 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 };

View File

@@ -0,0 +1,7 @@
export { ConvexClientProvider } from './ConvexClientProvider';
//export { NotificationsPermission } from './notification-permission';
export {
ThemeProvider,
ThemeToggle,
type ThemeToggleProps,
} from './ThemeProvider';

View File

@@ -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',
});

View 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;

View 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);
};

View 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,
},
},
*/
};
};

View 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,
});