Compare commits

11 Commits
stats ... main

Author SHA1 Message Date
172a5a3d5b fix version for prisma generate
All checks were successful
Build / build (push) Successful in 14s
2025-12-10 21:09:54 +01:00
1ca305a73b fix version for prisma generate
All checks were successful
Build / build (push) Successful in 2m25s
2025-12-10 21:03:21 +01:00
932ea8d58c update next.js
Some checks failed
Build / build (push) Failing after 1m24s
2025-12-10 19:08:00 +01:00
614adcb9f8 use UK date format
All checks were successful
Build / build (push) Successful in 3m5s
2025-04-22 13:25:28 +02:00
a5b8e40761 wording
All checks were successful
Build / build (push) Successful in 3m9s
2025-04-22 12:56:20 +02:00
e56bb8ac1c fx
Some checks failed
Build / build (push) Has been cancelled
2025-04-22 12:55:10 +02:00
d9e9824146 cleanup tags on edit
All checks were successful
Build / build (push) Successful in 3m11s
2025-04-22 12:48:23 +02:00
8b46bf354e ensure login for clipboard
All checks were successful
Build / build (push) Successful in 4m33s
2025-04-22 12:22:06 +02:00
86ec00de4c fix
All checks were successful
Build / build (push) Successful in 10s
2025-04-08 00:40:18 +02:00
49917e1eb0 deploy db
All checks were successful
Build / build (push) Successful in 16s
2025-04-08 00:36:11 +02:00
edc575b153 add clipboard
All checks were successful
Build / build (push) Successful in 3m28s
2025-04-08 00:23:36 +02:00
14 changed files with 175 additions and 52 deletions

View File

@@ -6,7 +6,7 @@ COPY package.json package-lock.json ./
RUN npm install RUN npm install
COPY ./prisma ./prisma COPY ./prisma ./prisma
RUN npx prisma generate RUN npx prisma@6.5.0 generate
COPY ./next.config.ts ./tsconfig.json ./eslint.config.mjs ./ COPY ./next.config.ts ./tsconfig.json ./eslint.config.mjs ./
COPY ./src ./src COPY ./src ./src
@@ -16,6 +16,7 @@ RUN cp -r .next/static .next/standalone/.next/
FROM node:current-alpine AS production FROM node:current-alpine AS production
COPY --from=build /app/.next/standalone /app COPY --from=build /app/.next/standalone /app
COPY --from=build /app/prisma /app/prisma
EXPOSE 3000 EXPOSE 3000
WORKDIR /app WORKDIR /app
CMD ["node", "server.js"] CMD ["/bin/sh", "-c", "npx --yes prisma@6.5.0 migrate deploy && node server.js"]

80
package-lock.json generated
View File

@@ -14,7 +14,7 @@
"cheerio": "^1.0.0", "cheerio": "^1.0.0",
"highlight.js": "^11.11.1", "highlight.js": "^11.11.1",
"markdown-it": "^14.1.0", "markdown-it": "^14.1.0",
"next": "15.2.4", "next": "^15.2.6",
"next-auth": "^5.0.0-beta.25", "next-auth": "^5.0.0-beta.25",
"nodemailer": "^6.10.0", "nodemailer": "^6.10.0",
"react": "^19.0.0", "react": "^19.0.0",
@@ -710,9 +710,9 @@
} }
}, },
"node_modules/@next/env": { "node_modules/@next/env": {
"version": "15.2.4", "version": "15.2.6",
"resolved": "https://registry.npmjs.org/@next/env/-/env-15.2.4.tgz", "resolved": "https://registry.npmjs.org/@next/env/-/env-15.2.6.tgz",
"integrity": "sha512-+SFtMgoiYP3WoSswuNmxJOCwi06TdWE733D+WPjpXIe4LXGULwEaofiiAy6kbS0+XjM5xF5n3lKuBwN2SnqD9g==", "integrity": "sha512-kp1Mpm4K1IzSSJ5ZALfek0JBD2jBw9VGMXR/aT7ykcA2q/ieDARyBzg+e8J1TkeIb5AFj/YjtZdoajdy5uNy6w==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@next/eslint-plugin-next": { "node_modules/@next/eslint-plugin-next": {
@@ -726,9 +726,9 @@
} }
}, },
"node_modules/@next/swc-darwin-arm64": { "node_modules/@next/swc-darwin-arm64": {
"version": "15.2.4", "version": "15.2.5",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.2.4.tgz", "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.2.5.tgz",
"integrity": "sha512-1AnMfs655ipJEDC/FHkSr0r3lXBgpqKo4K1kiwfUf3iE68rDFXZ1TtHdMvf7D0hMItgDZ7Vuq3JgNMbt/+3bYw==", "integrity": "sha512-4OimvVlFTbgzPdA0kh8A1ih6FN9pQkL4nPXGqemEYgk+e7eQhsst/p35siNNqA49eQA6bvKZ1ASsDtu9gtXuog==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -742,9 +742,9 @@
} }
}, },
"node_modules/@next/swc-darwin-x64": { "node_modules/@next/swc-darwin-x64": {
"version": "15.2.4", "version": "15.2.5",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.2.4.tgz", "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.2.5.tgz",
"integrity": "sha512-3qK2zb5EwCwxnO2HeO+TRqCubeI/NgCe+kL5dTJlPldV/uwCnUgC7VbEzgmxbfrkbjehL4H9BPztWOEtsoMwew==", "integrity": "sha512-ohzRaE9YbGt1ctE0um+UGYIDkkOxHV44kEcHzLqQigoRLaiMtZzGrA11AJh2Lu0lv51XeiY1ZkUvkThjkVNBMA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -758,9 +758,9 @@
} }
}, },
"node_modules/@next/swc-linux-arm64-gnu": { "node_modules/@next/swc-linux-arm64-gnu": {
"version": "15.2.4", "version": "15.2.5",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.2.4.tgz", "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.2.5.tgz",
"integrity": "sha512-HFN6GKUcrTWvem8AZN7tT95zPb0GUGv9v0d0iyuTb303vbXkkbHDp/DxufB04jNVD+IN9yHy7y/6Mqq0h0YVaQ==", "integrity": "sha512-FMSdxSUt5bVXqqOoZCc/Seg4LQep9w/fXTazr/EkpXW2Eu4IFI9FD7zBDlID8TJIybmvKk7mhd9s+2XWxz4flA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -774,9 +774,9 @@
} }
}, },
"node_modules/@next/swc-linux-arm64-musl": { "node_modules/@next/swc-linux-arm64-musl": {
"version": "15.2.4", "version": "15.2.5",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.2.4.tgz", "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.2.5.tgz",
"integrity": "sha512-Oioa0SORWLwi35/kVB8aCk5Uq+5/ZIumMK1kJV+jSdazFm2NzPDztsefzdmzzpx5oGCJ6FkUC7vkaUseNTStNA==", "integrity": "sha512-4ZNKmuEiW5hRKkGp2HWwZ+JrvK4DQLgf8YDaqtZyn7NYdl0cHfatvlnLFSWUayx9yFAUagIgRGRk8pFxS8Qniw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -790,9 +790,9 @@
} }
}, },
"node_modules/@next/swc-linux-x64-gnu": { "node_modules/@next/swc-linux-x64-gnu": {
"version": "15.2.4", "version": "15.2.5",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.2.4.tgz", "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.2.5.tgz",
"integrity": "sha512-yb5WTRaHdkgOqFOZiu6rHV1fAEK0flVpaIN2HB6kxHVSy/dIajWbThS7qON3W9/SNOH2JWkVCyulgGYekMePuw==", "integrity": "sha512-bE6lHQ9GXIf3gCDE53u2pTl99RPZW5V1GLHSRMJ5l/oB/MT+cohu9uwnCK7QUph2xIOu2a6+27kL0REa/kqwZw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -806,9 +806,9 @@
} }
}, },
"node_modules/@next/swc-linux-x64-musl": { "node_modules/@next/swc-linux-x64-musl": {
"version": "15.2.4", "version": "15.2.5",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.2.4.tgz", "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.2.5.tgz",
"integrity": "sha512-Dcdv/ix6srhkM25fgXiyOieFUkz+fOYkHlydWCtB0xMST6X9XYI3yPDKBZt1xuhOytONsIFJFB08xXYsxUwJLw==", "integrity": "sha512-y7EeQuSkQbTAkCEQnJXm1asRUuGSWAchGJ3c+Qtxh8LVjXleZast8Mn/rL7tZOm7o35QeIpIcid6ufG7EVTTcA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -822,9 +822,9 @@
} }
}, },
"node_modules/@next/swc-win32-arm64-msvc": { "node_modules/@next/swc-win32-arm64-msvc": {
"version": "15.2.4", "version": "15.2.5",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.2.4.tgz", "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.2.5.tgz",
"integrity": "sha512-dW0i7eukvDxtIhCYkMrZNQfNicPDExt2jPb9AZPpL7cfyUo7QSNl1DjsHjmmKp6qNAqUESyT8YFl/Aw91cNJJg==", "integrity": "sha512-gQMz0yA8/dskZM2Xyiq2FRShxSrsJNha40Ob/M2n2+JGRrZ0JwTVjLdvtN6vCxuq4ByhOd4a9qEf60hApNR2gQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -838,9 +838,9 @@
} }
}, },
"node_modules/@next/swc-win32-x64-msvc": { "node_modules/@next/swc-win32-x64-msvc": {
"version": "15.2.4", "version": "15.2.5",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.2.4.tgz", "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.2.5.tgz",
"integrity": "sha512-SbnWkJmkS7Xl3kre8SdMF6F/XDh1DTFEhp0jRTj/uB8iPKoU2bb2NDfcu+iifv1+mxQEd1g2vvSxcZbXSKyWiQ==", "integrity": "sha512-tBDNVUcI7U03+3oMvJ11zrtVin5p0NctiuKmTGyaTIEAVj9Q77xukLXGXRnWxKRIIdFG4OTA2rUVGZDYOwgmAA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -4208,12 +4208,12 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/next": { "node_modules/next": {
"version": "15.2.4", "version": "15.2.6",
"resolved": "https://registry.npmjs.org/next/-/next-15.2.4.tgz", "resolved": "https://registry.npmjs.org/next/-/next-15.2.6.tgz",
"integrity": "sha512-VwL+LAaPSxEkd3lU2xWbgEOtrM8oedmyhBqaVNmgKB+GvZlCy9rgaEc+y2on0wv+l0oSFqLtYD6dcC1eAedUaQ==", "integrity": "sha512-DIKFctUpZoCq5ok2ztVU+PqhWsbiqM9xNP7rHL2cAp29NQcmDp7Y6JnBBhHRbFt4bCsCZigj6uh+/Gwh2158Wg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@next/env": "15.2.4", "@next/env": "15.2.6",
"@swc/counter": "0.1.3", "@swc/counter": "0.1.3",
"@swc/helpers": "0.5.15", "@swc/helpers": "0.5.15",
"busboy": "1.6.0", "busboy": "1.6.0",
@@ -4228,14 +4228,14 @@
"node": "^18.18.0 || ^19.8.0 || >= 20.0.0" "node": "^18.18.0 || ^19.8.0 || >= 20.0.0"
}, },
"optionalDependencies": { "optionalDependencies": {
"@next/swc-darwin-arm64": "15.2.4", "@next/swc-darwin-arm64": "15.2.5",
"@next/swc-darwin-x64": "15.2.4", "@next/swc-darwin-x64": "15.2.5",
"@next/swc-linux-arm64-gnu": "15.2.4", "@next/swc-linux-arm64-gnu": "15.2.5",
"@next/swc-linux-arm64-musl": "15.2.4", "@next/swc-linux-arm64-musl": "15.2.5",
"@next/swc-linux-x64-gnu": "15.2.4", "@next/swc-linux-x64-gnu": "15.2.5",
"@next/swc-linux-x64-musl": "15.2.4", "@next/swc-linux-x64-musl": "15.2.5",
"@next/swc-win32-arm64-msvc": "15.2.4", "@next/swc-win32-arm64-msvc": "15.2.5",
"@next/swc-win32-x64-msvc": "15.2.4", "@next/swc-win32-x64-msvc": "15.2.5",
"sharp": "^0.33.5" "sharp": "^0.33.5"
}, },
"peerDependencies": { "peerDependencies": {

View File

@@ -15,7 +15,7 @@
"cheerio": "^1.0.0", "cheerio": "^1.0.0",
"highlight.js": "^11.11.1", "highlight.js": "^11.11.1",
"markdown-it": "^14.1.0", "markdown-it": "^14.1.0",
"next": "15.2.4", "next": "^15.2.6",
"next-auth": "^5.0.0-beta.25", "next-auth": "^5.0.0-beta.25",
"nodemailer": "^6.10.0", "nodemailer": "^6.10.0",
"react": "^19.0.0", "react": "^19.0.0",

View File

@@ -0,0 +1,7 @@
-- CreateTable
CREATE TABLE "Clipboard" (
"id" SERIAL NOT NULL,
"content" TEXT NOT NULL,
CONSTRAINT "Clipboard_pkey" PRIMARY KEY ("id")
);

View File

@@ -30,3 +30,8 @@ model User {
username String @unique username String @unique
password String password String
} }
model Clipboard {
id Int @id @default(autoincrement())
content String @db.Text
}

View File

@@ -114,7 +114,9 @@ export default function PostSummary({
<div> <div>
<div> <div>
<span>published: </span> <span>published: </span>
<span>{metadata.publishedDate.toLocaleDateString()}</span> <span>
{metadata.publishedDate.toLocaleDateString("en-UK")}
</span>
</div> </div>
{loggedIn && ( {loggedIn && (
<div <div

View File

@@ -100,7 +100,9 @@ export default function PostDisplay({
<div> <div>
<div> <div>
<span>published: </span> <span>published: </span>
<span>{post.publishedDate.toLocaleDateString()}</span> <span>
{post.publishedDate.toLocaleDateString("en-UK")}
</span>
</div> </div>
{loggedIn && ( {loggedIn && (
<div <div

View File

@@ -39,7 +39,22 @@ export async function savePostServer(
const slug = slugify(title, { lower: true, strict: true }); const slug = slugify(title, { lower: true, strict: true });
if (existingSlug) { if (existingSlug) {
const post = await prisma.post.findUnique({
where: { slug: existingSlug },
include: { tags: true },
});
if (!post) {
throw new Error("Post not found");
}
await prisma.post.delete({ where: { slug: existingSlug } }); await prisma.post.delete({ where: { slug: existingSlug } });
for (const tag of post.tags) {
const postsWithTag = await prisma.post.count({
where: { tags: { some: { id: tag.id } } },
});
if (postsWithTag == 0) {
await prisma.tag.delete({ where: { id: tag.id } });
}
}
} }
await prisma.post.create({ await prisma.post.create({

View File

@@ -0,0 +1,46 @@
"use client";
import { useState } from "react";
import { updateClipboard } from "./action";
export default function ClipboardComponent({
initialContent,
}: {
initialContent: string;
}) {
const [clipboardContent, setClipboardContent] = useState(initialContent);
const [typingTimeout, setTypingTimeout] = useState<NodeJS.Timeout | null>(
null
);
const contentChanged = (content: string) => {
setClipboardContent(content);
if (typingTimeout) {
clearTimeout(typingTimeout);
}
const timeout = setTimeout(async () => {
await updateClipboard(content);
}, 500);
setTypingTimeout(timeout);
};
return (
<>
<textarea
style={{
width: "100%",
resize: "none",
borderStyle: "none",
backgroundColor: "#333",
height: "20rem",
maxHeight: "80vh",
color: "#eee",
}}
onChange={(e) => contentChanged(e.target.value)}
onBlur={async (e) => {
await updateClipboard(e.target.value);
}}
value={clipboardContent}
></textarea>
</>
);
}

View File

@@ -0,0 +1,19 @@
"use server";
import { PrismaClient } from "@prisma/client";
export async function updateClipboard(content: string) {
const prisma = new PrismaClient();
await prisma.clipboard.upsert({
where: { id: 1 },
update: { content },
create: { content },
});
}
export async function getClipboard(): Promise<string> {
const prisma = new PrismaClient();
const clipboard = await prisma.clipboard.findUnique({
where: { id: 1 },
});
return clipboard?.content || "";
}

View File

@@ -0,0 +1,18 @@
"use server";
import { auth, signIn } from "@/auth";
import { getClipboard } from "./action";
import ClipboardComponent from "./ClipboardComponent";
export default async function ClipboardPage() {
if ((await auth())?.user == null) {
await signIn();
}
const clipboard = await getClipboard();
return (
<>
<ClipboardComponent initialContent={clipboard} />
</>
);
}

View File

@@ -4,17 +4,20 @@ import Title from "@/components/Title";
import Navbar from "@/components/Navbar"; import Navbar from "@/components/Navbar";
import { bodyFont } from "@/components/fonts"; import { bodyFont } from "@/components/fonts";
import Link from "next/link"; import Link from "next/link";
import { auth } from "@/auth";
export const metadata: Metadata = { export const metadata: Metadata = {
title: "nrx.sh", title: "nrx.sh",
description: "naresh's site", description: "naresh's site",
}; };
export default function RootLayout({ export default async function RootLayout({
children, children,
}: Readonly<{ }: Readonly<{
children: React.ReactNode; children: React.ReactNode;
}>) { }>) {
const isLoggedIn = (await auth())?.user != null;
return ( return (
<html lang="en"> <html lang="en">
<body> <body>
@@ -28,7 +31,7 @@ export default function RootLayout({
> >
<Title /> <Title />
<Navbar /> <Navbar isLoggedIn={isLoggedIn} />
<div <div
className={bodyFont.className} className={bodyFont.className}
style={{ style={{
@@ -53,7 +56,7 @@ export default function RootLayout({
}} }}
className={bodyFont.className} className={bodyFont.className}
> >
this site is built from scratch using <b>next.js</b> - it is&nbsp; i built this site from scratch using <b>next.js</b> - it is&nbsp;
<Link <Link
style={{ color: "#88f" }} style={{ color: "#88f" }}
href={"https://git.nrx.sh/naresh/nrx.sh"} href={"https://git.nrx.sh/naresh/nrx.sh"}

View File

@@ -4,6 +4,7 @@ import { useState } from "react";
import React from "react"; import React from "react";
import { navBarFont } from "./fonts"; import { navBarFont } from "./fonts";
import { import {
ADMIN_PAGES,
PAGES, PAGES,
Pages, Pages,
pathNameFromSelectedPage, pathNameFromSelectedPage,
@@ -11,7 +12,7 @@ import {
} from "./pages"; } from "./pages";
import { usePathname, useRouter } from "next/navigation"; import { usePathname, useRouter } from "next/navigation";
export default function Navbar() { export default function Navbar({ isLoggedIn }: { isLoggedIn: boolean }) {
const [hoveredPage, setHoveredPage] = useState<Pages | null>(null); const [hoveredPage, setHoveredPage] = useState<Pages | null>(null);
const pathName = usePathname(); const pathName = usePathname();
@@ -41,7 +42,10 @@ export default function Navbar() {
justifyContent: "center", justifyContent: "center",
}} }}
> >
{PAGES.map((page, index) => ( {PAGES.filter((p) => {
if (!isLoggedIn && ADMIN_PAGES.includes(p)) return false;
return true;
}).map((page, index) => (
<React.Fragment key={page}> <React.Fragment key={page}>
<div <div
style={navbarItem(page)} style={navbarItem(page)}

View File

@@ -1,5 +1,6 @@
export type Pages = "home" | "about" | "links" | "contact" | "blog"; export type Pages = "home" | "about" | "links" | "contact" | "blog" | "clipboard";
export const PAGES: Pages[] = ["home", "about", "blog", "links", "contact"]; export const PAGES: Pages[] = ["home", "about", "blog", "links", "contact", "clipboard"];
export const ADMIN_PAGES: Pages[] = ["clipboard"]
export function selectedPageFromPathName(pathName: string): Pages { export function selectedPageFromPathName(pathName: string): Pages {
if (pathName === "/") { if (pathName === "/") {