Added posthog and sentry to days-since project
This commit is contained in:
parent
ba98783058
commit
f37137d3b7
17 changed files with 2225 additions and 95 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -147,3 +147,6 @@ dist
|
||||||
|
|
||||||
# End of https://www.toptal.com/developers/gitignore/api/node,direnv
|
# End of https://www.toptal.com/developers/gitignore/api/node,direnv
|
||||||
|
|
||||||
|
|
||||||
|
# Sentry Config File
|
||||||
|
.env.sentry-build-plugin
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,74 @@
|
||||||
import "./src/env.js";
|
import "./src/env.js";
|
||||||
|
|
||||||
/** @type {import("next").NextConfig} */
|
/** @type {import("next").NextConfig} */
|
||||||
const config = {};
|
const coreConfig = {
|
||||||
|
typescript: {
|
||||||
|
ignoreBuildErrors: true,
|
||||||
|
},
|
||||||
|
eslint: {
|
||||||
|
ignoreDuringBuilds: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
async rewrites() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
source: "/ingest/static/:path*",
|
||||||
|
destination: "https://us-assets.i.posthog.com/static/:path*",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: "/ingest/:path*",
|
||||||
|
destination: "https://us.i.posthog.com/:path*",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: "/ingest/decide",
|
||||||
|
destination: "https://us.i.posthog.com/decide",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
skipTrailingSlashRedirect: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
import { withSentryConfig } from "@sentry/nextjs";
|
||||||
|
|
||||||
|
const config = withSentryConfig(coreConfig, {
|
||||||
|
// For all available options, see:
|
||||||
|
// https://github.com/getsentry/sentry-webpack-plugin#options
|
||||||
|
|
||||||
|
org: "c4thebomb",
|
||||||
|
project: "days-since",
|
||||||
|
|
||||||
|
// Only print logs for uploading source maps in CI
|
||||||
|
silent: !process.env.CI,
|
||||||
|
|
||||||
|
// For all available options, see:
|
||||||
|
// https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/
|
||||||
|
|
||||||
|
// Upload a larger set of source maps for prettier stack traces (increases build time)
|
||||||
|
widenClientFileUpload: true,
|
||||||
|
|
||||||
|
// Automatically annotate React components to show their full name in breadcrumbs and session replay
|
||||||
|
reactComponentAnnotation: {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Route browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers.
|
||||||
|
// This can increase your server load as well as your hosting bill.
|
||||||
|
// Note: Check that the configured route will not match with your Next.js middleware, otherwise reporting of client-
|
||||||
|
// side errors will fail.
|
||||||
|
tunnelRoute: "/monitoring",
|
||||||
|
|
||||||
|
// Hides source maps from generated client bundles
|
||||||
|
hideSourceMaps: true,
|
||||||
|
|
||||||
|
// Automatically tree-shake Sentry logger statements to reduce bundle size
|
||||||
|
disableLogger: true,
|
||||||
|
|
||||||
|
// Enables automatic instrumentation of Vercel Cron Monitors. (Does not yet work with App Router route handlers.)
|
||||||
|
// See the following for more information:
|
||||||
|
// https://docs.sentry.io/product/crons/
|
||||||
|
// https://vercel.com/docs/cron-jobs
|
||||||
|
automaticVercelMonitors: true,
|
||||||
|
});
|
||||||
|
|
||||||
export default config;
|
export default config;
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@
|
||||||
"db:migrate": "drizzle-kit migrate",
|
"db:migrate": "drizzle-kit migrate",
|
||||||
"db:push": "drizzle-kit push",
|
"db:push": "drizzle-kit push",
|
||||||
"db:studio": "drizzle-kit studio",
|
"db:studio": "drizzle-kit studio",
|
||||||
"dev": "next dev --turbo",
|
"dev": "next dev",
|
||||||
"lint": "next lint",
|
"lint": "next lint",
|
||||||
"lint:fix": "next lint --fix",
|
"lint:fix": "next lint --fix",
|
||||||
"preview": "next build && next start",
|
"preview": "next build && next start",
|
||||||
|
|
@ -23,6 +23,7 @@
|
||||||
"@radix-ui/react-avatar": "^1.1.1",
|
"@radix-ui/react-avatar": "^1.1.1",
|
||||||
"@radix-ui/react-dialog": "^1.1.2",
|
"@radix-ui/react-dialog": "^1.1.2",
|
||||||
"@radix-ui/react-navigation-menu": "^1.2.1",
|
"@radix-ui/react-navigation-menu": "^1.2.1",
|
||||||
|
"@sentry/nextjs": "^8",
|
||||||
"@t3-oss/env-nextjs": "^0.10.1",
|
"@t3-oss/env-nextjs": "^0.10.1",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
|
@ -31,6 +32,7 @@
|
||||||
"lucide-react": "^0.460.0",
|
"lucide-react": "^0.460.0",
|
||||||
"next": "^15.0.3",
|
"next": "^15.0.3",
|
||||||
"postgres": "^3.4.5",
|
"postgres": "^3.4.5",
|
||||||
|
"posthog-js": "^1.194.1",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"server-only": "^0.0.1",
|
"server-only": "^0.0.1",
|
||||||
|
|
|
||||||
1916
pnpm-lock.yaml
generated
1916
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
26
sentry.client.config.ts
Normal file
26
sentry.client.config.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
// This file configures the initialization of Sentry on the client.
|
||||||
|
// The config you add here will be used whenever a users loads a page in their browser.
|
||||||
|
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
|
||||||
|
|
||||||
|
import * as Sentry from "@sentry/nextjs";
|
||||||
|
|
||||||
|
Sentry.init({
|
||||||
|
dsn: "https://03edba4d067507bcde69b46755933f24@o4508389509758976.ingest.us.sentry.io/4508389512052736",
|
||||||
|
|
||||||
|
// Add optional integrations for additional features
|
||||||
|
integrations: [Sentry.replayIntegration()],
|
||||||
|
|
||||||
|
// Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control.
|
||||||
|
tracesSampleRate: 1,
|
||||||
|
|
||||||
|
// Define how likely Replay events are sampled.
|
||||||
|
// This sets the sample rate to be 10%. You may want this to be 100% while
|
||||||
|
// in development and sample at a lower rate in production
|
||||||
|
replaysSessionSampleRate: 0.1,
|
||||||
|
|
||||||
|
// Define how likely Replay events are sampled when an error occurs.
|
||||||
|
replaysOnErrorSampleRate: 1.0,
|
||||||
|
|
||||||
|
// Setting this option to true will print useful information to the console while you're setting up Sentry.
|
||||||
|
debug: false,
|
||||||
|
});
|
||||||
16
sentry.edge.config.ts
Normal file
16
sentry.edge.config.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
// This file configures the initialization of Sentry for edge features (middleware, edge routes, and so on).
|
||||||
|
// The config you add here will be used whenever one of the edge features is loaded.
|
||||||
|
// Note that this config is unrelated to the Vercel Edge Runtime and is also required when running locally.
|
||||||
|
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
|
||||||
|
|
||||||
|
import * as Sentry from "@sentry/nextjs";
|
||||||
|
|
||||||
|
Sentry.init({
|
||||||
|
dsn: "https://03edba4d067507bcde69b46755933f24@o4508389509758976.ingest.us.sentry.io/4508389512052736",
|
||||||
|
|
||||||
|
// Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control.
|
||||||
|
tracesSampleRate: 1,
|
||||||
|
|
||||||
|
// Setting this option to true will print useful information to the console while you're setting up Sentry.
|
||||||
|
debug: false,
|
||||||
|
});
|
||||||
15
sentry.server.config.ts
Normal file
15
sentry.server.config.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
// This file configures the initialization of Sentry on the server.
|
||||||
|
// The config you add here will be used whenever the server handles a request.
|
||||||
|
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
|
||||||
|
|
||||||
|
import * as Sentry from "@sentry/nextjs";
|
||||||
|
|
||||||
|
Sentry.init({
|
||||||
|
dsn: "https://03edba4d067507bcde69b46755933f24@o4508389509758976.ingest.us.sentry.io/4508389512052736",
|
||||||
|
|
||||||
|
// Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control.
|
||||||
|
tracesSampleRate: 1,
|
||||||
|
|
||||||
|
// Setting this option to true will print useful information to the console while you're setting up Sentry.
|
||||||
|
debug: false,
|
||||||
|
});
|
||||||
14
src/app/_analytics/providers.tsx
Normal file
14
src/app/_analytics/providers.tsx
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import posthog from "posthog-js";
|
||||||
|
import { PostHogProvider } from "posthog-js/react";
|
||||||
|
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY!, {
|
||||||
|
api_host: "/ingest",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CSPostHogProvider({ children }: { children: React.ReactNode }) {
|
||||||
|
return <PostHogProvider client={posthog}>{children}</PostHogProvider>;
|
||||||
|
}
|
||||||
|
|
@ -2,7 +2,11 @@
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
const formatTime = (elapsed: number): [number, number, number, number] => {
|
const formatTime = (elapsed: number | null) => {
|
||||||
|
if (elapsed === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const seconds = Math.floor(elapsed / 1000);
|
const seconds = Math.floor(elapsed / 1000);
|
||||||
const minutes = Math.floor(seconds / 60);
|
const minutes = Math.floor(seconds / 60);
|
||||||
const hours = Math.floor(minutes / 60);
|
const hours = Math.floor(minutes / 60);
|
||||||
|
|
@ -12,10 +16,19 @@ const formatTime = (elapsed: number): [number, number, number, number] => {
|
||||||
const remainingMinutes = minutes % 60;
|
const remainingMinutes = minutes % 60;
|
||||||
const remainingHours = hours % 24;
|
const remainingHours = hours % 24;
|
||||||
|
|
||||||
return [days, remainingHours, remainingMinutes, remainingSeconds];
|
return {
|
||||||
|
days,
|
||||||
|
hours: remainingHours,
|
||||||
|
mins: remainingMinutes,
|
||||||
|
secs: remainingSeconds,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const calculateTimeSince = (start: Date) => {
|
const calculateTimeSince = (start: Date) => {
|
||||||
|
if (start === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
return now.getTime() - start.getTime();
|
return now.getTime() - start.getTime();
|
||||||
};
|
};
|
||||||
|
|
@ -34,7 +47,7 @@ export default function Timer(props: { eventTime: Date; caption: string }) {
|
||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, [props.eventTime]);
|
}, [props.eventTime]);
|
||||||
|
|
||||||
const [days, hours, mins, secs] = formatTime(timeSince);
|
const formattedTime = formatTime(timeSince);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full w-full flex-col justify-center">
|
<div className="flex h-full w-full flex-col justify-center">
|
||||||
|
|
@ -44,25 +57,33 @@ export default function Timer(props: { eventTime: Date; caption: string }) {
|
||||||
<div className="flex justify-center gap-20 py-12">
|
<div className="flex justify-center gap-20 py-12">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="text-8xl font-semibold">
|
<div className="text-8xl font-semibold">
|
||||||
{days.toString().padStart(2, "0")}
|
{formattedTime
|
||||||
|
? formattedTime.days.toString().padStart(2, "0")
|
||||||
|
: "00"}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-md px-1 text-gray-400">DAYS</div>
|
<div className="text-md px-1 text-gray-400">DAYS</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="text-8xl font-semibold">
|
<div className="text-8xl font-semibold">
|
||||||
{hours.toString().padStart(2, "0")}
|
{formattedTime
|
||||||
|
? formattedTime.hours.toString().padStart(2, "0")
|
||||||
|
: "00"}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-md px-1 text-gray-400">HOURS</div>
|
<div className="text-md px-1 text-gray-400">HOURS</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="text-8xl font-semibold">
|
<div className="text-8xl font-semibold">
|
||||||
{mins.toString().padStart(2, "0")}
|
{formattedTime
|
||||||
|
? formattedTime.mins.toString().padStart(2, "0")
|
||||||
|
: "00"}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-md px-1 text-gray-400">MINUTES</div>
|
<div className="text-md px-1 text-gray-400">MINUTES</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="text-8xl font-semibold" suppressHydrationWarning>
|
<div className="text-8xl font-semibold" suppressHydrationWarning>
|
||||||
{secs.toString().padStart(2, "0")}
|
{formattedTime
|
||||||
|
? formattedTime.secs.toString().padStart(2, "0")
|
||||||
|
: "00"}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-md px-1 text-gray-400">SECONDS</div>
|
<div className="text-md px-1 text-gray-400">SECONDS</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,13 @@ import {
|
||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from "~/components/ui/card";
|
} from "~/components/ui/card";
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from "~/components/ui/avatar";
|
import { Avatar, AvatarImage } from "~/components/ui/avatar";
|
||||||
|
|
||||||
|
const formatTime = (elapsed: number | null) => {
|
||||||
|
if (elapsed === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const formatTime = (elapsed: number): [number, number, number, number] => {
|
|
||||||
const seconds = Math.floor(elapsed / 1000);
|
const seconds = Math.floor(elapsed / 1000);
|
||||||
const minutes = Math.floor(seconds / 60);
|
const minutes = Math.floor(seconds / 60);
|
||||||
const hours = Math.floor(minutes / 60);
|
const hours = Math.floor(minutes / 60);
|
||||||
|
|
@ -20,16 +24,25 @@ const formatTime = (elapsed: number): [number, number, number, number] => {
|
||||||
const remainingMinutes = minutes % 60;
|
const remainingMinutes = minutes % 60;
|
||||||
const remainingHours = hours % 24;
|
const remainingHours = hours % 24;
|
||||||
|
|
||||||
return [days, remainingHours, remainingMinutes, remainingSeconds];
|
return {
|
||||||
|
days,
|
||||||
|
hours: remainingHours,
|
||||||
|
mins: remainingMinutes,
|
||||||
|
secs: remainingSeconds,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const calculateTimeSince = (start: Date) => {
|
const calculateTimeSince = (start: Date | null) => {
|
||||||
|
if (start === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
return now.getTime() - start.getTime();
|
return now.getTime() - start.getTime();
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Timer(props: {
|
export default function Timer(props: {
|
||||||
eventTime: Date;
|
eventTime: Date | null;
|
||||||
caption?: string;
|
caption?: string;
|
||||||
email?: string;
|
email?: string;
|
||||||
emailHash?: string;
|
emailHash?: string;
|
||||||
|
|
@ -47,7 +60,7 @@ export default function Timer(props: {
|
||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, [props.eventTime]);
|
}, [props.eventTime]);
|
||||||
|
|
||||||
const [days, hours, mins, secs] = formatTime(timeSince);
|
const formattedTime = formatTime(timeSince);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className={"flex h-full w-full flex-col justify-center"}>
|
<Card className={"flex h-full w-full flex-col justify-center"}>
|
||||||
|
|
@ -64,25 +77,33 @@ export default function Timer(props: {
|
||||||
<div className="flex justify-center gap-8 py-10">
|
<div className="flex justify-center gap-8 py-10">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="text-6xl font-semibold">
|
<div className="text-6xl font-semibold">
|
||||||
{days.toString().padStart(2, "0")}
|
{formattedTime
|
||||||
|
? formattedTime?.days.toString().padStart(2, "0")
|
||||||
|
: "00"}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm text-gray-400">DAYS</div>
|
<div className="text-sm text-gray-400">DAYS</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="text-6xl font-semibold">
|
<div className="text-6xl font-semibold">
|
||||||
{hours.toString().padStart(2, "0")}
|
{formattedTime
|
||||||
|
? formattedTime?.hours.toString().padStart(2, "0")
|
||||||
|
: "00"}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm text-gray-400">HOURS</div>
|
<div className="text-sm text-gray-400">HOURS</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="text-6xl font-semibold">
|
<div className="text-6xl font-semibold">
|
||||||
{mins.toString().padStart(2, "0")}
|
{formattedTime
|
||||||
|
? formattedTime?.mins.toString().padStart(2, "0")
|
||||||
|
: "00"}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm text-gray-400">MINUTES</div>
|
<div className="text-sm text-gray-400">MINUTES</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="text-6xl font-semibold" suppressHydrationWarning>
|
<div className="text-6xl font-semibold" suppressHydrationWarning>
|
||||||
{secs.toString().padStart(2, "0")}
|
{formattedTime
|
||||||
|
? formattedTime?.secs.toString().padStart(2, "0")
|
||||||
|
: "00"}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm text-gray-400">SECONDS</div>
|
<div className="text-sm text-gray-400">SECONDS</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
9
src/app/api/sentry-example-api/route.ts
Normal file
9
src/app/api/sentry-example-api/route.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { NextResponse } from "next/server";
|
||||||
|
|
||||||
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
|
// A faulty API route to test Sentry's error monitoring
|
||||||
|
export function GET() {
|
||||||
|
throw new Error("Sentry Example API Route Error");
|
||||||
|
return NextResponse.json({ data: "Testing Sentry Error..." });
|
||||||
|
}
|
||||||
19
src/app/global-error.tsx
Normal file
19
src/app/global-error.tsx
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as Sentry from "@sentry/nextjs";
|
||||||
|
import NextError from "next/error";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
export default function GlobalError(props: { error: unknown }) {
|
||||||
|
useEffect(() => {
|
||||||
|
Sentry.captureException(props.error);
|
||||||
|
}, [props.error]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<NextError statusCode={500} />
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -3,6 +3,7 @@ import "~/styles/globals.css";
|
||||||
import { Inter } from "next/font/google";
|
import { Inter } from "next/font/google";
|
||||||
import { type Metadata } from "next";
|
import { type Metadata } from "next";
|
||||||
import { TopNav } from "./_components/navigation";
|
import { TopNav } from "./_components/navigation";
|
||||||
|
import { CSPostHogProvider } from "./_analytics/providers";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Days Since",
|
title: "Days Since",
|
||||||
|
|
@ -21,14 +22,16 @@ export default function RootLayout({
|
||||||
}: Readonly<{ children: React.ReactNode; modal: React.ReactNode }>) {
|
}: Readonly<{ children: React.ReactNode; modal: React.ReactNode }>) {
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<body className={`font-sans ${inter.variable} dark`}>
|
<CSPostHogProvider>
|
||||||
<div className="grid h-screen grid-rows-[auto,1fr]">
|
<body className={`font-sans ${inter.variable} dark`}>
|
||||||
<TopNav />
|
<div className="grid h-screen grid-rows-[auto,1fr]">
|
||||||
<main className="grid items-start overflow-y-auto">{children}</main>
|
<TopNav />
|
||||||
{modal}
|
<main className="grid items-start overflow-y-auto">{children}</main>
|
||||||
<div id="modal-root" />
|
{modal}
|
||||||
</div>
|
<div id="modal-root" />
|
||||||
</body>
|
</div>
|
||||||
|
</body>
|
||||||
|
</CSPostHogProvider>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
import Timer from "~/app/_components/timer";
|
|
||||||
|
|
||||||
import { createHash } from "crypto";
|
import { createHash } from "crypto";
|
||||||
import { getAllTimers } from "~/server/queries";
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import Timer from "~/app/_components/timer";
|
||||||
|
import { getAllTimers } from "~/server/queries";
|
||||||
|
|
||||||
const hashEmail = (email: string) => {
|
const hashEmail = (email: string) => {
|
||||||
const hash = createHash("sha256")
|
const hash = createHash("sha256")
|
||||||
|
|
@ -15,11 +14,13 @@ const hashEmail = (email: string) => {
|
||||||
export default async function HomePage() {
|
export default async function HomePage() {
|
||||||
const timers = await getAllTimers();
|
const timers = await getAllTimers();
|
||||||
|
|
||||||
|
const modulo = timers.length > 4 ? Math.floor(Math.random() * 4) + 2 : 1;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={"m-4 grid grid-cols-4 gap-4"}>
|
<div className={"m-4 grid grid-cols-4 gap-4"}>
|
||||||
{timers.map((timer, idx) =>
|
{timers.map((timer, idx) =>
|
||||||
idx % 4 == 0 ? (
|
idx % modulo == 0 ? (
|
||||||
<div key={idx} className="row-span-2">
|
<div key={timer.id} className="row-span-2">
|
||||||
<Link href={`/timer/${timer.id}`}>
|
<Link href={`/timer/${timer.id}`}>
|
||||||
<Timer
|
<Timer
|
||||||
eventTime={timer.updatedAt}
|
eventTime={timer.updatedAt}
|
||||||
|
|
@ -30,15 +31,16 @@ export default async function HomePage() {
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Link href={`/timer/${timer.id}`}>
|
<div key={timer.id} className="row-span-1">
|
||||||
<Timer
|
<Link href={`/timer/${timer.id}`}>
|
||||||
key={idx}
|
<Timer
|
||||||
eventTime={timer.updatedAt}
|
eventTime={timer.updatedAt}
|
||||||
caption={timer.caption}
|
caption={timer.caption}
|
||||||
email={timer.userEmail}
|
email={timer.userEmail}
|
||||||
emailHash={hashEmail(timer.userEmail)}
|
emailHash={hashEmail(timer.userEmail)}
|
||||||
/>
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
|
</div>
|
||||||
),
|
),
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ export const env = createEnv({
|
||||||
NODE_ENV: z
|
NODE_ENV: z
|
||||||
.enum(["development", "test", "production"])
|
.enum(["development", "test", "production"])
|
||||||
.default("development"),
|
.default("development"),
|
||||||
|
SENTRY_AUTH_TOKEN: z.string(),
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -19,7 +20,7 @@ export const env = createEnv({
|
||||||
* `NEXT_PUBLIC_`.
|
* `NEXT_PUBLIC_`.
|
||||||
*/
|
*/
|
||||||
client: {
|
client: {
|
||||||
// NEXT_PUBLIC_CLIENTVAR: z.string(),
|
NEXT_PUBLIC_POSTHOG_KEY: z.string(),
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -29,7 +30,8 @@ export const env = createEnv({
|
||||||
runtimeEnv: {
|
runtimeEnv: {
|
||||||
DATABASE_URL: process.env.DATABASE_URL,
|
DATABASE_URL: process.env.DATABASE_URL,
|
||||||
NODE_ENV: process.env.NODE_ENV,
|
NODE_ENV: process.env.NODE_ENV,
|
||||||
// NEXT_PUBLIC_CLIENTVAR: process.env.NEXT_PUBLIC_CLIENTVAR,
|
NEXT_PUBLIC_POSTHOG_KEY: process.env.NEXT_PUBLIC_POSTHOG_KEY,
|
||||||
|
SENTRY_AUTH_TOKEN: process.env.SENTRY_AUTH_TOKEN,
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially
|
* Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially
|
||||||
|
|
|
||||||
13
src/instrumentation.ts
Normal file
13
src/instrumentation.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
import * as Sentry from "@sentry/nextjs";
|
||||||
|
|
||||||
|
export async function register() {
|
||||||
|
if (process.env.NEXT_RUNTIME === "nodejs") {
|
||||||
|
await import("../sentry.server.config");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.env.NEXT_RUNTIME === "edge") {
|
||||||
|
await import("../sentry.edge.config");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const onRequestError = Sentry.captureRequestError;
|
||||||
|
|
@ -7,7 +7,6 @@ import {
|
||||||
timestamp,
|
timestamp,
|
||||||
varchar,
|
varchar,
|
||||||
boolean,
|
boolean,
|
||||||
uuid,
|
|
||||||
} from "drizzle-orm/pg-core";
|
} from "drizzle-orm/pg-core";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -28,9 +27,8 @@ export const timers = createTable("timer", {
|
||||||
createdAt: timestamp("created_at", { withTimezone: true })
|
createdAt: timestamp("created_at", { withTimezone: true })
|
||||||
.default(sql`CURRENT_TIMESTAMP`)
|
.default(sql`CURRENT_TIMESTAMP`)
|
||||||
.notNull(),
|
.notNull(),
|
||||||
updatedAt: timestamp("updated_at", { withTimezone: true })
|
updatedAt: timestamp("updated_at", { withTimezone: true }).$onUpdate(
|
||||||
.$onUpdate(() => new Date())
|
() => new Date(),
|
||||||
.default(sql`CURRENT_TIMESTAMP`)
|
),
|
||||||
.notNull(),
|
|
||||||
public: boolean("public").default(false).notNull(),
|
public: boolean("public").default(false).notNull(),
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Reference in a new issue