semi working scheduling

This commit is contained in:
2025-02-19 18:35:39 +01:00
parent 3df5ae6b80
commit 4af62033c6
12 changed files with 296 additions and 121 deletions
+29
View File
@@ -24,6 +24,7 @@
"@schedule-x/events-service": "^2.19.0", "@schedule-x/events-service": "^2.19.0",
"@schedule-x/react": "^2.19.0", "@schedule-x/react": "^2.19.0",
"@schedule-x/theme-shadcn": "^2.19.0", "@schedule-x/theme-shadcn": "^2.19.0",
"@types/node-cron": "^3.0.11",
"@types/react-big-calendar": "^1.16.1", "@types/react-big-calendar": "^1.16.1",
"axios": "^1.7.9", "axios": "^1.7.9",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
@@ -35,6 +36,7 @@
"mongodb": "^6.13.0", "mongodb": "^6.13.0",
"next": "15.1.7", "next": "15.1.7",
"next-themes": "^0.4.4", "next-themes": "^0.4.4",
"node-cron": "^3.0.3",
"react": "^18.0.0", "react": "^18.0.0",
"react-day-picker": "^8.10.1", "react-day-picker": "^8.10.1",
"react-dom": "^18.0.0", "react-dom": "^18.0.0",
@@ -1785,6 +1787,12 @@
"undici-types": "~6.19.2" "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": { "node_modules/@types/prop-types": {
"version": "15.7.14", "version": "15.7.14",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz",
@@ -2982,6 +2990,18 @@
"node": "^10 || ^12 || >=14" "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": { "node_modules/normalize-path": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
@@ -4024,6 +4044,15 @@
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT" "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": { "node_modules/victory-vendor": {
"version": "36.9.2", "version": "36.9.2",
"resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz",
+4 -1
View File
@@ -7,7 +7,8 @@
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"lint": "next lint", "lint": "next lint",
"test": "node test.mjs" "test": "node test.mjs",
"error": "tsc --watch --noEmit --project './tsconfig.json"
}, },
"dependencies": { "dependencies": {
"@radix-ui/react-checkbox": "^1.1.4", "@radix-ui/react-checkbox": "^1.1.4",
@@ -26,6 +27,7 @@
"@schedule-x/events-service": "^2.19.0", "@schedule-x/events-service": "^2.19.0",
"@schedule-x/react": "^2.19.0", "@schedule-x/react": "^2.19.0",
"@schedule-x/theme-shadcn": "^2.19.0", "@schedule-x/theme-shadcn": "^2.19.0",
"@types/node-cron": "^3.0.11",
"@types/react-big-calendar": "^1.16.1", "@types/react-big-calendar": "^1.16.1",
"axios": "^1.7.9", "axios": "^1.7.9",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
@@ -37,6 +39,7 @@
"mongodb": "^6.13.0", "mongodb": "^6.13.0",
"next": "15.1.7", "next": "15.1.7",
"next-themes": "^0.4.4", "next-themes": "^0.4.4",
"node-cron": "^3.0.3",
"react": "^18.0.0", "react": "^18.0.0",
"react-day-picker": "^8.10.1", "react-day-picker": "^8.10.1",
"react-dom": "^18.0.0", "react-dom": "^18.0.0",
+3 -2
View File
@@ -28,9 +28,10 @@ export async function POST(req: NextRequest) {
{ status: 409 } { 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( return NextResponse.json(
{ {
+2 -1
View File
@@ -1,6 +1,6 @@
import { MongoClient } from 'mongodb'; import { MongoClient } from 'mongodb';
import config from './env'; import config from './env';
import { Session, User } from '@/utils'; import { Schedule, Session, User } from '@/utils';
const client = new MongoClient(config.mongoUrl); const client = new MongoClient(config.mongoUrl);
@@ -9,4 +9,5 @@ const db = client.db('klayrion');
export default { export default {
users: db.collection<User>('users'), users: db.collection<User>('users'),
sessions: db.collection<Session>('sessions'), sessions: db.collection<Session>('sessions'),
schedules: db.collection<Schedule>('schedules'),
}; };
+52
View File
@@ -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);
+58 -31
View File
@@ -1,38 +1,14 @@
import { NextResponse, NextRequest } from 'next/server'; import { NextResponse, NextRequest } from 'next/server';
import { TwitterApi } from 'twitter-api-v2'; import database from '../database';
import config from '@/app/api/env'; import { PostRequest, Schedule } from '@/utils';
import { PostRequest } from '@/utils'; import post from './post';
import { inspect } from 'util'; import { ObjectId } from 'mongodb';
import { use } from 'react';
export async function POST(req: NextRequest) { export async function POST(req: NextRequest) {
const { content, platforms }: PostRequest = await req.json(); const request: PostRequest = await req.json();
try { post(request);
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));
}
return NextResponse.json( return NextResponse.json(
{ {
@@ -41,3 +17,54 @@ export async function POST(req: NextRequest) {
{ status: 200 } { 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 }
);
}
+3 -1
View File
@@ -80,7 +80,9 @@ export async function POST(req: NextRequest) {
{ status: 401 } { status: 401 }
); );
const sessionId = (await database.sessions.insertOne({ email })).insertedId; const sessionId = (
await database.sessions.insertOne({ user: user._id.toString() })
).insertedId;
return NextResponse.json( return NextResponse.json(
{ {
+29
View File
@@ -1,4 +1,5 @@
'use client'; 'use client';
import { useCalendarApp, ScheduleXCalendar } from '@schedule-x/react'; import { useCalendarApp, ScheduleXCalendar } from '@schedule-x/react';
import { createViewWeek } from '@schedule-x/calendar'; import { createViewWeek } from '@schedule-x/calendar';
@@ -19,6 +20,9 @@ import { PlusCircle, Trash } from 'lucide-react';
import { format, parse } from 'date-fns'; import { format, parse } from 'date-fns';
import PostDialog, { today } from '@/components/postDialog'; 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) { function closestChild(children: any, clientX: number, clientY: number) {
let closestElement = null; let closestElement = null;
@@ -43,6 +47,7 @@ function CalendarApp() {
const eventsService = useState(() => createEventsServicePlugin())[0]; const eventsService = useState(() => createEventsServicePlugin())[0];
const dragAndDrop = useState(() => createDragAndDropPlugin())[0]; const dragAndDrop = useState(() => createDragAndDropPlugin())[0];
const [eventCount, setEventCount] = useState(0); const [eventCount, setEventCount] = useState(0);
const sessionId = Cookies.get('session_id') || '';
const calendar = useCalendarApp({ const calendar = useCalendarApp({
views: [createViewWeek()], views: [createViewWeek()],
@@ -51,6 +56,30 @@ function CalendarApp() {
isDark: true, 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 [newPostDialogOpen, setNewPostDialogOpen] = useState(false);
const [date, setDate] = useState<Date>(today); const [date, setDate] = useState<Date>(today);
const [selected, setSelected] = useState<string | null | undefined>(null); const [selected, setSelected] = useState<string | null | undefined>(null);
+17 -4
View File
@@ -6,14 +6,27 @@ import App from '@/components/app';
import { Calendar } from '@/components/ui/calendar'; import { Calendar } from '@/components/ui/calendar';
import { cookies } from 'next/headers'; import { cookies } from 'next/headers';
import axios from 'axios'; 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() { export default async function Dashboard() {
const cookieStore = await cookies(); const cookieStore = await cookies();
const sessionId = cookieStore.get('session_id')?.value; const sessionId = cookieStore.get('session_id')?.value || '';
const data = ( const data = await getSession(sessionId);
await axios.get(`http:/localhost:3000/api/session/?id=${sessionId}`)
).data.dashboard; if (!data) {
return '';
}
return ( return (
<App> <App>
+26 -24
View File
@@ -16,6 +16,30 @@ export default function Login() {
const [email, setEmail] = useState(''); const [email, setEmail] = useState('');
const [password, setPassword] = 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 ( return (
<div className="bg-white dark:bg-black w-full h-screen flex justify-center items-center"> <div className="bg-white dark:bg-black w-full h-screen flex justify-center items-center">
<div className="text-center"> <div className="text-center">
@@ -36,32 +60,10 @@ export default function Login() {
type="password" type="password"
placeholder="Your password" placeholder="Your password"
onChange={(event) => setPassword(event.target.value)} onChange={(event) => setPassword(event.target.value)}
onClick={handleLogin}
/> />
<Button <Button className="mt-5 w-80 mx-auto" onClick={handleLogin}>
className="mt-5 w-80 mx-auto"
onClick={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),
});
}
}}
>
Log in Log in
</Button> </Button>
</div> </div>
+10 -2
View File
@@ -24,6 +24,8 @@ import { CalendarEventExternal } from '@schedule-x/calendar';
import { Textarea } from './ui/textarea'; import { Textarea } from './ui/textarea';
import { Label } from './ui/label'; import { Label } from './ui/label';
import { useState } from 'react'; import { useState } from 'react';
import axios from 'axios';
import { Schedule } from '@/utils';
type PostDialogProps = { type PostDialogProps = {
newPostDialogOpen: boolean; newPostDialogOpen: boolean;
@@ -42,7 +44,7 @@ export default function PostDialog({
setNewPostDialogOpen, setNewPostDialogOpen,
date, date,
setDate, setDate,
setEvents, setEvents: addEvent,
eventId, eventId,
}: PostDialogProps) { }: PostDialogProps) {
const [content, setContent] = useState(''); const [content, setContent] = useState('');
@@ -123,12 +125,18 @@ export default function PostDialog({
const end = new Date(date); const end = new Date(date);
end.setHours(date.getHours() + 1); end.setHours(date.getHours() + 1);
setEvents({ addEvent({
title: 'Post', title: 'Post',
id: eventId, id: eventId,
start: format(date, 'y-MM-dd hh:mm'), start: format(date, 'y-MM-dd hh:mm'),
end: format(end, '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); setNewPostDialogOpen(false);
}} }}
+9 -1
View File
@@ -1,3 +1,5 @@
import { ObjectId } from 'mongodb';
export type PlatformKind = 'x' | 'linkedin'; export type PlatformKind = 'x' | 'linkedin';
export interface Platform { export interface Platform {
@@ -7,6 +9,7 @@ export interface Platform {
} }
export interface PostRequest { export interface PostRequest {
account: string;
content: string; content: string;
platforms: Platform[]; platforms: Platform[];
} }
@@ -15,6 +18,11 @@ export interface User {
name: string; name: string;
email: string; email: string;
password: string; password: string;
platforms: Platform[];
} }
export type Session = { email: string }; export interface Schedule extends PostRequest {
scheduled: string;
}
export type Session = { user: string };