diff --git a/drizzle.config.ts b/drizzle.config.ts index 289a03c..011e1e7 100644 --- a/drizzle.config.ts +++ b/drizzle.config.ts @@ -1,6 +1,5 @@ import "dotenv/config"; import type { Config } from "drizzle-kit"; - import { env } from "@/env.mjs"; export default { diff --git a/package.json b/package.json index 98f4c2a..6a657e6 100644 --- a/package.json +++ b/package.json @@ -97,7 +97,7 @@ "eslint-config-next": "^14.0.0", "pg": "^8.11.3", "postcss": "^8.4.31", - "prettier": "^2.8.8", + "prettier": "^3.0.3", "prettier-plugin-tailwindcss": "^0.5.6", "tailwindcss": "^3.3.5", "ts-node": "^10.9.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e5ecebb..0e3f2c3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -195,7 +195,7 @@ dependencies: devDependencies: "@ianvs/prettier-plugin-sort-imports": specifier: ^4.1.1 - version: 4.1.1(prettier@2.8.8) + version: 4.1.1(prettier@3.0.3) "@tailwindcss/typography": specifier: ^0.5.10 version: 0.5.10(tailwindcss@3.3.5) @@ -242,11 +242,11 @@ devDependencies: specifier: ^8.4.31 version: 8.4.31 prettier: - specifier: ^2.8.8 - version: 2.8.8 + specifier: ^3.0.3 + version: 3.0.3 prettier-plugin-tailwindcss: specifier: ^0.5.6 - version: 0.5.6(@ianvs/prettier-plugin-sort-imports@4.1.1)(prettier@2.8.8) + version: 0.5.6(@ianvs/prettier-plugin-sort-imports@4.1.1)(prettier@3.0.3) tailwindcss: specifier: ^3.3.5 version: 3.3.5(ts-node@10.9.1) @@ -1071,7 +1071,7 @@ packages: } dev: true - /@ianvs/prettier-plugin-sort-imports@4.1.1(prettier@2.8.8): + /@ianvs/prettier-plugin-sort-imports@4.1.1(prettier@3.0.3): resolution: { integrity: sha512-kJhXq63ngpTQ2dxgf5GasbPJWsJA3LgoOdd7WGhpUSzLgLgI4IsIzYkbJf9kmpOHe7Vdm/o3PcRA3jmizXUuAQ==, @@ -1088,7 +1088,7 @@ packages: "@babel/parser": 7.23.0 "@babel/traverse": 7.23.2 "@babel/types": 7.23.0 - prettier: 2.8.8 + prettier: 3.0.3 semver: 7.5.4 transitivePeerDependencies: - supports-color @@ -7352,7 +7352,7 @@ packages: engines: { node: ">= 0.8.0" } dev: true - /prettier-plugin-tailwindcss@0.5.6(@ianvs/prettier-plugin-sort-imports@4.1.1)(prettier@2.8.8): + /prettier-plugin-tailwindcss@0.5.6(@ianvs/prettier-plugin-sort-imports@4.1.1)(prettier@3.0.3): resolution: { integrity: sha512-2Xgb+GQlkPAUCFi3sV+NOYcSI5XgduvDBL2Zt/hwJudeKXkyvRS65c38SB0yb9UB40+1rL83I6m0RtlOQ8eHdg==, @@ -7407,16 +7407,16 @@ packages: prettier-plugin-twig-melody: optional: true dependencies: - "@ianvs/prettier-plugin-sort-imports": 4.1.1(prettier@2.8.8) - prettier: 2.8.8 + "@ianvs/prettier-plugin-sort-imports": 4.1.1(prettier@3.0.3) + prettier: 3.0.3 dev: true - /prettier@2.8.8: + /prettier@3.0.3: resolution: { - integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==, + integrity: sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg==, } - engines: { node: ">=10.13.0" } + engines: { node: ">=14" } hasBin: true dev: true diff --git a/prettier.config.mjs b/prettier.config.cjs similarity index 88% rename from prettier.config.mjs rename to prettier.config.cjs index c03a3f6..4c054d5 100644 --- a/prettier.config.mjs +++ b/prettier.config.cjs @@ -12,16 +12,13 @@ const config = { "^(react/(.*)$)|^(react$)|^(react-native(.*)$)", "^(next/(.*)$)|^(next$)", "", - "", "^@/public/(.*)$", - "", "^@/", - "^~/", "^[../]", "^[./]", ], importOrderParserPlugins: ["typescript", "jsx", "decorators-legacy"], - importOrderTypeScriptVersion: "4.4.0", + importOrderTypeScriptVersion: "5.2.2", }; -export default config; +module.exports = config; diff --git a/src/app/(home)/page.tsx b/src/app/(home)/page.tsx index fcd3f5e..efa4089 100644 --- a/src/app/(home)/page.tsx +++ b/src/app/(home)/page.tsx @@ -1,10 +1,9 @@ import Image from "next/image"; import Link from "next/link"; import { ChevronRight, Github } from "lucide-react"; - import OpenBio from "@/public/openbio.png"; -import { Button } from "@/components/ui/button"; import Pricing from "@/components/pricing"; +import { Button } from "@/components/ui/button"; export default function Page() { return ( diff --git a/src/app/[link]/_components/action-bar.tsx b/src/app/[link]/_components/action-bar.tsx index 536a0cd..dba1277 100644 --- a/src/app/[link]/_components/action-bar.tsx +++ b/src/app/[link]/_components/action-bar.tsx @@ -1,9 +1,8 @@ "use client"; import { Link, Rocket } from "lucide-react"; - -import { cn } from "@/lib/utils"; import CreateLinkBentoModal from "@/components/modals/create-link-bento"; +import { cn } from "@/lib/utils"; export default function ActionBar() { const btnClass = diff --git a/src/app/[link]/_components/avatar.tsx b/src/app/[link]/_components/avatar.tsx index 95c345c..9e9db80 100644 --- a/src/app/[link]/_components/avatar.tsx +++ b/src/app/[link]/_components/avatar.tsx @@ -2,14 +2,13 @@ import { useCallback, useState } from "react"; import Image from "next/image"; -import { useDropzone, type FileWithPath } from "react-dropzone"; import { UploadCloud } from "lucide-react"; +import { useDropzone, type FileWithPath } from "react-dropzone"; import { generateClientDropzoneAccept } from "uploadthing/client"; - -import { type RouterOutputs } from "@/trpc/react"; +import { toast } from "@/components/ui/use-toast"; import { useUploadThing } from "@/lib/uploadthing"; import { cn } from "@/lib/utils"; -import { toast } from "@/components/ui/use-toast"; +import { type RouterOutputs } from "@/trpc/react"; type Props = { profileLink: NonNullable; @@ -39,7 +38,7 @@ export default function ProfileLinkAvatar({ profileLink }: Props) { }); setImg(URL.createObjectURL(acceptedFiles[0]!)); }, - [profileLink.id, startUpload] + [profileLink.id, startUpload], ); const { getRootProps, getInputProps } = useDropzone({ @@ -56,7 +55,7 @@ export default function ProfileLinkAvatar({ profileLink }: Props) { "flex h-[100px] w-[100px] flex-col items-center justify-center gap-y-1 rounded-full border border-border bg-background/50 md:h-[150px] md:w-[150px]", !profileLink.image && profileLink.isOwner && "border-dashed", profileLink.isOwner && "cursor-pointer", - img && "border-0 border-transparent bg-transparent" + img && "border-0 border-transparent bg-transparent", )} > {img && ( diff --git a/src/app/[link]/_components/bento-layout.tsx b/src/app/[link]/_components/bento-layout.tsx index 0958b23..ed7b39e 100644 --- a/src/app/[link]/_components/bento-layout.tsx +++ b/src/app/[link]/_components/bento-layout.tsx @@ -2,17 +2,15 @@ import "react-grid-layout/css/styles.css"; import "react-resizable/css/styles.css"; - import { useMemo } from "react"; +import { useParams } from "next/navigation"; import { - type Layouts, Responsive, WidthProvider, + type Layouts, type ResponsiveProps, } from "react-grid-layout"; - import { api } from "@/trpc/react"; -import { useParams } from "next/navigation"; export default function BentoLayout({ children, @@ -22,7 +20,7 @@ export default function BentoLayout({ const { link } = useParams<{ link: string }>(); const ResponsiveGridLayout = useMemo( () => WidthProvider(Responsive) as React.ComponentType, - [] + [], ); const [profileLink] = api.profileLink.getByLink.useSuspenseQuery({ diff --git a/src/app/[link]/_components/bento.tsx b/src/app/[link]/_components/bento.tsx index d592924..6c1ef4c 100644 --- a/src/app/[link]/_components/bento.tsx +++ b/src/app/[link]/_components/bento.tsx @@ -1,9 +1,9 @@ "use client"; -import { api } from "@/trpc/react"; +import { useParams } from "next/navigation"; import BentoCard from "@/components/bento/card"; +import { api } from "@/trpc/react"; import BentoLayout from "./bento-layout"; -import { useParams } from "next/navigation"; export default function Bento() { const { link } = useParams<{ link: string }>(); diff --git a/src/app/[link]/_components/header.tsx b/src/app/[link]/_components/header.tsx index 7f628c7..b27f157 100644 --- a/src/app/[link]/_components/header.tsx +++ b/src/app/[link]/_components/header.tsx @@ -1,17 +1,16 @@ "use client"; import { useEffect, useState, useTransition } from "react"; -import { useEditor, EditorContent, type Extension } from "@tiptap/react"; -import StarterKit from "@tiptap/starter-kit"; +import { useParams } from "next/navigation"; import Placeholder from "@tiptap/extension-placeholder"; +import { EditorContent, useEditor, type Extension } from "@tiptap/react"; +import StarterKit from "@tiptap/starter-kit"; import { QrCode } from "lucide-react"; - -import { api } from "@/trpc/react"; import LinkQRModal from "@/components/modals/link-qr-modal"; import { Button } from "@/components/ui/button"; import { toast } from "@/components/ui/use-toast"; +import { api } from "@/trpc/react"; import ProfileLinkAvatar from "./avatar"; -import { useParams } from "next/navigation"; const extensions = [ StarterKit, @@ -79,7 +78,7 @@ export default function ProfileLinkHeader() { disabled={saving} onClick={() => { void navigator.clipboard.writeText( - `https://openbio.app/${profileLink.link}` + `https://openbio.app/${profileLink.link}`, ); toast({ title: "Copied to clipboard!", diff --git a/src/app/[link]/page.tsx b/src/app/[link]/page.tsx index 918c49f..6bf2754 100644 --- a/src/app/[link]/page.tsx +++ b/src/app/[link]/page.tsx @@ -1,17 +1,16 @@ +import { Suspense } from "react"; import type { Metadata } from "next"; - +import { notFound } from "next/navigation"; import { defaultMetadata, - twitterMetadata, ogMetadata, + twitterMetadata, } from "@/app/shared-metadata"; +import { Skeleton } from "@/components/ui/skeleton"; import { api } from "@/trpc/server"; -import ProfileLinkHeader from "./_components/header"; -import Bento from "./_components/bento"; import ActionBar from "./_components/action-bar"; -import { notFound } from "next/navigation"; -import { Suspense } from "react"; -import { Skeleton } from "@/components/ui/skeleton"; +import Bento from "./_components/bento"; +import ProfileLinkHeader from "./_components/header"; type Props = { params: { diff --git a/src/app/actions/claim-link.tsx b/src/app/actions/claim-link.tsx index 398c872..c8ea6bc 100644 --- a/src/app/actions/claim-link.tsx +++ b/src/app/actions/claim-link.tsx @@ -8,7 +8,7 @@ export const claimLink = (link: string) => { if (!userId) { return redirect( - `/app/sign-up?redirectUrl=/create-link?link=${link.toLowerCase()}` + `/app/sign-up?redirectUrl=/create-link?link=${link.toLowerCase()}`, ); } diff --git a/src/app/api/og/route.tsx b/src/app/api/og/route.tsx index 2c93f84..34345ab 100644 --- a/src/app/api/og/route.tsx +++ b/src/app/api/og/route.tsx @@ -4,10 +4,10 @@ export const runtime = "edge"; export async function GET(req: Request) { const calSans = await fetch( - new URL("../../../../public/fonts/CalSans-SemiBold.ttf", import.meta.url) + new URL("../../../../public/fonts/CalSans-SemiBold.ttf", import.meta.url), ).then((res) => res.arrayBuffer()); const inter = await fetch( - new URL("../../../../public/fonts/Inter-Regular.ttf", import.meta.url) + new URL("../../../../public/fonts/Inter-Regular.ttf", import.meta.url), ).then((res) => res.arrayBuffer()); const { searchParams } = new URL(req.url); @@ -49,6 +49,6 @@ export async function GET(req: Request) { weight: 400, }, ], - } + }, ); } diff --git a/src/app/api/trpc/[trpc]/route.ts b/src/app/api/trpc/[trpc]/route.ts index cdd6dac..1b47535 100644 --- a/src/app/api/trpc/[trpc]/route.ts +++ b/src/app/api/trpc/[trpc]/route.ts @@ -1,8 +1,7 @@ import type { NextRequest } from "next/server"; import { fetchRequestHandler } from "@trpc/server/adapters/fetch"; - -import { createTRPCContext } from "@/server/api/trpc"; import { appRouter } from "@/server/api/root"; +import { createTRPCContext } from "@/server/api/trpc"; const handler = (req: NextRequest) => fetchRequestHandler({ diff --git a/src/app/api/trpc/edge/[trpc]/route.ts b/src/app/api/trpc/edge/[trpc]/route.ts index 2ee41f9..af8a438 100644 --- a/src/app/api/trpc/edge/[trpc]/route.ts +++ b/src/app/api/trpc/edge/[trpc]/route.ts @@ -1,8 +1,7 @@ import type { NextRequest } from "next/server"; import { fetchRequestHandler } from "@trpc/server/adapters/fetch"; - -import { createTRPCContext } from "@/server/api/trpc"; import { edgeRouter } from "@/server/api/edge"; +import { createTRPCContext } from "@/server/api/trpc"; export const runtime = "edge"; export const preferredRegion = "iad1"; diff --git a/src/app/api/trpc/serverless/[trpc]/route.ts b/src/app/api/trpc/serverless/[trpc]/route.ts index 390cf72..f03f4d3 100644 --- a/src/app/api/trpc/serverless/[trpc]/route.ts +++ b/src/app/api/trpc/serverless/[trpc]/route.ts @@ -1,8 +1,7 @@ import type { NextRequest } from "next/server"; import { fetchRequestHandler } from "@trpc/server/adapters/fetch"; - -import { createTRPCContext } from "@/server/api/trpc"; import { serverlessRouter } from "@/server/api/serverless"; +import { createTRPCContext } from "@/server/api/trpc"; const handler = (req: NextRequest) => fetchRequestHandler({ diff --git a/src/app/api/uploadthing/route.ts b/src/app/api/uploadthing/route.ts index 925d5f0..0d6b976 100644 --- a/src/app/api/uploadthing/route.ts +++ b/src/app/api/uploadthing/route.ts @@ -1,5 +1,4 @@ import { createNextRouteHandler } from "uploadthing/next"; - import { appFileRouter } from "@/server/uploadthing"; export const { GET, POST } = createNextRouteHandler({ diff --git a/src/app/api/webhook/clerk/route.ts b/src/app/api/webhook/clerk/route.ts index 0ba0933..2345285 100644 --- a/src/app/api/webhook/clerk/route.ts +++ b/src/app/api/webhook/clerk/route.ts @@ -1,14 +1,11 @@ -import type { NextRequest } from "next/server"; -import { NextResponse } from "next/server"; -import type { WebhookEvent } from "@clerk/nextjs/server"; -import type { WebhookRequiredHeaders } from "svix"; -import { Webhook } from "svix"; import { type IncomingHttpHeaders } from "http"; - -import { createTRPCContext } from "@/server/api/trpc"; -import { serverlessRouter } from "@/server/api/serverless"; -import { clerkEvent } from "@/server/api/routers/clerk/type"; +import { NextResponse, type NextRequest } from "next/server"; +import type { WebhookEvent } from "@clerk/nextjs/server"; +import { Webhook, type WebhookRequiredHeaders } from "svix"; import { env } from "@/env.mjs"; +import { clerkEvent } from "@/server/api/routers/clerk/type"; +import { serverlessRouter } from "@/server/api/serverless"; +import { createTRPCContext } from "@/server/api/trpc"; export async function POST(req: NextRequestWithSvixRequiredHeaders) { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment @@ -17,7 +14,7 @@ export async function POST(req: NextRequestWithSvixRequiredHeaders) { if (!parsed.success) { return NextResponse.json( { error: "Internal Server Error" }, - { status: 500 } + { status: 500 }, ); } const payload = parsed.data; diff --git a/src/app/api/webhook/stripe/route.ts b/src/app/api/webhook/stripe/route.ts index 9aacf69..d6c5593 100644 --- a/src/app/api/webhook/stripe/route.ts +++ b/src/app/api/webhook/stripe/route.ts @@ -1,10 +1,8 @@ -import { type NextRequest } from "next/server"; -import { NextResponse } from "next/server"; - -import { createTRPCContext } from "@/server/api/trpc"; -import { serverlessRouter } from "@/server/api/serverless"; -import { stripe } from "@/lib/stripe"; +import { NextResponse, type NextRequest } from "next/server"; import { env } from "@/env.mjs"; +import { stripe } from "@/lib/stripe"; +import { serverlessRouter } from "@/server/api/serverless"; +import { createTRPCContext } from "@/server/api/trpc"; export async function POST(req: NextRequest) { const payload = await req.text(); @@ -18,7 +16,7 @@ export async function POST(req: NextRequest) { const event = stripe.webhooks.constructEvent( payload, signature, - env.STRIPE_WEBHOOK_SECRET + env.STRIPE_WEBHOOK_SECRET, ); const ctx = createTRPCContext({ req }); diff --git a/src/app/app/(dashboard)/loading.tsx b/src/app/app/(dashboard)/loading.tsx index 71d6adf..dde2178 100644 --- a/src/app/app/(dashboard)/loading.tsx +++ b/src/app/app/(dashboard)/loading.tsx @@ -1,5 +1,5 @@ -import { Skeleton } from "@/components/ui/skeleton"; import { ProfileLinkCardSkeleton } from "@/components/profile-link-card"; +import { Skeleton } from "@/components/ui/skeleton"; export default function Page() { return ( diff --git a/src/app/app/(dashboard)/page.tsx b/src/app/app/(dashboard)/page.tsx index 6cf70d6..1e1376e 100644 --- a/src/app/app/(dashboard)/page.tsx +++ b/src/app/app/(dashboard)/page.tsx @@ -1,10 +1,9 @@ import Link from "next/link"; - -import { api } from "@/trpc/server"; +import ProfileLinkCard from "@/components/profile-link-card"; import { Button } from "@/components/ui/button"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import ProfileLinkCard from "@/components/profile-link-card"; import UserSettings from "@/components/user-settings"; +import { api } from "@/trpc/server"; export default async function Page() { const links = await api.profileLink.getAll.query(); diff --git a/src/app/claim-link/page.tsx b/src/app/claim-link/page.tsx index 1c8ac7a..0561ba3 100644 --- a/src/app/claim-link/page.tsx +++ b/src/app/claim-link/page.tsx @@ -1,8 +1,7 @@ import Link from "next/link"; - -import { Button } from "@/components/ui/button"; import ClaimLinkForm from "@/components/forms/claim-link"; import AppNavbar from "@/components/navbar/app"; +import { Button } from "@/components/ui/button"; export default function Page() { return ( diff --git a/src/app/client-providers.tsx b/src/app/client-providers.tsx index 4700da1..ce07a51 100644 --- a/src/app/client-providers.tsx +++ b/src/app/client-providers.tsx @@ -1,16 +1,15 @@ "use client"; import { useState } from "react"; +import { Elements as StripeElements } from "@stripe/react-stripe-js"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { ReactQueryStreamedHydration } from "@tanstack/react-query-next-experimental"; import { loggerLink } from "@trpc/client"; -import { Elements as StripeElements } from "@stripe/react-stripe-js"; import superjson from "superjson"; - +import { ThemeProvider } from "@/components/theme-provider"; import { getStripe } from "@/lib/stripe/client"; import { api } from "@/trpc/react"; import { endingLink } from "@/trpc/shared"; -import { ThemeProvider } from "@/components/theme-provider"; export default function ClientProviders({ children, @@ -25,7 +24,7 @@ export default function ClientProviders({ staleTime: 5 * 1000, }, }, - }) + }), ); const [trpcClient] = useState(() => @@ -39,7 +38,7 @@ export default function ClientProviders({ }), endingLink(), ], - }) + }), ); return ( diff --git a/src/app/create-link/loading.tsx b/src/app/create-link/loading.tsx index d2eb19f..8cc6b45 100644 --- a/src/app/create-link/loading.tsx +++ b/src/app/create-link/loading.tsx @@ -1,5 +1,5 @@ -import { Skeleton } from "@/components/ui/skeleton"; import HomeNavbar from "@/components/navbar/home"; +import { Skeleton } from "@/components/ui/skeleton"; export default function Page() { return ( diff --git a/src/app/create-link/page.tsx b/src/app/create-link/page.tsx index e890bc8..d77b0ac 100644 --- a/src/app/create-link/page.tsx +++ b/src/app/create-link/page.tsx @@ -1,6 +1,5 @@ import { redirect } from "next/navigation"; import { currentUser } from "@clerk/nextjs"; - import SetupLink from "@/components/forms/setup-link"; import HomeNavbar from "@/components/navbar/home"; diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 777dff8..38ca1bd 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,20 +1,18 @@ import "@/styles/globals.css"; - import type { Metadata, Viewport } from "next"; import { Inter } from "next/font/google"; import LocalFont from "next/font/local"; -import { Analytics } from "@vercel/analytics/react"; import { ClerkProvider } from "@clerk/nextjs"; - +import { Analytics } from "@vercel/analytics/react"; +import ClientProviders from "@/app/client-providers"; import { defaultMetadata, - twitterMetadata, ogMetadata, + twitterMetadata, } from "@/app/shared-metadata"; -import ClientProviders from "@/app/client-providers"; +import Background from "@/components/background"; import { TailwindIndicator } from "@/components/tailwind-indicator"; import { Toaster } from "@/components/ui/toaster"; -import Background from "@/components/background"; const inter = Inter({ subsets: ["latin"], diff --git a/src/app/legal/layout.tsx b/src/app/legal/layout.tsx index 0d8a239..3e24df4 100644 --- a/src/app/legal/layout.tsx +++ b/src/app/legal/layout.tsx @@ -1,9 +1,8 @@ import type { Metadata } from "next"; - import { defaultMetadata, - twitterMetadata, ogMetadata, + twitterMetadata, } from "@/app/shared-metadata"; import HomeFooter from "@/components/footer/home"; import HomeNavbar from "@/components/navbar/home"; diff --git a/src/app/not-found.tsx b/src/app/not-found.tsx index fcbaff3..fe88799 100644 --- a/src/app/not-found.tsx +++ b/src/app/not-found.tsx @@ -1,5 +1,5 @@ -import { Button } from "@/components/ui/button"; import Link from "next/link"; +import { Button } from "@/components/ui/button"; export default function NotFound() { return ( diff --git a/src/components/bento/card.tsx b/src/components/bento/card.tsx index 033a318..3d82c3e 100644 --- a/src/components/bento/card.tsx +++ b/src/components/bento/card.tsx @@ -1,13 +1,12 @@ import type * as z from "zod"; - -import { type bentoSchema } from "@/server/db"; +import { type BentoSchema } from "@/server/db"; import LinkCard from "./link"; export default function BentoCard({ bento, editable, }: { - bento: z.infer; + bento: z.infer; editable?: boolean; }) { if (bento.type === "link") { diff --git a/src/components/bento/link.tsx b/src/components/bento/link.tsx index d569395..7eeca81 100644 --- a/src/components/bento/link.tsx +++ b/src/components/bento/link.tsx @@ -3,16 +3,15 @@ import Image from "next/image"; import Link from "next/link"; import { Github, Instagram, Linkedin, Twitch, Twitter } from "lucide-react"; -import { BsDiscord } from "react-icons/bs"; import { BiLogoTelegram } from "react-icons/bi"; +import { BsDiscord } from "react-icons/bs"; import type * as z from "zod"; - -import { type linkBentoSchema } from "@/types"; +import CardOverlay from "@/components/bento/overlay"; +import { Button } from "@/components/ui/button"; import { type getMetadata } from "@/lib/metadata"; import { cn } from "@/lib/utils"; -import { Button } from "@/components/ui/button"; -import CardOverlay from "@/components/bento/overlay"; import { api } from "@/trpc/react"; +import { type LinkBentoSchema } from "@/types"; const getBackgroundColor = (url: string) => { const urlObj = new URL(url); @@ -43,7 +42,7 @@ const getBackgroundColor = (url: string) => { const getIcon = ( url: string, - metadata?: Awaited> + metadata?: Awaited>, ) => { const urlObj = new URL(url); const hostname = urlObj.hostname; @@ -89,7 +88,7 @@ const getIcon = ( const getTitle = ( url: string, - metadata?: Awaited> + metadata?: Awaited>, ) => { const urlObj = new URL(url); const hostname = urlObj.hostname; @@ -120,7 +119,7 @@ const getTitle = ( const getDescription = ( url: string, - _metadata?: Awaited> + _metadata?: Awaited>, ) => { const urlObj = new URL(url); const hostname = urlObj.hostname; @@ -225,7 +224,7 @@ export default function LinkCard({ bento, editable, }: { - bento: z.infer; + bento: z.infer; editable?: boolean; }) { if (!bento.href) return null; @@ -249,7 +248,7 @@ export default function LinkCard({ getBackgroundColor(bento.href), editable ? "transition-transform duration-200 ease-in-out md:cursor-move" - : "cursor-pointer transition-all duration-200 ease-in-out hover:bg-opacity-80 active:scale-95" + : "cursor-pointer transition-all duration-200 ease-in-out hover:bg-opacity-80 active:scale-95", )} > {editable && } @@ -263,7 +262,7 @@ export default function LinkCard({ className={cn( "mt-2 font-cal text-sm", bento.size.sm === "4x1" ? "" : "hidden", - bento.size.md === "4x1" ? "" : "hidden" + bento.size.md === "4x1" ? "" : "hidden", )} > {title} @@ -274,7 +273,7 @@ export default function LinkCard({ className={cn( "mt-2 font-cal text-sm", bento.size.sm === "4x1" ? "hidden" : "", - bento.size.md === "4x1" ? "hidden" : "" + bento.size.md === "4x1" ? "hidden" : "", )} > {title} @@ -285,7 +284,7 @@ export default function LinkCard({ className={cn( "truncate text-xs", bento.size.sm === "4x1" ? "hidden" : "", - bento.size.md === "4x1" ? "hidden" : "" + bento.size.md === "4x1" ? "hidden" : "", )} > {description} diff --git a/src/components/bento/overlay/delete-button.tsx b/src/components/bento/overlay/delete-button.tsx index 0ac65a4..4b76967 100644 --- a/src/components/bento/overlay/delete-button.tsx +++ b/src/components/bento/overlay/delete-button.tsx @@ -3,15 +3,14 @@ import { useParams, useRouter } from "next/navigation"; import { Trash } from "lucide-react"; import type * as z from "zod"; - -import { type bentoSchema } from "@/server/db"; -import { api } from "@/trpc/react"; import { Button } from "@/components/ui/button"; +import { type BentoSchema } from "@/server/db"; +import { api } from "@/trpc/react"; export default function DeleteButton({ bento, }: { - bento: z.infer; + bento: z.infer; }) { const router = useRouter(); const { link } = useParams<{ link: string }>(); @@ -31,7 +30,7 @@ export default function DeleteButton({ ...old, bento: old.bento.filter((b) => b.id !== bento.id), }; - } + }, ); }, onSuccess: () => { diff --git a/src/components/bento/overlay/drag-handle.tsx b/src/components/bento/overlay/drag-handle.tsx index 3178f24..439b263 100644 --- a/src/components/bento/overlay/drag-handle.tsx +++ b/src/components/bento/overlay/drag-handle.tsx @@ -1,5 +1,4 @@ import { Hand } from "lucide-react"; - import { Button } from "@/components/ui/button"; export default function DragHandle() { diff --git a/src/components/bento/overlay/index.tsx b/src/components/bento/overlay/index.tsx index 8d1ec36..50032b7 100644 --- a/src/components/bento/overlay/index.tsx +++ b/src/components/bento/overlay/index.tsx @@ -2,17 +2,16 @@ import { useState } from "react"; import type * as z from "zod"; - -import { type bentoSchema } from "@/server/db"; -import { cn } from "@/lib/utils"; import DeleteButton from "@/components/bento/overlay/delete-button"; import DragHandle from "@/components/bento/overlay/drag-handle"; import ManageSize from "@/components/bento/overlay/manage-size"; +import { cn } from "@/lib/utils"; +import { type BentoSchema } from "@/server/db"; export default function CardOverlay({ bento, }: { - bento: z.infer; + bento: z.infer; }) { const [active, setActive] = useState(false); @@ -20,7 +19,7 @@ export default function CardOverlay({
{ if (window.outerWidth < 500) { diff --git a/src/components/bento/overlay/manage-size.tsx b/src/components/bento/overlay/manage-size.tsx index 94c2e6b..36311f8 100644 --- a/src/components/bento/overlay/manage-size.tsx +++ b/src/components/bento/overlay/manage-size.tsx @@ -1,17 +1,16 @@ "use client"; -import { createPortal } from "react-dom"; import { useParams, useRouter } from "next/navigation"; +import { createPortal } from "react-dom"; import type * as z from "zod"; - -import { bentoSchema } from "@/types"; -import { api } from "@/trpc/react"; -import { cn } from "@/lib/utils"; import Size2x2 from "@/components/icons/size_2x2"; -import Size4x2 from "@/components/icons/size_4x2"; import Size2x4 from "@/components/icons/size_2x4"; +import Size4x2 from "@/components/icons/size_4x2"; import Size4x4 from "@/components/icons/size_4x4"; import { Button } from "@/components/ui/button"; +import { cn } from "@/lib/utils"; +import { api } from "@/trpc/react"; +import { BentoSchema } from "@/types"; function ResponsivePortal({ children }: { children: React.ReactNode }) { if (window.outerWidth > 500) { @@ -25,7 +24,7 @@ export default function ManageSize({ bento, close, }: { - bento: z.infer; + bento: z.infer; close: () => void; }) { const router = useRouter(); @@ -67,13 +66,13 @@ export default function ManageSize({ ...old, bento: old.bento.map((b) => { if (b.id === bento.bento.id) { - return bentoSchema.parse(bento.bento); + return BentoSchema.parse(bento.bento); } return b; }), }; - } + }, ); }, onSuccess: () => { @@ -93,7 +92,7 @@ export default function ManageSize({ className={cn( "inline-flex items-center justify-center p-2 transition-transform duration-200 ease-in-out active:scale-95", size === o.key && - "rounded-sm bg-secondary text-secondary-foreground" + "rounded-sm bg-secondary text-secondary-foreground", )} onClick={() => { void updateBento({ diff --git a/src/components/footer/marketing.tsx b/src/components/footer/marketing.tsx index 0f096bd..a18a2f0 100644 --- a/src/components/footer/marketing.tsx +++ b/src/components/footer/marketing.tsx @@ -3,7 +3,6 @@ import Link from "next/link"; import { useParams } from "next/navigation"; import { Eye } from "lucide-react"; - import { api } from "@/trpc/react"; export default function MarketingFooter() { @@ -16,7 +15,7 @@ export default function MarketingFooter() { { staleTime: Infinity, enabled: !!link, - } + }, ); const { data: views } = api.profileLink.getViews.useQuery( @@ -26,7 +25,7 @@ export default function MarketingFooter() { { staleTime: Infinity, enabled: !!profileLink, - } + }, ); return ( diff --git a/src/components/forms/claim-link.tsx b/src/components/forms/claim-link.tsx index f7dc7e3..53606d5 100644 --- a/src/components/forms/claim-link.tsx +++ b/src/components/forms/claim-link.tsx @@ -2,12 +2,11 @@ import { useState } from "react"; import { Check, Loader, X } from "lucide-react"; - -import { api } from "@/trpc/react"; -import { cn } from "@/lib/utils"; import { claimLink } from "@/app/actions/claim-link"; -import { useDebounce } from "@/hooks/use-debounce"; import { Button } from "@/components/ui/button"; +import { useDebounce } from "@/hooks/use-debounce"; +import { cn } from "@/lib/utils"; +import { api } from "@/trpc/react"; export default function ClaimLinkForm({ className }: { className?: string }) { const [link, setLink] = useState(""); @@ -22,7 +21,7 @@ export default function ClaimLinkForm({ className }: { className?: string }) { { enabled: !!debouncedLink, staleTime: Infinity, - } + }, ); return ( diff --git a/src/components/forms/setup-link.tsx b/src/components/forms/setup-link.tsx index 8f514f0..bb6ad36 100644 --- a/src/components/forms/setup-link.tsx +++ b/src/components/forms/setup-link.tsx @@ -1,7 +1,6 @@ "use client"; -import { useSearchParams, useRouter } from "next/navigation"; -import * as z from "zod"; +import { useRouter, useSearchParams } from "next/navigation"; import { AtSign, Github, @@ -13,9 +12,7 @@ import { } from "lucide-react"; import { BiLogoTelegram } from "react-icons/bi"; import { BsDiscord } from "react-icons/bs"; - -import { api } from "@/trpc/react"; -import { useZodForm } from "@/hooks/use-zod-form"; +import * as z from "zod"; import { Button } from "@/components/ui/button"; import { Form, @@ -25,6 +22,8 @@ import { FormMessage, } from "@/components/ui/form"; import { toast } from "@/components/ui/use-toast"; +import { useZodForm } from "@/hooks/use-zod-form"; +import { api } from "@/trpc/react"; const setupLinkSchema = z.object({ twitter: z.string().optional(), diff --git a/src/components/modals/create-link-bento.tsx b/src/components/modals/create-link-bento.tsx index 0d7a7e9..1ecdbae 100644 --- a/src/components/modals/create-link-bento.tsx +++ b/src/components/modals/create-link-bento.tsx @@ -2,9 +2,7 @@ import { useState } from "react"; import { useParams, useRouter } from "next/navigation"; - -import { api } from "@/trpc/react"; -import { linkBentoSchema } from "@/types"; +import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, @@ -13,7 +11,8 @@ import { DialogTrigger, } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; -import { Button } from "@/components/ui/button"; +import { api } from "@/trpc/react"; +import { LinkBentoSchema } from "@/types"; export default function CreateLinkBentoModal({ children, @@ -40,9 +39,9 @@ export default function CreateLinkBentoModal({ return { ...old, - bento: [...old.bento, linkBentoSchema.parse(bento.bento)], + bento: [...old.bento, LinkBentoSchema.parse(bento.bento)], }; - } + }, ); }, onSuccess: () => { diff --git a/src/components/modals/link-qr-modal.tsx b/src/components/modals/link-qr-modal.tsx index 4b33297..202147c 100644 --- a/src/components/modals/link-qr-modal.tsx +++ b/src/components/modals/link-qr-modal.tsx @@ -4,9 +4,13 @@ import { useRef, useState } from "react"; import { useParams } from "next/navigation"; import { Copy, Download } from "lucide-react"; import { HexColorPicker } from "react-colorful"; - -import { type RouterOutputs } from "@/trpc/react"; -import { QRCodeSVG, getQRAsCanvas } from "@/lib/qr"; +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from "@/components/ui/accordion"; +import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, @@ -14,21 +18,16 @@ import { DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; -import { Button } from "@/components/ui/button"; import { Label } from "@/components/ui/label"; -import { Switch } from "@/components/ui/switch"; -import { - Accordion, - AccordionContent, - AccordionItem, - AccordionTrigger, -} from "@/components/ui/accordion"; -import { toast } from "@/components/ui/use-toast"; import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; +import { Switch } from "@/components/ui/switch"; +import { toast } from "@/components/ui/use-toast"; +import { getQRAsCanvas, QRCodeSVG } from "@/lib/qr"; +import { type RouterOutputs } from "@/trpc/react"; export default function LinkQRModal({ children, @@ -67,7 +66,7 @@ export default function LinkQRModal({ imageSettings: showLogo ? qrConfig.imageSettings : undefined, }, "image/png", - true + true, ); (canvas as HTMLCanvasElement).toBlob((blob) => { const url = URL.createObjectURL(blob!); @@ -96,7 +95,7 @@ export default function LinkQRModal({ imageSettings: showLogo ? qrConfig.imageSettings : undefined, }, "image/png", - true + true, ); (canvas as HTMLCanvasElement).toBlob((blob) => { const item = new ClipboardItem({ "image/png": blob! }); diff --git a/src/components/navbar/app.tsx b/src/components/navbar/app.tsx index cfaad2d..e49981e 100644 --- a/src/components/navbar/app.tsx +++ b/src/components/navbar/app.tsx @@ -4,7 +4,6 @@ import Image from "next/image"; import Link from "next/link"; import { useClerk } from "@clerk/clerk-react"; import { LogOut } from "lucide-react"; - import OpenBio from "@/public/openbio.png"; import { Button } from "@/components/ui/button"; diff --git a/src/components/navbar/auth.tsx b/src/components/navbar/auth.tsx index af69f57..49e2768 100644 --- a/src/components/navbar/auth.tsx +++ b/src/components/navbar/auth.tsx @@ -1,6 +1,5 @@ import Image from "next/image"; import Link from "next/link"; - import OpenBio from "@/public/openbio.png"; export default function AuthNavbar() { diff --git a/src/components/navbar/home.tsx b/src/components/navbar/home.tsx index 5957cfc..db20637 100644 --- a/src/components/navbar/home.tsx +++ b/src/components/navbar/home.tsx @@ -3,7 +3,6 @@ import Image from "next/image"; import Link from "next/link"; import { useUser } from "@clerk/nextjs"; - import OpenBio from "@/public/openbio.png"; import { Button } from "@/components/ui/button"; diff --git a/src/components/pricing.tsx b/src/components/pricing.tsx index c1749d8..739b537 100644 --- a/src/components/pricing.tsx +++ b/src/components/pricing.tsx @@ -1,23 +1,22 @@ "use client"; import { useState, useTransition } from "react"; -import { redirect } from "next/navigation"; import Link from "next/link"; -import Confetti from "react-dom-confetti"; +import { redirect } from "next/navigation"; import { Check, HelpCircle, Loader, X } from "lucide-react"; - -import { PLANS } from "@/lib/stripe/plans"; -import { getStripe } from "@/lib/stripe/client"; -import { type RouterOutputs, api } from "@/trpc/react"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import Confetti from "react-dom-confetti"; import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip"; -import { Button } from "@/components/ui/button"; +import { getStripe } from "@/lib/stripe/client"; +import { PLANS } from "@/lib/stripe/plans"; +import { api, type RouterOutputs } from "@/trpc/react"; type Billing = "monthly" | "annually"; diff --git a/src/components/profile-link-card.tsx b/src/components/profile-link-card.tsx index c4d5332..1056205 100644 --- a/src/components/profile-link-card.tsx +++ b/src/components/profile-link-card.tsx @@ -1,8 +1,7 @@ import Link from "next/link"; import { Eye } from "lucide-react"; - -import { type RouterOutputs, api } from "@/trpc/server"; import { Skeleton } from "@/components/ui/skeleton"; +import { api, type RouterOutputs } from "@/trpc/server"; export function ProfileLinkCardSkeleton() { return ( diff --git a/src/components/theme-toggle.tsx b/src/components/theme-toggle.tsx index 4d9d9a3..73b22ca 100644 --- a/src/components/theme-toggle.tsx +++ b/src/components/theme-toggle.tsx @@ -3,7 +3,6 @@ import * as React from "react"; import { Moon, Sun } from "lucide-react"; import { useTheme } from "next-themes"; - import { Button } from "@/components/ui/button"; import { DropdownMenu, diff --git a/src/components/ui/accordion.tsx b/src/components/ui/accordion.tsx index 1b4c75b..54672be 100644 --- a/src/components/ui/accordion.tsx +++ b/src/components/ui/accordion.tsx @@ -3,7 +3,6 @@ import * as React from "react"; import * as AccordionPrimitive from "@radix-ui/react-accordion"; import { ChevronDownIcon } from "@radix-ui/react-icons"; - import { cn } from "@/lib/utils"; const Accordion = AccordionPrimitive.Root; @@ -29,7 +28,7 @@ const AccordionTrigger = React.forwardRef< ref={ref} className={cn( "flex flex-1 items-center justify-between py-4 text-sm font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180", - className + className, )} {...props} > @@ -48,7 +47,7 @@ const AccordionContent = React.forwardRef< ref={ref} className={cn( "overflow-hidden text-sm data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down", - className + className, )} {...props} > diff --git a/src/components/ui/avatar.tsx b/src/components/ui/avatar.tsx index d260623..145b126 100644 --- a/src/components/ui/avatar.tsx +++ b/src/components/ui/avatar.tsx @@ -2,7 +2,6 @@ import * as React from "react"; import * as AvatarPrimitive from "@radix-ui/react-avatar"; - import { cn } from "@/lib/utils"; const Avatar = React.forwardRef< @@ -13,7 +12,7 @@ const Avatar = React.forwardRef< ref={ref} className={cn( "relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full", - className + className, )} {...props} /> @@ -40,7 +39,7 @@ const AvatarFallback = React.forwardRef< ref={ref} className={cn( "flex h-full w-full items-center justify-center rounded-full bg-muted", - className + className, )} {...props} /> diff --git a/src/components/ui/badge.tsx b/src/components/ui/badge.tsx index fd92f29..2f45007 100644 --- a/src/components/ui/badge.tsx +++ b/src/components/ui/badge.tsx @@ -1,6 +1,5 @@ import * as React from "react"; import { cva, type VariantProps } from "class-variance-authority"; - import { cn } from "@/lib/utils"; const badgeVariants = cva( @@ -20,7 +19,7 @@ const badgeVariants = cva( defaultVariants: { variant: "default", }, - } + }, ); export interface BadgeProps diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx index 3e91ce1..ebebeb0 100644 --- a/src/components/ui/button.tsx +++ b/src/components/ui/button.tsx @@ -1,7 +1,6 @@ import * as React from "react"; import { Slot } from "@radix-ui/react-slot"; import { cva, type VariantProps } from "class-variance-authority"; - import { cn } from "@/lib/utils"; const buttonVariants = cva( @@ -31,7 +30,7 @@ const buttonVariants = cva( variant: "default", size: "default", }, - } + }, ); export interface ButtonProps @@ -50,7 +49,7 @@ const Button = React.forwardRef( {...props} /> ); - } + }, ); Button.displayName = "Button"; diff --git a/src/components/ui/dialog.tsx b/src/components/ui/dialog.tsx index 507122a..4885c20 100644 --- a/src/components/ui/dialog.tsx +++ b/src/components/ui/dialog.tsx @@ -3,7 +3,6 @@ import * as React from "react"; import * as DialogPrimitive from "@radix-ui/react-dialog"; import { Cross2Icon } from "@radix-ui/react-icons"; - import { cn } from "@/lib/utils"; const Dialog = DialogPrimitive.Root; @@ -26,7 +25,7 @@ const DialogOverlay = React.forwardRef< ref={ref} className={cn( "fixed inset-0 z-50 bg-background/80 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0", - className + className, )} {...props} /> @@ -45,7 +44,7 @@ const DialogContent = React.forwardRef< ref={ref} className={cn( "fixed left-[50%] top-[50%] z-50 grid w-[90%] max-w-md translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] md:w-full", - className + className, )} {...props} > @@ -79,7 +78,7 @@ const DialogFooter = ({
@@ -94,7 +93,7 @@ const DialogTitle = React.forwardRef< ref={ref} className={cn( "text-lg font-semibold leading-none tracking-tight", - className + className, )} {...props} /> diff --git a/src/components/ui/dropdown-menu.tsx b/src/components/ui/dropdown-menu.tsx index 748fd95..228d8f8 100644 --- a/src/components/ui/dropdown-menu.tsx +++ b/src/components/ui/dropdown-menu.tsx @@ -7,7 +7,6 @@ import { ChevronRightIcon, DotFilledIcon, } from "@radix-ui/react-icons"; - import { cn } from "@/lib/utils"; const DropdownMenu = DropdownMenuPrimitive.Root; @@ -33,7 +32,7 @@ const DropdownMenuSubTrigger = React.forwardRef< className={cn( "flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent", inset && "pl-8", - className + className, )} {...props} > @@ -52,7 +51,7 @@ const DropdownMenuSubContent = React.forwardRef< ref={ref} className={cn( "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", - className + className, )} {...props} /> @@ -71,7 +70,7 @@ const DropdownMenuContent = React.forwardRef< className={cn( "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md", "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", - className + className, )} {...props} /> @@ -90,7 +89,7 @@ const DropdownMenuItem = React.forwardRef< className={cn( "relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50", inset && "pl-8", - className + className, )} {...props} /> @@ -105,7 +104,7 @@ const DropdownMenuCheckboxItem = React.forwardRef< ref={ref} className={cn( "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50", - className + className, )} checked={checked} {...props} @@ -129,7 +128,7 @@ const DropdownMenuRadioItem = React.forwardRef< ref={ref} className={cn( "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50", - className + className, )} {...props} > @@ -154,7 +153,7 @@ const DropdownMenuLabel = React.forwardRef< className={cn( "px-2 py-1.5 text-sm font-semibold", inset && "pl-8", - className + className, )} {...props} /> diff --git a/src/components/ui/form.tsx b/src/components/ui/form.tsx index fbe53a9..66a546b 100644 --- a/src/components/ui/form.tsx +++ b/src/components/ui/form.tsx @@ -3,32 +3,31 @@ import type * as LabelPrimitive from "@radix-ui/react-label"; import { Slot } from "@radix-ui/react-slot"; import { Controller, + FormProvider, + useFormContext, type ControllerProps, type FieldPath, type FieldValues, - FormProvider, - useFormContext, } from "react-hook-form"; - -import { cn } from "@/lib/utils"; import { Label } from "@/components/ui/label"; +import { cn } from "@/lib/utils"; const Form = FormProvider; type FormFieldContextValue< TFieldValues extends FieldValues = FieldValues, - TName extends FieldPath = FieldPath + TName extends FieldPath = FieldPath, > = { name: TName; }; const FormFieldContext = React.createContext( - {} as FormFieldContextValue + {} as FormFieldContextValue, ); const FormField = < TFieldValues extends FieldValues = FieldValues, - TName extends FieldPath = FieldPath + TName extends FieldPath = FieldPath, >({ ...props }: ControllerProps) => { @@ -67,7 +66,7 @@ type FormItemContextValue = { }; const FormItemContext = React.createContext( - {} as FormItemContextValue + {} as FormItemContextValue, ); const FormItem = React.forwardRef< diff --git a/src/components/ui/hover-card.tsx b/src/components/ui/hover-card.tsx index d130a8b..7dcb3d2 100644 --- a/src/components/ui/hover-card.tsx +++ b/src/components/ui/hover-card.tsx @@ -2,7 +2,6 @@ import * as React from "react"; import * as HoverCardPrimitive from "@radix-ui/react-hover-card"; - import { cn } from "@/lib/utils"; const HoverCard = HoverCardPrimitive.Root; @@ -19,7 +18,7 @@ const HoverCardContent = React.forwardRef< sideOffset={sideOffset} className={cn( "z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", - className + className, )} {...props} /> diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx index 25b8a34..3fed80f 100644 --- a/src/components/ui/input.tsx +++ b/src/components/ui/input.tsx @@ -1,5 +1,4 @@ import * as React from "react"; - import { cn } from "@/lib/utils"; export type InputProps = React.InputHTMLAttributes; @@ -11,13 +10,13 @@ const Input = React.forwardRef( type={type} className={cn( "flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50", - className + className, )} ref={ref} {...props} /> ); - } + }, ); Input.displayName = "Input"; diff --git a/src/components/ui/label.tsx b/src/components/ui/label.tsx index 40378d4..c195ab7 100644 --- a/src/components/ui/label.tsx +++ b/src/components/ui/label.tsx @@ -3,11 +3,10 @@ import * as React from "react"; import * as LabelPrimitive from "@radix-ui/react-label"; import { cva, type VariantProps } from "class-variance-authority"; - import { cn } from "@/lib/utils"; const labelVariants = cva( - "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" + "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70", ); const Label = React.forwardRef< diff --git a/src/components/ui/popover.tsx b/src/components/ui/popover.tsx index f14fc6b..a58024f 100644 --- a/src/components/ui/popover.tsx +++ b/src/components/ui/popover.tsx @@ -2,7 +2,6 @@ import * as React from "react"; import * as PopoverPrimitive from "@radix-ui/react-popover"; - import { cn } from "@/lib/utils"; const Popover = PopoverPrimitive.Root; @@ -20,7 +19,7 @@ const PopoverContent = React.forwardRef< sideOffset={sideOffset} className={cn( "z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", - className + className, )} {...props} /> diff --git a/src/components/ui/switch.tsx b/src/components/ui/switch.tsx index 3e60b4c..72f4286 100644 --- a/src/components/ui/switch.tsx +++ b/src/components/ui/switch.tsx @@ -2,7 +2,6 @@ import * as React from "react"; import * as SwitchPrimitives from "@radix-ui/react-switch"; - import { cn } from "@/lib/utils"; const Switch = React.forwardRef< @@ -12,14 +11,14 @@ const Switch = React.forwardRef< diff --git a/src/components/ui/tabs.tsx b/src/components/ui/tabs.tsx index 35f66dd..564a7b7 100644 --- a/src/components/ui/tabs.tsx +++ b/src/components/ui/tabs.tsx @@ -2,7 +2,6 @@ import * as React from "react"; import * as TabsPrimitive from "@radix-ui/react-tabs"; - import { cn } from "@/lib/utils"; const Tabs = TabsPrimitive.Root; @@ -15,7 +14,7 @@ const TabsList = React.forwardRef< ref={ref} className={cn( "inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground", - className + className, )} {...props} /> @@ -30,7 +29,7 @@ const TabsTrigger = React.forwardRef< ref={ref} className={cn( "inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow", - className + className, )} {...props} /> @@ -45,7 +44,7 @@ const TabsContent = React.forwardRef< ref={ref} className={cn( "mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2", - className + className, )} {...props} /> diff --git a/src/components/ui/toast.tsx b/src/components/ui/toast.tsx index bc249b2..0108480 100644 --- a/src/components/ui/toast.tsx +++ b/src/components/ui/toast.tsx @@ -2,7 +2,6 @@ import * as React from "react"; import { Cross2Icon } from "@radix-ui/react-icons"; import * as ToastPrimitives from "@radix-ui/react-toast"; import { cva, type VariantProps } from "class-variance-authority"; - import { cn } from "@/lib/utils"; const ToastProvider = ToastPrimitives.Provider; @@ -15,7 +14,7 @@ const ToastViewport = React.forwardRef< ref={ref} className={cn( "fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]", - className + className, )} {...props} /> @@ -35,7 +34,7 @@ const toastVariants = cva( defaultVariants: { variant: "default", }, - } + }, ); const Toast = React.forwardRef< @@ -61,7 +60,7 @@ const ToastAction = React.forwardRef< ref={ref} className={cn( "inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium transition-colors hover:bg-secondary focus:outline-none focus:ring-1 focus:ring-ring disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive", - className + className, )} {...props} /> @@ -76,7 +75,7 @@ const ToastClose = React.forwardRef< ref={ref} className={cn( "absolute right-1 top-1 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-1 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600", - className + className, )} toast-close="" {...props} diff --git a/src/components/ui/tooltip.tsx b/src/components/ui/tooltip.tsx index 6fdf76a..857f240 100644 --- a/src/components/ui/tooltip.tsx +++ b/src/components/ui/tooltip.tsx @@ -2,7 +2,6 @@ import * as React from "react"; import * as TooltipPrimitive from "@radix-ui/react-tooltip"; - import { cn } from "@/lib/utils"; const TooltipProvider = TooltipPrimitive.Provider; @@ -20,7 +19,7 @@ const TooltipContent = React.forwardRef< sideOffset={sideOffset} className={cn( "z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", - className + className, )} {...props} /> diff --git a/src/components/ui/use-toast.ts b/src/components/ui/use-toast.ts index 46bd4ac..49b96a5 100644 --- a/src/components/ui/use-toast.ts +++ b/src/components/ui/use-toast.ts @@ -1,6 +1,5 @@ // Inspired by react-hot-toast library import * as React from "react"; - import type { ToastActionElement, ToastProps } from "@/components/ui/toast"; const TOAST_LIMIT = 1; @@ -81,7 +80,7 @@ export const reducer = (state: State, action: Action): State => { return { ...state, toasts: state.toasts.map((t) => - t.id === action.toast.id ? { ...t, ...action.toast } : t + t.id === action.toast.id ? { ...t, ...action.toast } : t, ), }; @@ -106,7 +105,7 @@ export const reducer = (state: State, action: Action): State => { ...t, open: false, } - : t + : t, ), }; } diff --git a/src/components/user-settings.tsx b/src/components/user-settings.tsx index c7f5b60..a91cbef 100644 --- a/src/components/user-settings.tsx +++ b/src/components/user-settings.tsx @@ -1,12 +1,11 @@ import { currentUser } from "@clerk/nextjs"; - -import { type RouterOutputs } from "@/trpc/server"; +import { PricingCards } from "@/components/pricing"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; -import { Label } from "@/components/ui/label"; -import { Input } from "@/components/ui/input"; import { Badge } from "@/components/ui/badge"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { PricingCards } from "@/components/pricing"; +import { type RouterOutputs } from "@/trpc/server"; export default async function UserSettings({ user, diff --git a/src/hooks/use-debounce.ts b/src/hooks/use-debounce.ts index 0888359..100322b 100644 --- a/src/hooks/use-debounce.ts +++ b/src/hooks/use-debounce.ts @@ -1,4 +1,4 @@ -import { useState, useEffect } from "react"; +import { useEffect, useState } from "react"; export function useDebounce(value: T, delay?: number): T { const [debouncedValue, setDebouncedValue] = useState(value); diff --git a/src/hooks/use-zod-form.tsx b/src/hooks/use-zod-form.tsx index 6663b77..2435af8 100644 --- a/src/hooks/use-zod-form.tsx +++ b/src/hooks/use-zod-form.tsx @@ -5,7 +5,7 @@ import { type ZodType } from "zod"; export function useZodForm( props: Omit, "resolver"> & { schema: TSchema; - } + }, ) { const form = useForm({ ...props, diff --git a/src/lib/qr/generator.tsx b/src/lib/qr/generator.tsx index 3e8febd..201f152 100644 --- a/src/lib/qr/generator.tsx +++ b/src/lib/qr/generator.tsx @@ -51,7 +51,7 @@ namespace qrcodegen { // The ECC level of the result may be higher than the ecl argument if it can be done without increasing the version. public static encodeBinary( data: Readonly>, - ecl: QrCode.Ecc + ecl: QrCode.Ecc, ): QrCode { const seg: QrSegment = qrcodegen.QrSegment.makeBytes(data); return QrCode.encodeSegments([seg], ecl); @@ -74,7 +74,7 @@ namespace qrcodegen { minVersion: int = 1, maxVersion: int = 40, mask: int = -1, - boostEcl = true + boostEcl = true, ): QrCode { if ( !( @@ -146,7 +146,7 @@ namespace qrcodegen { const dataCodewords: Array = []; while (dataCodewords.length * 8 < bb.length) dataCodewords.push(0); bb.forEach( - (b: bit, i: int) => (dataCodewords[i >>> 3] |= b << (7 - (i & 7))) + (b: bit, i: int) => (dataCodewords[i >>> 3] |= b << (7 - (i & 7))), ); // Create the QR Code object @@ -187,7 +187,7 @@ namespace qrcodegen { dataCodewords: Readonly>, - msk: int + msk: int, ) { // Check scalar arguments if (version < QrCode.MIN_VERSION || version > QrCode.MAX_VERSION) @@ -354,7 +354,7 @@ namespace qrcodegen { this.setFunctionModule( x + dx, y + dy, - Math.max(Math.abs(dx), Math.abs(dy)) != 1 + Math.max(Math.abs(dx), Math.abs(dy)) != 1, ); } } @@ -381,7 +381,7 @@ namespace qrcodegen { QrCode.NUM_ERROR_CORRECTION_BLOCKS[ecl.ordinal][ver]; const blockEccLen: int = QrCode.ECC_CODEWORDS_PER_BLOCK[ecl.ordinal][ver]; const rawCodewords: int = Math.floor( - QrCode.getNumRawDataModules(ver) / 8 + QrCode.getNumRawDataModules(ver) / 8, ); const numShortBlocks: int = numBlocks - (rawCodewords % numBlocks); const shortBlockLen: int = Math.floor(rawCodewords / numBlocks); @@ -392,7 +392,7 @@ namespace qrcodegen { for (let i = 0, k = 0; i < numBlocks; i++) { const dat: Array = data.slice( k, - k + shortBlockLen - blockEccLen + (i < numShortBlocks ? 0 : 1) + k + shortBlockLen - blockEccLen + (i < numShortBlocks ? 0 : 1), ); k += dat.length; const ecc: Array = QrCode.reedSolomonComputeRemainder(dat, rsDiv); @@ -641,7 +641,7 @@ namespace qrcodegen { // Returns the Reed-Solomon error correction codeword for the given data and divisor polynomials. private static reedSolomonComputeRemainder( data: Readonly>, - divisor: Readonly> + divisor: Readonly>, ): Array { const result: Array = divisor.map((_) => 0); for (const b of data) { @@ -649,7 +649,7 @@ namespace qrcodegen { const factor: byte = b ^ result.shift()!; result.push(0); divisor.forEach( - (coef, i) => (result[i] ^= QrCode.reedSolomonMultiply(coef, factor)) + (coef, i) => (result[i] ^= QrCode.reedSolomonMultiply(coef, factor)), ); } return result; @@ -691,7 +691,7 @@ namespace qrcodegen { private finderPenaltyTerminateAndCount( currentRunColor: boolean, currentRunLength: int, - runHistory: Array + runHistory: Array, ): int { if (currentRunColor) { // Terminate dark run @@ -706,7 +706,7 @@ namespace qrcodegen { // Pushes the given value to the front and drops the last value. A helper function for getPenaltyScore(). private finderPenaltyAddHistory( currentRunLength: int, - runHistory: Array + runHistory: Array, ): void { if (runHistory[0] == 0) currentRunLength += this.size; // Add light border to initial run runHistory.pop(); @@ -844,7 +844,7 @@ namespace qrcodegen { public static makeAlphanumeric(text: string): QrSegment { if (!QrSegment.isAlphanumeric(text)) throw new RangeError( - "String contains unencodable characters in alphanumeric mode" + "String contains unencodable characters in alphanumeric mode", ); const bb: Array = []; let i: int; @@ -860,7 +860,7 @@ namespace qrcodegen { appendBits( QrSegment.ALPHANUMERIC_CHARSET.indexOf(text.charAt(i)), 6, - bb + bb, ); return new QrSegment(QrSegment.Mode.ALPHANUMERIC, text.length, bb); } @@ -921,7 +921,7 @@ namespace qrcodegen { public readonly numChars: int, // The data bits of this segment. Accessed through getData(). - private readonly bitData: Array + private readonly bitData: Array, ) { if (numChars < 0) throw new RangeError("Invalid argument"); this.bitData = bitData.slice(); // Make defensive copy @@ -938,7 +938,7 @@ namespace qrcodegen { // the given version. The result is infinity if a segment has too many characters to fit its length field. public static getTotalBits( segs: Readonly>, - version: int + version: int, ): number { let result = 0; for (const seg of segs) { @@ -1001,7 +1001,7 @@ namespace qrcodegen.QrCode { // In the range 0 to 3 (unsigned 2-bit integer). public readonly ordinal: int, // (Package-private) In the range 0 to 3 (unsigned 2-bit integer). - public readonly formatBits: int + public readonly formatBits: int, ) {} } } @@ -1029,7 +1029,7 @@ namespace qrcodegen.QrSegment { // The mode indicator bits, which is a uint4 value (range 0 to 15). public readonly modeBits: int, // Number of character count bits for three different version ranges. - private readonly numBitsCharCount: [int, int, int] + private readonly numBitsCharCount: [int, int, int], ) {} /*-- Method --*/ diff --git a/src/lib/qr/index.tsx b/src/lib/qr/index.tsx index 6a34c15..68b640c 100644 --- a/src/lib/qr/index.tsx +++ b/src/lib/qr/index.tsx @@ -12,11 +12,10 @@ // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-nocheck -import type { CSSProperties } from "react"; -import React, { useEffect, useRef, useState } from "react"; +import React, { useEffect, useRef, useState, type CSSProperties } from "react"; +import NextImage from "next/image"; import { escape } from "html-escaper"; import qrcodegen from "./generator"; -import NextImage from "next/image"; type Modules = ReturnType; type Excavation = { x: number; y: number; w: number; h: number }; @@ -74,7 +73,7 @@ function generatePath(modules: Modules, margin = 0): string { // M0 0h7v1H0z injects the space with the move and drops the comma, // saving a char per operation ops.push( - `M${start + margin} ${y + margin}h${x - start}v1H${start + margin}z` + `M${start + margin} ${y + margin}h${x - start}v1H${start + margin}z`, ); start = null; return; @@ -95,7 +94,7 @@ function generatePath(modules: Modules, margin = 0): string { ops.push( `M${start + margin},${y + margin} h${x + 1 - start}v1H${ start + margin - }z` + }z`, ); } return; @@ -129,7 +128,7 @@ function getImageSettings( cells: Modules, size: number, includeMargin: boolean, - imageSettings?: ImageSettings + imageSettings?: ImageSettings, ): null | { x: number; y: number; @@ -216,7 +215,7 @@ export function QRCodeCanvas(props: QRPropsCanvas) { let cells = qrcodegen.QrCode.encodeText( value, - ERROR_LEVEL_MAP[level] + ERROR_LEVEL_MAP[level], ).getModules(); const margin = includeMargin ? MARGIN_SIZE : 0; @@ -225,7 +224,7 @@ export function QRCodeCanvas(props: QRPropsCanvas) { cells, size, includeMargin, - imageSettings + imageSettings, ); const image = _image.current; @@ -275,7 +274,7 @@ export function QRCodeCanvas(props: QRPropsCanvas) { calculatedImageSettings.x + margin, calculatedImageSettings.y + margin, calculatedImageSettings.w, - calculatedImageSettings.h + calculatedImageSettings.h, ); } } @@ -333,7 +332,7 @@ export function QRCodeSVG(props: QRPropsSVG) { let cells = qrcodegen.QrCode.encodeText( value, - ERROR_LEVEL_MAP[level] + ERROR_LEVEL_MAP[level], ).getModules(); const margin = includeMargin ? MARGIN_SIZE : 0; @@ -342,7 +341,7 @@ export function QRCodeSVG(props: QRPropsSVG) { cells, size, includeMargin, - imageSettings + imageSettings, ); let image = null; @@ -402,7 +401,7 @@ export function getQRAsSVGDataUri(props: QRProps) { let cells = qrcodegen.QrCode.encodeText( value, - ERROR_LEVEL_MAP[level] + ERROR_LEVEL_MAP[level], ).getModules(); const margin = includeMargin ? MARGIN_SIZE : 0; @@ -411,7 +410,7 @@ export function getQRAsSVGDataUri(props: QRProps) { cells, size, includeMargin, - imageSettings + imageSettings, ); let image = ""; @@ -457,7 +456,7 @@ function waitUntilImageLoaded(img: HTMLImageElement, src: string) { export async function getQRAsCanvas( props: QRProps, type: string, - getCanvas?: boolean + getCanvas?: boolean, ): Promise { const { value, @@ -474,7 +473,7 @@ export async function getQRAsCanvas( let cells = qrcodegen.QrCode.encodeText( value, - ERROR_LEVEL_MAP[level] + ERROR_LEVEL_MAP[level], ).getModules(); const margin = includeMargin ? MARGIN_SIZE : 0; const numCells = cells.length + margin * 2; @@ -482,7 +481,7 @@ export async function getQRAsCanvas( cells, size, includeMargin, - imageSettings + imageSettings, ); const image = new Image(); @@ -529,7 +528,7 @@ export async function getQRAsCanvas( calculatedImageSettings.x + margin, calculatedImageSettings.y + margin, calculatedImageSettings.w, - calculatedImageSettings.h + calculatedImageSettings.h, ); } diff --git a/src/lib/stripe/client.ts b/src/lib/stripe/client.ts index 6447397..cb3b049 100644 --- a/src/lib/stripe/client.ts +++ b/src/lib/stripe/client.ts @@ -1,5 +1,5 @@ +import { loadStripe, type Stripe } from "@stripe/stripe-js"; import { env } from "@/env.mjs"; -import { type Stripe, loadStripe } from "@stripe/stripe-js"; let stripePromise: Promise; diff --git a/src/lib/stripe/index.ts b/src/lib/stripe/index.ts index 857180f..90f15bb 100644 --- a/src/lib/stripe/index.ts +++ b/src/lib/stripe/index.ts @@ -1,5 +1,5 @@ -import { env } from "@/env.mjs"; import Stripe from "stripe"; +import { env } from "@/env.mjs"; export const stripe = new Stripe(env.STRIPE_SECRET_KEY, { apiVersion: "2023-08-16", diff --git a/src/lib/uploadthing.ts b/src/lib/uploadthing.ts index 4c45f34..8ecc276 100644 --- a/src/lib/uploadthing.ts +++ b/src/lib/uploadthing.ts @@ -1,6 +1,5 @@ import { generateComponents } from "@uploadthing/react"; import { generateReactHelpers } from "@uploadthing/react/hooks"; - import type { AppFileRouter } from "@/server/uploadthing"; export const { UploadButton, UploadDropzone, Uploader } = diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 365058c..a5ef193 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -1,4 +1,4 @@ -import { type ClassValue, clsx } from "clsx"; +import { clsx, type ClassValue } from "clsx"; import { twMerge } from "tailwind-merge"; export function cn(...inputs: ClassValue[]) { diff --git a/src/middleware.ts b/src/middleware.ts index fc3e229..7858c00 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -1,7 +1,5 @@ -import type { NextRequest } from "next/server"; -import { NextResponse } from "next/server"; +import { NextResponse, type NextRequest } from "next/server"; import { authMiddleware } from "@clerk/nextjs"; - import { env } from "@/env.mjs"; const before = (req: NextRequest) => { diff --git a/src/server/api/edge.ts b/src/server/api/edge.ts index f02b8fd..8230e5a 100644 --- a/src/server/api/edge.ts +++ b/src/server/api/edge.ts @@ -1,6 +1,6 @@ -import { createTRPCRouter } from "@/server/api/trpc"; -import { userRouter } from "@/server/api/routers/user"; import { profileLinkRouter } from "@/server/api/routers/profile-link"; +import { userRouter } from "@/server/api/routers/user"; +import { createTRPCRouter } from "@/server/api/trpc"; export const edgeRouter = createTRPCRouter({ user: userRouter, diff --git a/src/server/api/root.ts b/src/server/api/root.ts index e715001..8397e3e 100644 --- a/src/server/api/root.ts +++ b/src/server/api/root.ts @@ -1,6 +1,6 @@ -import { mergeRouters } from "@/server/api/trpc"; import { edgeRouter } from "@/server/api/edge"; import { serverlessRouter } from "@/server/api/serverless"; +import { mergeRouters } from "@/server/api/trpc"; export const appRouter = mergeRouters(edgeRouter, serverlessRouter); diff --git a/src/server/api/routers/clerk/index.ts b/src/server/api/routers/clerk/index.ts index d531521..32c1a72 100644 --- a/src/server/api/routers/clerk/index.ts +++ b/src/server/api/routers/clerk/index.ts @@ -1,6 +1,5 @@ -import { createTRPCRouter } from "@/server/api/trpc"; - import { webhookRouter } from "@/server/api/routers/clerk/webhook"; +import { createTRPCRouter } from "@/server/api/trpc"; export const clerkRouter = createTRPCRouter({ webhook: webhookRouter, diff --git a/src/server/api/routers/clerk/type.ts b/src/server/api/routers/clerk/type.ts index 9ef2c4a..a5ce425 100644 --- a/src/server/api/routers/clerk/type.ts +++ b/src/server/api/routers/clerk/type.ts @@ -10,7 +10,7 @@ export const clerkEvent = z.discriminatedUnion("type", [ email_addresses: z.array( z.object({ email_address: z.string(), - }) + }), ), first_name: z.string(), last_name: z.string().nullable(), diff --git a/src/server/api/routers/clerk/webhook.ts b/src/server/api/routers/clerk/webhook.ts index 86d8392..bdee39e 100644 --- a/src/server/api/routers/clerk/webhook.ts +++ b/src/server/api/routers/clerk/webhook.ts @@ -1,15 +1,14 @@ import * as z from "zod"; - -import { createTRPCRouter, publicProcedure } from "@/server/api/trpc"; +import WelcomeEmail from "@/components/emails/welcome"; import { clerkEvent } from "@/server/api/routers/clerk/type"; +import { createTRPCRouter, publicProcedure } from "@/server/api/trpc"; import { user } from "@/server/db"; import { sendEmail } from "@/server/emails"; -import WelcomeEmail from "@/components/emails/welcome"; export const webhookProcedure = publicProcedure.input( z.object({ data: clerkEvent, - }) + }), ); export const webhookRouter = createTRPCRouter({ diff --git a/src/server/api/routers/profile-link.ts b/src/server/api/routers/profile-link.ts index d06295e..4c5e83e 100644 --- a/src/server/api/routers/profile-link.ts +++ b/src/server/api/routers/profile-link.ts @@ -1,523 +1,276 @@ -import * as z from "zod"; -import { kv } from "@vercel/kv"; - import { type SignedInAuthObject } from "@clerk/nextjs/api"; +import * as z from "zod"; +import { getMetadata } from "@/lib/metadata"; import { - type Context, - createTRPCRouter, - protectedProcedure, - publicProcedure, + createTRPCRouter, + protectedProcedure, + publicProcedure, + type Context, } from "@/server/api/trpc"; import { - link, - type InferSelectModel, - eq, - sql, - linkView, - type sizeSchema, - type positionSchema, - bentoSchema, + addProfileLinkBento, + canModifyProfileLink, + canUserCreateProfileLink, + createProfileLink, + deleteProfileLink, + deleteProfileLinkBento, + getProfileLinkByLink, + getProfileLinksOfUser, + getProfileLinkViews, + getUserByProviderId, + isProfileLinkAvailable, + recordLinkView, + updateProfileLink, + updateProfileLinkBento, } from "@/server/db"; -import { getMetadata } from "@/lib/metadata"; -import { validLinkSchema } from "@/types"; +import { type LinkBento } from "@/types"; +import { + CreateLinkBentoSchema, + CreateLinkSchema, + DeleteLinkBentoSchema, + DeleteLinkSchema, + GetByLinkSchema, + GetLinkViewsSchema, + LinkAvailableSchema, + UpdateLinkBentoSchema, + UpdateLinkSchema, +} from "../schemas"; const getUser = async ( - ctx: Context & { - auth: SignedInAuthObject; - } + ctx: Context & { + auth: SignedInAuthObject; + }, ) => { - const user = await ctx.db.query.user.findFirst({ - where: (user, { eq }) => eq(user.providerId, ctx.auth.userId), - columns: { - id: true, - plan: true, - subscriptionEndsAt: true, - }, - }); + const user = await getUserByProviderId(ctx.auth.userId, { + id: true, + plan: true, + subscriptionEndsAt: true, + }); - if (!user) throw new Error("User not found"); + if (!user) throw new Error("User not found"); - return user; + return user; }; export const profileLinkRouter = createTRPCRouter({ - linkAvailable: publicProcedure - .input( - z.object({ - link: z.string().toLowerCase(), - }) - ) - .query(async ({ input, ctx }) => { - const profileLink = await ctx.db.query.link.findFirst({ - where: (link, { eq }) => eq(link.link, input.link), - columns: { - id: true, - }, - }); - - if (profileLink) return false; - - return validLinkSchema.safeParse(input.link).success; - }), - - create: protectedProcedure - .input( - z.object({ - link: validLinkSchema, - twitter: z.string().optional(), - github: z.string().optional(), - linkedin: z.string().optional(), - instagram: z.string().optional(), - telegram: z.string().optional(), - discord: z.string().optional(), - youtube: z.string().optional(), - twitch: z.string().optional(), - }) - ) - .mutation(async ({ input, ctx }) => { - const user = await getUser(ctx); - - const profileLinks = await ctx.db - .select({ - count: sql`count(*)`, - }) - .from(link) - .where(eq(link.userId, user.id)); - const nProfileLinks = profileLinks[0]?.count ?? 0; - - const isPremium = - user.plan === "pro" && - user.subscriptionEndsAt && - user.subscriptionEndsAt > new Date(); - - if (nProfileLinks >= 1 && !isPremium) { - throw new Error( - "You can't create more profile links, upgrade your plan" - ); - } - - const bento: { - id: string; - type: "link"; - href: string; - clicks: number; - - size: z.infer; - position: z.infer; - }[] = []; - - let position = { - sm: { - x: 0, - y: 0, - }, - md: { - x: 0, - y: 0, - }, - }; - for (const [key, value] of Object.entries(input)) { - if (key !== "link" && value) { - let url = `https://${key}.com/${value}`; - - if (key === "linkedin") { - url = `https://www.${key}.com/in/${value}`; - } - - if (key === "youtube") { - url = `https://www.${key}.com/@${value.replace( - "@", - "" - )}`; - } - - if (key === "twitch") { - url = `https://www.${key}.tv/${value}`; - } - - if (key === "telegram") { - url = `https://t.me/${value}`; - } - - bento.push({ - id: crypto.randomUUID(), - type: "link", - - href: url, - clicks: 0, - - size: { - sm: "2x2", - md: "2x2", - }, - - position, - }); - - position = { - sm: { - x: position.sm.x % 2 === 0 ? position.sm.x + 1 : 0, - y: - position.sm.x % 2 === 0 - ? position.sm.y + 1 - : position.sm.y, - }, - md: { - x: position.md.x % 4 === 0 ? position.md.x + 1 : 0, - y: - position.md.x % 4 === 0 - ? position.md.y + 1 - : position.md.y, - }, - }; - } - } - - const profileLink = await ctx.db - .insert(link) - .values({ - link: input.link, - name: input.link, - bio: "I'm using OpenBio.app!", - bento, - userId: user.id, - }) - .returning() - .execute(); - - await kv.set(`profile-link:${input.link}`, profileLink[0], { - ex: 30 * 60, - }); - - return profileLink; - }), - - getAll: protectedProcedure.input(z.undefined()).query(async ({ ctx }) => { - const user = await getUser(ctx); - - const profileLinks = await ctx.db.query.link.findMany({ - where: (link, { eq }) => eq(link.userId, user.id), - }); - - return profileLinks; + linkAvailable: publicProcedure + .input(LinkAvailableSchema) + .query(async ({ input }) => { + return isProfileLinkAvailable(input.link); + }), + + create: protectedProcedure + .input(CreateLinkSchema) + .mutation(async ({ input, ctx }) => { + const user = await getUser(ctx); + + const canCreate = await canUserCreateProfileLink(user); + if (!canCreate) { + throw new Error( + "You can't create more profile links, upgrade your plan", + ); + } + + const isAvailable = await isProfileLinkAvailable(input.link); + if (!isAvailable) { + throw new Error("This profile link is not available"); + } + + const bento: LinkBento[] = []; + + let position = { + sm: { + x: 0, + y: 0, + }, + md: { + x: 0, + y: 0, + }, + }; + for (const [key, value] of Object.entries(input)) { + if (key !== "link" && value) { + let url = `https://${key}.com/${value}`; + + if (key === "linkedin") { + url = `https://www.${key}.com/in/${value}`; + } + + if (key === "youtube") { + url = `https://www.${key}.com/@${value.replace("@", "")}`; + } + + if (key === "twitch") { + url = `https://www.${key}.tv/${value}`; + } + + if (key === "telegram") { + url = `https://t.me/${value}`; + } + + bento.push({ + id: crypto.randomUUID(), + type: "link", + + href: url, + clicks: 0, + + size: { + sm: "2x2", + md: "2x2", + }, + + position, + }); + + position = { + sm: { + x: position.sm.x % 2 === 0 ? position.sm.x + 1 : 0, + y: position.sm.x % 2 === 0 ? position.sm.y + 1 : position.sm.y, + }, + md: { + x: position.md.x % 4 === 0 ? position.md.x + 1 : 0, + y: position.md.x % 4 === 0 ? position.md.y + 1 : position.md.y, + }, + }; + } + } + + const profileLink = await createProfileLink({ + link: input.link, + name: input.link, + bio: "I'm using OpenBio.app!", + bento, + userId: user.id, + }); + + return profileLink; + }), + + getAll: protectedProcedure.input(z.undefined()).query(async ({ ctx }) => { + const user = await getUser(ctx); + const profileLinks = await getProfileLinksOfUser(user.id); + + return profileLinks; + }), + + getByLink: publicProcedure + .input(GetByLinkSchema) + .query(async ({ input, ctx }) => { + const authedUserId = ctx.auth?.userId; + + const user = authedUserId + ? await ctx.db.query.user.findFirst({ + where: (user, { eq }) => eq(user.providerId, authedUserId), + columns: { + id: true, + plan: true, + subscriptionEndsAt: true, + }, + }) + : null; + + const profileLink = await getProfileLinkByLink(input.link); + + if (!profileLink) { + return null; + } + + let ip = ctx.req.ip ?? ctx.req.headers.get("x-real-ip"); + const forwardedFor = ctx.req.headers.get("x-forwarded-for"); + if (!ip && forwardedFor) { + ip = forwardedFor.split(",").at(0) ?? "Unknown"; + } + + await recordLinkView(profileLink.id, { + ip: ip ?? "Unknown", + userAgent: ctx.req.headers.get("user-agent") ?? "Unknown", + }); + + return { + ...profileLink, + isOwner: user?.id === profileLink.userId, + isPremium: + user?.id === profileLink.userId && + user?.plan === "pro" && + user?.subscriptionEndsAt && + user?.subscriptionEndsAt > new Date(), + }; + }), + + getViews: publicProcedure + .input(GetLinkViewsSchema) + .query(async ({ input }) => { + return getProfileLinkViews(input.id); }), - getByLink: publicProcedure - .input( - z.object({ - link: z.string(), - }) - ) - .query(async ({ input, ctx }) => { - const authedUserId = ctx.auth?.userId; - - const user = authedUserId - ? await ctx.db.query.user.findFirst({ - where: (user, { eq }) => - eq(user.providerId, authedUserId), - columns: { - id: true, - plan: true, - subscriptionEndsAt: true, - }, - }) - : null; - - const cached = await kv.get | null>( - `profile-link:${input.link}` - ); - - if (cached) { - return { - ...cached, - isOwner: user?.id === cached.userId, - isPremium: - user?.id === cached.userId && - user?.plan === "pro" && - user?.subscriptionEndsAt && - user?.subscriptionEndsAt > new Date(), - }; - } - - const profileLink = await ctx.db.query.link.findFirst({ - where: (link, { eq }) => eq(link.link, input.link), - }); - - if (!profileLink) { - return null; - } - - await kv.set(`profile-link:${input.link}`, profileLink, { - ex: 30 * 60, - }); - - let ip = ctx.req.ip ?? ctx.req.headers.get("x-real-ip"); - const forwardedFor = ctx.req.headers.get("x-forwarded-for"); - if (!ip && forwardedFor) { - ip = forwardedFor.split(",").at(0) ?? "Unknown"; - } - - const exists = await ctx.db.query.linkView.findFirst({ - where: (linkView, { eq, and, sql }) => - and( - eq(linkView.ip, ip ?? "Unknown"), - eq(linkView.linkId, profileLink.id), - sql`created_at > now() - interval '1 hour'` - ), - columns: { - id: true, - }, - }); - - if (!exists) { - await ctx.db.insert(linkView).values({ - ip: ip ?? "Unknown", - userAgent: ctx.req.headers.get("user-agent") ?? "Unknown", - linkId: profileLink.id, - }); - } - - return { - ...profileLink, - isOwner: user?.id === profileLink.userId, - isPremium: - user?.id === profileLink.userId && - user?.plan === "pro" && - user?.subscriptionEndsAt && - user?.subscriptionEndsAt > new Date(), - }; - }), - - getViews: publicProcedure - .input( - z.object({ - id: z.string(), - }) - ) - .query(async ({ input, ctx }) => { - const cached = await kv.get( - `profile-link-views:${input.id}` - ); - - if (cached) { - return cached; - } - - const views = await ctx.db - .select({ - count: sql`count(*)`, - }) - .from(linkView) - .where(eq(linkView.linkId, input.id)); - - await kv.set( - `profile-link-views:${input.id}`, - views[0]?.count ?? 0, - { - ex: 30 * 60, - } - ); - - return views[0]?.count ?? 0; - }), - - update: protectedProcedure - .input( - z.object({ - id: z.string(), - name: z.string().optional(), - bio: z.string().optional(), - }) - ) - .mutation(async ({ input, ctx }) => { - const update = await ctx.db - .update(link) - .set({ - name: input.name, - bio: input.bio, - }) - .where(eq(link.id, input.id)) - .returning() - .execute(); - - await kv.set(`profile-link:${update[0]?.link}`, update[0], { - ex: 30 * 60, - }); - - return update; - }), - - delete: protectedProcedure - .input( - z.object({ - link: z.string(), - }) - ) - .mutation(async ({ input, ctx }) => { - await ctx.db.delete(link).where(eq(link.link, input.link)); - - await kv.del(`profile-link:${input.link}`); - - return true; - }), - - createBento: protectedProcedure - .input( - z.object({ - link: z.string(), - bento: bentoSchema, - }) - ) - .mutation(async ({ input, ctx }) => { - const profileLink = await ctx.db.query.link.findFirst({ - where: (link, { eq }) => eq(link.link, input.link), - columns: { - id: true, - bento: true, - }, - }); - - if (!profileLink) { - throw new Error("Profile link not found"); - } - - const cached = await kv.get | null>( - `profile-link:${input.link}` - ); - - const update = await ctx.db - .update(link) - .set({ - bento: (cached?.bento ?? []).concat([input.bento]), - }) - .where(eq(link.link, input.link)) - .returning() - .execute(); - - if (cached) { - await kv.set( - `profile-link:${cached.link}`, - { - ...cached, - bento: cached.bento.concat([input.bento]), - }, - { - ex: 30 * 60, - } - ); - } - - return update[0]?.bento; - }), - - deleteBento: protectedProcedure - .input( - z.object({ - link: z.string(), - id: z.string(), - }) - ) - .mutation(async ({ input, ctx }) => { - const profileLink = await ctx.db.query.link.findFirst({ - where: (link, { eq }) => eq(link.link, input.link), - columns: { - id: true, - bento: true, - }, - }); - - if (!profileLink) { - throw new Error("Profile link not found"); - } - - await ctx.db - .update(link) - .set({ - bento: profileLink.bento.filter((b) => b.id !== input.id), - }) - .where(eq(link.link, input.link)); - - const cached = await kv.get | null>( - `profile-link:${input.link}` - ); - - if (cached) { - await kv.set( - `profile-link:${cached.link}`, - { - ...cached, - bento: cached.bento.filter((b) => b.id !== input.id), - }, - { - ex: 30 * 60, - } - ); - } - - return true; - }), - - updateBento: protectedProcedure - .input( - z.object({ - link: z.string(), - bento: bentoSchema, - }) - ) - .mutation(async ({ input, ctx }) => { - const profileLink = await ctx.db.query.link.findFirst({ - where: (link, { eq }) => eq(link.link, input.link), - columns: { - id: true, - bento: true, - }, - }); - - if (!profileLink) { - throw new Error("Profile link not found"); - } - - const update = await ctx.db - .update(link) - .set({ - bento: [ - ...profileLink.bento.filter( - (b) => b.id !== input.bento.id - ), - input.bento, - ], - }) - .where(eq(link.link, input.link)) - .returning() - .execute(); - - const cached = await kv.get | null>( - `profile-link:${input.link}` - ); - - if (cached) { - await kv.set( - `profile-link:${cached.link}`, - { - ...cached, - ...update[0], - }, - { - ex: 30 * 60, - } - ); - } - - return true; - }), - - getMetadataOfURL: publicProcedure - .input( - z.object({ - url: z.string(), - }) - ) - .query(async ({ input }) => { - return getMetadata(input.url); - }), + update: protectedProcedure + .input(UpdateLinkSchema) + .mutation(async ({ input, ctx }) => { + const { id: userId } = await getUser(ctx); + await canModifyProfileLink({ + userId, + linkId: input.id, + }); + + return updateProfileLink(input); + }), + + delete: protectedProcedure + .input(DeleteLinkSchema) + .mutation(async ({ input, ctx }) => { + const { id: userId } = await getUser(ctx); + await canModifyProfileLink({ + userId, + link: input.link, + }); + + return deleteProfileLink(input.link); + }), + + createBento: protectedProcedure + .input(CreateLinkBentoSchema) + .mutation(async ({ input, ctx }) => { + const { id: userId } = await getUser(ctx); + await canModifyProfileLink({ + userId, + link: input.link, + }); + + return addProfileLinkBento(input.link, input.bento); + }), + + deleteBento: protectedProcedure + .input(DeleteLinkBentoSchema) + .mutation(async ({ input, ctx }) => { + const { id: userId } = await getUser(ctx); + await canModifyProfileLink({ + userId, + link: input.link, + }); + + return deleteProfileLinkBento(input.link, input.id); + }), + + updateBento: protectedProcedure + .input(UpdateLinkBentoSchema) + .mutation(async ({ input, ctx }) => { + const { id: userId } = await getUser(ctx); + await canModifyProfileLink({ + userId, + link: input.link, + }); + + return updateProfileLinkBento(input.link, input.bento); + }), + + getMetadataOfURL: publicProcedure + .input( + z.object({ + url: z.string(), + }), + ) + .query(async ({ input }) => { + return getMetadata(input.url); + }), }); diff --git a/src/server/api/routers/stripe/index.ts b/src/server/api/routers/stripe/index.ts index 336394f..f2f47bd 100644 --- a/src/server/api/routers/stripe/index.ts +++ b/src/server/api/routers/stripe/index.ts @@ -1,9 +1,8 @@ import * as z from "zod"; - import { env } from "@/env.mjs"; import { stripe } from "@/lib/stripe"; -import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc"; import { webhookRouter } from "@/server/api/routers/stripe/webhook"; +import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc"; import { db, eq, user } from "@/server/db"; const getStripeCustomer = async ({ @@ -69,7 +68,7 @@ export const stripeRouter = createTRPCRouter({ .input( z.object({ billing: z.enum(["monthly", "annually"]), - }) + }), ) .mutation(async ({ ctx, input }) => { const res = await ctx.db.query.user.findFirst({ diff --git a/src/server/api/routers/stripe/webhook.ts b/src/server/api/routers/stripe/webhook.ts index 4e80b9d..02778a1 100644 --- a/src/server/api/routers/stripe/webhook.ts +++ b/src/server/api/routers/stripe/webhook.ts @@ -1,13 +1,12 @@ -import * as z from "zod"; import { TRPCError } from "@trpc/server"; import type Stripe from "stripe"; - -import { createTRPCRouter, publicProcedure } from "@/server/api/trpc"; -import { sendEmail } from "@/server/emails"; -import { stripe } from "@/lib/stripe"; -import UpgradedEmail from "@/components/emails/upgraded"; +import * as z from "zod"; import CancelledEmail from "@/components/emails/cancelled"; +import UpgradedEmail from "@/components/emails/upgraded"; +import { stripe } from "@/lib/stripe"; +import { createTRPCRouter, publicProcedure } from "@/server/api/trpc"; import { eq, user } from "@/server/db"; +import { sendEmail } from "@/server/emails"; export const webhookProcedure = publicProcedure.input( z.object({ @@ -20,7 +19,7 @@ export const webhookProcedure = publicProcedure.input( }), type: z.string(), }), - }) + }), ); export const webhookRouter = createTRPCRouter({ @@ -35,7 +34,7 @@ export const webhookRouter = createTRPCRouter({ } const subscription = await stripe.subscriptions.retrieve( - session.subscription + session.subscription, ); const stripeCustomerId = @@ -103,6 +102,6 @@ export const webhookRouter = createTRPCRouter({ to: [res.email], react: CancelledEmail(), }); - } + }, ), }); diff --git a/src/server/api/routers/user.ts b/src/server/api/routers/user.ts index aeb7d55..035ce16 100644 --- a/src/server/api/routers/user.ts +++ b/src/server/api/routers/user.ts @@ -1,5 +1,4 @@ import * as z from "zod"; - import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc"; export const userRouter = createTRPCRouter({ diff --git a/src/server/api/schemas/index.ts b/src/server/api/schemas/index.ts new file mode 100644 index 0000000..8c82a9c --- /dev/null +++ b/src/server/api/schemas/index.ts @@ -0,0 +1 @@ +export * from "./profile-link"; diff --git a/src/server/api/schemas/profile-link.ts b/src/server/api/schemas/profile-link.ts new file mode 100644 index 0000000..7f9aed5 --- /dev/null +++ b/src/server/api/schemas/profile-link.ts @@ -0,0 +1,51 @@ +import * as z from "zod"; +import { BentoSchema, ValidLinkSchema } from "@/types"; + +export const LinkAvailableSchema = z.object({ + link: z.string().toLowerCase(), +}); + +export const CreateLinkSchema = z.object({ + link: ValidLinkSchema, + twitter: z.string().optional(), + github: z.string().optional(), + linkedin: z.string().optional(), + instagram: z.string().optional(), + telegram: z.string().optional(), + discord: z.string().optional(), + youtube: z.string().optional(), + twitch: z.string().optional(), +}); + +export const GetByLinkSchema = z.object({ + link: z.string(), +}); + +export const GetLinkViewsSchema = z.object({ + id: z.string(), +}); + +export const UpdateLinkSchema = z.object({ + id: z.string(), + name: z.string().optional(), + bio: z.string().optional(), +}); + +export const DeleteLinkSchema = z.object({ + link: z.string(), +}); + +export const CreateLinkBentoSchema = z.object({ + link: z.string(), + bento: BentoSchema, +}); + +export const DeleteLinkBentoSchema = z.object({ + link: z.string(), + id: z.string(), +}); + +export const UpdateLinkBentoSchema = z.object({ + link: z.string(), + bento: BentoSchema, +}); diff --git a/src/server/api/serverless.ts b/src/server/api/serverless.ts index 469f7b8..6407f25 100644 --- a/src/server/api/serverless.ts +++ b/src/server/api/serverless.ts @@ -1,6 +1,6 @@ -import { createTRPCRouter } from "@/server/api/trpc"; import { clerkRouter } from "@/server/api/routers/clerk"; import { stripeRouter } from "@/server/api/routers/stripe"; +import { createTRPCRouter } from "@/server/api/trpc"; export const serverlessRouter = createTRPCRouter({ clerk: clerkRouter, diff --git a/src/server/api/trpc.ts b/src/server/api/trpc.ts index c8921b8..3eca492 100644 --- a/src/server/api/trpc.ts +++ b/src/server/api/trpc.ts @@ -1,52 +1,49 @@ import type { NextRequest } from "next/server"; import type { - SignedInAuthObject, - SignedOutAuthObject, + SignedInAuthObject, + SignedOutAuthObject, } from "@clerk/nextjs/api"; import { getAuth } from "@clerk/nextjs/server"; -import { type inferAsyncReturnType, initTRPC, TRPCError } from "@trpc/server"; +import { initTRPC, TRPCError, type inferAsyncReturnType } from "@trpc/server"; import superjson from "superjson"; import { ZodError } from "zod"; - import { db } from "@/server/db"; type CreateContextOptions = { - auth: SignedInAuthObject | SignedOutAuthObject | null; - req: NextRequest; + auth: SignedInAuthObject | SignedOutAuthObject | null; + req: NextRequest; }; export const createInnerTRPCContext = (opts: CreateContextOptions) => { - return { - ...opts, - db, - }; + return { + ...opts, + db, + }; }; export const createTRPCContext = (opts: { req: NextRequest }) => { - const auth = getAuth(opts.req); + const auth = getAuth(opts.req); - return createInnerTRPCContext({ - auth, - req: opts.req, - }); + return createInnerTRPCContext({ + auth, + req: opts.req, + }); }; export type Context = inferAsyncReturnType; const t = initTRPC.context().create({ - transformer: superjson, - errorFormatter({ shape, error }) { - return { - ...shape, - data: { - ...shape.data, - zodError: - error.cause instanceof ZodError - ? error.cause.flatten() - : null, - }, - }; - }, + transformer: superjson, + errorFormatter({ shape, error }) { + return { + ...shape, + data: { + ...shape.data, + zodError: + error.cause instanceof ZodError ? error.cause.flatten() : null, + }, + }; + }, }); export const createTRPCRouter = t.router; @@ -55,17 +52,17 @@ export const mergeRouters = t.mergeRouters; export const publicProcedure = t.procedure; const enforceUserIsAuthed = t.middleware(({ ctx, next }) => { - if (!ctx.auth?.userId) { - throw new TRPCError({ code: "UNAUTHORIZED" }); - } - return next({ - ctx: { - auth: { - ...ctx.auth, - userId: ctx.auth.userId, - }, - }, - }); + if (!ctx.auth?.userId) { + throw new TRPCError({ code: "UNAUTHORIZED" }); + } + return next({ + ctx: { + auth: { + ...ctx.auth, + userId: ctx.auth.userId, + }, + }, + }); }); export const protectedProcedure = t.procedure.use(enforceUserIsAuthed); diff --git a/src/server/db/db.ts b/src/server/db/db.ts index 33d5210..326f5f8 100644 --- a/src/server/db/db.ts +++ b/src/server/db/db.ts @@ -1,6 +1,5 @@ import { neon, neonConfig } from "@neondatabase/serverless"; import { drizzle as drizzleNeon } from "drizzle-orm/neon-http"; - import { env } from "@/env.mjs"; import * as schema from "./schema"; diff --git a/src/server/db/index.ts b/src/server/db/index.ts index 35684c2..2e08d6d 100644 --- a/src/server/db/index.ts +++ b/src/server/db/index.ts @@ -3,3 +3,4 @@ export * from "drizzle-zod"; export * from "./db"; export * from "./schema"; export * as schema from "./schema"; +export * from "./utils"; diff --git a/src/server/db/migrate.mts b/src/server/db/migrate.mts index 21f48e5..fc61d61 100644 --- a/src/server/db/migrate.mts +++ b/src/server/db/migrate.mts @@ -2,7 +2,6 @@ import "dotenv/config"; import { neon } from "@neondatabase/serverless"; import { drizzle as drizzleNeon } from "drizzle-orm/neon-http"; import { migrate as migrateNeon } from "drizzle-orm/neon-http/migrator"; - import { env } from "../../env.mjs"; const main = async () => { diff --git a/src/server/db/schema/link-view.ts b/src/server/db/schema/link-view.ts index 30c6ac3..b4eeda8 100644 --- a/src/server/db/schema/link-view.ts +++ b/src/server/db/schema/link-view.ts @@ -1,6 +1,5 @@ import { relations } from "drizzle-orm"; -import { uuid, pgTable, timestamp, text } from "drizzle-orm/pg-core"; - +import { pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core"; import { link } from "./link"; export const linkView = pgTable("link_view", { diff --git a/src/server/db/schema/link.ts b/src/server/db/schema/link.ts index 7c9a3f6..1b3a7d0 100644 --- a/src/server/db/schema/link.ts +++ b/src/server/db/schema/link.ts @@ -1,12 +1,11 @@ import { relations } from "drizzle-orm"; -import { uuid, pgTable, timestamp, text, json } from "drizzle-orm/pg-core"; +import { json, pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core"; import type * as z from "zod"; - -import { sizeSchema, positionSchema, bentoSchema } from "@/types"; -import { user } from "./user"; +import { BentoSchema, PositionSchema, SizeSchema } from "@/types"; import { linkView } from "./link-view"; +import { user } from "./user"; -export { sizeSchema, positionSchema, bentoSchema }; +export { SizeSchema, PositionSchema, BentoSchema }; export const link = pgTable("link", { id: uuid("id").primaryKey().defaultRandom(), @@ -18,7 +17,7 @@ export const link = pgTable("link", { bio: text("bio"), bento: json("bento") - .$type[]>() + .$type[]>() .default([]) .notNull(), diff --git a/src/server/db/schema/user.ts b/src/server/db/schema/user.ts index f07b607..583efa9 100644 --- a/src/server/db/schema/user.ts +++ b/src/server/db/schema/user.ts @@ -1,6 +1,5 @@ import { relations } from "drizzle-orm"; -import { uuid, pgTable, timestamp, text } from "drizzle-orm/pg-core"; - +import { pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core"; import { link } from "./link"; export const user = pgTable("user", { diff --git a/src/server/db/utils/index.ts b/src/server/db/utils/index.ts new file mode 100644 index 0000000..6fbf51d --- /dev/null +++ b/src/server/db/utils/index.ts @@ -0,0 +1,3 @@ +export * from "./user"; +export * from "./link"; +export * from "./link-view"; diff --git a/src/server/db/utils/link-view.ts b/src/server/db/utils/link-view.ts new file mode 100644 index 0000000..bbb804c --- /dev/null +++ b/src/server/db/utils/link-view.ts @@ -0,0 +1,56 @@ +import { kv } from "@vercel/kv"; +import { eq, sql } from ".."; +import { db } from "../db"; +import { linkView } from "../schema"; + +export const getProfileLinkViews = async (linkId: string) => { + const cached = await kv.get(`profile-link-views:${linkId}`); + + if (cached) { + return cached; + } + + const views = await db + .select({ + count: sql`count(*)`, + }) + .from(linkView) + .where(eq(linkView.linkId, linkId)); + + await kv.set(`profile-link-views:${linkId}`, views[0]?.count ?? 0, { + ex: 30 * 60, + }); + + return views[0]?.count ?? 0; +}; + +export const recordLinkView = async ( + linkId: string, + { + ip, + userAgent, + }: { + ip: string; + userAgent: string; + }, +) => { + const exists = await db.query.linkView.findFirst({ + where: (linkView, { eq, and, sql }) => + and( + eq(linkView.ip, ip ?? "Unknown"), + eq(linkView.linkId, linkId), + sql`created_at > now() - interval '1 hour'`, + ), + columns: { + id: true, + }, + }); + + if (!exists) { + await db.insert(linkView).values({ + linkId, + ip, + userAgent, + }); + } +}; diff --git a/src/server/db/utils/link.ts b/src/server/db/utils/link.ts new file mode 100644 index 0000000..eb092aa --- /dev/null +++ b/src/server/db/utils/link.ts @@ -0,0 +1,231 @@ +import { kv } from "@vercel/kv"; +import type * as z from "zod"; +import { ValidLinkSchema, type BentoSchema, type LinkBento } from "@/types"; +import { eq, isUserPremium, sql, type InferSelectModel } from ".."; +import { db } from "../db"; +import { link } from "../schema"; + +type SelectProfileLinkColumns = { + id?: boolean | undefined; + link?: boolean | undefined; + image?: boolean | undefined; + name?: boolean | undefined; + bio?: boolean | undefined; + bento?: boolean | undefined; + createdAt?: boolean | undefined; + updatedAt?: boolean | undefined; + userId?: boolean | undefined; +}; + +export const getProfileLinkByLink = async ( + inputLink: string, + columns?: SelectProfileLinkColumns, +) => { + const cached = await kv.get | null>( + `profile-link:${inputLink}`, + ); + + if (cached) { + return cached; + } + + const result = await db.query.link.findFirst({ + where: (_link, { eq }) => eq(_link.link, inputLink), + columns, + }); + + if (result) { + await kv.set(`profile-link:${inputLink}`, result, { + ex: 30 * 60, + }); + } + + return result; +}; + +export const getProfileLinkById = async ( + id: string, + columns?: SelectProfileLinkColumns, +) => { + const result = await db.query.link.findFirst({ + where: (_link, { eq }) => eq(_link.id, id), + columns, + }); + + return result; +}; + +export const isProfileLinkAvailable = async (link: string) => { + const profileLink = await getProfileLinkByLink(link); + + return profileLink ? false : ValidLinkSchema.safeParse(link).success; +}; + +export const getProfileLinksOfUser = async (userId: string) => { + const result = await db.query.link.findMany({ + where: (link, { eq }) => eq(link.userId, userId), + }); + + return result; +}; + +export const getProfileLinksCountOfUser = async (userId: string) => { + const profileLinks = await db + .select({ + count: sql`count(*)`, + }) + .from(link) + .where(eq(link.userId, userId)); + + const nProfileLinks = profileLinks[0]?.count ?? 0; + + return nProfileLinks; +}; + +export const canUserCreateProfileLink = async ({ + id, + plan, + subscriptionEndsAt, +}: { + id: string; + plan: "free" | "pro"; + subscriptionEndsAt?: Date | null; +}) => { + const nProfileLinks = await getProfileLinksCountOfUser(id); + + const isPremium = isUserPremium({ plan, subscriptionEndsAt }); + const canCreateProfileLink = isPremium || nProfileLinks < 1; + + return canCreateProfileLink; +}; + +export const createProfileLink = async (data: { + link: string; + userId: string; + image?: string; + name: string; + bio?: string; + bento: LinkBento[]; +}) => { + const result = await db.insert(link).values(data).returning().execute(); + + await kv.set(`profile-link:${data.link}`, result[0], { + ex: 30 * 60, + }); + + return result[0]!; +}; + +export const canModifyProfileLink = async ({ + userId, + linkId, + link, +}: { + userId: string; + linkId?: string; + link?: string; +}) => { + const profileLink = linkId + ? await getProfileLinkById(linkId) + : link + ? await getProfileLinkByLink(link) + : null; + + const canModify = + profileLink?.userId === userId && profileLink?.id === linkId; + + if (!canModify) { + throw new Error("You can't modify this profile link"); + } + + return canModify; +}; + +export const updateProfileLink = async (data: { + id: string; + name?: string; + bio?: string; +}) => { + const result = await db + .update(link) + .set(data) + .where(eq(link.id, data.id)) + .returning() + .execute(); + + await kv.set(`profile-link:${result[0]!.link}`, result[0], { + ex: 30 * 60, + }); + + return result[0]!; +}; + +export const deleteProfileLink = async (inputLink: string) => { + await db.delete(link).where(eq(link.link, inputLink)).execute(); + + await kv.del(`profile-link:${inputLink}`); +}; + +export const addProfileLinkBento = async ( + inputLink: string, + bento: z.infer, +) => { + const profileLink = await getProfileLinkByLink(inputLink); + + const result = await db + .update(link) + .set({ + bento: (profileLink?.bento ?? []).concat(bento), + }) + .where(eq(link.link, inputLink)) + .returning() + .execute(); + + await kv.set(`profile-link:${result[0]!.link}`, result[0], { + ex: 30 * 60, + }); + + return result[0]!.bento; +}; + +export const deleteProfileLinkBento = async ( + inputLink: string, + bentoId: string, +) => { + const profileLink = await getProfileLinkByLink(inputLink); + + const result = await db + .update(link) + .set({ + bento: (profileLink?.bento ?? []).filter((b) => b.id !== bentoId), + }) + .where(eq(link.link, inputLink)) + .returning() + .execute(); + + await kv.set(`profile-link:${result[0]!.link}`, result[0], { + ex: 30 * 60, + }); +}; + +export const updateProfileLinkBento = async ( + inputLink: string, + bento: z.infer, +) => { + const profileLink = await getProfileLinkByLink(inputLink); + + const result = await db + .update(link) + .set({ + bento: (profileLink?.bento ?? []).map((b) => + b.id === bento.id ? bento : b, + ), + }) + .where(eq(link.link, inputLink)) + .returning() + .execute(); + + await kv.set(`profile-link:${result[0]!.link}`, result[0], { + ex: 30 * 60, + }); +}; diff --git a/src/server/db/utils/user.ts b/src/server/db/utils/user.ts new file mode 100644 index 0000000..252936b --- /dev/null +++ b/src/server/db/utils/user.ts @@ -0,0 +1,38 @@ +import { db } from "../db"; + +export const getUserByProviderId = async ( + providerId: string, + columns?: { + id?: boolean | undefined; + providerId?: boolean | undefined; + email?: boolean | undefined; + firstName?: boolean | undefined; + lastName?: boolean | undefined; + plan?: boolean | undefined; + stripeCustomerId?: boolean | undefined; + subscriptionId?: boolean | undefined; + subscriptionEndsAt?: boolean | undefined; + createdAt?: boolean | undefined; + updatedAt?: boolean | undefined; + }, +) => { + const result = await db.query.user.findFirst({ + where: (_user, { eq }) => eq(_user.providerId, providerId), + columns, + }); + + return result; +}; + +export const isUserPremium = ({ + plan, + subscriptionEndsAt, +}: { + plan: "free" | "pro"; + subscriptionEndsAt?: Date | null; +}) => { + const isPremium = + plan === "pro" && !!subscriptionEndsAt && subscriptionEndsAt > new Date(); + + return isPremium; +}; diff --git a/src/server/emails.ts b/src/server/emails.ts index 4d34091..28e63b5 100644 --- a/src/server/emails.ts +++ b/src/server/emails.ts @@ -2,7 +2,6 @@ import type { ReactElement } from "react"; import { Resend } from "resend"; - import { env } from "@/env.mjs"; const resend = new Resend(env.RESEND_API_KEY); diff --git a/src/server/uploadthing.ts b/src/server/uploadthing.ts index 19285e9..a975ae0 100644 --- a/src/server/uploadthing.ts +++ b/src/server/uploadthing.ts @@ -1,9 +1,8 @@ -import * as z from "zod"; -import { createUploadthing, type FileRouter } from "uploadthing/next"; import { currentUser } from "@clerk/nextjs"; - -import { db, eq, link } from "@/server/db"; +import { createUploadthing, type FileRouter } from "uploadthing/next"; import { utapi } from "uploadthing/server"; +import * as z from "zod"; +import { db, eq, link } from "@/server/db"; const f = createUploadthing({ errorFormatter: (err) => { @@ -21,7 +20,7 @@ export const appFileRouter = { .input( z.object({ profileLinkId: z.string(), - }) + }), ) .middleware(async ({ input }) => { const user = await currentUser(); diff --git a/src/trpc/react.ts b/src/trpc/react.ts index 5944888..135d7bc 100644 --- a/src/trpc/react.ts +++ b/src/trpc/react.ts @@ -1,6 +1,5 @@ import { createTRPCReact } from "@trpc/react-query"; import { type inferRouterInputs, type inferRouterOutputs } from "@trpc/server"; - import { type AppRouter } from "@/server/api/root"; export const api = createTRPCReact(); diff --git a/src/trpc/server.ts b/src/trpc/server.ts index 23dd573..ed9d7c0 100644 --- a/src/trpc/server.ts +++ b/src/trpc/server.ts @@ -5,10 +5,9 @@ import { loggerLink } from "@trpc/client"; import { experimental_createTRPCNextAppDirServer } from "@trpc/next/app-dir/server"; import { type inferRouterInputs, type inferRouterOutputs } from "@trpc/server"; import superjson from "superjson"; - +import { env } from "@/env.mjs"; import { type AppRouter } from "@/server/api/root"; import { endingLink } from "@/trpc/shared"; -import { env } from "@/env.mjs"; export const api = experimental_createTRPCNextAppDirServer({ config() { diff --git a/src/trpc/shared.ts b/src/trpc/shared.ts index 66f4054..26509a7 100644 --- a/src/trpc/shared.ts +++ b/src/trpc/shared.ts @@ -1,6 +1,10 @@ +import { + unstable_httpBatchStreamLink, + type HTTPBatchLinkOptions, + type HTTPHeaders, + type TRPCLink, +} from "@trpc/client"; import { type AppRouter } from "@/server/api/root"; -import type { HTTPBatchLinkOptions, HTTPHeaders, TRPCLink } from "@trpc/client"; -import { unstable_httpBatchStreamLink } from "@trpc/client"; const getBaseUrl = () => { if (typeof window !== "undefined") return ""; diff --git a/src/types.ts b/src/types.ts index bb12b45..5dd290c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,13 +1,13 @@ import * as z from "zod"; -export const sizeSchema = z +export const SizeSchema = z .record(z.enum(["sm", "md"]), z.enum(["4x1", "2x2", "2x4", "4x2", "4x4"])) .default({ sm: "2x2", md: "2x2", }); -export const positionSchema = z +export const PositionSchema = z .record( z.enum(["sm", "md"]), z @@ -15,48 +15,47 @@ export const positionSchema = z x: z.number().int().min(0).default(0), y: z.number().int().min(0).default(0), }) - .default({ x: 0, y: 0 }) + .default({ x: 0, y: 0 }), ) .default({ sm: { x: 0, y: 0 }, md: { x: 0, y: 0 }, }); -export const linkBentoSchema = z.object({ +export const LinkBentoSchema = z.object({ id: z.string(), type: z.literal("link"), href: z.string().url(), clicks: z.number().int().min(0).default(0), - size: sizeSchema, - position: positionSchema, + size: SizeSchema, + position: PositionSchema, }); -export const noteBentoSchema = z.object({ +export const NoteBentoSchema = z.object({ id: z.string(), type: z.literal("note"), text: z.string(), - size: sizeSchema, - position: positionSchema, + size: SizeSchema, + position: PositionSchema, }); -export const assetBentoSchema = z.object({ +export const AssetBentoSchema = z.object({ id: z.string(), type: z.enum(["image", "video"]), url: z.string().url(), caption: z.string().optional(), - size: sizeSchema, - position: positionSchema, + size: SizeSchema, + position: PositionSchema, }); -export const bentoSchema = linkBentoSchema - .or(noteBentoSchema) - .or(assetBentoSchema); +export const BentoSchema = + LinkBentoSchema.or(NoteBentoSchema).or(AssetBentoSchema); export const RESERVED_LINKS = [ "sign-up", @@ -104,7 +103,7 @@ export const RESERVED_LINKS = [ "openbio", ]; -export const validLinkSchema = z +export const ValidLinkSchema = z .string() .min(3, { message: "Link must be at least 3 characters long.", @@ -119,3 +118,13 @@ export const validLinkSchema = z .refine((value) => !RESERVED_LINKS.includes(value), { message: "This link is reserved.", }); + +export type LinkBento = { + id: string; + type: "link"; + href: string; + clicks: number; + + size: z.infer; + position: z.infer; +}; diff --git a/tsconfig.json b/tsconfig.json index 95d4682..ccc12b7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,11 +1,7 @@ { "compilerOptions": { "target": "es2017", - "lib": [ - "dom", - "dom.iterable", - "esnext" - ], + "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "checkJs": true, "skipLibCheck": true, @@ -22,12 +18,8 @@ "noUncheckedIndexedAccess": true, "baseUrl": ".", "paths": { - "@/*": [ - "./src/*" - ], - "@/public/*": [ - "./public/*" - ] + "@/*": ["./src/*"], + "@/public/*": ["./public/*"] }, "plugins": [ { @@ -45,7 +37,5 @@ ".next/types/**/*.ts", "src/server/db/migrate.mts" ], - "exclude": [ - "node_modules" - ] -} \ No newline at end of file + "exclude": ["node_modules"] +}