mirror of
https://github.com/fergalmoran/opengifame.git
synced 2025-12-22 09:38:44 +00:00
Added some migrations and lots of good stuff
This commit is contained in:
@@ -1,8 +1,11 @@
|
||||
DATABASE_URL="postgresql://postgres:hackme@localhost:5432/opengifame"
|
||||
|
||||
|
||||
UPLOAD_PATH=/srv/dev/opengifame/working/uploads
|
||||
NEXTAUTH_SECRET="tAOVgxpY1U0BsnPCr6Gf8WVkmRMkp06ztUfwMhBKMQ4="
|
||||
NEXTAUTH_URL="https://opengifame.dev.fergl.ie:3000"
|
||||
|
||||
NEXT_PUBLIC_DEBUG_MODE=1
|
||||
NEXT_PUBLIC_SITE_NAME=Open Gifame
|
||||
NEXT_PUBLIC_SITE_DESCRIPTION=Robot powered giffage
|
||||
NEXT_PUBLIC_SITE_URL=https://opengifame.dev.fergl.ie:3000
|
||||
20
.env.example
20
.env.example
@@ -1,20 +0,0 @@
|
||||
# Since the ".env" file is gitignored, you can use the ".env.example" file to
|
||||
# build a new ".env" file when you clone the repo. Keep this file up-to-date
|
||||
# when you add new variables to `.env`.
|
||||
|
||||
# This file will be committed to version control, so make sure not to have any
|
||||
# secrets in it. If you are cloning this repo, create a copy of this file named
|
||||
# ".env" and populate it with your secrets.
|
||||
|
||||
# When adding additional environment variables, the schema in "/src/env.js"
|
||||
# should be updated accordingly.
|
||||
|
||||
# Drizzle
|
||||
DATABASE_URL="postgresql://postgres:password@localhost:5432/opengifame"
|
||||
|
||||
# Next Auth
|
||||
# You can generate a new secret on the command line with:
|
||||
# openssl rand -base64 32
|
||||
# https://next-auth.js.org/configuration/options#secret
|
||||
# NEXTAUTH_SECRET=""
|
||||
NEXTAUTH_URL="http://localhost:3000"
|
||||
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
@@ -1,4 +1,4 @@
|
||||
{
|
||||
// "workbench.colorTheme": "Tinacious Design (High Contrast)"
|
||||
"workbench.colorTheme": "Moonlight",
|
||||
"workbench.colorTheme": "Tinacious Design (High Contrast)",
|
||||
}
|
||||
15
drizzle/0001_wooden_ink.sql
Normal file
15
drizzle/0001_wooden_ink.sql
Normal file
@@ -0,0 +1,15 @@
|
||||
CREATE TABLE IF NOT EXISTS "image" (
|
||||
"id" varchar(255) PRIMARY KEY NOT NULL,
|
||||
"name" varchar,
|
||||
"tags" text[],
|
||||
"created_by" varchar(255) NOT NULL,
|
||||
"created_at" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
"updated_at" timestamp with time zone
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "post" ALTER COLUMN "id" SET DATA TYPE varchar(255);--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "image" ADD CONSTRAINT "image_created_by_user_id_fk" FOREIGN KEY ("created_by") REFERENCES "public"."user"("id") ON DELETE no action ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
418
drizzle/meta/0001_snapshot.json
Normal file
418
drizzle/meta/0001_snapshot.json
Normal file
@@ -0,0 +1,418 @@
|
||||
{
|
||||
"id": "01ae1334-57ff-4e93-8599-bdc2689aea71",
|
||||
"prevId": "3cde7a7b-e714-4d18-a93a-9e07c9e48f5a",
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"tables": {
|
||||
"public.account": {
|
||||
"name": "account",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"type": {
|
||||
"name": "type",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"provider": {
|
||||
"name": "provider",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"provider_account_id": {
|
||||
"name": "provider_account_id",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"refresh_token": {
|
||||
"name": "refresh_token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"access_token": {
|
||||
"name": "access_token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"expires_at": {
|
||||
"name": "expires_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"token_type": {
|
||||
"name": "token_type",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"scope": {
|
||||
"name": "scope",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"id_token": {
|
||||
"name": "id_token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"session_state": {
|
||||
"name": "session_state",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"account_user_id_idx": {
|
||||
"name": "account_user_id_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "user_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"account_user_id_user_id_fk": {
|
||||
"name": "account_user_id_user_id_fk",
|
||||
"tableFrom": "account",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {
|
||||
"account_provider_provider_account_id_pk": {
|
||||
"name": "account_provider_provider_account_id_pk",
|
||||
"columns": [
|
||||
"provider",
|
||||
"provider_account_id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {}
|
||||
},
|
||||
"public.image": {
|
||||
"name": "image",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "varchar",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"tags": {
|
||||
"name": "tags",
|
||||
"type": "text[]",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_by": {
|
||||
"name": "created_by",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "CURRENT_TIMESTAMP"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"image_created_by_user_id_fk": {
|
||||
"name": "image_created_by_user_id_fk",
|
||||
"tableFrom": "image",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": [
|
||||
"created_by"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {}
|
||||
},
|
||||
"public.post": {
|
||||
"name": "post",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "varchar(256)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_by": {
|
||||
"name": "created_by",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "CURRENT_TIMESTAMP"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"created_by_idx": {
|
||||
"name": "created_by_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "created_by",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
},
|
||||
"name_idx": {
|
||||
"name": "name_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "name",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"post_created_by_user_id_fk": {
|
||||
"name": "post_created_by_user_id_fk",
|
||||
"tableFrom": "post",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": [
|
||||
"created_by"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {}
|
||||
},
|
||||
"public.session": {
|
||||
"name": "session",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"session_token": {
|
||||
"name": "session_token",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"expires": {
|
||||
"name": "expires",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"session_user_id_idx": {
|
||||
"name": "session_user_id_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "user_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"session_user_id_user_id_fk": {
|
||||
"name": "session_user_id_user_id_fk",
|
||||
"tableFrom": "session",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {}
|
||||
},
|
||||
"public.user": {
|
||||
"name": "user",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"email_verified": {
|
||||
"name": "email_verified",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": "CURRENT_TIMESTAMP"
|
||||
},
|
||||
"image": {
|
||||
"name": "image",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"password": {
|
||||
"name": "password",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {}
|
||||
},
|
||||
"public.verification_token": {
|
||||
"name": "verification_token",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"identifier": {
|
||||
"name": "identifier",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"token": {
|
||||
"name": "token",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"expires": {
|
||||
"name": "expires",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"verification_token_identifier_token_pk": {
|
||||
"name": "verification_token_identifier_token_pk",
|
||||
"columns": [
|
||||
"identifier",
|
||||
"token"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {}
|
||||
}
|
||||
},
|
||||
"enums": {},
|
||||
"schemas": {},
|
||||
"sequences": {},
|
||||
"_meta": {
|
||||
"columns": {},
|
||||
"schemas": {},
|
||||
"tables": {}
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,13 @@
|
||||
"when": 1725454391060,
|
||||
"tag": "0000_dazzling_cerebro",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 1,
|
||||
"version": "7",
|
||||
"when": 1726068700784,
|
||||
"tag": "0001_wooden_ink",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
14
package.json
14
package.json
@@ -16,7 +16,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@auth/drizzle-adapter": "^1.4.2",
|
||||
"@headlessui/react": "^2.1.6",
|
||||
"@headlessui/react": "^2.1.8",
|
||||
"@hookform/resolvers": "^3.9.0",
|
||||
"@radix-ui/react-accordion": "^1.2.0",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.1",
|
||||
@@ -47,7 +47,7 @@
|
||||
"@radix-ui/react-toggle-group": "^1.1.0",
|
||||
"@radix-ui/react-tooltip": "^1.1.2",
|
||||
"@t3-oss/env-nextjs": "^0.11.1",
|
||||
"@tanstack/react-query": "^5.55.4",
|
||||
"@tanstack/react-query": "^5.56.2",
|
||||
"@trpc/client": "^11.0.0-rc.446",
|
||||
"@trpc/react-query": "^11.0.0-rc.446",
|
||||
"@trpc/server": "^11.0.0-rc.446",
|
||||
@@ -62,14 +62,14 @@
|
||||
"cmdk": "1.0.0",
|
||||
"date-fns": "^3.6.0",
|
||||
"drizzle-orm": "^0.33.0",
|
||||
"embla-carousel-react": "^8.2.1",
|
||||
"embla-carousel-react": "^8.3.0",
|
||||
"geist": "^1.3.1",
|
||||
"input-otp": "^1.2.4",
|
||||
"lodash": "^4.17.21",
|
||||
"loglevel": "^1.9.2",
|
||||
"loglevel-plugin-prefix": "^0.8.4",
|
||||
"lucide-react": "^0.439.0",
|
||||
"next": "^14.2.9",
|
||||
"lucide-react": "^0.441.0",
|
||||
"next": "^14.2.11",
|
||||
"next-auth": "^4.24.7",
|
||||
"next-themes": "^0.3.0",
|
||||
"postgres": "^3.4.4",
|
||||
@@ -99,12 +99,12 @@
|
||||
"@typescript-eslint/parser": "^8.5.0",
|
||||
"drizzle-kit": "^0.24.2",
|
||||
"eslint": "^9.10.0",
|
||||
"eslint-config-next": "^14.2.9",
|
||||
"eslint-config-next": "^14.2.11",
|
||||
"eslint-plugin-drizzle": "^0.2.3",
|
||||
"postcss": "^8.4.45",
|
||||
"prettier": "^3.3.3",
|
||||
"prettier-plugin-tailwindcss": "^0.6.6",
|
||||
"tailwindcss": "^3.4.10",
|
||||
"tailwindcss": "^3.4.11",
|
||||
"typescript": "^5.6.2"
|
||||
},
|
||||
"ct3aMetadata": {
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import UploadPage from "@/components/pages/upload-page";
|
||||
|
||||
const Upload = () => <UploadPage />;
|
||||
const Upload = () => (
|
||||
<div className="mt-4">
|
||||
<UploadPage />
|
||||
</div>
|
||||
);
|
||||
|
||||
export default Upload;
|
||||
|
||||
6
src/app/api/upload/post/route.ts
Normal file
6
src/app/api/upload/post/route.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { getServerSession } from "next-auth";
|
||||
import { NextRequest } from "next/server";
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const session = await getServerSession();
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import { dashboardConfig } from "@/config/top-nav.config";
|
||||
import { siteConfig } from "@/config/site.config";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { SessionProvider } from "next-auth/react";
|
||||
import { GlobalPasteListener } from "@/components/global-paste-listener";
|
||||
|
||||
export const viewport: Viewport = {
|
||||
themeColor: [
|
||||
@@ -36,6 +37,7 @@ export default async function RootLayout({
|
||||
children,
|
||||
}: Readonly<{ children: React.ReactNode }>) {
|
||||
const session = await getServerSession();
|
||||
|
||||
return (
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<head>
|
||||
@@ -53,8 +55,10 @@ export default async function RootLayout({
|
||||
>
|
||||
<TRPCReactProvider>
|
||||
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
|
||||
<GlobalPasteListener />
|
||||
<Toaster />
|
||||
<TailwindIndicator />
|
||||
|
||||
<TopNavbar items={dashboardConfig.mainNav} session={session} />
|
||||
<main className="m-4">{children}</main>
|
||||
</ThemeProvider>
|
||||
|
||||
3
src/components/global-paste-listener.tsx
Normal file
3
src/components/global-paste-listener.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export function GlobalPasteListener() {
|
||||
return <></>;
|
||||
}
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
Terminal,
|
||||
LogIn,
|
||||
Upload,
|
||||
Menu,
|
||||
} from "lucide-react";
|
||||
|
||||
export type Icon = typeof LucideIcon;
|
||||
@@ -111,6 +112,7 @@ export const Icons = {
|
||||
></path>
|
||||
</svg>
|
||||
),
|
||||
hamburger: Menu,
|
||||
twitter: Twitter,
|
||||
check: Check,
|
||||
upload: Upload,
|
||||
|
||||
44
src/components/navbar/mobile-navbar.tsx
Normal file
44
src/components/navbar/mobile-navbar.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import * as React from "react";
|
||||
import Link from "next/link";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Icons } from "@/components/icons";
|
||||
import { NavItem } from "@/types";
|
||||
import { siteConfig } from "@/config/site.config";
|
||||
|
||||
interface MobileNavProps {
|
||||
items: NavItem[];
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export function MobileNav({ items, children }: MobileNavProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"fixed inset-0 top-16 z-50 grid h-[calc(100vh-4rem)] grid-flow-row auto-rows-max overflow-auto p-6 pb-32 shadow-md animate-in slide-in-from-bottom-80 md:hidden",
|
||||
)}
|
||||
>
|
||||
<div className="relative z-20 grid gap-6 rounded-md bg-popover p-4 text-popover-foreground shadow-md">
|
||||
<Link href="/" className="flex items-center space-x-2">
|
||||
<Icons.logo />
|
||||
<span className="font-bold">{siteConfig.name}</span>
|
||||
</Link>
|
||||
<nav className="grid grid-flow-row auto-rows-max text-sm">
|
||||
{items.map((item, index) => (
|
||||
<Link
|
||||
key={index}
|
||||
href={item.disabled ? "#" : item.href}
|
||||
className={cn(
|
||||
"flex w-full items-center rounded-md p-2 text-sm font-medium hover:underline",
|
||||
item.disabled && "cursor-not-allowed opacity-60",
|
||||
)}
|
||||
>
|
||||
{item.title}
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -11,6 +11,8 @@ import { cn } from "@/lib/utils";
|
||||
import { useSelectedLayoutSegment } from "next/navigation";
|
||||
|
||||
import LoginButton from "@/components/widgets/login/login-button";
|
||||
import { MobileNav } from "./mobile-navbar";
|
||||
import { buttonVariants } from "@/components/ui/button";
|
||||
|
||||
type TopNavbarProps = {
|
||||
items: NavItem[];
|
||||
@@ -19,6 +21,7 @@ type TopNavbarProps = {
|
||||
|
||||
const TopNavbar: React.FC<TopNavbarProps> = ({ items, session }) => {
|
||||
const segment = useSelectedLayoutSegment();
|
||||
const [showMobileMenu, setShowMobileMenu] = React.useState<boolean>(false);
|
||||
return (
|
||||
<div className="mx-auto px-2 sm:px-4 lg:divide-y lg:divide-gray-200 lg:px-8">
|
||||
<div className="relative flex h-16 justify-between">
|
||||
@@ -30,7 +33,13 @@ const TopNavbar: React.FC<TopNavbarProps> = ({ items, session }) => {
|
||||
{siteConfig.name}
|
||||
</span>
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
className={buttonVariants({ variant: "secondary" })}
|
||||
href={"/upload"}
|
||||
>
|
||||
<Icons.upload className="mr-2 h-4 w-4" />
|
||||
Upload
|
||||
</Link>
|
||||
{items?.length ? (
|
||||
<nav className="hidden gap-6 md:flex">
|
||||
{items?.map((item, index) => (
|
||||
@@ -69,10 +78,20 @@ const TopNavbar: React.FC<TopNavbarProps> = ({ items, session }) => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative z-10 flex items-center lg:hidden">
|
||||
Mobile menu
|
||||
<div className="relative z-10 flex items-center md:hidden">
|
||||
<button
|
||||
className="flex items-center space-x-2 md:hidden"
|
||||
onClick={() => setShowMobileMenu(!showMobileMenu)}
|
||||
>
|
||||
{showMobileMenu ? (
|
||||
<Icons.close className="w-8" />
|
||||
) : (
|
||||
<Icons.logo className="w-8" />
|
||||
)}
|
||||
</button>
|
||||
{showMobileMenu && items && <MobileNav items={items} />}
|
||||
</div>
|
||||
<div className="hidden lg:relative lg:z-10 lg:ml-4 lg:flex lg:items-center">
|
||||
<div className="hidden md:relative md:z-10 md:ml-4 md:flex md:items-center">
|
||||
<LoginButton session={session} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -14,6 +14,11 @@ import {
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { env } from "@/env";
|
||||
import { logger } from "@/lib/logger";
|
||||
import { api } from "@/trpc/react";
|
||||
import { STATUS_CODES } from "http";
|
||||
|
||||
type FormValues = {
|
||||
title: string;
|
||||
@@ -23,31 +28,56 @@ type FormValues = {
|
||||
};
|
||||
|
||||
const UploadPage: React.FC = () => {
|
||||
const utils = api.useUtils();
|
||||
const router = useRouter();
|
||||
const form = useForm<FormValues>({
|
||||
defaultValues: {
|
||||
title: "",
|
||||
description: "",
|
||||
title: env.NEXT_PUBLIC_DEBUG_MODE ? "This is my title" : "",
|
||||
description: env.NEXT_PUBLIC_DEBUG_MODE ? "This is my description" : "",
|
||||
tags: [],
|
||||
image: undefined,
|
||||
},
|
||||
});
|
||||
const createImage = api.image.create.useMutation({
|
||||
onSuccess: async (e) => {
|
||||
console.log("upload-page", "onSuccess", e);
|
||||
const file = form.getValues().image;
|
||||
if (e.id && file) {
|
||||
const body = new FormData();
|
||||
body.set("image", file);
|
||||
body.set("id", e.id);
|
||||
const response = await fetch("/api/upload/profile-image", {
|
||||
method: "POST",
|
||||
body,
|
||||
});
|
||||
|
||||
await utils.image.invalidate();
|
||||
router.replace("/");
|
||||
} else {
|
||||
//TODO: Probably need to delete the image from the database
|
||||
logger.error("upload-page", "onSuccess", "Error uploading image");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const _submit: SubmitHandler<FormValues> = async (data) => {
|
||||
console.log(data);
|
||||
if (data.image) {
|
||||
const body = new FormData();
|
||||
body.append("title", data.title);
|
||||
body.append("description", data.description);
|
||||
body.append("tags", data.tags.join("|"));
|
||||
body.append("file", data.image);
|
||||
const response = await fetch("api/upload", {
|
||||
method: "POST",
|
||||
body,
|
||||
});
|
||||
if (response.status === 201) {
|
||||
await router.replace("/");
|
||||
}
|
||||
try {
|
||||
await createImage.mutateAsync(data);
|
||||
} catch (error) {
|
||||
logger.error("UploadPage", "error", error);
|
||||
}
|
||||
// if (data.image) {
|
||||
// const body = new FormData();
|
||||
// body.append("file", data.image);
|
||||
// const response = await fetch("api/upload", {
|
||||
// method: "POST",
|
||||
// body,
|
||||
// });
|
||||
// if (response.status === 201) {
|
||||
// router.replace("/");
|
||||
// }
|
||||
// }
|
||||
};
|
||||
return (
|
||||
<div className="md:grid md:grid-cols-3 md:gap-6">
|
||||
@@ -62,70 +92,68 @@ const UploadPage: React.FC = () => {
|
||||
<div className="md:col-span-2">
|
||||
<FormProvider {...form}>
|
||||
<form onSubmit={form.handleSubmit(_submit)}>
|
||||
<div className="shadow sm:overflow-hidden sm:rounded-md">
|
||||
<div className="space-y-4 px-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="title"
|
||||
defaultValue={"fergal.moran+opengifame@gmail.com"}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Title</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="text" {...field} />
|
||||
</FormControl>
|
||||
{form.formState.errors.title && (
|
||||
<FormMessage>
|
||||
{form.formState.errors.title.message}
|
||||
</FormMessage>
|
||||
)}
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="description"
|
||||
defaultValue={"fergal.moran+opengifame@gmail.com"}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Description</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea {...field} rows={3} />
|
||||
</FormControl>
|
||||
{form.formState.errors.description && (
|
||||
<FormMessage>
|
||||
{form.formState.errors.description.message}
|
||||
</FormMessage>
|
||||
)}
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
control={form.control}
|
||||
name="image"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<ImageUpload value={value} onChange={onChange} />
|
||||
)}
|
||||
/>
|
||||
<div className="divider pt-4">optional stuff</div>
|
||||
<Controller
|
||||
control={form.control}
|
||||
name="tags"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<TaggedInput
|
||||
label="Tags"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full px-4 py-3 text-right">
|
||||
<button type="submit" className="btn btn-primary w-full">
|
||||
Upload Gif
|
||||
</button>
|
||||
</div>
|
||||
<div className="space-y-4 px-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="title"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Let's give your post a title"
|
||||
type="text"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
{form.formState.errors.title && (
|
||||
<FormMessage>
|
||||
{form.formState.errors.title.message}
|
||||
</FormMessage>
|
||||
)}
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={form.control}
|
||||
name="image"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<ImageUpload value={value} onChange={onChange} />
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="description"
|
||||
defaultValue={"fergal.moran+opengifame@gmail.com"}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
placeholder="Add a description (if you want?)."
|
||||
{...field}
|
||||
rows={3}
|
||||
/>
|
||||
</FormControl>
|
||||
{form.formState.errors.description && (
|
||||
<FormMessage>
|
||||
{form.formState.errors.description.message}
|
||||
</FormMessage>
|
||||
)}
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<div className="divider pt-4">optional stuff</div>
|
||||
<Controller
|
||||
control={form.control}
|
||||
name="tags"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<TaggedInput label="Tags" value={value} onChange={onChange} />
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full px-4 py-3 text-right">
|
||||
<Button type="submit" className="btn btn-primary w-full">
|
||||
Upload Gif
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</FormProvider>
|
||||
|
||||
@@ -24,13 +24,6 @@ export function TrendingImages() {
|
||||
) : (
|
||||
<p>No images yet.</p>
|
||||
)}
|
||||
|
||||
<ImageUpload
|
||||
value="Farts"
|
||||
onChange={(e) => {
|
||||
console.log("trending-images", "uploading", e);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,9 +2,9 @@ import { DashboardConfig } from "@/types";
|
||||
|
||||
export const dashboardConfig: DashboardConfig = {
|
||||
mainNav: [
|
||||
{
|
||||
title: "Upload image",
|
||||
href: "/upload",
|
||||
},
|
||||
// {
|
||||
// title: "Upload image",
|
||||
// href: "/upload",
|
||||
// },
|
||||
],
|
||||
};
|
||||
|
||||
@@ -8,6 +8,7 @@ export const env = createEnv({
|
||||
*/
|
||||
server: {
|
||||
DATABASE_URL: z.string().url(),
|
||||
UPLOAD_PATH: z.string().default("uploads"),
|
||||
NODE_ENV: z
|
||||
.enum(["development", "test", "production"])
|
||||
.default("development"),
|
||||
@@ -31,6 +32,8 @@ export const env = createEnv({
|
||||
*/
|
||||
client: {
|
||||
// NEXT_PUBLIC_CLIENTVAR: z.string(),
|
||||
|
||||
NEXT_PUBLIC_DEBUG_MODE: z.boolean(),
|
||||
NEXT_PUBLIC_SITE_NAME: z.string().default("My Site"),
|
||||
NEXT_PUBLIC_SITE_DESCRIPTION: z.string().default("My site description"),
|
||||
NEXT_PUBLIC_SITE_URL: z.string().default("https://opengifame.com"),
|
||||
@@ -47,11 +50,14 @@ export const env = createEnv({
|
||||
*/
|
||||
runtimeEnv: {
|
||||
DATABASE_URL: process.env.DATABASE_URL,
|
||||
UPLOAD_PATH: process.env.UPLOAD_PATH,
|
||||
NODE_ENV: process.env.NODE_ENV,
|
||||
NEXTAUTH_SECRET: process.env.NEXTAUTH_SECRET,
|
||||
NEXTAUTH_URL: process.env.NEXTAUTH_URL,
|
||||
|
||||
NEXT_PUBLIC_SITE_NAME: process.env.NEXT_PUBLIC_SITE_NAME,
|
||||
NEXT_PUBLIC_DEBUG_MODE:
|
||||
process.env.NEXT_PUBLIC_DEBUG_MODE === "1" ? true : false,
|
||||
NEXT_PUBLIC_SITE_DESCRIPTION: process.env.NEXT_PUBLIC_SITE_NAME,
|
||||
NEXT_PUBLIC_SITE_URL: process.env.NEXT_PUBLIC_SITE_NAME,
|
||||
NEXT_PUBLIC_SITE_OG_IMAGE: process.env.NEXT_PUBLIC_SITE_NAME,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { postRouter } from "@/server/api/routers/post";
|
||||
import { createCallerFactory, createTRPCRouter } from "@/server/api/trpc";
|
||||
import { authRouter } from "./routers/auth";
|
||||
import { postRouter } from "@/server/api/routers/post";
|
||||
import { imageRouter } from "@/server/api/routers/image";
|
||||
|
||||
/**
|
||||
* This is the primary router for your server.
|
||||
@@ -10,6 +11,7 @@ import { authRouter } from "./routers/auth";
|
||||
export const appRouter = createTRPCRouter({
|
||||
post: postRouter,
|
||||
auth: authRouter,
|
||||
image: imageRouter,
|
||||
});
|
||||
|
||||
// export type definition of API
|
||||
|
||||
31
src/server/api/routers/image.ts
Normal file
31
src/server/api/routers/image.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
|
||||
import { z } from "zod";
|
||||
import { images } from "@/server/db/schema";
|
||||
|
||||
const imageCreateType = {
|
||||
title: z.string().min(5),
|
||||
description: z.string(),
|
||||
tags: z.array(z.string()),
|
||||
};
|
||||
|
||||
export const imageRouter = createTRPCRouter({
|
||||
create: protectedProcedure
|
||||
.input(z.object(imageCreateType))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const post = await ctx.db
|
||||
.insert(images)
|
||||
.values({
|
||||
title: input.title,
|
||||
description: input.description,
|
||||
tags: input.tags,
|
||||
createdById: ctx.session.user.id,
|
||||
})
|
||||
.returning();
|
||||
if (!post[0]) {
|
||||
throw new Error("Failed to create image");
|
||||
}
|
||||
return {
|
||||
id: post[0].id,
|
||||
};
|
||||
}),
|
||||
});
|
||||
@@ -25,17 +25,47 @@ declare module "next-auth" {
|
||||
} & DefaultSession["user"];
|
||||
}
|
||||
}
|
||||
declare module "next-auth/jwt" {
|
||||
interface JWT {
|
||||
id: string;
|
||||
}
|
||||
}
|
||||
|
||||
export const authOptions: NextAuthOptions = {
|
||||
callbacks: {
|
||||
session: ({ session, user }) => {
|
||||
const s = {
|
||||
...session,
|
||||
user: {
|
||||
...session.user,
|
||||
},
|
||||
session: ({ token, session }) => {
|
||||
if (token) {
|
||||
session.user.id = token.id;
|
||||
session.user.name = token.name;
|
||||
session.user.email = token.email;
|
||||
session.user.image = token.picture;
|
||||
}
|
||||
|
||||
return session;
|
||||
},
|
||||
async jwt({ token, user }) {
|
||||
if (!user || !user.email) {
|
||||
return token;
|
||||
}
|
||||
const u = await db
|
||||
.select()
|
||||
.from(users)
|
||||
.where(and(eq(users.email, user.email)))
|
||||
.limit(1);
|
||||
|
||||
if (!u || !u[0]) {
|
||||
if (user) {
|
||||
token.id = user?.id;
|
||||
}
|
||||
return token;
|
||||
}
|
||||
const session = u[0];
|
||||
return {
|
||||
id: session.id,
|
||||
name: session.name,
|
||||
email: session.email,
|
||||
picture: session.image,
|
||||
};
|
||||
return s;
|
||||
},
|
||||
},
|
||||
session: {
|
||||
@@ -70,7 +100,8 @@ export const authOptions: NextAuthOptions = {
|
||||
if (!(await bcrypt.compare(credentials.password, user[0]!.password!))) {
|
||||
return null;
|
||||
}
|
||||
return { id: user[0]!.id, email: user[0]!.email };
|
||||
const session = { id: user[0]!.id, email: user[0]!.email };
|
||||
return session;
|
||||
},
|
||||
}),
|
||||
],
|
||||
|
||||
@@ -13,10 +13,32 @@ import { type AdapterAccount } from "next-auth/adapters";
|
||||
|
||||
export const createTable = pgTableCreator((name) => `${name}`);
|
||||
|
||||
export const images = createTable("image", {
|
||||
id: varchar("id", { length: 255 })
|
||||
.notNull()
|
||||
.primaryKey()
|
||||
.$defaultFn(() => crypto.randomUUID()),
|
||||
title: varchar("title", { length: 256 }),
|
||||
description: varchar("description"),
|
||||
tags: text("tags").array(),
|
||||
createdById: varchar("created_by", { length: 255 })
|
||||
.notNull()
|
||||
.references(() => users.id),
|
||||
createdAt: timestamp("created_at", { withTimezone: true })
|
||||
.default(sql`CURRENT_TIMESTAMP`)
|
||||
.notNull(),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }).$onUpdate(
|
||||
() => new Date(),
|
||||
),
|
||||
});
|
||||
|
||||
export const posts = createTable(
|
||||
"post",
|
||||
{
|
||||
id: serial("id").primaryKey(),
|
||||
id: varchar("id", { length: 255 })
|
||||
.notNull()
|
||||
.primaryKey()
|
||||
.$defaultFn(() => crypto.randomUUID()),
|
||||
name: varchar("name", { length: 256 }),
|
||||
createdById: varchar("created_by", { length: 255 })
|
||||
.notNull()
|
||||
|
||||
Reference in New Issue
Block a user