structuring
This commit is contained in:
@@ -19,7 +19,7 @@ import {
|
||||
import { PlusCircle, Trash } from 'lucide-react';
|
||||
|
||||
import { format, parse } from 'date-fns';
|
||||
import PostDialog, { today } from '@/components/postDialog';
|
||||
import PostDialog, { today } from './postDialog';
|
||||
import axios from 'axios';
|
||||
import Cookies from 'js-cookie';
|
||||
import { Schedule } from '@/utils';
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
'use client';
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from '@/components/ui/popover';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { format } from 'date-fns';
|
||||
import { CalendarIcon } from 'lucide-react';
|
||||
import { Calendar } from '@/components/ui/calendar';
|
||||
import TimeSelect from './timeSelect';
|
||||
import { CalendarEventExternal } from '@schedule-x/calendar';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { useState } from 'react';
|
||||
import axios from 'axios';
|
||||
import { Schedule } from '@/utils';
|
||||
|
||||
type PostDialogProps = {
|
||||
newPostDialogOpen: boolean;
|
||||
setNewPostDialogOpen: (b: boolean) => void;
|
||||
date: Date;
|
||||
setDate: (d: Date) => void;
|
||||
eventId: number;
|
||||
setEvents: (e: CalendarEventExternal) => void;
|
||||
};
|
||||
|
||||
export const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
|
||||
export default function PostDialog({
|
||||
newPostDialogOpen,
|
||||
setNewPostDialogOpen,
|
||||
date,
|
||||
setDate,
|
||||
setEvents: addEvent,
|
||||
eventId,
|
||||
}: PostDialogProps) {
|
||||
const [content, setContent] = useState('');
|
||||
|
||||
return (
|
||||
<Dialog open={newPostDialogOpen} onOpenChange={setNewPostDialogOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>New post</DialogTitle>
|
||||
<DialogDescription>
|
||||
Schedule posts on multiple accounts
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="flex flex-row gap-4">
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant={'outline'}
|
||||
className={cn(
|
||||
'w-[280px] justify-start text-left font-normal',
|
||||
!date && 'text-muted-foreground'
|
||||
)}
|
||||
>
|
||||
<CalendarIcon className="mr-2 h-4 w-4" />
|
||||
{date ? (
|
||||
format(date, 'MMMM dd y')
|
||||
) : (
|
||||
<span>Pick a date</span>
|
||||
)}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0">
|
||||
<Calendar
|
||||
selected={date}
|
||||
onSelect={(d: Date | undefined) => {
|
||||
if (d) {
|
||||
const newDate = new Date(d);
|
||||
newDate.setHours(
|
||||
date.getHours(),
|
||||
date.getMinutes(),
|
||||
0,
|
||||
0
|
||||
);
|
||||
setDate(newDate);
|
||||
}
|
||||
}}
|
||||
mode="single"
|
||||
disabled={(date) => date < today}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
<TimeSelect
|
||||
date={date || new Date()}
|
||||
setDate={setDate}
|
||||
className=""
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid w-full gap-1.5">
|
||||
<Label htmlFor="message">Content</Label>
|
||||
<Textarea
|
||||
className="resize-none"
|
||||
placeholder="Enter your content here"
|
||||
onChange={(event) => setContent(event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setNewPostDialogOpen(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
const end = new Date(date);
|
||||
end.setHours(date.getHours() + 1);
|
||||
|
||||
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);
|
||||
}}
|
||||
>
|
||||
Schedule post
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
|
||||
interface TimeSelectProps {
|
||||
date: Date;
|
||||
setDate: (newDate: Date) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const TimeSelect: React.FC<TimeSelectProps> = ({
|
||||
date,
|
||||
setDate,
|
||||
className,
|
||||
}) => {
|
||||
const times = Array.from({ length: 24 }, (_, h) =>
|
||||
[0, 15, 30, 45].map(
|
||||
(m) =>
|
||||
`${(h+1).toString().padStart(2, '0')}:${m
|
||||
.toString()
|
||||
.padStart(2, '0')}`
|
||||
)
|
||||
).flat();
|
||||
|
||||
const currentHour = date.getHours().toString().padStart(2, '0');
|
||||
const currentMinute = date.getMinutes();
|
||||
const nearestMinute = [0, 15, 30, 45].reduce((prev, curr) =>
|
||||
Math.abs(curr - currentMinute) < Math.abs(prev - currentMinute)
|
||||
? curr
|
||||
: prev
|
||||
);
|
||||
const selectedTime = `${currentHour}:${nearestMinute
|
||||
.toString()
|
||||
.padStart(2, '0')}`;
|
||||
|
||||
const handleTimeChange = (newTime: string) => {
|
||||
const [hour, minute] = newTime.split(':').map(Number);
|
||||
const newDate = new Date(date);
|
||||
newDate.setHours(hour, minute, 0, 0);
|
||||
setDate(newDate);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Select onValueChange={handleTimeChange}>
|
||||
<SelectTrigger>
|
||||
<SelectValue
|
||||
placeholder={selectedTime}
|
||||
className={className}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{times.map((t) => (
|
||||
<SelectItem key={t} value={t}>
|
||||
{t}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TimeSelect;
|
||||
@@ -0,0 +1,51 @@
|
||||
'use client';
|
||||
|
||||
import { PlatformKind } from '@/utils';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { PlatformIcon } from '@/components/icons';
|
||||
|
||||
export default function AccountStatus({
|
||||
accounts,
|
||||
}: {
|
||||
accounts: { platform: PlatformKind; name: string; ok: boolean }[];
|
||||
}) {
|
||||
return (
|
||||
<Card className="pb-4">
|
||||
<CardHeader>
|
||||
<CardTitle>Account status</CardTitle>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="w-full h-[100px] grid gap-2 grid-cols-3">
|
||||
{accounts.map((account, i) => (
|
||||
<div key={i} className="flex">
|
||||
{
|
||||
<PlatformIcon
|
||||
platform={account.platform}
|
||||
width={20}
|
||||
height={20}
|
||||
className="ml-1"
|
||||
/>
|
||||
}
|
||||
|
||||
{account.ok ? (
|
||||
<p className="ml-1">{account.name}</p>
|
||||
) : (
|
||||
<p className="text-[#D21F1F] ml-1">
|
||||
{account.name}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/*RG version*/}
|
||||
{/* {account.ok ? (
|
||||
<p className="text-[#32B25C] ml-1">{account.name}</p>
|
||||
) : (
|
||||
<p className="text-[#D21F1F] ml-1">
|
||||
{account.name}
|
||||
</p>
|
||||
)} */}
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
'use client';
|
||||
|
||||
import { Area, AreaChart, CartesianGrid, XAxis } from 'recharts';
|
||||
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/components/ui/card';
|
||||
import {
|
||||
ChartConfig,
|
||||
ChartContainer,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
} from '@/components/ui/chart';
|
||||
|
||||
const chartConfig = {
|
||||
views: {
|
||||
label: 'Views',
|
||||
color: 'hsl(var(--foreground))',
|
||||
},
|
||||
} satisfies ChartConfig;
|
||||
|
||||
export default function Engagement({
|
||||
chartData,
|
||||
}: {
|
||||
chartData: {
|
||||
week: string;
|
||||
views: number;
|
||||
}[];
|
||||
}) {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Post engagement</CardTitle>
|
||||
<CardDescription>
|
||||
Showing engagements for the last week
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ChartContainer
|
||||
config={chartConfig}
|
||||
className="w-full xl:w-[680px] lg:h-[270px] 2xl:w-[880px]"
|
||||
>
|
||||
<AreaChart
|
||||
accessibilityLayer
|
||||
data={chartData}
|
||||
margin={{
|
||||
left: 12,
|
||||
right: 12,
|
||||
}}
|
||||
>
|
||||
<CartesianGrid vertical={false} />
|
||||
<XAxis
|
||||
dataKey="week"
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickMargin={8}
|
||||
tickFormatter={(value) => value.slice(0, 3)}
|
||||
/>
|
||||
<ChartTooltip
|
||||
cursor={false}
|
||||
content={<ChartTooltipContent indicator="line" />}
|
||||
/>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="fillViews"
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="0"
|
||||
y2="1"
|
||||
>
|
||||
<stop
|
||||
offset="5%"
|
||||
stopColor="var(--color-views)"
|
||||
stopOpacity={0.8}
|
||||
/>
|
||||
<stop
|
||||
offset="95%"
|
||||
stopColor="var(--color-views)"
|
||||
stopOpacity={0.1}
|
||||
/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<Area
|
||||
dataKey="views"
|
||||
type="natural"
|
||||
fill="url(#fillViews)"
|
||||
fillOpacity={0.4}
|
||||
stroke="var(--color-views)"
|
||||
/>
|
||||
</AreaChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
'use client';
|
||||
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
|
||||
export default function Ideas({
|
||||
ideas,
|
||||
}: {
|
||||
ideas: { idea: string; checked: boolean }[];
|
||||
}) {
|
||||
return (
|
||||
<Card className="pb-4">
|
||||
<CardHeader>
|
||||
<CardTitle>Daily content Ideas</CardTitle>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="w-full h-[100px] flex gap-5 flex-col">
|
||||
{ideas.map(({ idea, checked }, i) => (
|
||||
<div className="flex space-x-2" key={i}>
|
||||
<Checkbox
|
||||
id="terms"
|
||||
onClick={(e) => e.preventDefault()}
|
||||
checked={checked}
|
||||
/>
|
||||
<label
|
||||
htmlFor="terms"
|
||||
className="text-lg font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
>
|
||||
{idea}
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import AccountStatus from '@/components/accountStatus';
|
||||
import Engagement from '@/components/engagement';
|
||||
import Ideas from '@/components/ideas';
|
||||
import RecentPosts from '@/components/recentPosts';
|
||||
import AccountStatus from './accountStatus';
|
||||
import Engagement from './engagement';
|
||||
import Ideas from './ideas';
|
||||
import RecentPosts from './recentPosts';
|
||||
import App from '@/components/app';
|
||||
import { Calendar } from '@/components/ui/calendar';
|
||||
import { cookies } from 'next/headers';
|
||||
@@ -12,7 +12,7 @@ async function getSession(sessionId: string) {
|
||||
try {
|
||||
return (
|
||||
await axios.get(`http:/localhost:3000/api/session/?id=${sessionId}`)
|
||||
).data;
|
||||
).data.dashboard;
|
||||
} catch {
|
||||
redirect('/login');
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import { PlatformKind } from '@/utils';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { PlatformIcon } from '@/components/icons';
|
||||
import { format } from 'date-fns';
|
||||
|
||||
export default function RecentPosts({
|
||||
posts,
|
||||
}: {
|
||||
posts: {
|
||||
platform: PlatformKind;
|
||||
profileUrl: string;
|
||||
name: string;
|
||||
date: Date;
|
||||
content: string;
|
||||
}[];
|
||||
}) {
|
||||
return (
|
||||
<Card className="flex flex-col flex-grow">
|
||||
<CardHeader>
|
||||
<CardTitle>Recent posts</CardTitle>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="w-full h-full grid grid-rows-2 gap-4 items-stretch">
|
||||
{posts.map((post, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="border flex flex-col h-full p-4 rounded-xl"
|
||||
>
|
||||
<div className="relative">
|
||||
<div className="flex items-center w-full">
|
||||
<img
|
||||
src={post.profileUrl}
|
||||
alt="PFP"
|
||||
className="size-12 rounded-full"
|
||||
/>
|
||||
<p className="ml-4">{post.name}</p>
|
||||
<PlatformIcon
|
||||
platform={post.platform}
|
||||
width={20}
|
||||
height={20}
|
||||
className="ml-1"
|
||||
/>
|
||||
<p className="ml-1 text-muted-foreground">
|
||||
{format(post.date, 'MMM dd h:mm aa')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full mt-2 flex-1">
|
||||
<p className="line-clamp-2">{post.content}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
+23
-22
@@ -16,30 +16,27 @@ 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;
|
||||
const handleLogin = async () => {
|
||||
try {
|
||||
const { sessionId } = (
|
||||
await axios.post('/api/session/', {
|
||||
email,
|
||||
password,
|
||||
})
|
||||
).data;
|
||||
|
||||
cookies.set('session_id', sessionId);
|
||||
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),
|
||||
});
|
||||
}
|
||||
};
|
||||
router.replace('/dashboard');
|
||||
} catch (err: any) {
|
||||
toast({
|
||||
title: 'Uh oh! Something went wrong.',
|
||||
description: err.response
|
||||
? err.response.data.message
|
||||
: String(err),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white dark:bg-black w-full h-screen flex justify-center items-center">
|
||||
<div className="text-center">
|
||||
@@ -60,7 +57,11 @@ export default function Login() {
|
||||
type="password"
|
||||
placeholder="Your password"
|
||||
onChange={(event) => setPassword(event.target.value)}
|
||||
onClick={handleLogin}
|
||||
onKeyDown={async (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
await handleLogin();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button className="mt-5 w-80 mx-auto" onClick={handleLogin}>
|
||||
|
||||
Reference in New Issue
Block a user