Rename some files and tidy up navbar

This commit is contained in:
Fergal Moran
2024-09-09 18:46:25 +01:00
parent 96a63ace46
commit 2f3efa3aef
29 changed files with 159 additions and 155 deletions

19
.env
View File

@@ -1,24 +1,11 @@
# When adding additional environment variables, the schema in "/src/env.js"
# should be updated accordingly.
# Drizzle
DATABASE_URL="postgresql://postgres:hackme@localhost:5432/opengifame" DATABASE_URL="postgresql://postgres:hackme@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="tAOVgxpY1U0BsnPCr6Gf8WVkmRMkp06ztUfwMhBKMQ4=" NEXTAUTH_SECRET="tAOVgxpY1U0BsnPCr6Gf8WVkmRMkp06ztUfwMhBKMQ4="
NEXTAUTH_URL="http://localhost:3000" NEXTAUTH_URL="https://opengifame.dev.fergl.ie:3000"
# Next Auth Discord Provider
DISCORD_CLIENT_ID=""
DISCORD_CLIENT_SECRET=""
NEXT_PUBLIC_SITE_NAME=Open Gifame NEXT_PUBLIC_SITE_NAME=Open Gifame
NEXT_PUBLIC_SITE_DESCRIPTION=Robot powered giffage NEXT_PUBLIC_SITE_DESCRIPTION=Robot powered giffage
NEXT_PUBLIC_SITE_URL=https://gifs.ferg.al, NEXT_PUBLIC_SITE_URL=https://opengifame.dev.fergl.ie:3000
NEXT_PUBLIC_SITE_OG_IMAGE=http://localhost:3000/icon.png NEXT_PUBLIC_SITE_OG_IMAGE=https://opengifame.dev.fergl.ie:3000/icon.png
NEXT_PUBLIC_SITE_TWITTER=https://twitter.com/opengifame NEXT_PUBLIC_SITE_TWITTER=https://twitter.com/opengifame
NEXT_PUBLIC_SITE_GITHUB=https://github.com/fergalmoran/opengifame NEXT_PUBLIC_SITE_GITHUB=https://github.com/fergalmoran/opengifame

View File

@@ -1,61 +1,53 @@
/** @type {import("eslint").Linter.Config} */ /** @type {import("eslint").Linter.Config} */
const config = { const config = {
"parser": "@typescript-eslint/parser", parser: "@typescript-eslint/parser",
"parserOptions": { parserOptions: {
"project": true project: true,
}, },
"plugins": [ plugins: ["@typescript-eslint", "drizzle"],
"@typescript-eslint", extends: [
"drizzle"
],
"extends": [
"next/core-web-vitals", "next/core-web-vitals",
"plugin:@typescript-eslint/recommended-type-checked", "plugin:@typescript-eslint/recommended-type-checked",
"plugin:@typescript-eslint/stylistic-type-checked" "plugin:@typescript-eslint/stylistic-type-checked",
], ],
"rules": { rules: {
"@typescript-eslint/prefer-nullish-coalescing": "off",
"@typescript-eslint/array-type": "off", "@typescript-eslint/array-type": "off",
"@typescript-eslint/consistent-type-definitions": "off", "@typescript-eslint/consistent-type-definitions": "off",
"@typescript-eslint/consistent-type-imports": [ "@typescript-eslint/consistent-type-imports": [
"warn", "warn",
{ {
"prefer": "type-imports", prefer: "type-imports",
"fixStyle": "inline-type-imports" fixStyle: "inline-type-imports",
} },
], ],
"@typescript-eslint/no-unused-vars": [ "@typescript-eslint/no-unused-vars": [
"warn", "warn",
{ {
"argsIgnorePattern": "^_" argsIgnorePattern: "^_",
} },
], ],
"@typescript-eslint/require-await": "off", "@typescript-eslint/require-await": "off",
"@typescript-eslint/no-misused-promises": [ "@typescript-eslint/no-misused-promises": [
"error", "error",
{ {
"checksVoidReturn": { checksVoidReturn: {
"attributes": false attributes: false,
} },
} },
], ],
"drizzle/enforce-delete-with-where": [ "drizzle/enforce-delete-with-where": [
"error", "error",
{ {
"drizzleObjectName": [ drizzleObjectName: ["db", "ctx.db"],
"db", },
"ctx.db"
]
}
], ],
"drizzle/enforce-update-with-where": [ "drizzle/enforce-update-with-where": [
"error", "error",
{ {
"drizzleObjectName": [ drizzleObjectName: ["db", "ctx.db"],
"db", },
"ctx.db" ],
] },
} };
]
}
}
module.exports = config; module.exports = config;

View File

@@ -1,3 +1,4 @@
{ {
"workbench.colorTheme": "Tinacious Design (High Contrast)" // "workbench.colorTheme": "Tinacious Design (High Contrast)"
"workbench.colorTheme": "Cyberpunk 2077 rebuild",
} }

BIN
bun.lockb

Binary file not shown.

View File

@@ -90,14 +90,15 @@
"zod": "^3.23.8" "zod": "^3.23.8"
}, },
"devDependencies": { "devDependencies": {
"@types/eslint": "^9.6.1", "@faker-js/faker": "^9.0.0",
"@types/eslint": "^8.56.12",
"@types/node": "^22.5.4", "@types/node": "^22.5.4",
"@types/react": "^18.3.5", "@types/react": "^18.3.5",
"@types/react-dom": "^18.3.0", "@types/react-dom": "^18.3.0",
"@typescript-eslint/eslint-plugin": "^8.4.0", "@typescript-eslint/eslint-plugin": "^8.4.0",
"@typescript-eslint/parser": "^8.4.0", "@typescript-eslint/parser": "^8.4.0",
"drizzle-kit": "^0.24.2", "drizzle-kit": "^0.24.2",
"eslint": "^9.10.0", "eslint": "^8.57.0",
"eslint-config-next": "^14.2.8", "eslint-config-next": "^14.2.8",
"eslint-plugin-drizzle": "^0.2.3", "eslint-plugin-drizzle": "^0.2.3",
"postcss": "^8.4.45", "postcss": "^8.4.45",

View File

@@ -1,12 +1,10 @@
"use client"; "use client";
import React from "react"; import React from "react";
import RegistrationForm from "@/components/forms/auth/RegistrationForm"; import RegistrationForm from "@/components/forms/auth/registration-form";
import SocialLogin from "@/components/widgets/login/SocialLogin"; import SocialLogin from "@/components/widgets/login/social-login-button";
import Link from "next/link"; import Link from "next/link";
import { Icons } from "@/components/icons"; import { Icons } from "@/components/icons";
import { cn } from "@/lib/utils";
import { buttonVariants } from "@/components/ui/button";
const RegisterPage: React.FC = () => { const RegisterPage: React.FC = () => {
return ( return (

View File

@@ -1,11 +1,9 @@
"use client"; "use client";
import SocialLogin from "@/components/widgets/login/SocialLogin"; import SocialLogin from "@/components/widgets/login/social-login-button";
import Link from "next/link"; import Link from "next/link";
import React from "react"; import React from "react";
import { Icons } from "@/components/icons"; import { Icons } from "@/components/icons";
import { cn } from "@/lib/utils"; import SignInForm from "@/components/forms/auth/signin-form";
import { buttonVariants } from "@/components/ui/button";
import SignInForm from "@/components/forms/auth/SignInForm";
const SignInPage = () => { const SignInPage = () => {
return ( return (

View File

@@ -1,10 +0,0 @@
"use client";
import { useState } from "react";
import { api } from "@/trpc/react";
export function TrendingImages() {
const [name, setName] = useState("");
return <div className="w-full max-w-xs">Trending Images</div>;
}

View File

@@ -1,7 +1,7 @@
import { Inter as FontSans } from "next/font/google"; import { Roboto as font } from "next/font/google";
import "@/styles/globals.css"; import "@/styles/globals.css";
import { type Metadata, Viewport } from "next"; import { type Metadata, type Viewport } from "next";
import { TRPCReactProvider } from "@/trpc/react"; import { TRPCReactProvider } from "@/trpc/react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
@@ -12,17 +12,18 @@ import React from "react";
import TopNavbar from "@/components/navbar/top-navbar"; import TopNavbar from "@/components/navbar/top-navbar";
import { dashboardConfig } from "@/config/top-nav.config"; import { dashboardConfig } from "@/config/top-nav.config";
import { siteConfig } from "@/config/site.config"; import { siteConfig } from "@/config/site.config";
import { getServerSession } from "next-auth";
const fontSans = FontSans({
subsets: ["latin"],
variable: "--font-sans",
});
export const viewport: Viewport = { export const viewport: Viewport = {
themeColor: [ themeColor: [
{ media: "(prefers-color-scheme: light)", color: "white" }, { media: "(prefers-color-scheme: light)", color: "white" },
{ media: "(prefers-color-scheme: dark)", color: "black" }, { media: "(prefers-color-scheme: dark)", color: "black" },
], ],
}; };
const f = font({
weight: "400",
subsets: ["latin"],
});
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Open Gifame", title: "Open Gifame",
@@ -30,9 +31,10 @@ export const metadata: Metadata = {
icons: [{ rel: "icon", url: "/favicon.ico" }], icons: [{ rel: "icon", url: "/favicon.ico" }],
}; };
export default function RootLayout({ export default async function RootLayout({
children, children,
}: Readonly<{ children: React.ReactNode }>) { }: Readonly<{ children: React.ReactNode }>) {
const session = await getServerSession();
return ( return (
<html lang="en" suppressHydrationWarning> <html lang="en" suppressHydrationWarning>
<head> <head>
@@ -45,15 +47,15 @@ export default function RootLayout({
<body <body
className={cn( className={cn(
"min-h-screen bg-background font-sans antialiased", "min-h-screen bg-background font-sans antialiased",
fontSans.variable, f.className,
)} )}
> >
<TRPCReactProvider> <TRPCReactProvider>
<ThemeProvider attribute="class" defaultTheme="system" enableSystem> <ThemeProvider attribute="class" defaultTheme="system" enableSystem>
<Toaster /> <Toaster />
<TailwindIndicator /> <TailwindIndicator />
<TopNavbar items={dashboardConfig.mainNav} /> <TopNavbar items={dashboardConfig.mainNav} session={session} />
{children} <main className="m-4">{children}</main>
</ThemeProvider> </ThemeProvider>
</TRPCReactProvider> </TRPCReactProvider>
</body> </body>

View File

@@ -1,5 +1,5 @@
import { TrendingImages } from "@/app/_components/trending-images";
import LandingPage from "@/components/pages/landing-page"; import LandingPage from "@/components/pages/landing-page";
import { TrendingImages } from "@/components/trending-images";
import { getServerAuthSession } from "@/server/auth"; import { getServerAuthSession } from "@/server/auth";
import { HydrateClient } from "@/trpc/server"; import { HydrateClient } from "@/trpc/server";

View File

@@ -25,6 +25,7 @@ import {
X, X,
type Icon as LucideIcon, type Icon as LucideIcon,
Terminal, Terminal,
LogIn,
} from "lucide-react"; } from "lucide-react";
export type Icon = typeof LucideIcon; export type Icon = typeof LucideIcon;
@@ -46,9 +47,9 @@ export const Icons = {
user: User, user: User,
arrowRight: ArrowRight, arrowRight: ArrowRight,
help: HelpCircle, help: HelpCircle,
login: LogIn,
logo: ({ ...props }: LucideProps) => ( logo: ({ ...props }: LucideProps) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" {...props}> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" {...props}>
{/* Background */}
<rect <rect
x="0" x="0"
y="0" y="0"
@@ -58,11 +59,7 @@ export const Icons = {
ry="20" ry="20"
fill="#FF6B6B" fill="#FF6B6B"
/> />
{/* Abstract prism shape - significantly enlarged */}
<path d="M50,10 L90,80 L10,80 Z" fill="white" /> <path d="M50,10 L90,80 L10,80 Z" fill="white" />
{/* Color refraction lines - adjusted for new size */}
<path <path
d="M50,10 L62,80" d="M50,10 L62,80"
stroke="#FFD166" stroke="#FFD166"
@@ -81,8 +78,6 @@ export const Icons = {
strokeWidth={4} strokeWidth={4}
strokeLinecap="round" strokeLinecap="round"
/> />
{/* Circular highlight - adjusted position and size */}
<circle <circle
cx="50" cx="50"
cy="35" cy="35"

View File

@@ -1,35 +1,82 @@
import { NavItem } from "@/types"; "use client";
import Link from "next/link"; import Link from "next/link";
import React from "react"; import React from "react";
import { type NavItem } from "@/types";
import { type Session } from "next-auth";
import { Icons } from "../icons"; import { Icons } from "../icons";
import { siteConfig } from "@/config/site.config"; import { siteConfig } from "@/config/site.config";
import { cn } from "@/lib/utils";
import { useSelectedLayoutSegment } from "next/navigation";
import LoginButton from "../widgets/login/login-button";
type TopNavbarProps = { type TopNavbarProps = {
items: NavItem[]; items: NavItem[];
session: Session | null;
}; };
const TopNavbar: React.FC<TopNavbarProps> = ({ items }) => { const TopNavbar: React.FC<TopNavbarProps> = ({ items, session }) => {
const segment = useSelectedLayoutSegment();
return ( return (
<header className="sticky top-0 z-40 border-b bg-background"> <div className="mx-auto px-2 sm:px-4 lg:divide-y lg:divide-gray-200 lg:px-8">
<div className="container flex h-16 items-center justify-between py-4"> <div className="relative flex h-16 justify-between">
<div className="flex gap-6 md:gap-10"> <div className="relative z-10 flex px-2 lg:px-0">
<div className="flex flex-shrink-0 items-center gap-4">
<Link href="/" className="hidden items-center space-x-2 md:flex"> <Link href="/" className="hidden items-center space-x-2 md:flex">
<Icons.logo className="h-8 w-8" /> <Icons.logo className="h-8 w-8" />
<span className="hidden font-bold sm:inline-block"> <span className="hidden font-bold sm:inline-block">
{siteConfig.name} {siteConfig.name}
</span> </span>
</Link> </Link>
</div>
{/* <UserAccountNav {items?.length ? (
user={{ <nav className="hidden gap-6 md:flex">
name: user.name, {items?.map((item, index) => (
image: user.image, <Link
email: user.email, key={index}
}} href={item.disabled ? "#" : item.href}
/> */} className={cn(
"flex items-center text-lg font-medium transition-colors hover:text-foreground/80 sm:text-sm",
item.href.startsWith(`/${segment}`)
? "text-foreground"
: "text-foreground/60",
item.disabled && "cursor-not-allowed opacity-80",
)}
>
{item.title}
</Link>
))}
</nav>
) : null}
</div>
</div>
<div className="relative z-0 flex flex-1 items-center justify-center px-2 sm:absolute sm:inset-0">
<div className="w-full sm:max-w-xs">
<div className="flex flex-1 items-center justify-between space-x-2 md:justify-end">
<div className="w-full flex-1 md:w-auto md:flex-none">
<button className="relative inline-flex h-8 w-full items-center justify-start whitespace-nowrap rounded-[0.5rem] border border-input bg-muted/50 px-4 py-2 text-sm font-normal text-muted-foreground shadow-none transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 sm:pr-12 md:w-40 lg:w-64">
<span className="hidden lg:inline-flex">
Search images...
</span>
<span className="inline-flex lg:hidden">Search...</span>
<kbd className="pointer-events-none absolute right-[0.3rem] top-[0.3rem] hidden h-5 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium opacity-100 sm:flex">
<span className="text-xs"></span>K
</kbd>
</button>
</div>
</div>
</div>
</div>
<div className="relative z-10 flex items-center lg:hidden">
Mobile menu
</div>
<div className="hidden lg:relative lg:z-10 lg:ml-4 lg:flex lg:items-center">
<LoginButton session={session} />
</div>
</div>
</div> </div>
</header>
); );
}; };

View File

@@ -3,11 +3,11 @@ import React from "react";
const LandingPage: React.FC = () => { const LandingPage: React.FC = () => {
return ( return (
<div> <div>
<h1 className="text-5xl font-extrabold tracking-tight sm:text-[5rem]"> <div className="m-6">
Warning <span className="text-[hsl(280,100%,70%)]">contains</span> Gifs <h1 className="text-xl font-extrabold tracking-tight sm:text-[5rem]">
Warning <span className="text-[hsl(280,100%,70%)]">contains</span>{" "}
Gifs
</h1> </h1>
<div>
<a href="/signin">Sign In</a>
</div> </div>
</div> </div>
); );

View File

@@ -21,28 +21,21 @@ export function TrendingImages() {
{latestPost ? ( {latestPost ? (
<p className="truncate">Your most recent post: {latestPost.name}</p> <p className="truncate">Your most recent post: {latestPost.name}</p>
) : ( ) : (
<p>You have no posts yet.</p> <p>No images yet.</p>
)} )}
<form <form
onSubmit={(e) => { onSubmit={(e) => {
e.preventDefault(); e.preventDefault();
createPost.mutate({ name }); createPost.mutate({ name });
}} }}
className="flex flex-col gap-2" className="flex flex-col gap-2 my-4"
> >
<input
type="text"
placeholder="Title"
value={name}
onChange={(e) => setName(e.target.value)}
className="w-full rounded-full px-4 py-2 text-black"
/>
<button <button
type="submit" type="submit"
className="rounded-full bg-white/10 px-10 py-3 font-semibold transition hover:bg-white/20" className="rounded-full bg-white/10 px-10 py-3 font-semibold transition hover:bg-white/20"
disabled={createPost.isPending} disabled={createPost.isPending}
> >
{createPost.isPending ? "Submitting..." : "Submit"} {createPost.isPending ? "Submitting..." : "Upload an image"}
</button> </button>
</form> </form>
</div> </div>

View File

@@ -3,23 +3,23 @@
import React from "react"; import React from "react";
import { signIn } from "next-auth/react"; import { signIn } from "next-auth/react";
import { RiLoginCircleLine } from "react-icons/ri"; import { RiLoginCircleLine } from "react-icons/ri";
import UserNavDropdown from "../UserNavDropdown"; import UserNavDropdown from "../user-nav-dropdown";
import { type Session } from "next-auth";
import { Button } from "@/components/ui/button";
import { Icons } from "@/components/icons";
interface ILoginButtonProps { interface ILoginButtonProps {
session: any; session: Session | null;
} }
const LoginButton: React.FC<ILoginButtonProps> = ({ session }) => { const LoginButton: React.FC<ILoginButtonProps> = ({ session }) => {
return session ? ( return session ? (
<UserNavDropdown session={session} /> <UserNavDropdown session={session} />
) : ( ) : (
<button <Button onClick={() => signIn()} className="">
onClick={() => signIn()} <Icons.login className="mr-2 h-4 w-4" />
className="btn btn-ghost drawer-button normal-case"
>
<RiLoginCircleLine className="inline-block h-6 w-6 fill-current md:mr-1" />
Login Login
</button> </Button>
); );
}; };

View File

@@ -1,28 +1,25 @@
'use client'; "use client";
import { logger } from '@lib/logger'; import React, { Fragment } from "react";
import React, { Fragment } from 'react'; import { Menu, Transition } from "@headlessui/react";
import { Menu, Transition } from '@headlessui/react'; import { signOut } from "next-auth/react";
import { signOut } from 'next-auth/react'; import { logger } from "@/lib/logger";
interface IUserNavDropdownProps { interface IUserNavDropdownProps {
session: any; session: any;
} }
const UserNavDropdown: React.FC<IUserNavDropdownProps> = ({ session }) => { const UserNavDropdown: React.FC<IUserNavDropdownProps> = ({ session }) => {
React.useEffect(() => { React.useEffect(() => {
logger.debug('UserNavDropdown', 'session', session); logger.debug("UserNavDropdown", "session", session);
}, [session]); }, [session]);
return ( return (
<div className="flex items-center"> <div className="flex items-center">
<Menu <Menu as="div" className="relative ml-4 flex-shrink-0">
as="div"
className="relative flex-shrink-0 ml-4"
>
<div> <div>
<Menu.Button className="flex text-sm text-white bg-gray-800 rounded-full focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-800 focus:ring-white"> <Menu.Button className="flex rounded-full bg-gray-800 text-sm text-white focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-gray-800">
<span className="sr-only">Open user menu</span> <span className="sr-only">Open user menu</span>
<img <img
className="w-8 h-8 rounded-full" className="h-8 w-8 rounded-full"
src={session?.user?.image as string} src={session?.user?.image as string}
alt="Profile image" alt="Profile image"
/> />
@@ -37,7 +34,7 @@ const UserNavDropdown: React.FC<IUserNavDropdownProps> = ({ session }) => {
leaveFrom="transform opacity-100 scale-100" leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95" leaveTo="transform opacity-0 scale-95"
> >
<Menu.Items className="absolute right-0 z-50 w-48 py-1 mt-2 origin-top-right bg-white rounded-md shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none"> <Menu.Items className="absolute right-0 z-50 mt-2 w-48 origin-top-right rounded-md bg-white py-1 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
<Menu.Item> <Menu.Item>
{({ active }) => ( {({ active }) => (
<button <button

View File

@@ -3,13 +3,8 @@ import { DashboardConfig } from "@/types";
export const dashboardConfig: DashboardConfig = { export const dashboardConfig: DashboardConfig = {
mainNav: [ mainNav: [
{ {
title: "Documentation", title: "Upload image",
href: "/docs", href: "/upload",
},
{
title: "Support",
href: "/support",
disabled: true,
}, },
], ],
}; };

View File

@@ -2,18 +2,19 @@ import { createTRPCRouter, publicProcedure } from "../trpc";
import { z } from "zod"; import { z } from "zod";
import { users } from "@/server/db/schema"; import { users } from "@/server/db/schema";
import bcrypt from "bcrypt"; import bcrypt from "bcrypt";
import { and, eq } from "drizzle-orm"; import { faker } from "@faker-js/faker";
export const authRouter = createTRPCRouter({ export const authRouter = createTRPCRouter({
create: publicProcedure create: publicProcedure
.input(z.object({ email: z.string().email(), password: z.string().min(5) })) .input(z.object({ email: z.string().email(), password: z.string().min(5) }))
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
const profileImage = faker.image.avatar();
const hashedPassword = await bcrypt.hash(input.password, 10); const hashedPassword = await bcrypt.hash(input.password, 10);
const user = await ctx.db.insert(users).values({ const user = await ctx.db.insert(users).values({
email: input.email, email: input.email,
password: hashedPassword, password: hashedPassword,
image: profileImage,
}); });
return user; return user;
}), }),
}); });

View File

@@ -57,7 +57,7 @@ export function TRPCReactProvider(props: { children: React.ReactNode }) {
}, },
}), }),
], ],
}) }),
); );
return ( return (

View File

@@ -6,7 +6,6 @@ export type NavItem = {
export type DashboardConfig = { export type DashboardConfig = {
mainNav: NavItem[]; mainNav: NavItem[];
}; };
export type SiteConfig = { export type SiteConfig = {
name: string; name: string;
description: string; description: string;

8
ssl-proxy.json Normal file
View File

@@ -0,0 +1,8 @@
{
"Dev Proxy": {
"source": "3000",
"target": "3002",
"key": "/etc/letsencrypt/live/dev.fergl.ie/privkey.pem",
"cert": "/etc/letsencrypt/live/dev.fergl.ie/fullchain.pem"
}
}