Compare commits
8 Commits
Author | SHA1 | Date | |
---|---|---|---|
614adcb9f8 | |||
a5b8e40761 | |||
e56bb8ac1c | |||
d9e9824146 | |||
8b46bf354e | |||
86ec00de4c | |||
49917e1eb0 | |||
edc575b153 |
@ -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 migrate deploy && node server.js"]
|
||||||
|
@ -0,0 +1,7 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Clipboard" (
|
||||||
|
"id" SERIAL NOT NULL,
|
||||||
|
"content" TEXT NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "Clipboard_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
@ -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
|
||||||
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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({
|
||||||
|
46
src/app/clipboard/ClipboardComponent.tsx
Normal file
46
src/app/clipboard/ClipboardComponent.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
19
src/app/clipboard/action.ts
Normal file
19
src/app/clipboard/action.ts
Normal 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 || "";
|
||||||
|
}
|
18
src/app/clipboard/page.tsx
Normal file
18
src/app/clipboard/page.tsx
Normal 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} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -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
|
i built this site from scratch using <b>next.js</b> - it is
|
||||||
<Link
|
<Link
|
||||||
style={{ color: "#88f" }}
|
style={{ color: "#88f" }}
|
||||||
href={"https://git.nrx.sh/naresh/nrx.sh"}
|
href={"https://git.nrx.sh/naresh/nrx.sh"}
|
||||||
|
@ -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)}
|
||||||
|
@ -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 === "/") {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user