mirror of
https://github.com/yusufipk/dead-man-switch-2.0.git
synced 2026-09-11 09:26:07 +00:00
feat: implement message CRUD and dark mode
- Add Message, Recipient models and migration - Create message API routes (GET, POST, PATCH, DELETE) - Fix IDOR vulnerability in PATCH (verify ownership before delete) - Add dashboard and message pages (list, new, detail) - Add UI components (card, input, label, textarea) - Enable dark mode in layout - Add session user id typing to NextAuth - Add AGENTS.md memory bank documentation
This commit is contained in:
@@ -0,0 +1,115 @@
|
|||||||
|
# Cline's Memory Bank
|
||||||
|
|
||||||
|
I am Cline, an expert software engineer with a unique characteristic: my memory resets completely between sessions. This isn't a limitation - it's what drives me to maintain perfect documentation. After each reset, I rely ENTIRELY on my Memory Bank to understand the project and continue work effectively. I MUST read ALL memory bank files at the start of EVERY task - this is not optional.
|
||||||
|
|
||||||
|
## Memory Bank Structure
|
||||||
|
|
||||||
|
The Memory Bank consists of core files and optional context files, all in Markdown format. Files build upon each other in a clear hierarchy:
|
||||||
|
|
||||||
|
flowchart TD
|
||||||
|
PB[projectbrief.md] --> PC[productContext.md]
|
||||||
|
PB --> SP[systemPatterns.md]
|
||||||
|
PB --> TC[techContext.md]
|
||||||
|
|
||||||
|
PC --> AC[activeContext.md]
|
||||||
|
SP --> AC
|
||||||
|
TC --> AC
|
||||||
|
|
||||||
|
AC --> P[progress.md]
|
||||||
|
|
||||||
|
### Core Files (Required)
|
||||||
|
1. `projectbrief.md`
|
||||||
|
- Foundation document that shapes all other files
|
||||||
|
- Created at project start if it doesn't exist
|
||||||
|
- Defines core requirements and goals
|
||||||
|
- Source of truth for project scope
|
||||||
|
|
||||||
|
2. `productContext.md`
|
||||||
|
- Why this project exists
|
||||||
|
- Problems it solves
|
||||||
|
- How it should work
|
||||||
|
- User experience goals
|
||||||
|
|
||||||
|
3. `activeContext.md`
|
||||||
|
- Current work focus
|
||||||
|
- Recent changes
|
||||||
|
- Next steps
|
||||||
|
- Active decisions and considerations
|
||||||
|
- Important patterns and preferences
|
||||||
|
- Learnings and project insights
|
||||||
|
|
||||||
|
4. `systemPatterns.md`
|
||||||
|
- System architecture
|
||||||
|
- Key technical decisions
|
||||||
|
- Design patterns in use
|
||||||
|
- Component relationships
|
||||||
|
- Critical implementation paths
|
||||||
|
|
||||||
|
5. `techContext.md`
|
||||||
|
- Technologies used
|
||||||
|
- Development setup
|
||||||
|
- Technical constraints
|
||||||
|
- Dependencies
|
||||||
|
- Tool usage patterns
|
||||||
|
|
||||||
|
6. `progress.md`
|
||||||
|
- What works
|
||||||
|
- What's left to build
|
||||||
|
- Current status
|
||||||
|
- Known issues
|
||||||
|
- Evolution of project decisions
|
||||||
|
|
||||||
|
### Additional Context
|
||||||
|
Create additional files/folders within memory-bank/ when they help organize:
|
||||||
|
- Complex feature documentation
|
||||||
|
- Integration specifications
|
||||||
|
- API documentation
|
||||||
|
- Testing strategies
|
||||||
|
- Deployment procedures
|
||||||
|
|
||||||
|
## Core Workflows
|
||||||
|
|
||||||
|
### Plan Mode
|
||||||
|
flowchart TD
|
||||||
|
Start[Start] --> ReadFiles[Read Memory Bank]
|
||||||
|
ReadFiles --> CheckFiles{Files Complete?}
|
||||||
|
|
||||||
|
CheckFiles -->|No| Plan[Create Plan]
|
||||||
|
Plan --> Document[Document in Chat]
|
||||||
|
|
||||||
|
CheckFiles -->|Yes| Verify[Verify Context]
|
||||||
|
Verify --> Strategy[Develop Strategy]
|
||||||
|
Strategy --> Present[Present Approach]
|
||||||
|
|
||||||
|
### Act Mode
|
||||||
|
flowchart TD
|
||||||
|
Start[Start] --> Context[Check Memory Bank]
|
||||||
|
Context --> Update[Update Documentation]
|
||||||
|
Update --> Execute[Execute Task]
|
||||||
|
Execute --> Document[Document Changes]
|
||||||
|
|
||||||
|
## Documentation Updates
|
||||||
|
|
||||||
|
Memory Bank updates occur when:
|
||||||
|
1. Discovering new project patterns
|
||||||
|
2. After implementing significant changes
|
||||||
|
3. When user requests with **update memory bank** (MUST review ALL files)
|
||||||
|
4. When context needs clarification
|
||||||
|
|
||||||
|
flowchart TD
|
||||||
|
Start[Update Process]
|
||||||
|
|
||||||
|
subgraph Process
|
||||||
|
P1[Review ALL Files]
|
||||||
|
P2[Document Current State]
|
||||||
|
P3[Clarify Next Steps]
|
||||||
|
P4[Document Insights & Patterns]
|
||||||
|
|
||||||
|
P1 --> P2 --> P3 --> P4
|
||||||
|
end
|
||||||
|
|
||||||
|
Start --> Process
|
||||||
|
|
||||||
|
Note: When triggered by **update memory bank**, I MUST review every memory bank file, even if some don't require updates. Focus particularly on activeContext.md and progress.md as they track current state.
|
||||||
|
|
||||||
|
REMEMBER: After every memory reset, I begin completely fresh. The Memory Bank is my only link to previous work. It must be maintained with precision and clarity, as my effectiveness depends entirely on its accuracy.
|
||||||
Generated
+48
-1
@@ -11,6 +11,7 @@
|
|||||||
"@auth/prisma-adapter": "^2.11.1",
|
"@auth/prisma-adapter": "^2.11.1",
|
||||||
"@prisma/adapter-pg": "^7.2.0",
|
"@prisma/adapter-pg": "^7.2.0",
|
||||||
"@prisma/client": "^7.2.0",
|
"@prisma/client": "^7.2.0",
|
||||||
|
"@radix-ui/react-label": "^2.1.8",
|
||||||
"@radix-ui/react-slot": "^1.2.4",
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
@@ -1645,6 +1646,52 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-label": {
|
||||||
|
"version": "2.1.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.8.tgz",
|
||||||
|
"integrity": "sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-primitive": "2.1.4"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-primitive": {
|
||||||
|
"version": "2.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz",
|
||||||
|
"integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-slot": "1.2.4"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-slot": {
|
"node_modules/@radix-ui/react-slot": {
|
||||||
"version": "1.2.4",
|
"version": "1.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz",
|
||||||
@@ -2053,7 +2100,7 @@
|
|||||||
"version": "19.2.3",
|
"version": "19.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
|
||||||
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
|
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "^19.2.0"
|
"@types/react": "^19.2.0"
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
"@auth/prisma-adapter": "^2.11.1",
|
"@auth/prisma-adapter": "^2.11.1",
|
||||||
"@prisma/adapter-pg": "^7.2.0",
|
"@prisma/adapter-pg": "^7.2.0",
|
||||||
"@prisma/client": "^7.2.0",
|
"@prisma/client": "^7.2.0",
|
||||||
|
"@radix-ui/react-label": "^2.1.8",
|
||||||
"@radix-ui/react-slot": "^1.2.4",
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Message" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"userId" TEXT NOT NULL,
|
||||||
|
"title" TEXT NOT NULL,
|
||||||
|
"content" TEXT,
|
||||||
|
"status" TEXT NOT NULL DEFAULT 'DRAFT',
|
||||||
|
"lastPing" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"checkInterval" INTEGER NOT NULL DEFAULT 24,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "Message_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Recipient" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"messageId" TEXT NOT NULL,
|
||||||
|
"email" TEXT NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "Recipient_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Attachment" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"messageId" TEXT NOT NULL,
|
||||||
|
"fileUrl" TEXT NOT NULL,
|
||||||
|
"fileName" TEXT NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "Attachment_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Message" ADD CONSTRAINT "Message_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Recipient" ADD CONSTRAINT "Recipient_messageId_fkey" FOREIGN KEY ("messageId") REFERENCES "Message"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Attachment" ADD CONSTRAINT "Attachment_messageId_fkey" FOREIGN KEY ("messageId") REFERENCES "Message"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
+31
-1
@@ -1,4 +1,3 @@
|
|||||||
// ... existing code ...
|
|
||||||
generator client {
|
generator client {
|
||||||
provider = "prisma-client-js"
|
provider = "prisma-client-js"
|
||||||
}
|
}
|
||||||
@@ -42,6 +41,37 @@ model User {
|
|||||||
image String?
|
image String?
|
||||||
accounts Account[]
|
accounts Account[]
|
||||||
sessions Session[]
|
sessions Session[]
|
||||||
|
messages Message[]
|
||||||
|
}
|
||||||
|
|
||||||
|
model Message {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
userId String
|
||||||
|
title String
|
||||||
|
content String? @db.Text
|
||||||
|
status String @default("DRAFT")
|
||||||
|
lastPing DateTime @default(now())
|
||||||
|
checkInterval Int @default(24)
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
recipients Recipient[]
|
||||||
|
attachments Attachment[]
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
model Recipient {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
messageId String
|
||||||
|
email String
|
||||||
|
message Message @relation(fields: [messageId], references: [id], onDelete: Cascade)
|
||||||
|
}
|
||||||
|
|
||||||
|
model Attachment {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
messageId String
|
||||||
|
fileUrl String
|
||||||
|
fileName String
|
||||||
|
message Message @relation(fields: [messageId], references: [id], onDelete: Cascade)
|
||||||
}
|
}
|
||||||
|
|
||||||
model VerificationToken {
|
model VerificationToken {
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import { getServerSession } from "next-auth";
|
||||||
|
import { authOptions } from "@/lib/auth/options";
|
||||||
|
import { prisma } from "@/lib/prisma";
|
||||||
|
|
||||||
|
export async function PATCH(
|
||||||
|
req: Request,
|
||||||
|
{ params }: { params: Promise<{ messageId: string }> }
|
||||||
|
) {
|
||||||
|
const { messageId } = await params;
|
||||||
|
const session = await getServerSession(authOptions);
|
||||||
|
|
||||||
|
if (!session?.user?.id) {
|
||||||
|
return new NextResponse("Unauthorized", { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const body = await req.json();
|
||||||
|
const { title, content, recipients, checkInterval } = body;
|
||||||
|
|
||||||
|
const existingMessage = await prisma.message.findUnique({
|
||||||
|
where: { id: messageId, userId: session.user.id },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!existingMessage) {
|
||||||
|
return new NextResponse("Unauthorized", { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete existing recipients and recreate them for simplicity in update
|
||||||
|
await prisma.recipient.deleteMany({
|
||||||
|
where: { messageId: messageId },
|
||||||
|
});
|
||||||
|
|
||||||
|
const message = await prisma.message.update({
|
||||||
|
where: {
|
||||||
|
id: messageId,
|
||||||
|
userId: session.user.id,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
title,
|
||||||
|
content,
|
||||||
|
checkInterval,
|
||||||
|
recipients: {
|
||||||
|
create: recipients.map((email: string) => ({ email })),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return NextResponse.json(message);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("MESSAGE_PATCH_ERROR", error);
|
||||||
|
return new NextResponse("Internal Server Error", { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function DELETE(
|
||||||
|
req: Request,
|
||||||
|
{ params }: { params: Promise<{ messageId: string }> }
|
||||||
|
) {
|
||||||
|
const { messageId } = await params;
|
||||||
|
const session = await getServerSession(authOptions);
|
||||||
|
|
||||||
|
if (!session?.user?.id) {
|
||||||
|
return new NextResponse("Unauthorized", { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const message = await prisma.message.delete({
|
||||||
|
where: {
|
||||||
|
id: messageId,
|
||||||
|
userId: session.user.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return NextResponse.json(message);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("MESSAGE_DELETE_ERROR", error);
|
||||||
|
return new NextResponse("Internal Server Error", { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import { getServerSession } from "next-auth";
|
||||||
|
import { authOptions } from "@/lib/auth/options";
|
||||||
|
import { prisma } from "@/lib/prisma";
|
||||||
|
|
||||||
|
export async function POST(req: Request) {
|
||||||
|
const session = await getServerSession(authOptions);
|
||||||
|
|
||||||
|
if (!session?.user?.id) {
|
||||||
|
return new NextResponse("Unauthorized", { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const body = await req.json();
|
||||||
|
const { title, content, recipients, checkInterval } = body;
|
||||||
|
|
||||||
|
const message = await prisma.message.create({
|
||||||
|
data: {
|
||||||
|
userId: session.user.id,
|
||||||
|
title,
|
||||||
|
content,
|
||||||
|
checkInterval,
|
||||||
|
recipients: {
|
||||||
|
create: recipients.map((email: string) => ({ email })),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return NextResponse.json(message);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("MESSAGE_POST_ERROR", error);
|
||||||
|
return new NextResponse("Internal Server Error", { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function GET(req: Request) {
|
||||||
|
const session = await getServerSession(authOptions);
|
||||||
|
|
||||||
|
if (!session?.user?.id) {
|
||||||
|
return new NextResponse("Unauthorized", { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const messages = await prisma.message.findMany({
|
||||||
|
where: {
|
||||||
|
userId: session.user.id,
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
recipients: true,
|
||||||
|
},
|
||||||
|
orderBy: {
|
||||||
|
createdAt: "desc",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return NextResponse.json(messages);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("MESSAGES_GET_ERROR", error);
|
||||||
|
return new NextResponse("Internal Server Error", { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { prisma } from "@/lib/prisma";
|
||||||
|
import { getServerSession } from "next-auth";
|
||||||
|
import { authOptions } from "@/lib/auth/options";
|
||||||
|
import { redirect } from "next/navigation";
|
||||||
|
import MessageForm from "@/components/messages/message-form";
|
||||||
|
|
||||||
|
export default async function EditMessagePage({
|
||||||
|
params,
|
||||||
|
}: {
|
||||||
|
params: Promise<{ messageId: string }>;
|
||||||
|
}) {
|
||||||
|
const { messageId } = await params;
|
||||||
|
const session = await getServerSession(authOptions);
|
||||||
|
|
||||||
|
if (!session) {
|
||||||
|
redirect("/auth/signin");
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = await prisma.message.findUnique({
|
||||||
|
where: {
|
||||||
|
id: messageId,
|
||||||
|
userId: session.user.id,
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
recipients: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!message) {
|
||||||
|
redirect("/dashboard");
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialData = {
|
||||||
|
id: message.id,
|
||||||
|
title: message.title,
|
||||||
|
content: message.content || "",
|
||||||
|
checkInterval: message.checkInterval,
|
||||||
|
recipients: message.recipients.map((r) => r.email),
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto py-10">
|
||||||
|
<h1 className="text-2xl font-bold mb-6 text-center">Mesajı Düzenle</h1>
|
||||||
|
<MessageForm initialData={initialData} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import MessageForm from "@/components/messages/message-form";
|
||||||
|
|
||||||
|
export default function NewMessagePage() {
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto py-10">
|
||||||
|
<h1 className="text-2xl font-bold mb-6 text-center">Yeni Ölü Adam Anahtarı Oluştur</h1>
|
||||||
|
<MessageForm />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
|
||||||
|
import { Plus, MessageSquare, Edit2 } from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { getServerSession } from "next-auth";
|
||||||
|
import { authOptions } from "@/lib/auth/options";
|
||||||
|
import { prisma } from "@/lib/prisma";
|
||||||
|
import { redirect } from "next/navigation";
|
||||||
|
|
||||||
|
export default async function DashboardPage() {
|
||||||
|
const session = await getServerSession(authOptions);
|
||||||
|
|
||||||
|
if (!session) {
|
||||||
|
redirect("/auth/signin");
|
||||||
|
}
|
||||||
|
|
||||||
|
const messages = await prisma.message.findMany({
|
||||||
|
where: { userId: session.user.id },
|
||||||
|
include: { recipients: true },
|
||||||
|
orderBy: { createdAt: "desc" },
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto py-10 space-y-8">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<h1 className="text-3xl font-bold">Panelim</h1>
|
||||||
|
<Link href="/dashboard/messages/new">
|
||||||
|
<Button>
|
||||||
|
<Plus className="mr-2 h-4 w-4" /> Yeni Mesaj
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{messages.length === 0 ? (
|
||||||
|
<Card className="flex flex-col items-center justify-center p-12 text-center">
|
||||||
|
<MessageSquare className="h-12 w-12 text-muted-foreground mb-4" />
|
||||||
|
<h3 className="text-xl font-semibold">Henüz mesajınız yok</h3>
|
||||||
|
<p className="text-muted-foreground mb-6">
|
||||||
|
İlk "Ölü Adam Anahtarı" mesajınızı oluşturarak başlayın.
|
||||||
|
</p>
|
||||||
|
<Link href="/dashboard/messages/new">
|
||||||
|
<Button variant="outline">Oluştur</Button>
|
||||||
|
</Link>
|
||||||
|
</Card>
|
||||||
|
) : (
|
||||||
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{messages.map((msg) => (
|
||||||
|
<Card key={msg.id} className="relative group">
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between">
|
||||||
|
<CardTitle className="truncate pr-8">{msg.title}</CardTitle>
|
||||||
|
<Link href={`/dashboard/messages/${msg.id}`}>
|
||||||
|
<Button variant="ghost" size="icon" className="h-8 w-8">
|
||||||
|
<Edit2 className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<p className="text-sm text-muted-foreground mb-2">
|
||||||
|
Durum: <span className="font-medium text-foreground">{msg.status}</span>
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Alıcılar: {msg.recipients.length}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Kontrol: {msg.checkInterval} saat başı
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
+1
-1
@@ -24,7 +24,7 @@ export default function RootLayout({
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}>) {
|
}>) {
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en" className="dark">
|
||||||
<body
|
<body
|
||||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -0,0 +1,177 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Card, CardHeader, CardTitle, CardContent, CardFooter } from "@/components/ui/card";
|
||||||
|
import { Plus, Trash2 } from "lucide-react";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
|
||||||
|
interface MessageFormProps {
|
||||||
|
initialData?: {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
content: string;
|
||||||
|
recipients: string[];
|
||||||
|
checkInterval: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function MessageForm({ initialData }: MessageFormProps) {
|
||||||
|
const [title, setTitle] = useState(initialData?.title || "");
|
||||||
|
const [content, setContent] = useState(initialData?.content || "");
|
||||||
|
const [recipients, setRecipients] = useState(initialData?.recipients || [""]);
|
||||||
|
const [checkInterval, setCheckInterval] = useState(initialData?.checkInterval || 24);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const addRecipient = () => setRecipients([...recipients, ""]);
|
||||||
|
const removeRecipient = (index: number) => setRecipients(recipients.filter((_, i) => i !== index));
|
||||||
|
const updateRecipient = (index: number, value: string) => {
|
||||||
|
const newRecipients = [...recipients];
|
||||||
|
newRecipients[index] = value;
|
||||||
|
setRecipients(newRecipients);
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteMessage = async () => {
|
||||||
|
if (!initialData?.id || !confirm("Bu mesajı silmek istediğinize emin misiniz?")) return;
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/messages/${initialData.id}`, {
|
||||||
|
method: "DELETE",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
router.push("/dashboard");
|
||||||
|
router.refresh();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const url = initialData?.id
|
||||||
|
? `/api/messages/${initialData.id}`
|
||||||
|
: "/api/messages";
|
||||||
|
|
||||||
|
const method = initialData?.id ? "PATCH" : "POST";
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method,
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ title, content, recipients, checkInterval }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
router.push("/dashboard");
|
||||||
|
router.refresh();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<Card className="w-full max-w-2xl mx-auto">
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between">
|
||||||
|
<CardTitle>{initialData ? "Mesajı Düzenle" : "Yeni Mesaj Oluştur"}</CardTitle>
|
||||||
|
{initialData && (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="destructive"
|
||||||
|
size="sm"
|
||||||
|
onClick={deleteMessage}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4 mr-2" /> Sil
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="title">Başlık</Label>
|
||||||
|
<Input
|
||||||
|
id="title"
|
||||||
|
value={title}
|
||||||
|
onChange={(e) => setTitle(e.target.value)}
|
||||||
|
placeholder="Örn: Vasiyetim"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="content">Mesaj İçeriği</Label>
|
||||||
|
<Textarea
|
||||||
|
id="content"
|
||||||
|
value={content}
|
||||||
|
onChange={(e) => setContent(e.target.value)}
|
||||||
|
placeholder="Mesajınızı buraya yazın..."
|
||||||
|
rows={5}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Alıcılar</Label>
|
||||||
|
{recipients.map((recipient, index) => (
|
||||||
|
<div key={index} className="flex gap-2">
|
||||||
|
<Input
|
||||||
|
value={recipient}
|
||||||
|
onChange={(e) => updateRecipient(index, e.target.value)}
|
||||||
|
placeholder="[email protected]"
|
||||||
|
type="email"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
{recipients.length > 1 && (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="destructive"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => removeRecipient(index)}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={addRecipient}
|
||||||
|
className="mt-2"
|
||||||
|
>
|
||||||
|
<Plus className="h-4 w-4 mr-2" /> Alıcı Ekle
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="interval">Kontrol Aralığı (Saat)</Label>
|
||||||
|
<Input
|
||||||
|
id="interval"
|
||||||
|
type="number"
|
||||||
|
value={checkInterval}
|
||||||
|
onChange={(e) => setCheckInterval(parseInt(e.target.value))}
|
||||||
|
min={1}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
<CardFooter>
|
||||||
|
<Button type="submit" disabled={loading} className="w-full">
|
||||||
|
{loading ? "Kaydediliyor..." : "Mesajı Kaydet"}
|
||||||
|
</Button>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Card({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card"
|
||||||
|
className={cn(
|
||||||
|
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-header"
|
||||||
|
className={cn(
|
||||||
|
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-title"
|
||||||
|
className={cn("leading-none font-semibold", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-description"
|
||||||
|
className={cn("text-muted-foreground text-sm", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-action"
|
||||||
|
className={cn(
|
||||||
|
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-content"
|
||||||
|
className={cn("px-6", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-footer"
|
||||||
|
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Card,
|
||||||
|
CardHeader,
|
||||||
|
CardFooter,
|
||||||
|
CardTitle,
|
||||||
|
CardAction,
|
||||||
|
CardDescription,
|
||||||
|
CardContent,
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
type={type}
|
||||||
|
data-slot="input"
|
||||||
|
className={cn(
|
||||||
|
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||||
|
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
||||||
|
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Input }
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Label({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
||||||
|
return (
|
||||||
|
<LabelPrimitive.Root
|
||||||
|
data-slot="label"
|
||||||
|
className={cn(
|
||||||
|
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Label }
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
||||||
|
return (
|
||||||
|
<textarea
|
||||||
|
data-slot="textarea"
|
||||||
|
className={cn(
|
||||||
|
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Textarea }
|
||||||
@@ -1,8 +1,17 @@
|
|||||||
import { NextAuthOptions } from "next-auth";
|
import { NextAuthOptions } from "next-auth";
|
||||||
|
import { DefaultSession } from "next-auth";
|
||||||
import { PrismaAdapter } from "@auth/prisma-adapter";
|
import { PrismaAdapter } from "@auth/prisma-adapter";
|
||||||
import GoogleProvider from "next-auth/providers/google";
|
import GoogleProvider from "next-auth/providers/google";
|
||||||
import { prisma } from "@/lib/prisma";
|
import { prisma } from "@/lib/prisma";
|
||||||
|
|
||||||
|
declare module "next-auth" {
|
||||||
|
interface Session extends DefaultSession {
|
||||||
|
user: {
|
||||||
|
id: string;
|
||||||
|
} & DefaultSession["user"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const authOptions: NextAuthOptions = {
|
export const authOptions: NextAuthOptions = {
|
||||||
adapter: PrismaAdapter(prisma),
|
adapter: PrismaAdapter(prisma),
|
||||||
providers: [
|
providers: [
|
||||||
|
|||||||
Reference in New Issue
Block a user