From 4af62033c6c47c8c6ebbed2b3264babbbe9250bd Mon Sep 17 00:00:00 2001 From: Leo dev Date: Wed, 19 Feb 2025 18:35:39 +0100 Subject: [PATCH] semi working scheduling --- package-lock.json | 29 +++++++++ package.json | 113 +++++++++++++++++----------------- src/app/api/account/route.ts | 5 +- src/app/api/database.ts | 3 +- src/app/api/posts/post.ts | 52 ++++++++++++++++ src/app/api/posts/route.ts | 89 ++++++++++++++++---------- src/app/api/session/route.ts | 4 +- src/app/calendar/page.tsx | 29 +++++++++ src/app/dashboard/page.tsx | 21 +++++-- src/app/login/page.tsx | 50 +++++++-------- src/components/postDialog.tsx | 12 +++- src/utils.ts | 10 ++- 12 files changed, 296 insertions(+), 121 deletions(-) create mode 100644 src/app/api/posts/post.ts diff --git a/package-lock.json b/package-lock.json index d1e2dba..dc50e2e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,7 @@ "@schedule-x/events-service": "^2.19.0", "@schedule-x/react": "^2.19.0", "@schedule-x/theme-shadcn": "^2.19.0", + "@types/node-cron": "^3.0.11", "@types/react-big-calendar": "^1.16.1", "axios": "^1.7.9", "class-variance-authority": "^0.7.1", @@ -35,6 +36,7 @@ "mongodb": "^6.13.0", "next": "15.1.7", "next-themes": "^0.4.4", + "node-cron": "^3.0.3", "react": "^18.0.0", "react-day-picker": "^8.10.1", "react-dom": "^18.0.0", @@ -1785,6 +1787,12 @@ "undici-types": "~6.19.2" } }, + "node_modules/@types/node-cron": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/node-cron/-/node-cron-3.0.11.tgz", + "integrity": "sha512-0ikrnug3/IyneSHqCBeslAhlK2aBfYek1fGo4bP4QnZPmiqSGRK+Oy7ZMisLWkesffJvQ1cqAcBnJC+8+nxIAg==", + "license": "MIT" + }, "node_modules/@types/prop-types": { "version": "15.7.14", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz", @@ -2982,6 +2990,18 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/node-cron": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-3.0.3.tgz", + "integrity": "sha512-dOal67//nohNgYWb+nWmg5dkFdIwDm8EpeGYMekPMrngV3637lqnX0lbUcCtgibHTz6SEz7DAIjKvKDFYCnO1A==", + "license": "ISC", + "dependencies": { + "uuid": "8.3.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -4024,6 +4044,15 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/victory-vendor": { "version": "36.9.2", "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", diff --git a/package.json b/package.json index 4b9cf2e..f3cae07 100644 --- a/package.json +++ b/package.json @@ -1,57 +1,60 @@ { - "name": "klayrion", - "version": "0.0.1-alpha.0", - "private": true, - "scripts": { - "dev": "next dev", - "build": "next build", - "start": "next start", - "lint": "next lint", - "test": "node test.mjs" - }, - "dependencies": { - "@radix-ui/react-checkbox": "^1.1.4", - "@radix-ui/react-collapsible": "^1.1.3", - "@radix-ui/react-context-menu": "^2.2.6", - "@radix-ui/react-dialog": "^1.1.6", - "@radix-ui/react-label": "^2.1.2", - "@radix-ui/react-popover": "^1.1.6", - "@radix-ui/react-select": "^2.1.6", - "@radix-ui/react-separator": "^1.1.2", - "@radix-ui/react-slot": "^1.1.2", - "@radix-ui/react-tabs": "^1.1.3", - "@radix-ui/react-toast": "^1.2.6", - "@radix-ui/react-tooltip": "^1.1.8", - "@schedule-x/drag-and-drop": "^2.19.0", - "@schedule-x/events-service": "^2.19.0", - "@schedule-x/react": "^2.19.0", - "@schedule-x/theme-shadcn": "^2.19.0", - "@types/react-big-calendar": "^1.16.1", - "axios": "^1.7.9", - "class-variance-authority": "^0.7.1", - "clsx": "^2.1.1", - "date-fns": "^3.6.0", - "instagram-graph-api": "^7.0.0", - "js-cookie": "^3.0.5", - "lucide-react": "^0.475.0", - "mongodb": "^6.13.0", - "next": "15.1.7", - "next-themes": "^0.4.4", - "react": "^18.0.0", - "react-day-picker": "^8.10.1", - "react-dom": "^18.0.0", - "recharts": "^2.15.1", - "tailwind-merge": "^3.0.1", - "tailwindcss-animate": "^1.0.7", - "twitter-api-v2": "^1.19.1" - }, - "devDependencies": { - "@types/js-cookie": "^3.0.6", - "@types/node": "^20", - "@types/react": "^18", - "@types/react-dom": "^18", - "postcss": "^8", - "tailwindcss": "^3.4.1", - "typescript": "^5" - } + "name": "klayrion", + "version": "0.0.1-alpha.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint", + "test": "node test.mjs", + "error": "tsc --watch --noEmit --project './tsconfig.json" + }, + "dependencies": { + "@radix-ui/react-checkbox": "^1.1.4", + "@radix-ui/react-collapsible": "^1.1.3", + "@radix-ui/react-context-menu": "^2.2.6", + "@radix-ui/react-dialog": "^1.1.6", + "@radix-ui/react-label": "^2.1.2", + "@radix-ui/react-popover": "^1.1.6", + "@radix-ui/react-select": "^2.1.6", + "@radix-ui/react-separator": "^1.1.2", + "@radix-ui/react-slot": "^1.1.2", + "@radix-ui/react-tabs": "^1.1.3", + "@radix-ui/react-toast": "^1.2.6", + "@radix-ui/react-tooltip": "^1.1.8", + "@schedule-x/drag-and-drop": "^2.19.0", + "@schedule-x/events-service": "^2.19.0", + "@schedule-x/react": "^2.19.0", + "@schedule-x/theme-shadcn": "^2.19.0", + "@types/node-cron": "^3.0.11", + "@types/react-big-calendar": "^1.16.1", + "axios": "^1.7.9", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "date-fns": "^3.6.0", + "instagram-graph-api": "^7.0.0", + "js-cookie": "^3.0.5", + "lucide-react": "^0.475.0", + "mongodb": "^6.13.0", + "next": "15.1.7", + "next-themes": "^0.4.4", + "node-cron": "^3.0.3", + "react": "^18.0.0", + "react-day-picker": "^8.10.1", + "react-dom": "^18.0.0", + "recharts": "^2.15.1", + "tailwind-merge": "^3.0.1", + "tailwindcss-animate": "^1.0.7", + "twitter-api-v2": "^1.19.1" + }, + "devDependencies": { + "@types/js-cookie": "^3.0.6", + "@types/node": "^20", + "@types/react": "^18", + "@types/react-dom": "^18", + "postcss": "^8", + "tailwindcss": "^3.4.1", + "typescript": "^5" + } } diff --git a/src/app/api/account/route.ts b/src/app/api/account/route.ts index f97fc02..ce14249 100644 --- a/src/app/api/account/route.ts +++ b/src/app/api/account/route.ts @@ -28,9 +28,10 @@ export async function POST(req: NextRequest) { { status: 409 } ); - await database.users.insertOne({ email, password, name }); + await database.users.insertOne({ email, password, name, platforms: [] }); - const sessionId = (await database.sessions.insertOne({ email })).insertedId; + const sessionId = (await database.sessions.insertOne({ user: email })) + .insertedId; return NextResponse.json( { diff --git a/src/app/api/database.ts b/src/app/api/database.ts index 81758f0..1fde5dc 100644 --- a/src/app/api/database.ts +++ b/src/app/api/database.ts @@ -1,6 +1,6 @@ import { MongoClient } from 'mongodb'; import config from './env'; -import { Session, User } from '@/utils'; +import { Schedule, Session, User } from '@/utils'; const client = new MongoClient(config.mongoUrl); @@ -9,4 +9,5 @@ const db = client.db('klayrion'); export default { users: db.collection('users'), sessions: db.collection('sessions'), + schedules: db.collection('schedules'), }; diff --git a/src/app/api/posts/post.ts b/src/app/api/posts/post.ts new file mode 100644 index 0000000..9ff672c --- /dev/null +++ b/src/app/api/posts/post.ts @@ -0,0 +1,52 @@ +import { TwitterApi } from 'twitter-api-v2'; +import config from '@/app/api/env'; +import { inspect } from 'util'; +import { PostRequest } from '@/utils'; +import database from '../database'; +import { format } from 'date-fns'; +import cron from 'node-cron'; + +export default async function post({ platforms, content }: PostRequest) { + try { + for (const platform of platforms) { + switch (platform.platform) { + // X / Twitter + case 'x': + const twitterClient = new TwitterApi({ + appKey: config.twitter.key, + appSecret: config.twitter.secret, + accessToken: platform.token, + accessSecret: platform.secret, + }); + + await twitterClient.v2.tweet(content); + } + } + } catch (err) { + console.log(inspect(err, true, null, true)); + } +} + +export async function processTasks() { + try { + const scheduled = format(new Date(), 'y-MM-d-hh:mm'); + + const tasks = await database.schedules + .find({ + scheduled, + }) + .toArray(); + + for (const task of tasks) { + post(task); + } + + database.schedules.deleteMany({ + scheduled, + }); + } catch (error) { + console.error('Error processing tasks:', error); + } +} + +cron.schedule('*/1 * * * *', processTasks); diff --git a/src/app/api/posts/route.ts b/src/app/api/posts/route.ts index fe15cd3..3572152 100644 --- a/src/app/api/posts/route.ts +++ b/src/app/api/posts/route.ts @@ -1,38 +1,14 @@ import { NextResponse, NextRequest } from 'next/server'; -import { TwitterApi } from 'twitter-api-v2'; -import config from '@/app/api/env'; -import { PostRequest } from '@/utils'; -import { inspect } from 'util'; +import database from '../database'; +import { PostRequest, Schedule } from '@/utils'; +import post from './post'; +import { ObjectId } from 'mongodb'; +import { use } from 'react'; export async function POST(req: NextRequest) { - const { content, platforms }: PostRequest = await req.json(); + const request: PostRequest = await req.json(); - try { - for (const platform of platforms) { - switch (platform.platform) { - // X / Twitter - case 'x': - const twitterClient = new TwitterApi({ - appKey: config.twitter.key, - appSecret: config.twitter.secret, - accessToken: platform.token, - accessSecret: platform.secret, - }); - - await twitterClient.v2.tweet(content); - - default: - return NextResponse.json( - { - message: 'Not implemented', - }, - { status: 501 } - ); - } - } - } catch (err) { - console.log(inspect(err, true, null, true)); - } + post(request); return NextResponse.json( { @@ -41,3 +17,54 @@ export async function POST(req: NextRequest) { { status: 200 } ); } + +export async function PUT(req: NextRequest) { + const request: Schedule = await req.json(); + + database.schedules.insertOne(request); + + return NextResponse.json( + { + message: 'ok', + }, + { status: 200 } + ); +} + +export async function GET(req: NextRequest) { + const sessionId = new URL(req.url).searchParams.get('id'); + + if (typeof sessionId !== 'string') + return NextResponse.json( + { + message: 'Invalid structure, required param `id=`', + }, + { status: 400 } + ); + + const session = await database.sessions.findOne({ + _id: new ObjectId(sessionId), + }); + + if (!session) + return NextResponse.json( + { + message: 'Invalid session', + }, + { status: 404 } + ); + + const schedules = await database.schedules + .find({ + account: session.user, + }) + .toArray(); + + return NextResponse.json( + { + message: 'ok', + schedules, + }, + { status: 200 } + ); +} diff --git a/src/app/api/session/route.ts b/src/app/api/session/route.ts index 4659253..38b017b 100644 --- a/src/app/api/session/route.ts +++ b/src/app/api/session/route.ts @@ -80,7 +80,9 @@ export async function POST(req: NextRequest) { { status: 401 } ); - const sessionId = (await database.sessions.insertOne({ email })).insertedId; + const sessionId = ( + await database.sessions.insertOne({ user: user._id.toString() }) + ).insertedId; return NextResponse.json( { diff --git a/src/app/calendar/page.tsx b/src/app/calendar/page.tsx index 4a25529..5d4c165 100644 --- a/src/app/calendar/page.tsx +++ b/src/app/calendar/page.tsx @@ -1,4 +1,5 @@ 'use client'; + import { useCalendarApp, ScheduleXCalendar } from '@schedule-x/react'; import { createViewWeek } from '@schedule-x/calendar'; @@ -19,6 +20,9 @@ import { PlusCircle, Trash } from 'lucide-react'; import { format, parse } from 'date-fns'; import PostDialog, { today } from '@/components/postDialog'; +import axios from 'axios'; +import Cookies from 'js-cookie'; +import { Schedule } from '@/utils'; function closestChild(children: any, clientX: number, clientY: number) { let closestElement = null; @@ -43,6 +47,7 @@ function CalendarApp() { const eventsService = useState(() => createEventsServicePlugin())[0]; const dragAndDrop = useState(() => createDragAndDropPlugin())[0]; const [eventCount, setEventCount] = useState(0); + const sessionId = Cookies.get('session_id') || ''; const calendar = useCalendarApp({ views: [createViewWeek()], @@ -51,6 +56,30 @@ function CalendarApp() { isDark: true, }); + useEffect(() => { + axios.get(`/api/posts/?id=${sessionId}`).then((response) => { + for (const [id, sched] of ( + response.data.schedules as Schedule[] + ).entries()) { + const date = parse( + sched.scheduled, + 'y-MM-dd hh:mm', + new Date() + ); + const end = new Date(date); + end.setHours(date.getHours() + 1); + + calendar.events.add({ + title: 'Post', + id, + start: format(date, 'y-MM-dd hh:mm'), + end: format(end, 'y-MM-dd hh:mm'), + }); + setEventCount(eventCount + 1); + } + }); + }, []); + const [newPostDialogOpen, setNewPostDialogOpen] = useState(false); const [date, setDate] = useState(today); const [selected, setSelected] = useState(null); diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index 04c582b..7776513 100644 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -6,14 +6,27 @@ import App from '@/components/app'; import { Calendar } from '@/components/ui/calendar'; import { cookies } from 'next/headers'; import axios from 'axios'; +import { redirect } from 'next/navigation'; + +async function getSession(sessionId: string) { + try { + return ( + await axios.get(`http:/localhost:3000/api/session/?id=${sessionId}`) + ).data; + } catch { + redirect('/login'); + } +} export default async function Dashboard() { const cookieStore = await cookies(); - const sessionId = cookieStore.get('session_id')?.value; + const sessionId = cookieStore.get('session_id')?.value || ''; - const data = ( - await axios.get(`http:/localhost:3000/api/session/?id=${sessionId}`) - ).data.dashboard; + const data = await getSession(sessionId); + + if (!data) { + return ''; + } return ( diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index c664048..d9d8df4 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -16,6 +16,30 @@ export default function Login() { const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); + const handleLogin = () => { + async () => { + try { + const { sessionId } = ( + await axios.post('/api/session/', { + email, + password, + }) + ).data; + + cookies.set('session_id', sessionId); + + router.replace('/dashboard'); + } catch (err: any) { + toast({ + title: 'Uh oh! Something went wrong.', + description: err.response + ? err.response.data.message + : String(err), + }); + } + }; + }; + return (
@@ -36,32 +60,10 @@ export default function Login() { type="password" placeholder="Your password" onChange={(event) => setPassword(event.target.value)} + onClick={handleLogin} /> -
diff --git a/src/components/postDialog.tsx b/src/components/postDialog.tsx index 95bed1c..cf5ef31 100644 --- a/src/components/postDialog.tsx +++ b/src/components/postDialog.tsx @@ -24,6 +24,8 @@ import { CalendarEventExternal } from '@schedule-x/calendar'; import { Textarea } from './ui/textarea'; import { Label } from './ui/label'; import { useState } from 'react'; +import axios from 'axios'; +import { Schedule } from '@/utils'; type PostDialogProps = { newPostDialogOpen: boolean; @@ -42,7 +44,7 @@ export default function PostDialog({ setNewPostDialogOpen, date, setDate, - setEvents, + setEvents: addEvent, eventId, }: PostDialogProps) { const [content, setContent] = useState(''); @@ -123,12 +125,18 @@ export default function PostDialog({ const end = new Date(date); end.setHours(date.getHours() + 1); - setEvents({ + addEvent({ title: 'Post', id: eventId, start: format(date, 'y-MM-dd hh:mm'), end: format(end, 'y-MM-dd hh:mm'), }); + axios.put('http://localhost:3000/api/posts', { + scheduled: format(date, 'y-MM-dd hh:mm'), + account: '', + platforms: [], + content, + } satisfies Schedule); setNewPostDialogOpen(false); }} diff --git a/src/utils.ts b/src/utils.ts index 59ddee1..b9e32f2 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,3 +1,5 @@ +import { ObjectId } from 'mongodb'; + export type PlatformKind = 'x' | 'linkedin'; export interface Platform { @@ -7,6 +9,7 @@ export interface Platform { } export interface PostRequest { + account: string; content: string; platforms: Platform[]; } @@ -15,6 +18,11 @@ export interface User { name: string; email: string; password: string; + platforms: Platform[]; } -export type Session = { email: string }; +export interface Schedule extends PostRequest { + scheduled: string; +} + +export type Session = { user: string };