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"
# 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_URL="http://localhost:3000"
# Next Auth Discord Provider
DISCORD_CLIENT_ID=""
DISCORD_CLIENT_SECRET=""
NEXTAUTH_URL="https://opengifame.dev.fergl.ie:3000"
NEXT_PUBLIC_SITE_NAME=Open Gifame
NEXT_PUBLIC_SITE_DESCRIPTION=Robot powered giffage
NEXT_PUBLIC_SITE_URL=https://gifs.ferg.al,
NEXT_PUBLIC_SITE_OG_IMAGE=http://localhost:3000/icon.png
NEXT_PUBLIC_SITE_URL=https://opengifame.dev.fergl.ie:3000
NEXT_PUBLIC_SITE_OG_IMAGE=https://opengifame.dev.fergl.ie:3000/icon.png
NEXT_PUBLIC_SITE_TWITTER=https://twitter.com/opengifame
NEXT_PUBLIC_SITE_GITHUB=https://github.com/fergalmoran/opengifame

View File

@@ -1,61 +1,53 @@
/** @type {import("eslint").Linter.Config} */
const config = {
"parser": "@typescript-eslint/parser",
"parserOptions": {
"project": true
parser: "@typescript-eslint/parser",
parserOptions: {
project: true,
},
"plugins": [
"@typescript-eslint",
"drizzle"
],
"extends": [
plugins: ["@typescript-eslint", "drizzle"],
extends: [
"next/core-web-vitals",
"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/consistent-type-definitions": "off",
"@typescript-eslint/consistent-type-imports": [
"warn",
{
"prefer": "type-imports",
"fixStyle": "inline-type-imports"
}
prefer: "type-imports",
fixStyle: "inline-type-imports",
},
],
"@typescript-eslint/no-unused-vars": [
"warn",
{
"argsIgnorePattern": "^_"
}
argsIgnorePattern: "^_",
},
],
"@typescript-eslint/require-await": "off",
"@typescript-eslint/no-misused-promises": [
"error",
{
"checksVoidReturn": {
"attributes": false
}
}
checksVoidReturn: {
attributes: false,
},
},
],
"drizzle/enforce-delete-with-where": [
"error",
{
"drizzleObjectName": [
"db",
"ctx.db"
]
}
drizzleObjectName: ["db", "ctx.db"],
},
],
"drizzle/enforce-update-with-where": [
"error",
{
"drizzleObjectName": [
"db",
"ctx.db"
]
}
]
}
}
drizzleObjectName: ["db", "ctx.db"],
},
],
},
};
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"
},
"devDependencies": {
"@types/eslint": "^9.6.1",
"@faker-js/faker": "^9.0.0",
"@types/eslint": "^8.56.12",
"@types/node": "^22.5.4",
"@types/react": "^18.3.5",
"@types/react-dom": "^18.3.0",
"@typescript-eslint/eslint-plugin": "^8.4.0",
"@typescript-eslint/parser": "^8.4.0",
"drizzle-kit": "^0.24.2",
"eslint": "^9.10.0",
"eslint": "^8.57.0",
"eslint-config-next": "^14.2.8",
"eslint-plugin-drizzle": "^0.2.3",
"postcss": "^8.4.45",

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,35 +1,82 @@
import { NavItem } from "@/types";
"use client";
import Link from "next/link";
import React from "react";
import { type NavItem } from "@/types";
import { type Session } from "next-auth";
import { Icons } from "../icons";
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 = {
items: NavItem[];
session: Session | null;
};
const TopNavbar: React.FC<TopNavbarProps> = ({ items }) => {
const TopNavbar: React.FC<TopNavbarProps> = ({ items, session }) => {
const segment = useSelectedLayoutSegment();
return (
<header className="sticky top-0 z-40 border-b bg-background">
<div className="container flex h-16 items-center justify-between py-4">
<div className="flex gap-6 md:gap-10">
<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">
<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">
<Icons.logo className="h-8 w-8" />
<span className="hidden font-bold sm:inline-block">
{siteConfig.name}
</span>
</Link>
</div>
{/* <UserAccountNav
user={{
name: user.name,
image: user.image,
email: user.email,
}}
/> */}
{items?.length ? (
<nav className="hidden gap-6 md:flex">
{items?.map((item, index) => (
<Link
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>
</header>
);
};

View File

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

View File

@@ -21,28 +21,21 @@ export function TrendingImages() {
{latestPost ? (
<p className="truncate">Your most recent post: {latestPost.name}</p>
) : (
<p>You have no posts yet.</p>
<p>No images yet.</p>
)}
<form
onSubmit={(e) => {
e.preventDefault();
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
type="submit"
className="rounded-full bg-white/10 px-10 py-3 font-semibold transition hover:bg-white/20"
disabled={createPost.isPending}
>
{createPost.isPending ? "Submitting..." : "Submit"}
{createPost.isPending ? "Submitting..." : "Upload an image"}
</button>
</form>
</div>

View File

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

View File

@@ -1,28 +1,25 @@
'use client';
import { logger } from '@lib/logger';
import React, { Fragment } from 'react';
import { Menu, Transition } from '@headlessui/react';
import { signOut } from 'next-auth/react';
"use client";
import React, { Fragment } from "react";
import { Menu, Transition } from "@headlessui/react";
import { signOut } from "next-auth/react";
import { logger } from "@/lib/logger";
interface IUserNavDropdownProps {
session: any;
}
const UserNavDropdown: React.FC<IUserNavDropdownProps> = ({ session }) => {
React.useEffect(() => {
logger.debug('UserNavDropdown', 'session', session);
logger.debug("UserNavDropdown", "session", session);
}, [session]);
return (
<div className="flex items-center">
<Menu
as="div"
className="relative flex-shrink-0 ml-4"
>
<Menu as="div" className="relative ml-4 flex-shrink-0">
<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>
<img
className="w-8 h-8 rounded-full"
className="h-8 w-8 rounded-full"
src={session?.user?.image as string}
alt="Profile image"
/>
@@ -37,7 +34,7 @@ const UserNavDropdown: React.FC<IUserNavDropdownProps> = ({ session }) => {
leaveFrom="transform opacity-100 scale-100"
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>
{({ active }) => (
<button

View File

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

View File

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

View File

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

View File

@@ -6,7 +6,6 @@ export type NavItem = {
export type DashboardConfig = {
mainNav: NavItem[];
};
export type SiteConfig = {
name: 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"
}
}