initial commit

This commit is contained in:
Mrrp 2025-01-06 16:35:53 -08:00
commit ab06adb651
44 changed files with 8933 additions and 0 deletions

View file

@ -0,0 +1,24 @@
# Create a Docker container from this project and post it to Forgejo (a fork of Gitea) from this Forgejo runner.
name: container
on:
push:
branches:
- master
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Build the Docker image
run: docker build -t git.smgames.club/${{ env.GITHUB_REPOSITORY_OWNER }}/felidae-site:latest .
- name: Log in to the Forgejo Container Registry
run: echo "${{ secrets.FORGEJO_TOKEN }}" | docker login -u "${{ secrets.FORGEJO_USERNAME }}" --password-stdin git.smgames.club
- name: Push the Docker image
run: docker push git.smgames.club/${{ env.GITHUB_REPOSITORY_OWNER }}/felidae-site:latest

43
.gitignore vendored Normal file
View file

@ -0,0 +1,43 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
.direnv
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
.env*
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

8
.idea/.gitignore generated vendored Normal file
View file

@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

View file

@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
</profile>
</component>

8
.idea/modules.xml generated Normal file
View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/neo-blogsite.iml" filepath="$PROJECT_DIR$/.idea/neo-blogsite.iml" />
</modules>
</component>
</project>

12
.idea/neo-blogsite.iml generated Normal file
View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
<excludeFolder url="file://$MODULE_DIR$/temp" />
<excludeFolder url="file://$MODULE_DIR$/tmp" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

36
README.md Normal file
View file

@ -0,0 +1,36 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.

11
auth.ts Normal file
View file

@ -0,0 +1,11 @@
import NextAuth from "next-auth"
import { PrismaAdapter } from "@auth/prisma-adapter"
import { prisma } from "@/lib/prisma"
import GitHub from "next-auth/providers/github";
export const { handlers, auth, signIn, signOut } = NextAuth({
adapter: PrismaAdapter(prisma) as any,
providers: [
GitHub
],
})

16
eslint.config.mjs Normal file
View file

@ -0,0 +1,16 @@
import { dirname } from "path";
import { fileURLToPath } from "url";
import { FlatCompat } from "@eslint/eslintrc";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
});
const eslintConfig = [
...compat.extends("next/core-web-vitals", "next/typescript"),
];
export default eslintConfig;

130
flake.lock generated Normal file
View file

@ -0,0 +1,130 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"flake-utils_2": {
"inputs": {
"systems": "systems_2"
},
"locked": {
"lastModified": 1710146030,
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"pkgs": {
"locked": {
"lastModified": 1735915915,
"narHash": "sha256-Q4HuFAvoKAIiTRZTUxJ0ZXeTC7lLfC9/dggGHNXNlCw=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "a27871180d30ebee8aa6b11bf7fef8a52f024733",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"pkgs_2": {
"locked": {
"lastModified": 1718870667,
"narHash": "sha256-jab3Kpc8O1z3qxwVsCMHL4+18n5Wy/HHKyu1fcsF7gs=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "9b10b8f00cb5494795e5f51b39210fed4d2b0748",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"prisma-utils": {
"inputs": {
"flake-utils": "flake-utils_2",
"pkgs": "pkgs_2"
},
"locked": {
"lastModified": 1734558246,
"narHash": "sha256-XX7mKzz6yKEfB06OgqJ3ml8ST/IzQklfsR2/o0Pksgc=",
"owner": "VanCoding",
"repo": "nix-prisma-utils",
"rev": "f21d11cec47b1747ed44d912682e8f8edcc7d859",
"type": "github"
},
"original": {
"owner": "VanCoding",
"repo": "nix-prisma-utils",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"pkgs": "pkgs",
"prisma-utils": "prisma-utils"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"systems_2": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

29
flake.nix Normal file
View file

@ -0,0 +1,29 @@
{
inputs.pkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
inputs.prisma-utils.url = "github:VanCoding/nix-prisma-utils";
inputs.flake-utils.url = "github:numtide/flake-utils";
outputs =
{ pkgs, prisma-utils, flake-utils, ... }:
flake-utils.lib.eachDefaultSystem (system: let
nixpkgs = pkgs.legacyPackages.${system};
prisma =
(prisma-utils.lib.prisma-factory {
inherit nixpkgs;
prisma-fmt-hash = "sha256-1E0RSlpFrY4UroZjOug8tr28sV+1TUgts4508hm9WDQ="; # just copy these hashes for now, and then change them when nix complains about the mismatch
query-engine-hash = "sha256-ABmemJAwf2fM/2UkvFm56CJ29YBo7grAFFRs1R2O5qY=";
libquery-engine-hash = "sha256-S4byVCgcb7aT0yxuK+jiYu+FeJTPbUJeOeLJdUpCCeU=";
schema-engine-hash = "sha256-Fbi7bOjoN9uNnb7bF3CSVrHlIqrQHvXO70jZslVe4K0=";
}).fromNpmLock
./package-lock.json; # <--- path to our package-lock.json file that contains the version of prisma-engines
in
{
devShell = nixpkgs.mkShell {
buildInputs = with nixpkgs; [
nodejs_22
nodePackages.prisma
];
shellHook = prisma.shellHook;
};
});
}

1
middleware.ts Normal file
View file

@ -0,0 +1 @@
export { auth as middleware } from "./auth"

7
next.config.ts Normal file
View file

@ -0,0 +1,7 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
};
export default nextConfig;

7458
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

35
package.json Normal file
View file

@ -0,0 +1,35 @@
{
"name": "felidae",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev --turbopack",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@apollo/server": "^4.11.3",
"@as-integrations/next": "^3.2.0",
"@auth/prisma-adapter": "^2.7.4",
"@prisma/client": "^6.1.0",
"@types/animejs": "^3.1.12",
"animejs": "^3.2.2",
"next": "15.1.3",
"next-auth": "^5.0.0-beta.25",
"prisma": "^6.1.0",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "15.1.3",
"postcss": "^8",
"tailwindcss": "^3.4.1",
"typescript": "^5"
}
}

8
postcss.config.mjs Normal file
View file

@ -0,0 +1,8 @@
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
tailwindcss: {},
},
};
export default config;

BIN
prisma/dev.db Normal file

Binary file not shown.

BIN
prisma/dev.db-journal Normal file

Binary file not shown.

View file

@ -0,0 +1,31 @@
-- CreateEnum
CREATE TYPE "Role" AS ENUM ('USER', 'ADMIN');
-- CreateTable
CREATE TABLE "User" (
"id" SERIAL NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"email" TEXT NOT NULL,
"name" TEXT,
"role" "Role" NOT NULL DEFAULT 'USER',
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Post" (
"id" SERIAL NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"published" BOOLEAN NOT NULL DEFAULT false,
"title" VARCHAR(255) NOT NULL,
"authorId" INTEGER,
CONSTRAINT "Post_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
-- AddForeignKey
ALTER TABLE "Post" ADD CONSTRAINT "Post_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;

View file

@ -0,0 +1,82 @@
/*
Warnings:
- You are about to drop the `Post` table. If the table is not empty, all the data it contains will be lost.
- You are about to drop the `User` table. If the table is not empty, all the data it contains will be lost.
*/
-- DropForeignKey
ALTER TABLE "Post" DROP CONSTRAINT "Post_authorId_fkey";
-- DropTable
DROP TABLE "Post";
-- DropTable
DROP TABLE "User";
-- DropEnum
DROP TYPE "Role";
-- CreateTable
CREATE TABLE "accounts" (
"id" TEXT NOT NULL,
"user_id" TEXT NOT NULL,
"type" TEXT NOT NULL,
"provider" TEXT NOT NULL,
"provider_account_id" TEXT NOT NULL,
"refresh_token" TEXT,
"access_token" TEXT,
"expires_at" INTEGER,
"token_type" TEXT,
"scope" TEXT,
"id_token" TEXT,
"session_state" TEXT,
CONSTRAINT "accounts_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "sessions" (
"id" TEXT NOT NULL,
"session_token" TEXT NOT NULL,
"user_id" TEXT NOT NULL,
"expires" TIMESTAMP(3) NOT NULL,
CONSTRAINT "sessions_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "users" (
"id" TEXT NOT NULL,
"name" TEXT,
"email" TEXT,
"email_verified" TIMESTAMP(3),
"image" TEXT,
CONSTRAINT "users_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "verification_tokens" (
"identifier" TEXT NOT NULL,
"token" TEXT NOT NULL,
"expires" TIMESTAMP(3) NOT NULL
);
-- CreateIndex
CREATE UNIQUE INDEX "accounts_provider_provider_account_id_key" ON "accounts"("provider", "provider_account_id");
-- CreateIndex
CREATE UNIQUE INDEX "sessions_session_token_key" ON "sessions"("session_token");
-- CreateIndex
CREATE UNIQUE INDEX "users_email_key" ON "users"("email");
-- CreateIndex
CREATE UNIQUE INDEX "verification_tokens_identifier_token_key" ON "verification_tokens"("identifier", "token");
-- AddForeignKey
ALTER TABLE "accounts" ADD CONSTRAINT "accounts_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "sessions" ADD CONSTRAINT "sessions_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View file

@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
provider = "postgresql"

61
prisma/schema.prisma Normal file
View file

@ -0,0 +1,61 @@
// schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model Account {
id String @id @default(cuid())
userId String @map("user_id")
type String
provider String
providerAccountId String @map("provider_account_id")
refresh_token String? @db.Text
access_token String? @db.Text
expires_at Int?
token_type String?
scope String?
id_token String? @db.Text
session_state String?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([provider, providerAccountId])
@@map("accounts")
}
model Session {
id String @id @default(cuid())
sessionToken String @unique @map("session_token")
userId String @map("user_id")
expires DateTime
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@map("sessions")
}
model User {
id String @id @default(cuid())
name String?
email String? @unique
emailVerified DateTime? @map("email_verified")
image String?
accounts Account[]
sessions Session[]
@@map("users")
}
model VerificationToken {
identifier String
token String
expires DateTime
@@unique([identifier, token])
@@map("verification_tokens")
}

Binary file not shown.

BIN
public/cat.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

1
public/file.svg Normal file
View file

@ -0,0 +1 @@
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 391 B

1
public/globe.svg Normal file
View file

@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 1 KiB

1
public/next.svg Normal file
View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

1
public/vercel.svg Normal file
View file

@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 128 B

1
public/window.svg Normal file
View file

@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>

After

Width:  |  Height:  |  Size: 385 B

View file

@ -0,0 +1,2 @@
import { handlers } from "../../../auth";
export const { GET, POST } = handlers

View file

@ -0,0 +1,63 @@
import { startServerAndCreateNextHandler } from "@as-integrations/next";
import { ApolloServer } from "@apollo/server";
import { NextRequest } from "next/server";
import { gql } from "graphql-tag";
import { prisma } from "@/lib/prisma";
const typeDefs = gql`
type Query {
users: [User],
prisma_okay: String
}
type User {
id: ID!
name: String
email: String
}
`;
const resolvers = {
Query: {
users: async () => {
const db_users = await prisma.user.findMany();
if (!db_users) return [];
const map = db_users.map((user) => {
return {
id: user.id,
name: user.name,
email: user.email,
};
});
return map;
},
prisma_okay: async () => {
try {
await prisma.$connect();
return "Prisma is okay";
} catch (error) {
return "Prisma is not okay: " + error;
}
},
},
};
const server = new ApolloServer({
typeDefs,
resolvers,
});
const handler = startServerAndCreateNextHandler<NextRequest>(server, {
context: async (req, res) => ({
req,
res,
dataSources: {},
}),
});
export async function GET(request: NextRequest) {
return handler(request);
}
export async function POST(request: NextRequest) {
return handler(request);
}

BIN
src/app/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

50
src/app/globals.css Normal file
View file

@ -0,0 +1,50 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
--background: #ffffff;
--foreground: #171717;
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
}
body {
color: var(--foreground);
background: var(--background);
font-family: Arial, Helvetica, sans-serif;
}
/* Import the WOFF IBM VGA font */
@font-face {
font-family: 'IBM VGA 8x16';
src: url('/Web437_IBM_VGA_8x16.woff') format('woff');
font-weight: normal;
font-style: normal;
}
.vga-font {
font-family: 'IBM VGA 8x16', monospace;
}
/* Make sure to inherit the text color for the glow color */
.glow {
text-shadow: 0 0 10px var(--foreground);
}
.contrast {
filter: invert(1);
}
.hue-rotate {
filter: hue-rotate(180deg);
}
.grayscale {
filter: grayscale(1);
}

128
src/app/layout.tsx Normal file
View file

@ -0,0 +1,128 @@
"use client";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import IntroAnim from "@/components/intro_anim";
import React from "react";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
const [showIntro, setShowIntro] = React.useState(true);
// Increment this value by deltatime - If it exceeds 0.2 seconds, negate the check
const [checkExistsTime, setCheckExistsTime] = React.useState(0);
// Less is sharper, more is blurrier
const [focus, setFocus] = React.useState(1);
const [darkness, setDarkness] = React.useState(1);
const [lastFocusTime, setLastFocusTime] = React.useState(Date.now());
const [channel] = React.useState(new BroadcastChannel("felidae"));
// Skip the intro animation if the site is already open on another tab
React.useEffect(() => {
channel.onmessage = (event) => {
if (event.data === "ping" && checkExistsTime > 200) {
// Broadcast across ALL tabs
const channel = new BroadcastChannel("felidae");
channel.postMessage("pong");
console.log("Received ping message");
} else if (event.data === "pong" && checkExistsTime < 200) {
setShowIntro(false);
console.log("Received pong message");
}
};
});
// Initial ping to check if the site is already open on another tab
React.useEffect(() => {
channel.postMessage("ping");
});
// If the user has Reduced Motion enabled, skip the intro animation
React.useEffect(() => {
const mediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
if (mediaQuery.matches) {
setShowIntro(false);
setFocus(0);
setDarkness(1);
}
}, []);
// When showIntro is false, reduce pixelation towards zero by a factor of 0.1 every tenth of a second.
React.useEffect(() => {
if (!showIntro) {
setLastFocusTime(Date.now());
const interval = setInterval(() => {
const timeSinceLastFocus = Date.now() - lastFocusTime;
const amount = focus - timeSinceLastFocus * 0.0003 < 0 ? 0 : focus - timeSinceLastFocus * 0.0003;
setFocus(amount);
setDarkness(1 - amount);
setLastFocusTime(Date.now());
if (focus <= 0) {
clearInterval(interval);
}
}, 30);
return () => clearInterval(interval);
} else {
// Increment the checkExistsTime by the time since the last frame using an interval
setLastFocusTime(Date.now());
const interval = setInterval(() => {
const timeSinceLastFocus = Date.now() - lastFocusTime;
setCheckExistsTime(checkExistsTime + timeSinceLastFocus);
setLastFocusTime(Date.now());
if (checkExistsTime > 200) {
clearInterval(interval);
}
}, 30);
return () => clearInterval(interval);
}
}, [showIntro]);
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
{showIntro && <IntroAnim onFinish={() => setShowIntro(false)} />}
{!showIntro && <div>
{focus > 0 && <div>
<svg
xmlns="http://www.w3.org/2000/svg"
version="1.1"
width="0"
height="0"
>
<defs>
<filter id="blur">
<feGaussianBlur in="SourceGraphic" stdDeviation={focus * 3} />
</filter>
<filter id="darken">
<feComponentTransfer>
<feFuncA type="linear" slope={darkness} />
</feComponentTransfer>
</filter>
</defs>
</svg>
<div style={{ filter: "url(#blur) url(#darken)" }}>
{children}
</div>
</div>}
{focus <= 0 && children}
</div>}
</body>
</html>
);
}

101
src/app/page.tsx Normal file
View file

@ -0,0 +1,101 @@
import Image from "next/image";
export default function Home() {
return (
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
<main className="flex flex-col gap-8 row-start-2 items-center sm:items-start">
<Image
className="dark:invert"
src="/next.svg"
alt="Next.js logo"
width={180}
height={38}
priority
/>
<ol className="list-inside list-decimal text-sm text-center sm:text-left font-[family-name:var(--font-geist-mono)]">
<li className="mb-2">
Get started by editing{" "}
<code className="bg-black/[.05] dark:bg-white/[.06] px-1 py-0.5 rounded font-semibold">
src/app/page.tsx
</code>
.
</li>
<li>Save and see your changes instantly.</li>
</ol>
<div className="flex gap-4 items-center flex-col sm:flex-row">
<a
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
className="dark:invert"
src="/vercel.svg"
alt="Vercel logomark"
width={20}
height={20}
/>
Deploy now
</a>
<a
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:min-w-44"
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Read our docs
</a>
</div>
</main>
<footer className="row-start-3 flex gap-6 flex-wrap items-center justify-center">
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/file.svg"
alt="File icon"
width={16}
height={16}
/>
Learn
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/window.svg"
alt="Window icon"
width={16}
height={16}
/>
Examples
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/globe.svg"
alt="Globe icon"
width={16}
height={16}
/>
Go to nextjs.org
</a>
</footer>
</div>
);
}

View file

@ -0,0 +1,10 @@
import Image from "next/image";
export default function Page() {
return (
<div className="vga-font">
<p>Style Testing</p>
<Image src="/cat.jpg" width={500} height={500} alt="cat"></Image>
</div>
);
}

73
src/components/anime.tsx Normal file
View file

@ -0,0 +1,73 @@
"use client";
// Utility component for executing animations
import anime, { AnimeParams } from "animejs";
import React from "react";
interface IProps {
params: AnimeParams;
children: React.ReactNode;
}
interface IState {
scopeState: string;
}
export default class Anime extends React.Component<IProps, IState> {
private anime: anime.AnimeInstance | null;
constructor(props: IProps) {
super(props);
this.state = {
scopeState: "",
};
this.anime = null;
}
private applyDescendantSelector(selector: string): string {
console.log("Applying descendant selector to: " + selector);
return selector.split(",").map((s) => {
return "." + this.state.scopeState + ' ' + s;
}).join(", ");
}
componentDidMount(): void {
const scope = "anime-scope-" + Math.random().toString(36).substring(4);
this.setState({
scopeState: scope,
}, () => {
console.log("Anime scope state: " + this.state.scopeState);
const postTargets = '.' + this.state.scopeState;
console.log("Targets: " + postTargets);
// Update all params targets CSS selectors to include a descendant selector
if (this.props.params.targets !== undefined) {
// Identify if is single or is array (type AnimeTarget)
this.props.params.targets = this.applyDescendantSelector(this.props.params.targets?.toString() as string);
console.log("Updated targets: " + this.props.params.targets);
} else {
this.props.params.targets = postTargets;
}
this.anime = anime({
...this.props.params,
});
});
}
render() {
return (
<div>
<style>
{`.${this.state.scopeState} {}`}
</style>
<div className={this.state.scopeState}>
{this.props.children}
</div>
</div>
)
}
}

8
src/components/card.tsx Normal file
View file

@ -0,0 +1,8 @@
export default function Card(props: { children: React.ReactNode }) {
return (
<div className="p-4 bg-white dark:bg-black/[.5] rounded-lg shadow-md">
{props.children}
</div>
)
}

View file

@ -0,0 +1,94 @@
import anime from "animejs";
import Anime from "./anime";
import React from "react";
enum StatusSeverity {
OK = "OK",
WARNING = "WARN",
ERROR = "ERR",
}
interface Status {
severity: StatusSeverity;
message: string;
}
interface Diagnostic {
name: string;
status: Status;
get_status: () => Status;
}
const default_diagnostics: Diagnostic[] = [
{
name: "Example Diagnostic",
status: {
severity: StatusSeverity.OK,
message: "",
},
get_status: () => {
return {
severity: StatusSeverity.OK,
message: "This is an example diagnostic message",
}
}
}
];
export default function Diagnostics() {
const [diagnosticResults, setDiagnosticResults]: [Diagnostic[], React.Dispatch<React.SetStateAction<Diagnostic[]>>]
= React.useState(default_diagnostics);
const [diagnostics]: [Diagnostic[], React.Dispatch<React.SetStateAction<Diagnostic[]>>]
= React.useState(default_diagnostics);
React.useEffect(() => {
// Ensure diagnostics is always an array before trying to map
if (Array.isArray(diagnostics)) {
console.log("Diagnostics: ", diagnostics);
const new_diagnostics = diagnostics.map((d) => {
return {
name: d.name,
status: d.get_status(),
get_status: d.get_status,
}
});
// Update the diagnostics state with the new values
setDiagnosticResults(new_diagnostics);
}
}, [diagnostics]); // This will now only rerun when the state actually changes
return (
<div>
<Anime params={{
targets: ".text-stagger",
opacity: [0, 1],
duration: 1000,
delay: anime.stagger(300),
}}>
<ul>
{
diagnosticResults.map((d) => {
return (
<div key={d.name} className="text-stagger">
<h2>{d.name}</h2>
{/* SystemD style */}
<p>
[ <span style={{
color: d.status.severity === StatusSeverity.OK
? "lime" : d.status.severity === StatusSeverity.WARNING
? "yellow" : "red"
}}>
{d.status.severity}
</span> ] {d.status.message}
</p>
</div>
)
})
}
</ul>
</Anime>
</div>
)
}

View file

@ -0,0 +1,326 @@
"use client";
import React from "react";
class TextNode {
private time_elapsed: number = 0;
private last_time: number = Date.now();
public duration: number;
private finished_text: string;
private loading_text: string;
public active: boolean = false;
private status: "OK" | "WARN" | "FAIL" = "OK";
public decorational: boolean = false;
constructor(
duration: number,
loading_text: string,
finished_text: string | undefined = undefined,
decorational: boolean =false
) {
this.duration = duration;
this.loading_text = loading_text;
this.finished_text = finished_text ?? loading_text;
this.decorational = decorational;
}
public finish_now() {
this.time_elapsed = this.duration;
}
public get_text() {
return this.is_finished() ? this.finished_text : this.loading_text
}
public tick() {
if (this.is_finished()) {
return;
}
const now = Date.now();
const delta = now - this.last_time;
this.time_elapsed += delta;
this.last_time = now;
}
public is_finished(): boolean {
return this.time_elapsed >= this.duration;
}
public start() {
this.active = true;
this.last_time = Date.now();
}
public get_status_decorator() {
if (this.decorational) {
return "";
}
// [ ] (empty)
// (looping) [*** ] [**** ] [ **** ] [ ****] [ ***] (and 0.3 delay before looping, 0.2 delay interframe)
// [ OK ]
// [ WARN ]
// [ FAIL ]
// Use colors for the text, but not brackets - brackets stay white.
// Loading dots are orange, and the leftmost and rightmost are yellow
switch (this.is_finished()) {
case true:
switch (this.status) {
case "OK":
return (
<span>[ &nbsp;<span className="text-green-500">OK</span> &nbsp;]</span>
);
case "WARN":
return (
<span>[ <span className="text-yellow-500">WARN</span> ]</span>
);
case "FAIL":
return (
<span>[ <span className="text-red-500">FAIL</span> ]</span>
);
}
case false:
// Run the [*** ] [**** ] [ **** ] [ ****] [ ***] animation
const seconds = this.time_elapsed / 100;
// Divisible into 5 phases each second
let phase = Math.floor(seconds % 7) - 1;
if (phase === -1) {
phase = 0;
}
if (phase === 5) {
phase = 4;
}
const half = Math.floor(seconds % 14);
// Flip the phase if it's in the second half
if (half >= 7) {
phase = 4 - phase;
}
let spaces_left = 0;
let spaces_right = 0;
let dots = 0;
switch (phase) {
case 0:
spaces_left = 0;
spaces_right = 3;
dots = 3;
break;
case 1:
spaces_left = 0;
spaces_right = 2;
dots = 4;
break;
case 2:
spaces_left = 1;
spaces_right = 1;
dots = 4;
break;
case 3:
spaces_left = 2;
spaces_right = 0;
dots = 4;
break;
case 4:
spaces_left = 3;
spaces_right = 0;
dots = 3;
break;
}
return (
<span>
[<span>{"\xa0".repeat(spaces_left)}</span>
{<span className="text-orange-500">{"*".repeat(dots)}</span>}
<span>{"\xa0".repeat(spaces_right)}</span>]
</span>
);
}
}
}
// Decoration "loading" messages (fill with random things to do that aren't actually loading, etc.)
// Style it like it's an Init System (E.g. SystemD, OpenRC, etc. It isn't, but it looks like it)
const textNodes: TextNode[] = [
new TextNode(1000, `
\xa0___\xa0\xa0___\xa0\xa0\xa0\xa0\xa0\xa0\xa0\xa0\xa0__\xa0\xa0\xa0\xa0\xa0\xa0\xa0\xa0___\xa0
|__\xa0\xa0|__\xa0\xa0|\xa0\xa0\xa0\xa0|\xa0|\xa0\xa0\\\xa0\xa0/\\\xa0\xa0|__\xa0\xa0
|\xa0\xa0\xa0\xa0|___\xa0|___\xa0|\xa0|__/\xa0/~~\\\xa0|___\xa0
\xa0\xa0\xa0\xa0\xa0\xa0\xa0\xa0\xa0\xa0\xa0\xa0\xa0\xa0\xa0\xa0\xa0\xa0\xa0\xa0\xa0\xa0\xa0\xa0\xa0\xa0\xa0\xa0\xa0\xa0\xa0\xa0
`, undefined, true),
new TextNode(2000, "Starting services", "Started services"),
new TextNode(1000, "Checking for updates", "No updates found"),
// Begin the jokes here.
];
// These are comic ones.
// No jokes about destructive commands, etc.
const randomTextNodes: TextNode[] = [
new TextNode(1000, "Loading the loading screen", "Loaded the loading screen"),
new TextNode(500, "Checking for updates", "Found updates, but they're not important"),
// Math
new TextNode(500, "Counting past infinity", "Counted past infinity"),
new TextNode(500, "Solving P vs NP", "Solved P vs NP"),
new TextNode(500, "Finding the last digit of pi", "Found the last digit of pi"),
new TextNode(500, "Dividing by zero", "Divided by zero"),
// Science
new TextNode(500, "Creating a black hole", "Created a black hole"),
new TextNode(500, "Finding the Higgs Boson", "Found the Higgs Boson"),
new TextNode(500, "Creating a new element", "Created a new element"),
new TextNode(500, "Creating a new universe", "Created a new universe"),
// Computer Science
new TextNode(500, "Rewriting it in Rust", "Rewrote it in Rust"),
new TextNode(1000, "Generating a random number between " + Math.floor(Math.random() * 100000) + " and " + Math.floor(Math.random() * 100000)),
new TextNode(500, "Downloading more RAM", "Downloaded more RAM"),
new TextNode(500, "Compiling the kernel", "Compiled the kernel"),
new TextNode(500, "Running Arch Linux", "Ran Arch Linux (btw)"),
new TextNode(500, "Compiling Gentoo", "Compiled Gentoo"),
new TextNode(500, "Running Windows Update", "Ran Windows Update"),
// - Programming
new TextNode(500, "Writing a new programming language", "Wrote a new programming language"),
// Mundane
new TextNode(500, "Finding the meaning of life", "Found the meaning of life"),
new TextNode(500, "Making a cup of tea", "Made a cup of tea"),
new TextNode(500, "Taking a break", "Took a break"),
new TextNode(500, "Doing something else", "Did something else"),
new TextNode(500, "Changing the world", "Changed the world"),
new TextNode(500, "Making a sandwich", "Made a sandwich"),
new TextNode(500, "Doing absolutely nothing", "Did absolutely nothing"),
// Occult
new TextNode(500, "Summoning Cthulhu", "Summoned Cthulhu"),
new TextNode(500, "Summoning Satan himself", "Summoned Satan himself"),
new TextNode(500, "Summoning a demon", "Summoned a demon"),
// LGBTQ+
new TextNode(500, "Supporting LGBTQ+ rights", "Supported LGBTQ+ rights"),
new TextNode(500, "Respecting pronouns", "Respected pronouns"),
// Game references
new TextNode(500, "Running the Konami Code", "Ran the Konami Code"),
new TextNode(500, "Running the Doom cheat codes", "Ran the Doom cheat codes"),
new TextNode(500, "Teleporting bread", "Teleported bread"),
new TextNode(500, "Building a sentry", "Built a sentry"),
new TextNode(500, "Building a dispenser", "Built a dispenser"),
new TextNode(500, "Building a teleporter", "Built a teleporter"),
new TextNode(500, "Spy checking", "Checked for spies"),
new TextNode(500, "Rocket jumping", "Jumped with rockets"),
new TextNode(500, "Demoknighting", "Demoknighted"),
new TextNode(500, "Waiting for random crits", "Got random crits"),
// Pop culture references
new TextNode(500, "Finding the Holy Grail", "Found the Holy Grail"),
new TextNode(500, "Finding the One Ring", "Found the One Ring"),
new TextNode(500, "Finding the Death Star plans", "Found the Death Star plans"),
new TextNode(500, "Finding the Infinity Stones", "Found the Infinity Stones"),
new TextNode(500, "Finding the Ark of the Covenant", "Found the Ark of the Covenant"),
new TextNode(500, "Finding the Philosopher's Stone", "Found the Philosopher's Stone"),
// References towards franchises
new TextNode(500, "Containing SCP-173", "Contained SCP-173"),
// Joke programs that do weird, though not destructive or harmful things
new TextNode(5000, "Running cowsay", "Ran cowsa- Moo."),
];
const postNodes = [
new TextNode(10000, "Entering site now", "Entered site"),
];
// Randomly shuffle randomTextNodes into textNodes, but leave the elements already in textNodes in place
// Only pop in ~50% of the randomTextNodes so as to have some variety
const randomTextNodesLength = randomTextNodes.length / 2;
// Use to avoid duplicates
const pushedRandomTextNodes: Array<number> = [];
for (let i = 0; i < randomTextNodesLength; i++) {
let index = Math.floor(Math.random() * randomTextNodes.length);
while (pushedRandomTextNodes.includes(index)) {
index = Math.floor(Math.random() * randomTextNodes.length);
}
pushedRandomTextNodes.push(index);
const duration = randomTextNodes[index].duration * Math.random();
randomTextNodes[index].duration = duration;
textNodes.push(randomTextNodes[index]);
}
for (let i = 0; i < postNodes.length; i++) {
textNodes.push(postNodes[i]);
}
const speedMultiplier = 0.1;
// Multiply the duration of each textNode by the speedMultiplier
for (let i = 0; i < textNodes.length; i++) {
textNodes[i].duration *= speedMultiplier;
}
export default function IntroAnim(props: { onFinish: () => void }) {
// Run the textNodes tick function every frame for each textNode
const [textNodesState, setTextNodesState] = React.useState(textNodes);
React.useEffect(() => {
// Ensure only one interval is running (textNode.active)
const interval = setInterval(() => {
// Mark the last textNode active if
// - it's not active
// - it's not finished
// - the previous textNode is finished
for (let i = 0; i < textNodesState.length; i++) {
const t = textNodesState[i];
if (!t.active && !t.is_finished() && (i === 0 || textNodesState[i - 1].is_finished())) {
t.start();
break;
}
}
// Tick all active textNodes
for (let i = 0; i < textNodesState.length; i++) {
const t = textNodesState[i];
if (t.active) {
t.tick();
}
}
if (textNodesState[textNodesState.length - 1].is_finished()) {
props.onFinish();
}
// Update the state
setTextNodesState([...textNodesState]);
}, 16);
return () => clearInterval(interval);
});
return (
<div className="vga-font">
<div className="text-left">
{
textNodesState.filter(t => t.active || t.is_finished()).map((t) => (
<div style={
{
lineHeight: "1",
whiteSpace: 'pre-line'
}
} key={t.get_text()}>
{t.get_status_decorator()}
<span> </span>
{t.get_text()}
</div>
))
}
</div>
</div>
);
}

View file

@ -0,0 +1,6 @@
export default function Navbar() {
return (
<nav>
</nav>
)
}

7
src/lib/prisma.ts Normal file
View file

@ -0,0 +1,7 @@
import { PrismaClient } from "@prisma/client"
const globalForPrisma = globalThis as unknown as { prisma: PrismaClient }
export const prisma = globalForPrisma.prisma || new PrismaClient()
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma

24
tailwind.config.ts Normal file
View file

@ -0,0 +1,24 @@
import type { Config } from "tailwindcss";
export default {
content: [
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
],
theme: {
extend: {
colors: {
background: "var(--background)",
foreground: "var(--foreground)",
},
dropShadow: {
glow: [
"0 0px 20px rgba(255,255, 255, 0.35)",
"0 0px 65px rgba(255, 255,255, 0.2)"
]
}
},
},
plugins: [],
} satisfies Config;

27
tsconfig.json Normal file
View file

@ -0,0 +1,27 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}