Initial Commit
This commit is contained in:
commit
1cdfc5bad5
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"extends": "next/core-web-vitals"
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
.yarn/install-state.gz
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
.env
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"ci": {
|
||||
"collect": {
|
||||
"staticDistDir": "./public"
|
||||
},
|
||||
"assert": {
|
||||
"assertions": {}
|
||||
},
|
||||
"upload": {
|
||||
"target": "temporary-public-storage"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
|
||||
|
||||
## Getting Started
|
||||
|
||||
First, run the development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
# or
|
||||
pnpm dev
|
||||
# or
|
||||
bun dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
|
||||
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
||||
|
||||
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about Next.js, take a look at the following resources:
|
||||
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||
|
||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
|
||||
|
||||
## Deploy on Vercel
|
||||
|
||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||
|
||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
'use server'
|
||||
import { redirect } from "next/navigation";
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { postMessage } from "@/app/[locale]/(protected)/app/chat/utils";
|
||||
|
||||
|
||||
export const postMessageAction = async (id: string, message: string,) => {
|
||||
const response = await postMessage(id, message)
|
||||
revalidatePath("/");
|
||||
return response;
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
'use server'
|
||||
import { redirect } from "next/navigation";
|
||||
import { revalidatePath } from "next/cache";
|
||||
import {signIn} from "@/lib/auth";
|
||||
export const loginUser = async (data: any) => {
|
||||
try {
|
||||
const response = await signIn("credentials", {
|
||||
email: data.email,
|
||||
password: data.password,
|
||||
redirect: false,
|
||||
});
|
||||
return response;
|
||||
} catch (error) {
|
||||
throw new Error(error as string);
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,243 @@
|
|||
"use client";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import FullCalendar from "@fullcalendar/react"; // must go before plugins
|
||||
import dayGridPlugin from "@fullcalendar/daygrid";
|
||||
import timeGridPlugin from "@fullcalendar/timegrid";
|
||||
import interactionPlugin, { Draggable } from "@fullcalendar/interaction";
|
||||
import listPlugin from "@fullcalendar/list";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import ExternalDraggingevent from "./dragging-events";
|
||||
import { Calendar } from "@/components/ui/calendar";
|
||||
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
||||
import { Plus } from "lucide-react";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { CalendarEvent, CalendarCategory } from "./data"
|
||||
import {
|
||||
EventContentArg,
|
||||
} from '@fullcalendar/core'
|
||||
import EventModal from "./event-modal";
|
||||
import { useTranslations } from "next-intl";
|
||||
const wait = () => new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
interface CalendarViewProps {
|
||||
events: CalendarEvent[];
|
||||
categories: CalendarCategory[];
|
||||
|
||||
|
||||
}
|
||||
|
||||
const CalendarView = ({ events, categories }: CalendarViewProps) => {
|
||||
const [selectedCategory, setSelectedCategory] = useState<string[] | null>(null);
|
||||
const [selectedEventDate, setSelectedEventDate] = useState<Date | null>(null);
|
||||
const [selectedEvent, setSelectedEvent] = useState<CalendarEvent | null>(null);
|
||||
const [draggableInitialized, setDraggableInitialized] = useState<boolean>(false);
|
||||
const t = useTranslations("CalendarApp")
|
||||
// event canvas state
|
||||
const [sheetOpen, setSheetOpen] = useState<boolean>(false);
|
||||
const [date, setDate] = React.useState<Date>(new Date());
|
||||
|
||||
const [dragEvents] = useState([
|
||||
{ title: "New Event Planning", id: "101", tag: "business" },
|
||||
{ title: "Meeting", id: "102", tag: "meeting" },
|
||||
{ title: "Generating Reports", id: "103", tag: "holiday" },
|
||||
{ title: "Create New theme", id: "104", tag: "etc" },
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedCategory(categories?.map((c) => c.value));
|
||||
}, [events, categories]);
|
||||
|
||||
useEffect(() => {
|
||||
const draggableEl = document.getElementById("external-events");
|
||||
|
||||
const initDraggable = () => {
|
||||
if (draggableEl) {
|
||||
new Draggable(draggableEl, {
|
||||
itemSelector: ".fc-event",
|
||||
eventData: function (eventEl) {
|
||||
let title = eventEl.getAttribute("title");
|
||||
let id = eventEl.getAttribute("data");
|
||||
let event = dragEvents.find((e) => e.id === id);
|
||||
let tag = event ? event.tag : "";
|
||||
return {
|
||||
title: title,
|
||||
id: id,
|
||||
extendedProps: {
|
||||
calendar: tag,
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (dragEvents.length > 0) {
|
||||
initDraggable();
|
||||
}
|
||||
|
||||
return () => {
|
||||
draggableEl?.removeEventListener("mousedown", initDraggable);
|
||||
};
|
||||
}, [dragEvents]);
|
||||
// event click
|
||||
const handleEventClick = (arg: any) => {
|
||||
setSelectedEventDate(null);
|
||||
setSheetOpen(true);
|
||||
setSelectedEvent(arg);
|
||||
wait().then(() => (document.body.style.pointerEvents = "auto"));
|
||||
};
|
||||
// handle close modal
|
||||
const handleCloseModal = () => {
|
||||
setSheetOpen(false);
|
||||
setSelectedEvent(null);
|
||||
setSelectedEventDate(null);
|
||||
};
|
||||
const handleDateClick = (arg: any) => {
|
||||
setSheetOpen(true);
|
||||
setSelectedEventDate(arg);
|
||||
setSelectedEvent(null);
|
||||
wait().then(() => (document.body.style.pointerEvents = "auto"));
|
||||
};
|
||||
|
||||
const handleCategorySelection = (category: string) => {
|
||||
if (selectedCategory && selectedCategory.includes(category)) {
|
||||
setSelectedCategory(selectedCategory.filter((c) => c !== category));
|
||||
} else {
|
||||
setSelectedCategory([...selectedCategory || [], category]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClassName = (arg: EventContentArg) => {
|
||||
|
||||
if (arg.event.extendedProps.calendar === "holiday") {
|
||||
return "destructive";
|
||||
}
|
||||
else if (arg.event.extendedProps.calendar === "business") {
|
||||
return "primary";
|
||||
} else if (arg.event.extendedProps.calendar === "personal") {
|
||||
return "success";
|
||||
} else if (arg.event.extendedProps.calendar === "family") {
|
||||
return "info";
|
||||
} else if (arg.event.extendedProps.calendar === "etc") {
|
||||
return "info";
|
||||
} else if (arg.event.extendedProps.calendar === "meeting") {
|
||||
return "warning";
|
||||
}
|
||||
else {
|
||||
return "primary";
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
const filteredEvents = events?.filter((event) =>
|
||||
selectedCategory?.includes(event.extendedProps.calendar)
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="grid grid-cols-12 gap-6 divide-x divide-border">
|
||||
<Card className="col-span-12 lg:col-span-4 2xl:col-span-3 pb-5">
|
||||
<CardContent className="p-0">
|
||||
<CardHeader className="border-none mb-2 pt-5">
|
||||
<Button
|
||||
onClick={handleDateClick}
|
||||
className="dark:bg-background dark:text-foreground"
|
||||
>
|
||||
<Plus className="w-4 h-4 me-1" />
|
||||
{t("addEvent")}
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<div className="px-3">
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={date}
|
||||
onSelect={(s) => {
|
||||
handleDateClick(s);
|
||||
}}
|
||||
className="rounded-md border w-full p-0 border-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div id="external-events" className=" space-y-1.5 mt-6 px-4">
|
||||
<p className="text-sm font-medium text-default-700 mb-3">
|
||||
{t("shortDesc")}
|
||||
</p>
|
||||
{dragEvents.map((event) => (
|
||||
<ExternalDraggingevent key={event.id} event={event} />
|
||||
))}
|
||||
</div>
|
||||
<div className="py-4 text-default-800 font-semibold text-xs uppercase mt-4 mb-2 px-4">
|
||||
{t("filter")}
|
||||
</div>
|
||||
<ul className="space-y-3 px-4">
|
||||
<li className=" flex gap-3">
|
||||
<Checkbox
|
||||
checked={selectedCategory?.length === categories?.length}
|
||||
onClick={() => {
|
||||
if (selectedCategory?.length === categories?.length) {
|
||||
setSelectedCategory([]);
|
||||
} else {
|
||||
setSelectedCategory(categories.map((c) => c.value));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Label>All</Label>
|
||||
</li>
|
||||
{categories?.map((category) => (
|
||||
<li className="flex gap-3 " key={category.value}>
|
||||
<Checkbox
|
||||
className={category.className}
|
||||
id={category.label}
|
||||
checked={selectedCategory?.includes(category.value)}
|
||||
onClick={() => handleCategorySelection(category.value)}
|
||||
/>
|
||||
<Label htmlFor={category.label}>{category.label}</Label>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="col-span-12 lg:col-span-8 2xl:col-span-9 pt-5">
|
||||
<CardContent className="dashcode-app-calendar">
|
||||
<FullCalendar
|
||||
plugins={[
|
||||
dayGridPlugin,
|
||||
timeGridPlugin,
|
||||
interactionPlugin,
|
||||
listPlugin,
|
||||
]}
|
||||
headerToolbar={{
|
||||
left: "prev,next today",
|
||||
center: "title",
|
||||
right: "dayGridMonth,timeGridWeek,timeGridDay,listWeek",
|
||||
}}
|
||||
events={filteredEvents}
|
||||
editable={true}
|
||||
rerenderDelay={10}
|
||||
eventDurationEditable={false}
|
||||
selectable={true}
|
||||
selectMirror={true}
|
||||
droppable={true}
|
||||
dayMaxEvents={2}
|
||||
weekends={true}
|
||||
eventClassNames={handleClassName}
|
||||
dateClick={handleDateClick}
|
||||
eventClick={handleEventClick}
|
||||
initialView="dayGridMonth"
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
<EventModal
|
||||
open={sheetOpen}
|
||||
onClose={handleCloseModal}
|
||||
categories={categories}
|
||||
event={selectedEvent}
|
||||
selectedDate={selectedEventDate}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CalendarView;
|
||||
|
|
@ -0,0 +1,155 @@
|
|||
import { faker } from "@faker-js/faker";
|
||||
|
||||
const date = new Date();
|
||||
const prevDay = new Date().getDate() - 1;
|
||||
const nextDay = new Date(new Date().getTime() + 24 * 60 * 60 * 1000);
|
||||
|
||||
// prettier-ignore
|
||||
const nextMonth = date.getMonth() === 11 ? new Date(date.getFullYear() + 1, 0, 1) : new Date(date.getFullYear(), date.getMonth() + 1, 1)
|
||||
// prettier-ignore
|
||||
const prevMonth = date.getMonth() === 11 ? new Date(date.getFullYear() - 1, 0, 1) : new Date(date.getFullYear(), date.getMonth() - 1, 1)
|
||||
export const calendarEvents = [
|
||||
{
|
||||
id: faker.string.uuid() ,
|
||||
title: "All Day Event",
|
||||
start: date,
|
||||
end: nextDay,
|
||||
allDay: false,
|
||||
//className: "warning",
|
||||
extendedProps: {
|
||||
calendar: "business",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: faker.string.uuid(),
|
||||
title: "Meeting With Client",
|
||||
start: new Date(date.getFullYear(), date.getMonth() + 1, -11),
|
||||
end: new Date(date.getFullYear(), date.getMonth() + 1, -10),
|
||||
allDay: true,
|
||||
//className: "success",
|
||||
extendedProps: {
|
||||
calendar: "personal",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: faker.string.uuid(),
|
||||
title: "Lunch",
|
||||
allDay: true,
|
||||
start: new Date(date.getFullYear(), date.getMonth() + 1, -9),
|
||||
end: new Date(date.getFullYear(), date.getMonth() + 1, -7),
|
||||
// className: "info",
|
||||
extendedProps: {
|
||||
calendar: "family",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: faker.string.uuid(),
|
||||
title: "Birthday Party",
|
||||
start: new Date(date.getFullYear(), date.getMonth() + 1, -11),
|
||||
end: new Date(date.getFullYear(), date.getMonth() + 1, -10),
|
||||
allDay: true,
|
||||
//className: "primary",
|
||||
extendedProps: {
|
||||
calendar: "meeting",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: faker.string.uuid(),
|
||||
title: "Birthday Party",
|
||||
start: new Date(date.getFullYear(), date.getMonth() + 1, -13),
|
||||
end: new Date(date.getFullYear(), date.getMonth() + 1, -12),
|
||||
allDay: true,
|
||||
// className: "danger",
|
||||
extendedProps: {
|
||||
calendar: "holiday",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: faker.string.uuid(),
|
||||
title: "Monthly Meeting",
|
||||
start: nextMonth,
|
||||
end: nextMonth,
|
||||
allDay: true,
|
||||
//className: "primary",
|
||||
extendedProps: {
|
||||
calendar: "business",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export const calendarCategories = [
|
||||
{
|
||||
label: "Business",
|
||||
value: "business",
|
||||
activeClass: "ring-primary-500 bg-primary-500",
|
||||
className: "group-hover:border-blue-500",
|
||||
},
|
||||
{
|
||||
label: "Personal",
|
||||
value: "personal",
|
||||
activeClass: "ring-success-500 bg-success-500",
|
||||
className: " group-hover:border-green-500",
|
||||
},
|
||||
{
|
||||
label: "Holiday",
|
||||
value: "holiday",
|
||||
activeClass: "ring-danger-500 bg-danger-500",
|
||||
className: " group-hover:border-red-500",
|
||||
},
|
||||
{
|
||||
label: "Family",
|
||||
value: "family",
|
||||
activeClass: "ring-info-500 bg-info-500",
|
||||
className: " group-hover:border-cyan-500",
|
||||
},
|
||||
{
|
||||
label: "Meeting",
|
||||
value: "meeting",
|
||||
activeClass: "ring-warning-500 bg-warning-500",
|
||||
className: " group-hover:border-yellow-500",
|
||||
},
|
||||
{
|
||||
label: "Etc",
|
||||
value: "etc",
|
||||
activeClass: "ring-info-500 bg-info-500",
|
||||
className: " group-hover:border-cyan-500",
|
||||
}
|
||||
];
|
||||
|
||||
export const categories = [
|
||||
{
|
||||
label: "Business",
|
||||
value: "business",
|
||||
className: "data-[state=checked]:bg-primary data-[state=checked]:ring-primary",
|
||||
},
|
||||
{
|
||||
label: "Personal",
|
||||
value: "personal",
|
||||
|
||||
className: "data-[state=checked]:bg-success data-[state=checked]:ring-success",
|
||||
},
|
||||
{
|
||||
label: "Holiday",
|
||||
value: "holiday",
|
||||
className: "data-[state=checked]:bg-destructive data-[state=checked]:ring-destructive ",
|
||||
},
|
||||
{
|
||||
label: "Family",
|
||||
value: "family",
|
||||
className: "data-[state=checked]:bg-info data-[state=checked]:ring-info ",
|
||||
},
|
||||
{
|
||||
label: "Meeting",
|
||||
value: "meeting",
|
||||
className: "data-[state=checked]:bg-warning data-[state=checked]:ring-warning",
|
||||
},
|
||||
{
|
||||
label: "Etc",
|
||||
value: "etc",
|
||||
className: "data-[state=checked]:bg-info data-[state=checked]:ring-info",
|
||||
}
|
||||
];
|
||||
|
||||
export type CalendarEvent = (typeof calendarEvents)[number]
|
||||
export type CalendarCategory = (typeof calendarCategories)[number]
|
||||
export type Category = (typeof categories)[number]
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
import { cn } from "@/lib/utils";
|
||||
const ExternalDraggingevent = ({ event }: any) => {
|
||||
const { title, id, tag } = event;
|
||||
|
||||
return (
|
||||
<div
|
||||
title={title}
|
||||
data-id={id}
|
||||
className="fc-event px-4 py-1.5 bg-default-100 dark:bg-default-300 rounded text-sm flex items-center gap-2 shadow-sm cursor-move" >
|
||||
<span
|
||||
className={cn("h-2 w-2 rounded-full block", {
|
||||
"bg-primary": tag === "business",
|
||||
"bg-warning": tag === "meeting",
|
||||
"bg-destructive": tag === "holiday",
|
||||
"bg-info": tag === "etc",
|
||||
})}
|
||||
></span>
|
||||
<span className="text-sm font-medium text-default-900">{title}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExternalDraggingevent;
|
||||
|
|
@ -0,0 +1,277 @@
|
|||
"use client"
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { useForm, Controller } from "react-hook-form";
|
||||
import { cn, } from "@/lib/utils";
|
||||
import { format } from "date-fns"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Calendar } from "@/components/ui/calendar";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
import { Loader2, CalendarIcon } from "lucide-react";
|
||||
import DeleteConfirmationDialog from "@/components/delete-confirmation-dialog";
|
||||
import { CalendarCategory } from "./data";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
|
||||
const schema = z.object({
|
||||
title: z.string().min(3, { message: "Required" }),
|
||||
});
|
||||
|
||||
const EventModal = ({ open, onClose, categories, event, selectedDate }: {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
categories: any;
|
||||
event: any;
|
||||
selectedDate: any
|
||||
}) => {
|
||||
const [startDate, setStartDate] = useState<Date>(new Date());
|
||||
const [endDate, setEndDate] = useState<Date>(new Date());
|
||||
const [isPending, startTransition] = React.useTransition();
|
||||
const [calendarProps, setCalendarProps] = React.useState<any>(categories[0].value);
|
||||
// delete modal state
|
||||
const [deleteModalOpen, setDeleteModalOpen] = useState<boolean>(false);
|
||||
const [eventIdToDelete, setEventIdToDelete] = useState<string | null>(null);
|
||||
|
||||
const {
|
||||
register,
|
||||
control,
|
||||
reset,
|
||||
setValue,
|
||||
formState: { errors },
|
||||
handleSubmit,
|
||||
} = useForm({
|
||||
resolver: zodResolver(schema),
|
||||
mode: "all",
|
||||
});
|
||||
|
||||
const onSubmit = (data: any) => {
|
||||
startTransition(async () => {
|
||||
if (!event) {
|
||||
data.start = startDate;
|
||||
data.end = endDate;
|
||||
data.allDay = false;
|
||||
data.extendedProps = {
|
||||
calendar: calendarProps,
|
||||
};
|
||||
}
|
||||
if (event) {
|
||||
}
|
||||
});
|
||||
};
|
||||
useEffect(() => {
|
||||
if (selectedDate) {
|
||||
setStartDate(selectedDate.date);
|
||||
setEndDate(selectedDate.date);
|
||||
}
|
||||
if (event) {
|
||||
setStartDate(event?.event?.start);
|
||||
setEndDate(event?.event?.end);
|
||||
const eventCalendar = event?.event?.extendedProps?.calendar;
|
||||
if (eventCalendar) {
|
||||
setCalendarProps(eventCalendar);
|
||||
} else {
|
||||
setCalendarProps(categories[0].value);
|
||||
}
|
||||
}
|
||||
setValue("title", event?.event?.title || "");
|
||||
}, [event, selectedDate, open, categories, setValue]);
|
||||
|
||||
const onDeleteEventAction = async () => {
|
||||
try {
|
||||
|
||||
} catch (error) {
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenDeleteModal = (eventId: string) => {
|
||||
setEventIdToDelete(eventId);
|
||||
setDeleteModalOpen(true);
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<DeleteConfirmationDialog
|
||||
open={deleteModalOpen}
|
||||
onClose={() => setDeleteModalOpen(false)}
|
||||
onConfirm={onDeleteEventAction}
|
||||
defaultToast={false}
|
||||
/>
|
||||
<Dialog open={open} onOpenChange={onClose}>
|
||||
<DialogContent
|
||||
onPointerDownOutside={onClose}
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{event ? "Edit Event" : "Create Event"} {event?.title}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="mt-6 h-full">
|
||||
<form className="h-full" onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="space-y-4 pb-5 ">
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="title">Event Name</Label>
|
||||
<Input
|
||||
id="title"
|
||||
type="text"
|
||||
placeholder="Enter Event Name"
|
||||
{...register("title")}
|
||||
/>
|
||||
{errors?.title?.message && (
|
||||
<div className="text-destructive text-sm">
|
||||
{typeof errors?.title?.message === 'string'
|
||||
? errors?.title?.message
|
||||
: JSON.stringify(errors?.title?.message)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="startDate">Start Date </Label>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="md"
|
||||
className={cn(
|
||||
"w-full justify-between text-left font-normal border-default-200 text-default-600 md:px-4",
|
||||
!startDate && "text-muted-foreground"
|
||||
)}
|
||||
>
|
||||
{startDate ? (
|
||||
format(startDate, "PP")
|
||||
) : (
|
||||
<span>Pick a date</span>
|
||||
)}
|
||||
<CalendarIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0">
|
||||
<Controller
|
||||
name="startDate"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={startDate}
|
||||
onSelect={(date) => setStartDate(date as Date)}
|
||||
initialFocus
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="endDate">End Date</Label>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="md"
|
||||
className={cn(
|
||||
"w-full justify-between text-left font-normal border-default-200 text-default-600 md:px-4",
|
||||
!endDate && "text-muted-foreground"
|
||||
)}
|
||||
>
|
||||
{endDate ? (
|
||||
format(endDate, "PP")
|
||||
) : (
|
||||
<span>Pick a date</span>
|
||||
)}
|
||||
<CalendarIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0">
|
||||
<Controller
|
||||
name="endDate"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={endDate}
|
||||
onSelect={(date) => setEndDate(date as Date)}
|
||||
initialFocus
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="calendarProps">Label </Label>
|
||||
<Controller
|
||||
name="calendarProps"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Select
|
||||
value={calendarProps}
|
||||
onValueChange={(data) => setCalendarProps(data)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Label" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{categories.map((category: CalendarCategory) => (
|
||||
<SelectItem
|
||||
value={category.value}
|
||||
key={category.value}
|
||||
>
|
||||
{category.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-2 mt-10">
|
||||
<Button type="submit" disabled={isPending} className="flex-1">
|
||||
{isPending ? (
|
||||
<>
|
||||
<Loader2 className="me-2 h-4 w-4 animate-spin" />
|
||||
{event ? "Updating..." : "Adding..."}
|
||||
</>
|
||||
) : event ? (
|
||||
"Update Event"
|
||||
) : (
|
||||
"Add Event"
|
||||
)}
|
||||
</Button>
|
||||
{event && (
|
||||
<Button
|
||||
type="button"
|
||||
color="destructive"
|
||||
onClick={() => handleOpenDeleteModal(event?.event?.id)}
|
||||
className="flex-1"
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default EventModal;
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
export const metadata = {
|
||||
title: "Calender",
|
||||
};
|
||||
|
||||
const Layout = ({ children }: { children: React.ReactNode }) => {
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
export default Layout;
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
import { getEvents, getCategories } from "./utils";
|
||||
import { Category } from "./data"
|
||||
import CalendarView from "./calender-view";
|
||||
|
||||
|
||||
|
||||
const CalenderPage = async () => {
|
||||
const events = await getEvents();
|
||||
const categories = await getCategories();
|
||||
const formattedCategories = categories.map((category: Category) => ({
|
||||
...category,
|
||||
activeClass: "",
|
||||
}));
|
||||
return (
|
||||
<div>
|
||||
<CalendarView events={events} categories={formattedCategories} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CalenderPage;
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
import { calendarEvents, categories } from "./data";
|
||||
|
||||
// get events
|
||||
export const getEvents = async () => {
|
||||
return calendarEvents;
|
||||
};
|
||||
|
||||
// get categories
|
||||
export const getCategories = async () => {
|
||||
return categories;
|
||||
}
|
||||
|
|
@ -0,0 +1,123 @@
|
|||
"use client";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Icon } from "@/components/ui/icon";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { useMediaQuery } from "@/hooks/use-media-query";
|
||||
import { Contact, ProfileUser } from "@/app/api/chat/data";
|
||||
import { useChatConfig } from "@/hooks/use-chat";
|
||||
|
||||
const ChatHeader = ({ contact }: { contact: any }) => {
|
||||
let active = true;
|
||||
const isLg = useMediaQuery("(max-width: 1024px)");
|
||||
|
||||
const [chatConfig, setChatConfig] = useChatConfig()
|
||||
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
<div className="flex-1 flex gap-3 items-center">
|
||||
{isLg && (
|
||||
<Button size="icon" variant='ghost' color="secondary" onClick={() => setChatConfig({
|
||||
...chatConfig,
|
||||
isOpen: true
|
||||
})}>
|
||||
<Icon icon="heroicons-outline:menu-alt-1" className="w-5 h-5" />
|
||||
</Button>
|
||||
)}
|
||||
<div className="relative inline-block">
|
||||
<Avatar className="border-none shadow-none bg-transparent hover:bg-transparent">
|
||||
<AvatarImage src={contact?.avatar?.src} alt="" />
|
||||
<AvatarFallback>{contact?.fullName?.slice(0, 2)}</AvatarFallback>
|
||||
</Avatar>
|
||||
<Badge
|
||||
className=" h-2 w-2 p-0 ring-1 ring-border ring-offset-[1px] absolute top-2 -end-0.5"
|
||||
color={active ? "success" : "secondary"}
|
||||
></Badge>
|
||||
</div>
|
||||
<div className="hidden lg:block">
|
||||
<div className="text-default-800 text-sm font-medium mb-0.5 ">
|
||||
<span className="relative">{contact?.fullName}</span>
|
||||
</div>
|
||||
<div className="text-default-600 text-xs font-normal">
|
||||
{active ? "Active Now" : "Offline"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-none flex gap-2">
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
size="icon"
|
||||
className="rounded-full bg-default-100 text-default-900 hover:bg-default-100 hover:ring-0 hover:ring-transparent"
|
||||
>
|
||||
<Icon icon="heroicons-outline:phone" className="text-xl" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" align="end">
|
||||
<p>Start a voice call</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
size="icon"
|
||||
className="rounded-full bg-default-100 text-default-900 hover:bg-default-100 hover:ring-0 hover:ring-transparent"
|
||||
>
|
||||
<Icon icon="heroicons-outline:video-camera" className="text-xl" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" align="end">
|
||||
<p>Start a video call</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
{!isLg && (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
onClick={() => setChatConfig({ ...chatConfig, showInfo: !chatConfig.showInfo })}
|
||||
type="button"
|
||||
size="icon"
|
||||
className={cn(
|
||||
"rounded-full bg-default-100 text-primary hover:bg-default-100 hover:ring-0 hover:ring-transparent",
|
||||
{
|
||||
"text-default-900": !chatConfig.showInfo,
|
||||
}
|
||||
)}
|
||||
|
||||
>
|
||||
<span className="text-xl ">
|
||||
{chatConfig.showInfo ? (
|
||||
<Icon icon="heroicons-outline:dots-vertical" className="text-xl" />
|
||||
|
||||
) : (
|
||||
<Icon icon="heroicons-outline:dots-horizontal" className="text-xl" />
|
||||
)}
|
||||
</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" align="end">
|
||||
<p>Conversation information</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChatHeader;
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
'use client'
|
||||
|
||||
import React from 'react'
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { useChatConfig } from '@/hooks/use-chat';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
|
||||
const InfoWrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
const [chatConfig] = useChatConfig();
|
||||
if (!chatConfig.showInfo) return null
|
||||
return (
|
||||
<Card className='w-[285px]'>
|
||||
<ScrollArea className='h-full'>
|
||||
<CardContent className='p-0'> {children}</CardContent>
|
||||
</ScrollArea>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default InfoWrapper
|
||||
|
|
@ -0,0 +1,143 @@
|
|||
"use client";
|
||||
import React, { useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Icon } from "@/components/ui/icon";
|
||||
import { Annoyed, SendHorizontal } from "lucide-react";
|
||||
|
||||
import data from "@emoji-mart/data";
|
||||
import Picker from "@emoji-mart/react";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipArrow,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { Label } from "@/components/ui/label";
|
||||
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { postMessageAction } from "@/action/app-actions";
|
||||
import { useTheme } from "next-themes";
|
||||
|
||||
const MessageFooter = () => {
|
||||
const { theme: mode } = useTheme();
|
||||
const [message, setMessage] = useState("");
|
||||
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setMessage(e.target.value);
|
||||
e.target.style.height = "auto"; // Reset the height to auto to adjust
|
||||
e.target.style.height = `${e.target.scrollHeight - 15}px`;
|
||||
};
|
||||
|
||||
const handleSelectEmoji = (emoji: any) => {
|
||||
setMessage(message + emoji.native);
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
if (!message) return;
|
||||
const data: any = {
|
||||
message,
|
||||
};
|
||||
|
||||
await postMessageAction("55fe838e-9a09-4caf-a591-559803309ef1", "sfsfsf");
|
||||
setMessage("");
|
||||
|
||||
|
||||
};
|
||||
return (
|
||||
<>
|
||||
|
||||
|
||||
<div
|
||||
className="w-full flex items-end gap-1 lg:gap-4 lg:px-4 relative px-2 "
|
||||
>
|
||||
<div className="flex-none flex gap-1 absolute md:static top-0 left-1.5 z-10 ">
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
size="icon"
|
||||
className="rounded-full hover:ring-0 hover:ring-transparent bg-default-100 hover:bg-default-100 hover:text-default-900 text-default-900">
|
||||
<Icon icon="heroicons-outline:link" className="w-5 h-5" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Add link</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
size="icon"
|
||||
className="rounded-full hover:ring-0 hover:ring-transparent bg-default-100 hover:bg-default-100 hover:text-default-900 text-default-900">
|
||||
<Annoyed className="w-6 h-6 text-default" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent side="top" align="start" className="w-fit p-0 shadow-none border-none bottom-0 rtl:left-5 ltr:-left-[110px]">
|
||||
<Picker
|
||||
data={data}
|
||||
onEmojiSelect={handleSelectEmoji}
|
||||
theme={mode === "dark" ? "dark" : "light"}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="flex gap-1 relative">
|
||||
<textarea
|
||||
value={message}
|
||||
onChange={handleChange}
|
||||
placeholder="Type your message..."
|
||||
className="bg-background focus:outline-none rounded-xl break-words ps-8 md:ps-3 px-3 flex-1 h-10 pt-2 p-1 pr-8 no-scrollbar "
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
handleSubmit(e as any);
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
minHeight: "40px",
|
||||
maxHeight: "120px",
|
||||
overflowY: "auto",
|
||||
resize: "none",
|
||||
}}
|
||||
/>
|
||||
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
size="icon"
|
||||
type="submit"
|
||||
className="rounded-full hover:ring-0 hover:ring-transparent bg-default-100 hover:bg-default-100 hover:text-default-900 text-default-900"
|
||||
>
|
||||
<Icon
|
||||
icon="heroicons-outline:paper-airplane"
|
||||
className="transform rotate-[60deg] w-5 h-5"
|
||||
/>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent align="end">
|
||||
<p>Send Message</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default MessageFooter;
|
||||
|
|
@ -0,0 +1,241 @@
|
|||
|
||||
import { getChatsByContactId, getProfileUser } from '../utils'
|
||||
import { Icon } from "@/components/ui/icon";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Card, CardContent, CardHeader, CardFooter } from "@/components/ui/card";
|
||||
import Image from 'next/image';
|
||||
import { redirect } from '@/components/navigation';
|
||||
import MessageFooter from './components/message-footer';
|
||||
import ChatHeader from './components/chat-header';
|
||||
import InfoWrapper from './components/info-wrapper';
|
||||
import { MoreHorizontal } from 'lucide-react';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
||||
|
||||
const socials = [
|
||||
{
|
||||
name: "facebook",
|
||||
icon: "bi:facebook",
|
||||
link: "#",
|
||||
},
|
||||
{
|
||||
name: "twitter",
|
||||
link: "#",
|
||||
icon: "bi:twitter",
|
||||
},
|
||||
{
|
||||
name: "instagram",
|
||||
link: "#",
|
||||
icon: "bi:instagram",
|
||||
},
|
||||
];
|
||||
|
||||
const ChatPageSingle = async ({ params: { id } }: { params: { id: string }; }) => {
|
||||
|
||||
const { chat, contact } = await getChatsByContactId(id)
|
||||
const profile = await getProfileUser()
|
||||
|
||||
if (!contact) {
|
||||
redirect('/app/chat')
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card className="h-full flex flex-col flex-1 ">
|
||||
<CardHeader className="flex-none mb-0 border-b border-default-200 py-5">
|
||||
<ChatHeader contact={contact} />
|
||||
</CardHeader>
|
||||
<CardContent className=" relative flex-1 overflow-y-auto no-scrollbar">
|
||||
{chat && chat?.chat?.length > 0 ? (
|
||||
chat?.chat?.map(({ senderId, message, }, index) => (
|
||||
<div className="block " key={index}>
|
||||
{senderId === "e2c1a571-5f7e-4f56-9020-13f98b0eaba2" ? (
|
||||
<>
|
||||
<div className="flex gap-2 items-start justify-end group w-full mb-4">
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="opacity-0 invisible group-hover:opacity-100 group-hover:visible ">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<span className="w-7 h-7 rounded-full bg-default-200 flex items-center justify-center">
|
||||
<MoreHorizontal />
|
||||
</span>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
className="w-fit"
|
||||
align="end"
|
||||
side="top"
|
||||
>
|
||||
<DropdownMenuItem>Remove</DropdownMenuItem>
|
||||
<DropdownMenuItem>Forward</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
<div className="whitespace-pre-wrap break-all">
|
||||
<div className="bg-default-100 text-default-900 text-sm p-3 font-normal rounded-md ">
|
||||
{message}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="font-normal text-xs text-default-400 dark:text-default-600 text-start mt-1"> 2:40 pm </div>
|
||||
</div>
|
||||
<div className="flex-none self-end -translate-y-5">
|
||||
<div className="h-8 w-8 rounded-full ">
|
||||
<Image
|
||||
src={profile?.avatar}
|
||||
alt=""
|
||||
className="block w-full h-full object-cover rounded-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="flex gap-2 items-start group mb-4">
|
||||
<div className="flex-none self-end -translate-y-5">
|
||||
<div className="h-8 w-8 rounded-full">
|
||||
<Image
|
||||
src={contact?.avatar || `/images/users/user-5.jpg`}
|
||||
alt=""
|
||||
className="block w-full h-full object-cover rounded-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div className="flex-1 flex flex-col gap-2">
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="whitespace-pre-wrap break-all relative z-[1]">
|
||||
<div className="bg-default-100 text-default-900 text-sm p-3 font-normal rounded-md ">
|
||||
{message}
|
||||
</div>
|
||||
</div>
|
||||
<div className="opacity-0 invisible group-hover:opacity-100 group-hover:visible ">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<span className="w-7 h-7 rounded-full bg-default-200 flex items-center justify-center">
|
||||
<MoreHorizontal />
|
||||
</span>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
className="w-fit"
|
||||
align="end"
|
||||
side="top"
|
||||
>
|
||||
<DropdownMenuItem>Remove</DropdownMenuItem>
|
||||
<DropdownMenuItem>Forward</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
<div className="font-normal text-xs text-default-400 dark:text-default-600 text-start mt-1"> 2:40 pm </div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="text-center absolute start-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2">
|
||||
<Icon icon="typcn:messages" className="h-20 w-20 text-default-300 mx-auto" />
|
||||
<div className="mt-4 text-lg font-medium text-default-500">No messages </div>
|
||||
<div className="mt-1 text-sm font-medium text-default-400">{`don't worry, just take a deep breath & say "Hello"`}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</CardContent>
|
||||
<CardFooter className="flex-none flex-col px-0 py-4 border-t border-border">
|
||||
<MessageFooter />
|
||||
</CardFooter>
|
||||
</Card>
|
||||
|
||||
<InfoWrapper>
|
||||
<h4 className="text-xl text-default-900 font-medium mb-8 px-6 mt-6">
|
||||
About
|
||||
</h4>
|
||||
|
||||
<div className='flex flex-col items-center px-6'>
|
||||
<Avatar className="h-24 w-24 border-none shadow-none bg-transparent hover:bg-transparent">
|
||||
<AvatarImage src={contact?.avatar?.src || `/images/users/user-5.jpg`} alt="" />
|
||||
<AvatarFallback>{contact?.fullName?.slice(0, 2)}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="text-center mt-4 ">
|
||||
<h5 className="text-base text-default-600 font-medium mb-1">
|
||||
{contact?.fullName}
|
||||
</h5>
|
||||
<h6 className="text-xs text-default-600 font-normal">
|
||||
{contact?.role}
|
||||
</h6>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul className="mt-5 px-6 space-y-4 border-b border-default-200 pb-5 ">
|
||||
<li className="flex justify-between text-sm text-default-600 ">
|
||||
<div className="flex gap-2 items-start ">
|
||||
<Icon
|
||||
icon="heroicons-outline:location-marker"
|
||||
className="text-base"
|
||||
/>
|
||||
<span>Location</span>
|
||||
</div>
|
||||
<div className="font-medium">Bangladesh</div>
|
||||
</li>
|
||||
<li className="flex justify-between text-sm text-default-600 ">
|
||||
<div className="flex gap-2 items-start">
|
||||
<Icon icon="heroicons-outline:user" className="text-base" />
|
||||
<span>Members since</span>
|
||||
</div>
|
||||
<div className="font-medium">Oct 2021</div>
|
||||
</li>
|
||||
<li className="flex justify-between text-sm text-default-600 ">
|
||||
<div className="flex gap-2 items-start ">
|
||||
<Icon icon="heroicons-outline:translate" className="text-base" />
|
||||
<span>Language</span>
|
||||
</div>
|
||||
<div className="font-medium">English</div>
|
||||
</li>
|
||||
</ul>
|
||||
<ul className="mt-5 px-6 space-y-4 border-b border-default-200 pb-5 ">
|
||||
{socials?.map((slink, sindex) => (
|
||||
<li
|
||||
key={sindex}
|
||||
className="text-sm text-default-600"
|
||||
>
|
||||
<button className="flex gap-2">
|
||||
<Icon icon={slink.icon} className="text-base" />
|
||||
<span className="capitalize font-normal text-default-600">
|
||||
{slink.name}
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<h4 className="py-4 text-sm px-6 text-default-500 font-medium">
|
||||
Shared documents
|
||||
</h4>
|
||||
<div className="grid grid-cols-3 gap-2 px-6">
|
||||
{
|
||||
["/images/chat/sd1.png", "/images/chat/sd2.png", "/images/chat/sd3.png", "/images/chat/sd4.png", "/images/chat/sd5.png", "/images/chat/sd6.png"].map((image, index) => (
|
||||
<Image
|
||||
key={`image-${index}`}
|
||||
src={image}
|
||||
alt=""
|
||||
width={200}
|
||||
height={100}
|
||||
className='w-full h-12 object-cover rounded-md'
|
||||
/>
|
||||
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</InfoWrapper>
|
||||
</>
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
export default ChatPageSingle
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
'use client'
|
||||
import { useMediaQuery } from '@/hooks/use-media-query';
|
||||
import React from 'react'
|
||||
import { useChatConfig } from '@/hooks/use-chat';
|
||||
|
||||
const ChatWrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
const [chatConfig, setChatConfig] = useChatConfig();
|
||||
const { isOpen } = chatConfig
|
||||
const isTablet = useMediaQuery("(min-width: 1024px)");
|
||||
return (
|
||||
<div className=' app-height flex gap-5 relative'>
|
||||
{!isTablet && isOpen && (
|
||||
<div
|
||||
onClick={() => setChatConfig({ ...chatConfig, isOpen: false })}
|
||||
className="overlay bg-default-900 dark:bg-default-900 dark:bg-opacity-60 bg-opacity-60 backdrop-filter
|
||||
backdrop-blur-sm absolute w-full flex-1 inset-0 z-20 rounded-md"
|
||||
|
||||
></div>
|
||||
)}
|
||||
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ChatWrapper
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
"use client";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { useMediaQuery } from "@/hooks/use-media-query";
|
||||
import { Icon } from "@/components/ui/icon";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useChatConfig } from "@/hooks/use-chat";
|
||||
import { useTranslations } from "next-intl";
|
||||
const Blank = () => {
|
||||
const isLg = useMediaQuery("(max-width: 1024px)");
|
||||
const [chatConfig, setChatConfig] = useChatConfig()
|
||||
const t = useTranslations("ChatApp");
|
||||
return (
|
||||
<Card className="flex-1 h-full">
|
||||
<CardContent className="h-full flex justify-center items-center">
|
||||
<div className="text-center flex flex-col items-center">
|
||||
<Icon icon="uiw:message" className="text-7xl text-default-300" />
|
||||
<div className="mt-4 text-lg font-medium text-default-500">
|
||||
{t("blankMessageTitle")}
|
||||
</div>
|
||||
<p className="mt-1 text-sm font-medium text-default-400">
|
||||
{t("blankMessageDesc")}
|
||||
</p>
|
||||
{isLg && (
|
||||
<Button className="mt-2" onClick={() => setChatConfig({ ...chatConfig, isOpen: true })}>
|
||||
Start Conversation
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default Blank;
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
|
||||
'use client'
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Icon } from "@/components/ui/icon";
|
||||
import { type Contact as ContactType, type Chat as ChatType } from "../utils";
|
||||
import { Link, usePathname } from "@/components/navigation";
|
||||
import { useChatConfig } from "@/hooks/use-chat";
|
||||
|
||||
|
||||
const ContactList = ({ contact }: {
|
||||
contact: ContactType,
|
||||
|
||||
}) => {
|
||||
const { avatar, id, fullName, status, about, unreadmessage, date } =
|
||||
contact;
|
||||
|
||||
const pathname = usePathname();
|
||||
const [chatConfig, setChatConfig] = useChatConfig()
|
||||
return (
|
||||
<Link
|
||||
onClick={() => setChatConfig({
|
||||
...chatConfig,
|
||||
isOpen: false
|
||||
})}
|
||||
href={`/app/chat/${id}`} className={cn(
|
||||
" gap-4 py-2 lg:py-2.5 px-3 border-l-2 border-transparent hover:bg-default-100 cursor-pointer flex ",
|
||||
{
|
||||
"lg:bg-default-100 ": `/app/chat/${id}` === pathname
|
||||
}
|
||||
)} >
|
||||
|
||||
|
||||
<div className="flex-1 flex items-center gap-3 ">
|
||||
<div className="relative inline-block ">
|
||||
<Avatar className="border-none bg-transparent hover:bg-transparent">
|
||||
<AvatarImage src={avatar.src} />
|
||||
<AvatarFallback className="uppercase">
|
||||
{fullName.slice(0, 2)}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<Badge
|
||||
className=" h-2 w-2 p-0 ring-1 ring-border ring-offset-[1px] items-center justify-center absolute top-2 -end-[3px]"
|
||||
color={status === "online" ? "success" : "secondary"}
|
||||
></Badge>
|
||||
</div>
|
||||
<div className="block">
|
||||
<div className="truncate max-w-[120px]">
|
||||
<span className="text-sm text-default-900 font-medium">
|
||||
{" "}
|
||||
{fullName}
|
||||
</span>
|
||||
</div>
|
||||
<div className="truncate max-w-[120px]">
|
||||
<span className=" text-xs text-default-700 ">
|
||||
{about}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-none flex-col items-end gap-2 hidden lg:flex">
|
||||
<span className="text-xs text-default-600 text-end uppercase">
|
||||
{date}
|
||||
</span>
|
||||
<span
|
||||
className={cn(
|
||||
"h-[14px] w-[14px] flex items-center justify-center bg-default-400 rounded-full text-default-foreground text-[10px] font-medium",
|
||||
{
|
||||
"bg-[#FFC155]": unreadmessage > 0,
|
||||
}
|
||||
)}
|
||||
>
|
||||
{unreadmessage === 0 ? (
|
||||
<Icon icon="uil:check" className="text-sm" />
|
||||
) : (
|
||||
unreadmessage
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
export default ContactList;
|
||||
|
|
@ -0,0 +1,116 @@
|
|||
'use client'
|
||||
|
||||
import { Icon } from "@/components/ui/icon"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { useChatConfig } from "@/hooks/use-chat";
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
|
||||
const MyProfile = () => {
|
||||
const [chatConfig, setChatConfig] = useChatConfig();
|
||||
let status = "active";
|
||||
return (
|
||||
<div>
|
||||
<div className="flex justify-between gap-1">
|
||||
<div className="flex gap-3">
|
||||
<div className="flex-none">
|
||||
<Avatar>
|
||||
<AvatarImage src="/images/users/user-1.jpg" />
|
||||
<AvatarFallback>SC</AvatarFallback>
|
||||
</Avatar>
|
||||
</div>
|
||||
<div className="flex-1 text-start">
|
||||
<div className="text-default-800 text-sm font-medium mb-1">
|
||||
Jane Cooper
|
||||
<span className="bg-success inline-block h-2.5 w-2.5 rounded-full ms-3"></span>
|
||||
</div>
|
||||
<div className=" text-default-500 text-xs font-normal">
|
||||
Available
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
size="icon"
|
||||
className="w-8 h-8"
|
||||
color="secondary"
|
||||
rounded="full"
|
||||
onClick={() => setChatConfig({ ...chatConfig, showProfile: true })}
|
||||
>
|
||||
<Icon icon="heroicons-outline:dots-horizontal" className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className={cn('absolute bg-card rounded-md h-full start-0 top-0 bottom-0 w-full z-50', {
|
||||
'hidden -start-full': !chatConfig.showProfile
|
||||
})}>
|
||||
<ScrollArea className="h-full">
|
||||
<div className="p-6">
|
||||
<div className="flex justify-end">
|
||||
<Button size="icon" color="secondary" className="w-8 h-8" rounded="full" onClick={() => setChatConfig({ ...chatConfig, showProfile: false })}>
|
||||
<Icon icon="heroicons-outline:x" className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<div className="relative">
|
||||
<Avatar className="h-16 w-16 border border-default-200 p-1 bg-transparent hover:bg-transparent ">
|
||||
<AvatarImage src="/images/users/user-1.jpg" className="rounded-full" />
|
||||
<AvatarFallback>SC</AvatarFallback>
|
||||
</Avatar>
|
||||
<span
|
||||
className={cn("absolute top-3 -end-[3px] h-3 w-3 rounded-full bg-success border border-primary-foreground",
|
||||
{
|
||||
"bg-success": status === "active",
|
||||
"bg-warning": status === "away",
|
||||
"bg-destructive": status === "busy",
|
||||
"bg-secondary": status === "offline",
|
||||
}
|
||||
)}
|
||||
></span>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="text-default-800 text-sm font-medium mb-0.5">
|
||||
Jane Cooper
|
||||
</div>
|
||||
<div className=" text-default-500 text-xs font-normal">
|
||||
Admin
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="my-8">
|
||||
<Label htmlFor="bio" className="mb-2 block text-default-900"> About </Label>
|
||||
<Textarea id="bio" placeholder="About your self" />
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="status" className="block mb-3 text-default-700">Status</Label>
|
||||
<RadioGroup defaultValue="comfortable">
|
||||
<div className="flex items-center gap-2">
|
||||
<RadioGroupItem value="default" id="active" color="success" />
|
||||
<Label htmlFor="active">Active</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<RadioGroupItem value="busy" id="busy" color="destructive" />
|
||||
<Label htmlFor="busy">Do Not Disturb</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<RadioGroupItem value="away" id="away" color="warning" />
|
||||
<Label htmlFor="away">Away</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<RadioGroupItem value="offline" id="offline" color="warning" />
|
||||
<Label htmlFor="offline">Offline</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
<Button className="mt-7">Logout</Button>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MyProfile;
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
import { Input } from '@/components/ui/input';
|
||||
import React from 'react';
|
||||
import {Search as SearchIcon} from "lucide-react"
|
||||
const Search = () => {
|
||||
return (
|
||||
<div className='relative'>
|
||||
<SearchIcon className='absolute top-1/2 -translate-y-1/2 start-6 w-4 h-4 text-default-600' />
|
||||
<Input placeholder='Search...' className='dark:bg-transparent rounded-none border-l-0 border-r-0 dark:border-default-300 focus:border-default-200 ps-12 text-lg font-normal' size="lg"/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Search;
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
import { Metadata } from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Dashcode Next Js",
|
||||
description: "Dashcode is a popular dashboard template.",
|
||||
};
|
||||
const Layout = ({ children }: { children: React.ReactNode }) => {
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
export default Layout;
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
} from "@/components/ui/card";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import ContactList from "./components/contact-list";
|
||||
import { getContacts } from './utils';
|
||||
import MyProfile from './components/my-profile';
|
||||
import Search from './components/search';
|
||||
import ChatSidebarWrapper from './sidebar-wrapper';
|
||||
import ChatWrapper from './chat-wrapper';
|
||||
|
||||
|
||||
|
||||
const layout = async ({ children }: { children: React.ReactNode }) => {
|
||||
|
||||
const contacts = await getContacts()
|
||||
return (
|
||||
<ChatWrapper>
|
||||
<ChatSidebarWrapper>
|
||||
<Card className="h-full pb-0 ">
|
||||
<CardHeader className="border-none pb-3">
|
||||
<MyProfile />
|
||||
</CardHeader>
|
||||
<CardContent className="pt-0 px-0 h-full ">
|
||||
<ScrollArea className="lg:h-[calc(100%-62px)] h-[calc(100%-80px)] ">
|
||||
<div className='sticky top-0 z-10 bg-card'>
|
||||
<Search />
|
||||
</div>
|
||||
{
|
||||
contacts?.map((contact) => {
|
||||
return (
|
||||
<ContactList
|
||||
key={contact.id}
|
||||
contact={contact}
|
||||
/>
|
||||
)
|
||||
})
|
||||
}
|
||||
</ScrollArea>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</ChatSidebarWrapper>
|
||||
<div className='flex-1 h-full flex gap-5'>
|
||||
{children}
|
||||
</div>
|
||||
</ChatWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
export default layout
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
import Blank from "./components/blank-chat"
|
||||
const ChatPage = async () => {
|
||||
|
||||
return (
|
||||
<Blank />
|
||||
)
|
||||
}
|
||||
|
||||
export default ChatPage
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
'use client'
|
||||
import { useMediaQuery } from '@/hooks/use-media-query'
|
||||
import { cn } from '@/lib/utils'
|
||||
import React from 'react'
|
||||
import { useChatConfig } from '@/hooks/use-chat'
|
||||
const ChatSidebarWrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
const [chatConfig] = useChatConfig()
|
||||
const { isOpen } = chatConfig
|
||||
const isTablet = useMediaQuery("(min-width: 1024px)");
|
||||
if (!isTablet) {
|
||||
return (
|
||||
<div className={cn('absolute h-full start-0 w-[240px] z-50 ', {
|
||||
'-start-full': !isOpen
|
||||
})}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div className='relative w-[320px] h-full '>{children}</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ChatSidebarWrapper
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
import { chats, contacts, profileUser } from '@/app/api/chat/data'
|
||||
import { baseURL } from '@/config'
|
||||
|
||||
|
||||
export const getContacts = async () => {
|
||||
return contacts
|
||||
}
|
||||
|
||||
// get chats by contact id
|
||||
export const getChatsByContactId = async (contactId: string) => {
|
||||
|
||||
const chat = chats.find(chat => chat.id === contactId)
|
||||
const contact = contacts.find(contact => contact.id === contactId)
|
||||
return {
|
||||
chat,
|
||||
contact
|
||||
}
|
||||
}
|
||||
// get contact by id
|
||||
export const getContactById = async (contactId: string) => {
|
||||
return contacts.find(contact => contact.id === contactId)
|
||||
}
|
||||
|
||||
|
||||
// get profile user
|
||||
export const getProfileUser = async () => {
|
||||
return profileUser
|
||||
}
|
||||
export type Chat = typeof chats[number];
|
||||
export type Contact = typeof contacts[number];
|
||||
export type ProfileUser = typeof profileUser;
|
||||
|
||||
// post message
|
||||
export const postMessage = async (id: string, data: any) => {
|
||||
const res = await fetch(`${baseURL}/chat/${id}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
if (!res.ok) {
|
||||
throw new Error(`Error creating task`);
|
||||
}
|
||||
return res.json();
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
'use client'
|
||||
import {
|
||||
MoveLeft,
|
||||
|
||||
} from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useRouter } from '@/components/navigation';
|
||||
const GoBack = () => {
|
||||
const router = useRouter()
|
||||
return (
|
||||
<Button
|
||||
onClick={router.back}
|
||||
size="icon"
|
||||
className="rounded-full bg-default-100 hover:text-default-50 hover:outline-0 hover:outline-offset-0 hover:border-0 hover:ring-0 text-default-600 hover:ring-offset-0 p-4"
|
||||
>
|
||||
<MoveLeft className=" h-5 w-5" />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
export default GoBack
|
||||
|
|
@ -0,0 +1,186 @@
|
|||
import { Link } from '@/i18n/routing';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardHeader, CardContent } from "@/components/ui/card";
|
||||
import {
|
||||
Box,
|
||||
Ellipsis,
|
||||
LogOutIcon,
|
||||
Printer,
|
||||
Star,
|
||||
} from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import { getMailById } from "../utils";
|
||||
import { Alert } from "@/components/ui/alert";
|
||||
import GoBack from "./components/go-back"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip"
|
||||
import { Icon } from "@/components/ui/icon";
|
||||
const MailDetails = async ({ params: { id } }: { params: { id: string }; }) => {
|
||||
const mail = await getMailById(id)
|
||||
if (!mail) {
|
||||
return <Alert color="destructive"> Mail not found</Alert>
|
||||
}
|
||||
return (
|
||||
<Card className=" h-full overflow-auto">
|
||||
<CardHeader className="flex flex-row items-center justify-between p-4 border-b border-solid">
|
||||
<GoBack />
|
||||
<div className="flex gap-3">
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
size="icon"
|
||||
className="rounded-full bg-default-100 hover:text-default-50 hover:outline-0 hover:outline-offset-0 hover:border-0 hover:ring-0 text-default-600 hover:ring-offset-0 p-4"
|
||||
>
|
||||
<LogOutIcon className=" h-5 w-5" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Forward</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
size="icon"
|
||||
className="rounded-full bg-default-100 hover:text-default-50 hover:outline-0 hover:outline-offset-0 hover:border-0 hover:ring-0 text-default-600 hover:ring-offset-0 p-4"
|
||||
>
|
||||
<Star className=" h-5 w-5" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Favourite</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
size="icon"
|
||||
className="rounded-full bg-default-100 hover:text-default-50 hover:outline-0 hover:outline-offset-0 hover:border-0 hover:ring-0 text-default-600 hover:ring-offset-0 p-4"
|
||||
>
|
||||
<Box className=" h-5 w-5" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Archive</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
size="icon"
|
||||
className="rounded-full bg-default-100 hover:text-default-50 hover:outline-0 hover:outline-offset-0 hover:border-0 hover:ring-0 text-default-600 hover:ring-offset-0 p-4"
|
||||
>
|
||||
<Printer className=" h-5 w-5" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Print</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
size="icon"
|
||||
className="rounded-full bg-default-100 hover:text-default-50 hover:outline-0 hover:outline-offset-0 hover:border-0 hover:ring-0 text-default-600 hover:ring-offset-0 p-4"
|
||||
>
|
||||
<Ellipsis className=" h-5 w-5" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Actions</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent >
|
||||
<p className="text-lg font-semibold text-default-800 mt-6">
|
||||
Pay bills & win up to 600$ Cashback!
|
||||
</p>
|
||||
<div className="flex items-center mt-4 gap-4">
|
||||
<Avatar className=" h-8 w-8">
|
||||
<AvatarImage
|
||||
src="https://images.unsplash.com/photo-1531427186611-ecfd6d936c79?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=687&q=80"
|
||||
alt="Avatar image"
|
||||
/>
|
||||
<AvatarFallback>JD</AvatarFallback>
|
||||
</Avatar>
|
||||
<p className="text-sm text-default-700 font-semibold">John Doe</p>
|
||||
</div>
|
||||
<div className="my-4 border-b border-solid border-default-200 pb-6 space-y-4 text-default-600 text-base">
|
||||
<p>Hi Jane Cooper,</p>
|
||||
<p>
|
||||
Jornalists call this critical, introductory section the Lede, and
|
||||
when bridge properly executed, it is that carries your reader from
|
||||
an headine try at attention-grabbing to the body of your blog post,
|
||||
if you want to get it right on of these 10 clever ways to omen your
|
||||
next blog
|
||||
</p>
|
||||
<p>
|
||||
posr with a bang With resrpect, i must disagree with Mr.Zinsser. We
|
||||
all know the most part of important part of any article is the
|
||||
title.Without a compelleing title, your reader will not even get to
|
||||
the first sentence.After the title, however, the first few sentences
|
||||
of your article are certainly the most important part.
|
||||
</p>
|
||||
<div>
|
||||
<p>Best regards,</p>
|
||||
<p>Esther Howard</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="py-5 flex gap-5 flex-wrap items-center border-b border-solid border-default-200">
|
||||
<div className="flex flex-col items-center">
|
||||
<Image
|
||||
className="w-[150px] h-[95px]"
|
||||
width={500}
|
||||
height={300}
|
||||
src="/images/all-img/inbox-1.png"
|
||||
alt="mail"
|
||||
/>
|
||||
<Link className="text-primary mt-1" href="/">Download</Link>
|
||||
</div>
|
||||
<div className="flex flex-col items-center">
|
||||
<Image
|
||||
className="w-[150px] h-[95px]"
|
||||
width={500}
|
||||
height={300}
|
||||
src="/images/all-img/inbox-2.png"
|
||||
alt="mail"
|
||||
/>
|
||||
<Link className="text-primary mt-1" href="/">Download</Link>
|
||||
</div>
|
||||
<div className="flex flex-col items-center">
|
||||
<Image
|
||||
className="w-[150px] h-[95px]"
|
||||
width={500}
|
||||
height={300}
|
||||
src="/images/all-img/inbox-3.png"
|
||||
alt="mail"
|
||||
/>
|
||||
<Link className="text-primary mt-1" href="/">Download</Link>
|
||||
</div>
|
||||
</div>
|
||||
<Button color="secondary" variant="soft" size="md" className="mt-4">
|
||||
<Icon icon="heroicons:chat-bubble-left-right" className="h-5 w-5 me-3" /> Reply
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default MailDetails;
|
||||
|
|
@ -0,0 +1,121 @@
|
|||
'use client'
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import Select, { MultiValue } from 'react-select'
|
||||
import { Plus } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import { useForm, SubmitHandler, Controller } from "react-hook-form"
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { useState } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
|
||||
|
||||
interface Option {
|
||||
value: string;
|
||||
label: string;
|
||||
image?: string;
|
||||
}
|
||||
|
||||
type Inputs = {
|
||||
subject: string;
|
||||
description: string;
|
||||
mailTo: MultiValue<Option>;
|
||||
}
|
||||
|
||||
const mailToOptions: Option[] = [
|
||||
{ value: "mahedi", label: "Mahedi Amin", image: "/images/avatar/av-1.svg" },
|
||||
{ value: "sovo", label: "Sovo Haldar", image: "/images/avatar/av-2.svg" },
|
||||
{ value: "rakibul", label: "Rakibul Islam", image: "/images/avatar/av-3.svg" },
|
||||
{ value: "pritom", label: "Pritom Miha", image: "/images/avatar/av-4.svg" },
|
||||
];
|
||||
|
||||
|
||||
const Compose = () => {
|
||||
const t = useTranslations("EmailApp");
|
||||
const [open, setOpen] = useState<boolean>(false);
|
||||
const [value, setValue] = useState<string>('Hello World');
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
control
|
||||
} = useForm<Inputs>();
|
||||
const onSubmit: SubmitHandler<Inputs> = (data) => {
|
||||
setOpen(false)
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<Button
|
||||
fullWidth
|
||||
size="lg"
|
||||
onClick={() => setOpen(true)}
|
||||
className="dark:bg-background dark:ring-background dark:text-foreground"
|
||||
>
|
||||
<Plus className="w-6 h-6 me-1.5" />
|
||||
{t("compose")}
|
||||
</Button>
|
||||
|
||||
<DialogContent>
|
||||
<DialogHeader className="mb-6">
|
||||
<DialogTitle> {t("composeEmail")}</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-3.5">
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="assignee">To</Label>
|
||||
<Controller
|
||||
name="mailTo"
|
||||
control={control}
|
||||
defaultValue={[]}
|
||||
render={({ field }) => (
|
||||
<Select
|
||||
className="react-select"
|
||||
classNamePrefix="select"
|
||||
{...field}
|
||||
options={mailToOptions}
|
||||
isMulti
|
||||
onChange={(selectedOption) => field.onChange(selectedOption)}
|
||||
getOptionLabel={(option) => (
|
||||
<div className="flex items-center">
|
||||
<Image width={40} height={40} src={option.image as string} alt={option.label} className="w-8 h-8 rounded-full me-2" />
|
||||
<span className="text-sm font-medium">{option.label}</span>
|
||||
</div>
|
||||
) as unknown as string}
|
||||
placeholder="Select..."
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="subject">Subject</Label>
|
||||
<Input
|
||||
id="subject"
|
||||
placeholder="Type Subject..."
|
||||
{...register("subject")}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="description">Description</Label>
|
||||
<Textarea placeholder="Hello world" id="description" {...register("description")}/>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button>Submit</Button>
|
||||
</div>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default Compose;
|
||||
|
|
@ -0,0 +1,213 @@
|
|||
|
||||
export const mails = [
|
||||
{
|
||||
id: "fe3aa1aa-c89d-4539-8301-8bf18bd80e87",
|
||||
image: [
|
||||
{
|
||||
image: "/images/avatar/avatar-4.png",
|
||||
label: "Mahedi Amin",
|
||||
value: "mahedi",
|
||||
},
|
||||
],
|
||||
title: "laboriosam mollitia et enim quasi adipisci quia provident illum",
|
||||
isDone: false,
|
||||
name: "Ester Howard",
|
||||
isfav: false,
|
||||
time: "12.00 pm",
|
||||
isTrash: false,
|
||||
category: [
|
||||
{
|
||||
value: "team",
|
||||
label: "team",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "9d7135af-2c69-44f6-a1b5-e7c42126e1e9",
|
||||
image: [
|
||||
{
|
||||
image: "/images/avatar/avatar-2.png",
|
||||
label: "Rakibul Islam",
|
||||
value: "rakibul",
|
||||
},
|
||||
],
|
||||
title:
|
||||
"Amet minim mollit non deserunt ullamco est sit aliqua dolor do amet sint.",
|
||||
isDone: false,
|
||||
name: "Ester Howard",
|
||||
isfav: true,
|
||||
time: "12.00 pm",
|
||||
isTrash: false,
|
||||
category: [
|
||||
{
|
||||
value: "low",
|
||||
label: "low",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "fff45fba-157c-4a9a-a839-480e4c94f927",
|
||||
image: [
|
||||
{
|
||||
image: "/images/avatar/avatar-1.png",
|
||||
label: "Rakibul Islam",
|
||||
value: "rakibul",
|
||||
},
|
||||
],
|
||||
title:
|
||||
"Amet minim mollit non deserunt ullamco est sit aliqua dolor do amet sint.",
|
||||
isDone: true,
|
||||
isfav: true,
|
||||
time: "12.00 pm",
|
||||
name: "Ester Howard",
|
||||
isTrash: false,
|
||||
category: [
|
||||
{
|
||||
value: "medium",
|
||||
label: "medium",
|
||||
},
|
||||
{
|
||||
value: "low",
|
||||
label: "low",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "4c5bd598-f951-4a4e-981d-b1f0af0cc917",
|
||||
image: [
|
||||
{
|
||||
image: "/images/avatar/avatar-3.png",
|
||||
label: "Mahedi Amin",
|
||||
value: "mahedi",
|
||||
},
|
||||
],
|
||||
title: "illo expedita consequatur quia in",
|
||||
isDone: false,
|
||||
name: "Ester Howard",
|
||||
isfav: false,
|
||||
time: "12.00 pm",
|
||||
isTrash: false,
|
||||
category: [
|
||||
{
|
||||
value: "high",
|
||||
label: "high",
|
||||
},
|
||||
{
|
||||
value: "low",
|
||||
label: "low",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "bc1ad0b1-d6f8-42e1-9689-21c8f2814396",
|
||||
image: [
|
||||
{
|
||||
image: "/images/avatar/avatar-5.png",
|
||||
label: "Rakibul Islam",
|
||||
value: "rakibul",
|
||||
},
|
||||
],
|
||||
title: "illo expedita consequatur quia in",
|
||||
isDone: false,
|
||||
name: "Ester Howard",
|
||||
isfav: false,
|
||||
time: "12.00 pm",
|
||||
isTrash: false,
|
||||
category: [
|
||||
{
|
||||
value: "update",
|
||||
label: "update",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "6f4eabf5-e549-4fd1-a24f-87093ebd31be",
|
||||
image: [
|
||||
{
|
||||
image: "/images/avatar/avatar-5.png",
|
||||
label: "Rakibul Islam",
|
||||
value: "rakibul",
|
||||
},
|
||||
],
|
||||
title: "illo expedita consequatur quia in",
|
||||
isDone: false,
|
||||
name: "Ester Howard",
|
||||
isfav: false,
|
||||
time: "12.00 pm",
|
||||
isTrash: false,
|
||||
category: [
|
||||
{
|
||||
value: "update",
|
||||
label: "update",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "ed6df190-9141-4048-81d0-1ac067f37e46",
|
||||
image: [
|
||||
{
|
||||
image: "/images/avatar/avatar-5.png",
|
||||
label: "Rakibul Islam",
|
||||
value: "rakibul",
|
||||
},
|
||||
],
|
||||
title: "illo expedita consequatur quia in",
|
||||
isDone: false,
|
||||
name: "Ester Howard",
|
||||
isfav: false,
|
||||
time: "12.00 pm",
|
||||
isTrash: false,
|
||||
category: [
|
||||
{
|
||||
value: "update",
|
||||
label: "update",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "a296f702-a7ed-4127-a42a-121669b02b1a",
|
||||
image: [
|
||||
{
|
||||
image: "/images/avatar/avatar-5.png",
|
||||
label: "Rakibul Islam",
|
||||
value: "rakibul",
|
||||
},
|
||||
],
|
||||
title: "illo expedita consequatur quia in",
|
||||
isDone: false,
|
||||
name: "Ester Howard",
|
||||
isfav: false,
|
||||
time: "12.00 pm",
|
||||
isTrash: false,
|
||||
category: [
|
||||
{
|
||||
value: "update",
|
||||
label: "update",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "6d4be8c8-a382-4d4a-8df5-a2234e69b57f",
|
||||
image: [
|
||||
{
|
||||
image: "/images/avatar/avatar-5.png",
|
||||
label: "Rakibul Islam",
|
||||
value: "rakibul",
|
||||
},
|
||||
],
|
||||
title: "illo expedita consequatur quia in",
|
||||
isDone: false,
|
||||
name: "Ester Howard",
|
||||
isfav: false,
|
||||
time: "12.00 pm",
|
||||
isTrash: false,
|
||||
category: [
|
||||
{
|
||||
value: "update",
|
||||
label: "update",
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
export type Mail = (typeof mails)[number];
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
import { Metadata } from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Dashcode Next Js",
|
||||
description: "Dashcode is a popular dashboard template.",
|
||||
};
|
||||
const Layout = ({ children }: { children: React.ReactNode }) => {
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
export default Layout;
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
import { Metadata } from "next";
|
||||
import MailWrapper from "./mail-wrapper";
|
||||
import MailSidebarWrapper from "./sidebar-wrapper";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import Compose from "./compose";
|
||||
import Nav from "@/components/nav";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
export const metadata: Metadata = {
|
||||
title: "Mail",
|
||||
description: "Mail Application",
|
||||
};
|
||||
const Layout = ({ children }: { children: React.ReactNode }) => {
|
||||
|
||||
return <MailWrapper>
|
||||
<div className="flex gap-5 h-full ">
|
||||
<MailSidebarWrapper>
|
||||
<Card className=" h-full ">
|
||||
<CardContent className=" h-full space-y-2 pt-6 px-0">
|
||||
<div className="px-5">
|
||||
<Compose />
|
||||
</div>
|
||||
<ScrollArea className="h-[calc(100%-30px)]">
|
||||
<Nav
|
||||
links={[
|
||||
{
|
||||
title: "Inbox",
|
||||
icon: "uil:image-v",
|
||||
active: true,
|
||||
},
|
||||
{
|
||||
title: "Starred",
|
||||
icon: "heroicons:star",
|
||||
active: false,
|
||||
},
|
||||
{
|
||||
title: "Sent",
|
||||
icon: "heroicons:paper-airplane",
|
||||
active: false,
|
||||
},
|
||||
{
|
||||
title: "Drafts",
|
||||
icon: "heroicons:pencil-square",
|
||||
active: false,
|
||||
},
|
||||
{
|
||||
title: "Spam",
|
||||
icon: "heroicons:information-circle",
|
||||
active: false,
|
||||
},
|
||||
{
|
||||
title: "Trash",
|
||||
icon: "heroicons:trash",
|
||||
active: false,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<div className="space-y-2 mt-3">
|
||||
<p className="text-sm font-medium text-default-900 px-5">TAGS</p>
|
||||
<Nav
|
||||
dotStyle
|
||||
links={[
|
||||
{
|
||||
title: "personal",
|
||||
active: true,
|
||||
},
|
||||
{
|
||||
title: "social",
|
||||
active: false,
|
||||
},
|
||||
{
|
||||
title: "promotions",
|
||||
active: false,
|
||||
},
|
||||
{
|
||||
title: "business",
|
||||
active: false,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</MailSidebarWrapper>
|
||||
<div className="flex-1 w-full h-full">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</MailWrapper>;
|
||||
};
|
||||
|
||||
export default Layout;
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
import { Input } from "@/components/ui/input";
|
||||
import { MoreVertical, Search } from "lucide-react";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Icon } from "@/components/ui/icon";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import ToggleMailSidebar from "./toggle-mail-sidebar";
|
||||
|
||||
const MailHeader = () => {
|
||||
const actions = [
|
||||
{
|
||||
name: "Reset Sort",
|
||||
icon: "heroicons-outline:sort-ascending",
|
||||
},
|
||||
{
|
||||
name: "Sort A-Z ",
|
||||
icon: "heroicons-outline:sort-ascending",
|
||||
},
|
||||
{
|
||||
name: "Sort Z-A ",
|
||||
icon: "heroicons-outline:sort-descending",
|
||||
},
|
||||
];
|
||||
return (
|
||||
<div className="flex gap-2">
|
||||
<div className="flex-1 flex items-center gap-1">
|
||||
<ToggleMailSidebar />
|
||||
<Checkbox className="w-4 h-4 text-default-400 dark:bg-default-300" />
|
||||
<Input
|
||||
placeholder="Search Mail"
|
||||
className="max-w-[180px] border-none font-medium dark:bg-transparent"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-none">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button size="icon" className="w-8 h-8 rounded-full bg-default-100">
|
||||
<MoreVertical className="w-5 h-5 text-default hover:text-default-foreground" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="end"
|
||||
className="p-0 rounded-md overflow-hidden"
|
||||
>
|
||||
{actions.map((action, index) => (
|
||||
<DropdownMenuItem
|
||||
key={`action-${index}`}
|
||||
className="flex items-center gap-1.5 p-2 border-b text-default-600 group focus:bg-default focus:text-primary-foreground rounded-none group"
|
||||
>
|
||||
<Icon
|
||||
icon={action.icon}
|
||||
className="group-hover:text-primary-foreground w-4 h-4"
|
||||
/>
|
||||
<span className="text-default-700 group-hover:text-primary-foreground">
|
||||
{action.name}
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MailHeader;
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
'use client'
|
||||
import { useMediaQuery } from '@/hooks/use-media-query';
|
||||
import { useMailConfig } from '@/hooks/use-mail'
|
||||
import React from 'react'
|
||||
|
||||
const MailWrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
const [mailConfig, setMailConfig] = useMailConfig();
|
||||
const { isOpen } = mailConfig
|
||||
const isTablet = useMediaQuery("(min-width: 1024px)");
|
||||
return (
|
||||
<div className=' relative app-height'>
|
||||
{!isTablet && isOpen && (
|
||||
<div
|
||||
onClick={() => setMailConfig({ ...mailConfig, isOpen: false })}
|
||||
className="overlay bg-default-900 dark:bg-default-900 dark:bg-opacity-60 bg-opacity-60 backdrop-filter
|
||||
backdrop-blur-sm absolute w-full flex-1 inset-0 z-20 rounded-md"
|
||||
|
||||
></div>
|
||||
)}
|
||||
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default MailWrapper
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
"use client";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Icon } from "@/components/ui/icon";
|
||||
import { Avatar, AvatarImage } from "@/components/ui/avatar";
|
||||
import { AvatarFallback } from "@radix-ui/react-avatar";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import {ReactNode,useState} from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Trash2 } from "lucide-react";
|
||||
import DeleteConfirmationDialog from "@/components/delete-confirmation-dialog";
|
||||
import { Link } from "@/components/navigation"
|
||||
const MailItem = ({ mail }: { mail: any }) => {
|
||||
const { image, title, isfav, time, name, id } = mail;
|
||||
const [open, setOpen] = useState<boolean>(false)
|
||||
return (
|
||||
<>
|
||||
<DeleteConfirmationDialog
|
||||
open={open}
|
||||
onClose={() => setOpen(false)}
|
||||
/>
|
||||
<Link href={`/app/email/${id}`} className=" group block border-b border-default-200 dark:border-default-300 last:border-none" >
|
||||
<div className="flex items-center gap-4 group-hover:bg-default-50 last:border-none px-6 py-4 translate-y-0">
|
||||
<div>
|
||||
<Checkbox className="mt-0.5 dark:bg-default-300" />
|
||||
</div>
|
||||
<div className="ms-1">
|
||||
{isfav ? (
|
||||
<Icon
|
||||
icon="heroicons:star-20-solid"
|
||||
className="text-xl cursor-pointer text-[#FFCE30]"
|
||||
/>
|
||||
) : (
|
||||
<Icon
|
||||
icon="heroicons:star"
|
||||
className="text-xl cursor-pointer text-default-400"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="-space-x-1.5 rtl:space-x-reverse">
|
||||
{image.map(
|
||||
(
|
||||
item: {
|
||||
label: ReactNode;
|
||||
image: string;
|
||||
},
|
||||
index: any
|
||||
) => (
|
||||
<TooltipProvider key={`avatar-${index}`}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Avatar className="h-7 w-7 border-none shadow-none bg-transparent">
|
||||
<AvatarImage src={item.image} />
|
||||
<AvatarFallback>SA</AvatarFallback>
|
||||
</Avatar>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{item.label}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1 overflow-hidden text-sm text-default-600 truncate">
|
||||
<span className="me-3">{name}</span>
|
||||
<span className="text-sm text-default-600 truncate max-w-56">{title}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center">
|
||||
<p className="text-default-900 text-sm">{time}</p>
|
||||
<div className="group-hover:bg-default-100 hidden group-hover:flex absolute group-hover:transition-all duration-300 right-0 top-0 h-full items-center ">
|
||||
<Button
|
||||
className="bg-transparent ring-transparent hover:bg-transparent hover:ring-0 hover:ring-offset-0 hover:ring-transparent w-28 border-transparent"
|
||||
size="icon"
|
||||
onClick={(event) => {
|
||||
setOpen(true);
|
||||
event.preventDefault();
|
||||
}}
|
||||
>
|
||||
<Trash2 className="text-default-900 w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default MailItem;
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
||||
import MailItem from "./mail";
|
||||
import MailHeader from "./mail-header";
|
||||
import { getMail } from "./utils";
|
||||
const EmailApp = async () => {
|
||||
const mails = await getMail()
|
||||
return (
|
||||
<Card className="h-full rounded-md">
|
||||
<CardHeader className="border-b border-default-200 sticky top-0 z-10 bg-card py-4">
|
||||
<MailHeader />
|
||||
</CardHeader>
|
||||
<CardContent className="p-0 h-full">
|
||||
<div className="h-[calc(100%-80px)] overflow-y-auto no-scrollbar">
|
||||
{mails.map((mail, index) => (
|
||||
<MailItem key={`mail-${index}`} mail={mail} />
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmailApp;
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
'use client'
|
||||
import { useMediaQuery } from '@/hooks/use-media-query'
|
||||
import { useMailConfig } from '@/hooks/use-mail'
|
||||
import { cn } from '@/lib/utils'
|
||||
import React from 'react'
|
||||
const MailSidebarWrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
const [mailConfig] = useMailConfig()
|
||||
const { isOpen } = mailConfig
|
||||
const isTablet = useMediaQuery("(min-width: 1024px)");
|
||||
if (!isTablet) {
|
||||
return (
|
||||
<div className={cn('absolute h-full start-0 w-[240px] z-50 ', {
|
||||
'-start-full': !isOpen
|
||||
})}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div className='flex-none md:w-[270px] w-[200px] '>{children}</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default MailSidebarWrapper
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
"use client";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useMediaQuery } from "@/hooks/use-media-query";
|
||||
import { useMailConfig } from "@/hooks/use-mail";
|
||||
import { Menu } from "lucide-react";
|
||||
import React from "react";
|
||||
|
||||
const ToggleMailSidebar = () => {
|
||||
const isTablet = useMediaQuery("(min-width: 1024px)");
|
||||
const [mailConfig, setMailConfig] = useMailConfig();
|
||||
if (isTablet) return null;
|
||||
|
||||
return (
|
||||
<Button
|
||||
size="icon"
|
||||
color="secondary"
|
||||
onClick={() =>
|
||||
setMailConfig({ ...mailConfig, isOpen: !mailConfig.isOpen })
|
||||
}
|
||||
>
|
||||
<Menu className=" h-5 w-5" />
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export default ToggleMailSidebar;
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
import { mails, Mail} from "./data"
|
||||
|
||||
// get mail
|
||||
|
||||
export const getMail = async () => {
|
||||
return mails
|
||||
}
|
||||
|
||||
|
||||
// get mail by id
|
||||
|
||||
export const getMailById = async (mailId: string) => {
|
||||
return mails.find(mail => mail.id === mailId)
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Plus } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
const AddBoard = () => {
|
||||
const t = useTranslations("KanbanApp")
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button>
|
||||
<Plus className="h-4 w-4 me-1" />
|
||||
{t("addBoard")}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-[425px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create a new board</DialogTitle>
|
||||
</DialogHeader>
|
||||
<form className="space-y-4">
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="board-name">Board Name</Label>
|
||||
<Input id="board-name" placeholder="Board Name" />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="board-color">Board Color</Label>
|
||||
<Input id="board-color" type="color" placeholder="Board Name" className="p-0 border-0 shadow-none" />
|
||||
</div>
|
||||
</form>
|
||||
<DialogFooter>
|
||||
<Button type="submit">Add</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddBoard;
|
||||
|
|
@ -0,0 +1,125 @@
|
|||
'use client'
|
||||
import { SortableContext, useSortable } from "@dnd-kit/sortable";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Card, CardContent, CardFooter, CardHeader } from "@/components/ui/card";
|
||||
import { CSS } from "@dnd-kit/utilities";
|
||||
import { useMemo, useState } from "react";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip"
|
||||
|
||||
import { Column, Task } from "./data";
|
||||
import TaskCard from "./task";
|
||||
import { Plus, Trash2 } from "lucide-react";
|
||||
import EmptyTask from "./empty";
|
||||
|
||||
import DeleteConfirmationDialog from "@/components/delete-confirmation-dialog";
|
||||
|
||||
function ColumnContainer({ column, tasks, handleOpenTask }: { column: Column; tasks: Task[], handleOpenTask: () => void }) {
|
||||
const [editMode, setEditMode] = useState<boolean>(false);
|
||||
|
||||
const [deleteColumn, setDeleteColumn] = useState<boolean>(false);
|
||||
const tasksIds = useMemo(() => {
|
||||
return tasks.map((task) => task.id);
|
||||
}, [tasks]);
|
||||
|
||||
const {
|
||||
setNodeRef,
|
||||
attributes,
|
||||
listeners,
|
||||
transform,
|
||||
transition,
|
||||
isDragging,
|
||||
} = useSortable({
|
||||
id: column.id,
|
||||
data: {
|
||||
type: "Column",
|
||||
column,
|
||||
},
|
||||
disabled: editMode,
|
||||
});
|
||||
|
||||
const style = {
|
||||
transition,
|
||||
transform: CSS.Transform.toString(transform),
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<DeleteConfirmationDialog
|
||||
open={deleteColumn}
|
||||
onClose={() => setDeleteColumn(false)}
|
||||
/>
|
||||
|
||||
<Card
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
className={cn("flex-1 w-[280px] bg-default-200 shadow-none app-height flex flex-col relative", {
|
||||
"opacity-20": isDragging,
|
||||
})}
|
||||
>
|
||||
<CardHeader className="flex-none bg-card relative rounded-t-md py-4" {...attributes} {...listeners}>
|
||||
<div className={cn("absolute -start-[1px] top-1/2 -translate-y-1/2 h-[60%] w-0.5 bg-primary",
|
||||
{
|
||||
"bg-warning": (column.title).toLocaleLowerCase() === "work in progress",
|
||||
"bg-success": (column.title).toLocaleLowerCase() === "done",
|
||||
}
|
||||
)}></div>
|
||||
<div className="flex items-center gap-2" >
|
||||
<div className="flex-1 text-lg capitalize text-default-900 font-medium">{column.title}</div>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
size="icon"
|
||||
className="w-6 h-6 bg-transparent hover:bg-transparent border border-default-200 text-default-600 hover:ring-0 hover:ring-transparent"
|
||||
onClick={() => setDeleteColumn(true)}
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="bg-destructive">
|
||||
<p>Delete</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
size="icon"
|
||||
className="w-6 h-6 bg-transparent ring-offset-transparent hover:bg-transparent border border-default-200 text-default-600 hover:ring-0 hover:ring-transparent"
|
||||
onClick={handleOpenTask}
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Add Project</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="flex-1 pt-6 px-3.5 h-full overflow-y-auto no-scrollbar">
|
||||
{/* Column task container */}
|
||||
<div className=" space-y-6">
|
||||
{tasks?.length === 0 && <EmptyTask />}
|
||||
<SortableContext items={tasksIds}>
|
||||
{tasks.map((task) => (
|
||||
<TaskCard task={task} key={task.id} />
|
||||
))}
|
||||
</SortableContext>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
</Card>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default ColumnContainer;
|
||||
|
|
@ -0,0 +1,228 @@
|
|||
"use client";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Calendar } from "@/components/ui/calendar";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { format } from "date-fns";
|
||||
import { CalendarIcon } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import { useState } from "react";
|
||||
import { useForm, SubmitHandler, Controller } from "react-hook-form"
|
||||
import Select, { MultiValue } from 'react-select'
|
||||
interface CreateTaskProps {
|
||||
open: boolean;
|
||||
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
}
|
||||
interface Option {
|
||||
value: string;
|
||||
label: string;
|
||||
image?: string;
|
||||
}
|
||||
type Inputs = {
|
||||
title: string;
|
||||
description: string;
|
||||
tags: MultiValue<Option>;
|
||||
assignee: MultiValue<Option>;
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
}
|
||||
const options: Option[] = [
|
||||
{
|
||||
value: "team",
|
||||
label: "team",
|
||||
},
|
||||
{
|
||||
value: "low",
|
||||
label: "low",
|
||||
},
|
||||
{
|
||||
value: "medium",
|
||||
label: "medium",
|
||||
},
|
||||
{
|
||||
value: "high",
|
||||
label: "high",
|
||||
},
|
||||
{
|
||||
value: "update",
|
||||
label: "update",
|
||||
}
|
||||
];
|
||||
const assigneeOptions: Option[] = [
|
||||
{ value: "mahedi", label: "Mahedi Amin", image: "/images/avatar/av-1.svg" },
|
||||
{ value: "sovo", label: "Sovo Haldar", image: "/images/avatar/av-2.svg" },
|
||||
{ value: "rakibul", label: "Rakibul Islam", image: "/images/avatar/av-3.svg" },
|
||||
{ value: "pritom", label: "Pritom Miha", image: "/images/avatar/av-4.svg" },
|
||||
];
|
||||
|
||||
|
||||
const CreateTask = ({ open, setOpen }: CreateTaskProps) => {
|
||||
const [startDate, setStartDate] = useState<Date>(new Date());
|
||||
const [endDate, setEndtDate] = useState<Date>(new Date());
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
control,
|
||||
formState: { errors },
|
||||
} = useForm<Inputs>()
|
||||
const onSubmit: SubmitHandler<Inputs> = (data) => {
|
||||
console.log(data)
|
||||
setOpen(false)
|
||||
}
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create Task</DialogTitle>
|
||||
</DialogHeader>
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="projectName">Project Name</Label>
|
||||
<Input
|
||||
id="projectName"
|
||||
placeholder="Project Name"
|
||||
{...register("title", { required: "Title is required." })}
|
||||
color={errors.title ? "destructive" : "default"}
|
||||
/>
|
||||
{errors.title && <p className="text-destructive text-sm font-medium">{errors.title.message}</p>}
|
||||
</div>
|
||||
<div className="grid lg:grid-cols-2 grid-cols-1 gap-5">
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="startDate">Start Date</Label>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full justify-start text-start font-normal border border-default-200 text-default-700 md:px-2.5 hover:bg-transparent hover:text-default-700"
|
||||
size="md"
|
||||
>
|
||||
<CalendarIcon className="me-2 h-3.5 w-3.5 text-default-500" />
|
||||
{format(startDate, "PPP")}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0">
|
||||
<Controller
|
||||
name="startDate"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={startDate}
|
||||
onSelect={(date) => {
|
||||
field.onChange(date);
|
||||
setStartDate(date as Date);
|
||||
}}
|
||||
initialFocus
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="endDate">End Date</Label>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full justify-start text-start font-normal border border-default-200 text-default-700 md:px-2.5 hover:bg-transparent hover:text-default-700"
|
||||
size="md"
|
||||
>
|
||||
<CalendarIcon className="me-2 h-3.5 w-3.5 text-default-500" />
|
||||
{format(endDate, "PPP")}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0">
|
||||
<Controller
|
||||
name="startDate"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={endDate}
|
||||
onSelect={(date) => {
|
||||
field.onChange(date);
|
||||
setEndtDate(date as Date);
|
||||
}}
|
||||
initialFocus
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="assignee">Assignee</Label>
|
||||
<Controller
|
||||
name="assignee"
|
||||
control={control}
|
||||
defaultValue={[]}
|
||||
render={({ field }) => (
|
||||
<Select
|
||||
className="react-select"
|
||||
classNamePrefix="select"
|
||||
{...field}
|
||||
options={assigneeOptions}
|
||||
isMulti
|
||||
onChange={(selectedOption) => field.onChange(selectedOption)}
|
||||
getOptionLabel={(option) => (
|
||||
<div className="flex items-center">
|
||||
<Image width={40} height={40} src={option.image as string} alt={option.label} className="w-8 h-8 rounded-full me-2" />
|
||||
<span className="text-sm text-default-600 font-medium">{option.label}</span>
|
||||
</div>
|
||||
) as unknown as string}
|
||||
placeholder="Select assignee"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="tag">Tags</Label>
|
||||
<Controller
|
||||
name="tags"
|
||||
control={control}
|
||||
defaultValue={[]}
|
||||
render={({ field }) => (
|
||||
<Select
|
||||
className="react-select"
|
||||
classNamePrefix="select"
|
||||
{...field}
|
||||
options={options}
|
||||
isMulti
|
||||
onChange={(selectedOption) => field.onChange(selectedOption)}
|
||||
placeholder="Select tags"
|
||||
getOptionLabel={(option) => <span className="text-sm text-default-600 font-medium capitalize">{option.label}</span> as unknown as string}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="description">Description</Label>
|
||||
<Textarea
|
||||
id="description"
|
||||
placeholder="Project Description"
|
||||
{...register("description")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button type="submit">Add</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateTask;
|
||||
|
|
@ -0,0 +1,151 @@
|
|||
import { faker } from "@faker-js/faker";
|
||||
|
||||
export const defaultCols = [
|
||||
{
|
||||
id: faker.string.uuid(),
|
||||
title: "Todo",
|
||||
},
|
||||
{
|
||||
id: faker.string.uuid(),
|
||||
title: "Work in progress",
|
||||
},
|
||||
{
|
||||
id: faker.string.uuid(),
|
||||
title: "Done",
|
||||
},
|
||||
];
|
||||
|
||||
export const defaultTasks = [
|
||||
{
|
||||
id: faker.string.uuid(),
|
||||
columnId: defaultCols[0].id,
|
||||
title: "CRM Dashboard ",
|
||||
projectLogo: "/images/project/p-2.png",
|
||||
desc: "Amet minim mollit non deserunt ullamco est sit aliqua dolor do amet sint.",
|
||||
startDate: "2022-10-03",
|
||||
endDate: "2022-10-06",
|
||||
progress: 90,
|
||||
assignee: [
|
||||
{
|
||||
image: "/images/avatar/av-1.svg",
|
||||
name: "Mahedi Amin",
|
||||
},
|
||||
{
|
||||
image: "/images/avatar/av-2.svg",
|
||||
name: "Sovo Haldar",
|
||||
},
|
||||
{
|
||||
image: "/images/avatar/av-3.svg",
|
||||
name: "Rakibul Islam",
|
||||
}
|
||||
],
|
||||
remainingDays: 3
|
||||
},
|
||||
{
|
||||
id: faker.string.uuid(),
|
||||
columnId: defaultCols[0].id,
|
||||
title: "Business Dashboard ",
|
||||
projectLogo: "/images/project/p-2.png",
|
||||
desc: "Amet minim mollit non deserunt ullamco est sit aliqua dolor do amet sint.",
|
||||
startDate: "2022-10-03",
|
||||
endDate: "2022-10-06",
|
||||
progress: 90,
|
||||
assignee: [
|
||||
{
|
||||
image: "/images/avatar/av-1.svg",
|
||||
name: "Mahedi Amin",
|
||||
},
|
||||
{
|
||||
image: "/images/avatar/av-2.svg",
|
||||
name: "Sovo Haldar",
|
||||
},
|
||||
{
|
||||
image: "/images/avatar/av-3.svg",
|
||||
name: "Rakibul Islam",
|
||||
}
|
||||
],
|
||||
remainingDays: 3
|
||||
},
|
||||
{
|
||||
id: faker.string.uuid(),
|
||||
columnId: defaultCols[1].id,
|
||||
title: "Management Dashboard ",
|
||||
projectLogo: "/images/project/p-2.png",
|
||||
desc: "Amet minim mollit non deserunt ullamco est sit aliqua dolor do amet sint.",
|
||||
startDate: "2022-10-03",
|
||||
endDate: "2022-10-06",
|
||||
progress: 90,
|
||||
assignee: [
|
||||
{
|
||||
image: "/images/avatar/av-1.svg",
|
||||
name: "Mahedi Amin",
|
||||
},
|
||||
{
|
||||
image: "/images/avatar/av-2.svg",
|
||||
name: "Sovo Haldar",
|
||||
},
|
||||
{
|
||||
image: "/images/avatar/av-3.svg",
|
||||
name: "Rakibul Islam",
|
||||
}
|
||||
],
|
||||
remainingDays: 3
|
||||
},
|
||||
{
|
||||
id: faker.string.uuid(),
|
||||
columnId: defaultCols[1].id,
|
||||
title: "Analytics Dashboard ",
|
||||
projectLogo: "/images/project/p-2.png",
|
||||
desc: "Amet minim mollit non deserunt ullamco est sit aliqua dolor do amet sint.",
|
||||
startDate: "2022-10-03",
|
||||
endDate: "2022-10-06",
|
||||
progress: 90,
|
||||
assignee: [
|
||||
{
|
||||
image: "/images/avatar/av-1.svg",
|
||||
name: "Mahedi Amin",
|
||||
},
|
||||
{
|
||||
image: "/images/avatar/av-2.svg",
|
||||
name: "Sovo Haldar",
|
||||
},
|
||||
{
|
||||
image: "/images/avatar/av-3.svg",
|
||||
name: "Rakibul Islam",
|
||||
}
|
||||
],
|
||||
remainingDays: 3
|
||||
},
|
||||
|
||||
{
|
||||
id: faker.string.uuid(),
|
||||
columnId: defaultCols[1].id,
|
||||
title: "Marketing Dashboard ",
|
||||
projectLogo: "/images/project/p-2.png",
|
||||
desc: "Amet minim mollit non deserunt ullamco est sit aliqua dolor do amet sint.",
|
||||
startDate: "2022-10-03",
|
||||
endDate: "2022-10-06",
|
||||
progress: 90,
|
||||
assignee: [
|
||||
{
|
||||
image: "/images/avatar/av-1.svg",
|
||||
name: "Mahedi Amin",
|
||||
},
|
||||
{
|
||||
image: "/images/avatar/av-2.svg",
|
||||
name: "Sovo Haldar",
|
||||
},
|
||||
{
|
||||
image: "/images/avatar/av-3.svg",
|
||||
name: "Rakibul Islam",
|
||||
}
|
||||
],
|
||||
remainingDays: 3
|
||||
},
|
||||
|
||||
|
||||
];
|
||||
|
||||
export type Column = (typeof defaultCols)[number];
|
||||
|
||||
export type Task = (typeof defaultTasks)[number]
|
||||
|
|
@ -0,0 +1,226 @@
|
|||
"use client";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Calendar } from "@/components/ui/calendar";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { format } from "date-fns";
|
||||
import { CalendarIcon } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import { useState } from "react";
|
||||
import { useForm, SubmitHandler, Controller } from "react-hook-form"
|
||||
import Select, { MultiValue } from 'react-select'
|
||||
interface EditTaskProps {
|
||||
open: boolean;
|
||||
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
}
|
||||
interface Option {
|
||||
value: string;
|
||||
label: string;
|
||||
image?: string;
|
||||
}
|
||||
type Inputs = {
|
||||
title: string;
|
||||
description: string;
|
||||
tags: MultiValue<Option>;
|
||||
assignee: MultiValue<Option>;
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
}
|
||||
const options: Option[] = [
|
||||
{
|
||||
value: "team",
|
||||
label: "team",
|
||||
},
|
||||
{
|
||||
value: "low",
|
||||
label: "low",
|
||||
},
|
||||
{
|
||||
value: "medium",
|
||||
label: "medium",
|
||||
},
|
||||
{
|
||||
value: "high",
|
||||
label: "high",
|
||||
},
|
||||
{
|
||||
value: "update",
|
||||
label: "update",
|
||||
}
|
||||
];
|
||||
const assigneeOptions: Option[] = [
|
||||
{ value: "mahedi", label: "Mahedi Amin", image: "/images/avatar/av-1.svg" },
|
||||
{ value: "sovo", label: "Sovo Haldar", image: "/images/avatar/av-2.svg" },
|
||||
{ value: "rakibul", label: "Rakibul Islam", image: "/images/avatar/av-3.svg" },
|
||||
{ value: "pritom", label: "Pritom Miha", image: "/images/avatar/av-4.svg" },
|
||||
];
|
||||
|
||||
|
||||
const EditTask = ({ open, setOpen }: EditTaskProps) => {
|
||||
const [startDate, setStartDate] = useState<Date>(new Date());
|
||||
const [endDate, setEndtDate] = useState<Date>(new Date());
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
control,
|
||||
formState: { errors },
|
||||
} = useForm<Inputs>()
|
||||
const onSubmit: SubmitHandler<Inputs> = (data) => {
|
||||
console.log(data)
|
||||
setOpen(false)
|
||||
}
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create Task</DialogTitle>
|
||||
</DialogHeader>
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="projectName">Project Name</Label>
|
||||
<Input
|
||||
id="projectName"
|
||||
placeholder="Project Name"
|
||||
defaultValue="Project Title"
|
||||
{...register("title", { required: "Title is required." })}
|
||||
color={errors.title ? "destructive" : "default"}
|
||||
/>
|
||||
{errors.title && <p className="text-destructive text-sm font-medium">{errors.title.message}</p>}
|
||||
</div>
|
||||
<div className="grid lg:grid-cols-2 grid-cols-1 gap-5">
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="startDate">Start Date</Label>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full justify-start text-start font-normal border border-default-200 text-default-700 md:px-2.5 hover:bg-transparent hover:text-default-700"
|
||||
size="md"
|
||||
>
|
||||
<CalendarIcon className="me-2 h-3.5 w-3.5 text-default-500" />
|
||||
{format(startDate, "PPP")}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0">
|
||||
<Controller
|
||||
name="startDate"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={startDate}
|
||||
onSelect={(date) => {
|
||||
field.onChange(date);
|
||||
setStartDate(date as Date);
|
||||
}}
|
||||
initialFocus
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="endDate">End Date</Label>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full justify-start text-start font-normal border border-default-200 text-default-700 md:px-2.5 hover:bg-transparent hover:text-default-700"
|
||||
size="md"
|
||||
>
|
||||
<CalendarIcon className="me-2 h-3.5 w-3.5 text-default-500" />
|
||||
{format(endDate, "PPP")}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0">
|
||||
<Controller
|
||||
name="startDate"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={endDate}
|
||||
onSelect={(date) => {
|
||||
field.onChange(date);
|
||||
setEndtDate(date as Date);
|
||||
}}
|
||||
initialFocus
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="assignee">Assignee</Label>
|
||||
<Controller
|
||||
name="assignee"
|
||||
control={control}
|
||||
defaultValue={[{ value: "mahedi", label: "Mahedi Amin", image: "/images/avatar/av-1.svg" }]}
|
||||
render={({ field }) => (
|
||||
<Select
|
||||
{...field}
|
||||
options={assigneeOptions}
|
||||
isMulti
|
||||
onChange={(selectedOption) => field.onChange(selectedOption)}
|
||||
getOptionLabel={(option) => (
|
||||
<div className="flex items-center">
|
||||
<Image width={40} height={40} src={option.image as string} alt={option.label} className="w-8 h-8 rounded-full me-2" />
|
||||
<span className="text-sm text-default-600 font-medium">{option.label}</span>
|
||||
</div>
|
||||
) as unknown as string}
|
||||
placeholder="Select assignee"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="tag">Tags</Label>
|
||||
<Controller
|
||||
name="tags"
|
||||
control={control}
|
||||
defaultValue={[{ value: "team", label: "Team" }]}
|
||||
render={({ field }) => (
|
||||
<Select
|
||||
{...field}
|
||||
options={options}
|
||||
isMulti
|
||||
onChange={(selectedOption) => field.onChange(selectedOption)}
|
||||
placeholder="Select tags"
|
||||
getOptionLabel={(option) => <span className="text-sm text-default-600 font-medium capitalize">{option.label}</span> as unknown as string}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="description">Description</Label>
|
||||
<Textarea
|
||||
id="description"
|
||||
placeholder="Project Description"
|
||||
{...register("description")}
|
||||
defaultValue="Project Description"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button type="submit">Add</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditTask;
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
|
||||
import { Rabbit } from 'lucide-react';
|
||||
|
||||
|
||||
const EmptyTask = () => {
|
||||
return (
|
||||
<div className=" absolute start-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2">
|
||||
<Rabbit className=" h-[200px] w-[200px] text-default-400 " strokeWidth={1} />
|
||||
<div className=' text-sm text-default-500 text-center'>No tasks</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmptyTask;
|
||||
|
|
@ -0,0 +1,174 @@
|
|||
'use client'
|
||||
import React, { useState, useMemo } from 'react'
|
||||
import { type Column, type Task } from './data';
|
||||
import {
|
||||
DndContext,
|
||||
DragEndEvent,
|
||||
DragOverEvent,
|
||||
DragOverlay,
|
||||
DragStartEvent,
|
||||
PointerSensor,
|
||||
useSensor,
|
||||
useSensors,
|
||||
} from "@dnd-kit/core";
|
||||
import { SortableContext, arrayMove } from "@dnd-kit/sortable";
|
||||
import ColumnContainer from "./column"
|
||||
import TaskCard from './task';
|
||||
import { createPortal } from "react-dom";
|
||||
import AddBoard from './add-board';
|
||||
import CreateTask from "./create-task";
|
||||
import { useTranslations } from 'next-intl';
|
||||
const KanBanApp = ({ defaultCols, defaultTasks }: { defaultCols: Column[], defaultTasks: Task[] }) => {
|
||||
const t = useTranslations("KanbanApp");
|
||||
const [columns, setColumns] = useState<Column[]>(defaultCols);
|
||||
const columnsId = useMemo(() => columns.map((col) => col.id), [columns]);
|
||||
|
||||
const [tasks, setTasks] = useState<Task[]>(defaultTasks);
|
||||
|
||||
const [activeColumn, setActiveColumn] = useState<Column | null>(null);
|
||||
|
||||
const [activeTask, setActiveTask] = useState<Task | null>(null);
|
||||
|
||||
// create task state
|
||||
const [open, setOpen] = useState<boolean>(false);
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor, {
|
||||
activationConstraint: {
|
||||
distance: 10,
|
||||
},
|
||||
})
|
||||
);
|
||||
function onDragStart(event: DragStartEvent) {
|
||||
if (event.active.data.current?.type === "Column") {
|
||||
setActiveColumn(event.active.data.current.column);
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.active.data.current?.type === "Task") {
|
||||
setActiveTask(event.active.data.current.task);
|
||||
return;
|
||||
}
|
||||
}
|
||||
function onDragEnd(event: DragEndEvent) {
|
||||
setActiveColumn(null);
|
||||
setActiveTask(null);
|
||||
|
||||
const { active, over } = event;
|
||||
if (!over) return;
|
||||
|
||||
const activeId = active.id;
|
||||
const overId = over.id;
|
||||
|
||||
if (activeId === overId) return;
|
||||
|
||||
const isActiveAColumn = active.data.current?.type === "Column";
|
||||
if (!isActiveAColumn) return;
|
||||
|
||||
console.log("DRAG END");
|
||||
|
||||
setColumns((columns) => {
|
||||
const activeColumnIndex = columns.findIndex((col) => col.id === activeId);
|
||||
|
||||
const overColumnIndex = columns.findIndex((col) => col.id === overId);
|
||||
|
||||
return arrayMove(columns, activeColumnIndex, overColumnIndex);
|
||||
});
|
||||
}
|
||||
function onDragOver(event: DragOverEvent) {
|
||||
const { active, over } = event;
|
||||
if (!over) return;
|
||||
|
||||
const activeId = active.id;
|
||||
const overId = over.id;
|
||||
|
||||
if (activeId === overId) return;
|
||||
|
||||
const isActiveATask = active.data.current?.type === "Task";
|
||||
const isOverATask = over.data.current?.type === "Task";
|
||||
|
||||
if (!isActiveATask) return;
|
||||
|
||||
|
||||
if (isActiveATask && isOverATask) {
|
||||
setTasks((tasks) => {
|
||||
const activeIndex = tasks.findIndex((t) => t.id === activeId);
|
||||
const overIndex = tasks.findIndex((t) => t.id === overId);
|
||||
|
||||
if (tasks[activeIndex].columnId != tasks[overIndex].columnId) {
|
||||
|
||||
tasks[activeIndex].columnId = tasks[overIndex].columnId;
|
||||
return arrayMove(tasks, activeIndex, overIndex - 1);
|
||||
}
|
||||
|
||||
return arrayMove(tasks, activeIndex, overIndex);
|
||||
});
|
||||
}
|
||||
|
||||
const isOverAColumn = over.data.current?.type === "Column";
|
||||
|
||||
|
||||
if (isActiveATask && isOverAColumn) {
|
||||
setTasks((tasks) => {
|
||||
const activeIndex = tasks.findIndex((t) => t.id === activeId);
|
||||
tasks[activeIndex].columnId = overId.toString();
|
||||
return arrayMove(tasks, activeIndex, activeIndex);
|
||||
});
|
||||
}
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<div className="">
|
||||
<div className="flex gap-2 mb-5">
|
||||
<div className="flex-1 font-medium lg:text-2xl text-xl capitalize text-default-900">
|
||||
{t("title")}
|
||||
</div>
|
||||
<div className="flex-none">
|
||||
<AddBoard />
|
||||
</div>
|
||||
</div>
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
onDragStart={onDragStart}
|
||||
onDragEnd={onDragEnd}
|
||||
onDragOver={onDragOver}
|
||||
>
|
||||
<div className="flex gap-4 overflow-x-auto no-scrollbar">
|
||||
<div className="flex gap-4">
|
||||
<SortableContext items={columnsId}>
|
||||
{columns.map((col) => (
|
||||
<ColumnContainer
|
||||
key={col.id}
|
||||
column={col}
|
||||
tasks={tasks.filter((task) => task.columnId === col.id)}
|
||||
handleOpenTask={() => setOpen(true)}
|
||||
/>
|
||||
))}
|
||||
</SortableContext>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{createPortal(
|
||||
<DragOverlay>
|
||||
{activeColumn && (
|
||||
<ColumnContainer
|
||||
column={activeColumn}
|
||||
handleOpenTask={() => setOpen(true)}
|
||||
tasks={tasks.filter((task) => task.columnId === activeColumn.id)}
|
||||
/>
|
||||
)}
|
||||
{activeTask && <TaskCard task={activeTask} />}
|
||||
</DragOverlay>,
|
||||
document.body
|
||||
)}
|
||||
</DndContext>
|
||||
</div>
|
||||
<CreateTask
|
||||
open={open}
|
||||
setOpen={setOpen}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
export default KanBanApp
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
import { Metadata } from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Dashcode Next Js",
|
||||
description: "Dashcode is a popular dashboard template.",
|
||||
};
|
||||
const Layout = ({ children }: { children: React.ReactNode }) => {
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
export default Layout;
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
import React from 'react'
|
||||
import KanBanApp from './kanban-app'
|
||||
import { defaultCols, defaultTasks } from './data'
|
||||
|
||||
const KanBanPage = () => {
|
||||
return (
|
||||
<KanBanApp
|
||||
defaultCols={defaultCols}
|
||||
defaultTasks={defaultTasks}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default KanBanPage
|
||||
|
|
@ -0,0 +1,147 @@
|
|||
"use client"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { useSortable } from "@dnd-kit/sortable";
|
||||
import { CSS } from "@dnd-kit/utilities";
|
||||
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
||||
import { Task } from "./data";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu"
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { MoreVertical, SquarePen, Trash2 } from "lucide-react";
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
import DeleteConfirmationDialog from "@/components/delete-confirmation-dialog";
|
||||
import { useState } from "react";
|
||||
import EditTask from "./edit-task";
|
||||
import { Icon } from "@/components/ui/icon";
|
||||
|
||||
function TaskCard({ task }: { task: Task }) {
|
||||
const { projectLogo, title, desc, startDate, endDate, progress, assignee, remainingDays } = task;
|
||||
const [open, setOpen] = useState<boolean>(false);
|
||||
const [editTaskOpen, setEditTaskOpen] = useState<boolean>(false);
|
||||
const {
|
||||
setNodeRef,
|
||||
attributes,
|
||||
listeners,
|
||||
transform,
|
||||
transition,
|
||||
isDragging,
|
||||
} = useSortable({
|
||||
id: task.id,
|
||||
data: {
|
||||
type: "Task",
|
||||
task,
|
||||
},
|
||||
});
|
||||
|
||||
const style = {
|
||||
transition,
|
||||
transform: CSS.Transform.toString(transform),
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<DeleteConfirmationDialog
|
||||
open={open}
|
||||
onClose={() => setOpen(false)}
|
||||
/>
|
||||
<EditTask
|
||||
open={editTaskOpen}
|
||||
setOpen={setEditTaskOpen}
|
||||
/>
|
||||
<Card
|
||||
className={cn("", {
|
||||
"opacity-10 bg-primary/50 ": isDragging,
|
||||
})}
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
>
|
||||
<CardHeader className="flex-row gap-1 p-2.5 items-center space-y-0">
|
||||
<Avatar className="flex-none h-8 w-8 rounded bg-default-200 text-default hover:bg-default-200">
|
||||
<AvatarImage src={projectLogo} />
|
||||
<AvatarFallback className="uppercase"> {title.charAt(0) + title.charAt(1)}</AvatarFallback>
|
||||
</Avatar>
|
||||
<h3 className="flex-1 text-default-800 text-lg font-medium max-w-[160px] truncate text-center capitalize ">{title}</h3>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
size="icon"
|
||||
className="flex-none ring-offset-transparent bg-transparent hover:bg-transparent hover:ring-0 hover:ring-transparent w-6"
|
||||
>
|
||||
<MoreVertical className="h-4 w-4 text-default-900" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="p-0 overflow-hidden" align="end" >
|
||||
<DropdownMenuItem
|
||||
className="py-2 border-b border-default-200 text-default-600 focus:bg-default focus:text-default-foreground rounded-none cursor-pointer"
|
||||
onClick={() => setEditTaskOpen(true)}
|
||||
>
|
||||
<SquarePen className="w-3.5 h-3.5 me-1" />
|
||||
Edit
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="py-2 bg-destructive/30 text-destructive focus:bg-destructive focus:text-destructive-foreground rounded-none cursor-pointer"
|
||||
onClick={() => setOpen(true)}
|
||||
>
|
||||
<Trash2 className="w-3.5 h-3.5 me-1" />
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</CardHeader>
|
||||
<CardContent className="p-2.5 pt-1">
|
||||
<div className="text-default-600 text-sm">{desc}</div>
|
||||
<div className="flex gap-4 mt-6">
|
||||
<div>
|
||||
<div className="text-xs text-default-400 mb-1">Start Date</div>
|
||||
<div className="text-xs text-default-600 font-medium">{startDate}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-default-400 mb-1">End Date</div>
|
||||
<div className="text-xs text-default-600 font-medium">{endDate}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-1">
|
||||
<div className="text-end text-xs text-default-600 mb-1.5 font-medium">{progress}%</div>
|
||||
<Progress value={progress} color="primary" size="sm" />
|
||||
</div>
|
||||
<div className="flex mt-5">
|
||||
<div className="flex-1">
|
||||
<div className="text-default-400 text-sm font-normal mb-3">Assigned to</div>
|
||||
<div className="flex items-center -space-x-1">
|
||||
{
|
||||
assignee?.map((user, index) => (
|
||||
<Avatar
|
||||
key={`user-${index}`}
|
||||
className="h-6 w-6"
|
||||
>
|
||||
<AvatarImage src={user.image} />
|
||||
<AvatarFallback> {user.name.charAt(0) + user.name.charAt(1)}</AvatarFallback>
|
||||
</Avatar>
|
||||
))
|
||||
}
|
||||
<div className="bg-card text-default-900 text-xs ring-2 ring-default-100 rounded-full h-6 w-6 flex flex-col justify-center items-center">
|
||||
+2
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-none">
|
||||
<div className="flex items-center gap-1 bg-destructive/10 text-destructive rounded-full px-2 py-0.5 text-sm mt-1">
|
||||
<Icon icon="heroicons-outline:clock" />
|
||||
{remainingDays} days left
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default TaskCard;
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
import { Metadata } from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Dashcode Next Js",
|
||||
description: "Dashcode is a popular dashboard template.",
|
||||
};
|
||||
const Layout = ({ children }: { children: React.ReactNode }) => {
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
export default Layout;
|
||||
|
|
@ -0,0 +1,220 @@
|
|||
|
||||
export const meets = [
|
||||
{
|
||||
img: "/images/svg/sk.svg",
|
||||
title: "Meeting with client",
|
||||
date: "01 Nov 2021",
|
||||
meet: "Zoom meeting",
|
||||
},
|
||||
{
|
||||
img: "/images/svg/path.svg",
|
||||
title: "Design meeting (team)",
|
||||
date: "01 Nov 2021",
|
||||
meet: "Skyp meeting",
|
||||
},
|
||||
{
|
||||
img: "/images/svg/dc.svg",
|
||||
title: "Background research",
|
||||
date: "01 Nov 2021",
|
||||
meet: "Google meeting",
|
||||
},
|
||||
{
|
||||
img: "/images/svg/sk.svg",
|
||||
title: "Meeting with client",
|
||||
date: "01 Nov 2021",
|
||||
meet: "Zoom meeting",
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
|
||||
export const tasks = [
|
||||
{
|
||||
id: 1,
|
||||
image: "/images/users/user-1.jpg",
|
||||
title: "Amet minim mollit non deserunt ullam.",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
image: "/images/users/user-2.jpg",
|
||||
title: "Amet minim mollit non deserunt ullam.",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
image: "/images/users/user-3.jpg",
|
||||
title: "Amet minim mollit non deserunt ullam.",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
image: "/images/users/user-4.jpg",
|
||||
title: "Amet minim mollit non deserunt ullam.",
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
image: "/images/users/user-5.jpg",
|
||||
title: "Amet minim mollit non deserunt ullam.",
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
image: "/images/users/user-6.jpg",
|
||||
title: "Amet minim mollit non deserunt ullam.",
|
||||
}]
|
||||
|
||||
export const messagesData = [
|
||||
{
|
||||
title: "Wade Warren",
|
||||
desc: "Hi! How are you doing?.....",
|
||||
active: true,
|
||||
hasnotifaction: true,
|
||||
notification_count: 1,
|
||||
image: "/images/users/user-1.jpg"
|
||||
},
|
||||
{
|
||||
title: "Savannah Nguyen",
|
||||
desc: "Hi! How are you doing?.....",
|
||||
active: false,
|
||||
hasnotifaction: false,
|
||||
image: "/images/users/user-2.jpg"
|
||||
},
|
||||
{
|
||||
title: "Ralph Edwards",
|
||||
desc: "Hi! How are you doing?.....",
|
||||
active: false,
|
||||
hasnotifaction: true,
|
||||
notification_count: 8,
|
||||
image: "/images/users/user-3.jpg"
|
||||
},
|
||||
{
|
||||
title: "Cody Fisher",
|
||||
desc: "Hi! How are you doing?.....",
|
||||
active: true,
|
||||
hasnotifaction: false,
|
||||
image: "/images/users/user-4.jpg"
|
||||
},
|
||||
{
|
||||
title: "Savannah Nguyen",
|
||||
desc: "Hi! How are you doing?.....",
|
||||
active: false,
|
||||
hasnotifaction: false,
|
||||
image: "/images/users/user-4.jpg"
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
|
||||
export const activityList = [
|
||||
{
|
||||
title: "Project start date",
|
||||
desc: "This parcel is paid for by the customer. Please contact the customer for any further information.",
|
||||
date: "Sep 20, 2021 ",
|
||||
time: "12:32 AM",
|
||||
status: "ok",
|
||||
},
|
||||
{
|
||||
title: "Project start date",
|
||||
date: "Sep 20, 2021 ",
|
||||
desc: "This parcel is paid for by the customer. Please contact the customer for any further information.",
|
||||
time: "12:32 AM",
|
||||
status: "ok",
|
||||
},
|
||||
{
|
||||
title: "Project start date",
|
||||
date: "Sep 20, 2021 ",
|
||||
desc: "This parcel is paid for by the customer. Please contact the customer for any further information.",
|
||||
time: "12:32 AM",
|
||||
status: "ok",
|
||||
},
|
||||
{
|
||||
title: "Project start date",
|
||||
date: "Sep 20, 2021 ",
|
||||
desc: "This parcel is paid for by the customer. Please contact the customer for any further information.",
|
||||
time: "12:32 AM",
|
||||
},
|
||||
{
|
||||
title: "Project start date",
|
||||
date: "Sep 20, 2021 ",
|
||||
desc: "This parcel is paid for by the customer. Please contact the customer for any further information.",
|
||||
time: "12:32 AM",
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
|
||||
|
||||
export const files = [
|
||||
{
|
||||
img: "/images/icon/file-1.svg",
|
||||
title: "Dashboard.fig",
|
||||
date: "06 June 2021 / 155MB",
|
||||
},
|
||||
{
|
||||
img: "/images/icon/pdf-1.svg",
|
||||
title: "Ecommerce.pdf",
|
||||
date: "06 June 2021 / 155MB",
|
||||
},
|
||||
{
|
||||
img: "/images/icon/zip-1.svg",
|
||||
title: "Job portal_app.zip",
|
||||
date: "06 June 2021 / 155MB",
|
||||
},
|
||||
{
|
||||
img: "/images/icon/pdf-2.svg",
|
||||
title: "Ecommerce.pdf",
|
||||
date: "06 June 2021 / 155MB",
|
||||
},
|
||||
{
|
||||
img: "/images/icon/scr-1.svg",
|
||||
title: "Screenshot.jpg",
|
||||
date: "06 June 2021 / 155MB",
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
export const teamData = [
|
||||
{
|
||||
customer: {
|
||||
name: "Arlene McCoy",
|
||||
image: "/images/users/user-1.jpg",
|
||||
deg: "Ux designer",
|
||||
},
|
||||
status: "progress",
|
||||
time: "42.5 hours",
|
||||
chart: null,
|
||||
action: null,
|
||||
},
|
||||
{
|
||||
customer: {
|
||||
name: "Arlene McCoy",
|
||||
image: "/images/users/user-2.jpg",
|
||||
deg: "Ux designer",
|
||||
},
|
||||
status: "complete",
|
||||
time: "42.5 hours",
|
||||
chart: null,
|
||||
action: null,
|
||||
},
|
||||
{
|
||||
customer: {
|
||||
name: "Arlene McCoy",
|
||||
image: "/images/users/user-3.jpg",
|
||||
deg: "Ux designer",
|
||||
},
|
||||
status: "progress",
|
||||
time: "42.5 hours",
|
||||
chart: null,
|
||||
action: null,
|
||||
},
|
||||
{
|
||||
customer: {
|
||||
name: "Arlene McCoy",
|
||||
image: "/images/users/user-4.jpg",
|
||||
deg: "Ux designer",
|
||||
},
|
||||
status: "complete",
|
||||
time: "42.5 hours",
|
||||
chart: null,
|
||||
action: null
|
||||
}
|
||||
];
|
||||
|
||||
export type TeamDataProps = (typeof teamData)[]
|
||||
|
|
@ -0,0 +1,335 @@
|
|||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { BarChart } from "lucide-react";
|
||||
import { Icon } from "@/components/ui/icon";
|
||||
import ProgressBlock from "@/components/blocks/progress-block";
|
||||
import { Link } from '@/i18n/routing';
|
||||
|
||||
import { meets, tasks, messagesData, activityList, teamData, files } from "./data"
|
||||
import Image from "next/image";
|
||||
import DashboardDropdown from "@/components/dashboard-dropdown";
|
||||
|
||||
import TaskItem from "@/components/project/task-item";
|
||||
import MessageListItem from "@/components/project/message-list-item";
|
||||
import ActivityItem from "@/components/project/activity";
|
||||
import TeamTable from "@/components/project/team-table";
|
||||
import NotesCalendar from "@/components/project/notes-calendar";
|
||||
import { getProjectById } from "../data";
|
||||
import { Alert } from "@/components/ui/alert";
|
||||
|
||||
const SinglePage = async ({ params: { id } }: { params: { id: string }; }) => {
|
||||
const project = await getProjectById(id)
|
||||
if (!project) return <Alert color="destructive"> project id is not valid</Alert>
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
<div className="grid grid-cols-12 gap-5">
|
||||
|
||||
<Card className="col-span-12 xl:col-span-3">
|
||||
<CardContent className="p-6">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="lg:col-span-1 col-span-2">
|
||||
<Card className="bg-info/20 shadow-none border-none">
|
||||
<CardContent className=" p-4 text-center">
|
||||
<div className="mx-auto h-10 w-10 rounded-full flex items-center justify-center bg-white mb-4">
|
||||
<BarChart className=" h-6 w-6 text-info" />
|
||||
</div>
|
||||
<div className="block text-sm text-default-600 font-medium mb-1.5"> Total Task</div>
|
||||
<div className="text-2xl text-default-900 font-medium"> 64</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
<div className="lg:col-span-1 col-span-2">
|
||||
<Card className="bg-warning/20 shadow-none border-none">
|
||||
<CardContent className=" p-4 text-center">
|
||||
<div
|
||||
className="mx-auto h-10 w-10 rounded-full flex items-center justify-center bg-white mb-4"
|
||||
>
|
||||
<Icon className="w-6 h-6 text-warning" icon="heroicons:chart-pie" />
|
||||
</div>
|
||||
<div className="block text-sm text-default-600 font-medium mb-1.5">Completed</div>
|
||||
<div className="text-2xl text-default-900 font-medium">45</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
<div className="lg:col-span-1 col-span-2">
|
||||
<Card className="bg-primary/20 shadow-none border-none">
|
||||
<CardContent className=" p-4 text-center">
|
||||
<div
|
||||
className="mx-auto h-10 w-10 rounded-full flex items-center justify-center bg-white mb-4"
|
||||
>
|
||||
<Icon className="w-6 h-6 text-primary" icon="heroicons:clock" />
|
||||
</div>
|
||||
<div className="block text-sm text-default-600 font-medium mb-1.5">Hours</div>
|
||||
<div className="text-2xl text-default-900 font-medium">190</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
<div className="lg:col-span-1 col-span-2">
|
||||
<Card className="bg-success/20 shadow-none border-none">
|
||||
<CardContent className="p-4 text-center">
|
||||
<div
|
||||
className="mx-auto h-10 w-10 rounded-full flex items-center justify-center bg-white mb-4"
|
||||
>
|
||||
<Icon className="w-6 h-6 text-success" icon="heroicons:calculator" />
|
||||
</div>
|
||||
<div className="block text-sm text-default-600 font-medium mb-1.5">Spendings</div>
|
||||
<div className="text-2xl text-default-900 font-medium">$3,564</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<ProgressBlock
|
||||
title="Progress"
|
||||
height={183}
|
||||
className="border-none shadow-none"
|
||||
chartType="pie"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="col-span-12 xl:col-span-5">
|
||||
<CardHeader>
|
||||
<CardTitle>About Project</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="p-6">
|
||||
|
||||
<div className="text-base font-medium text-default-800 mb-3">
|
||||
Background information
|
||||
</div>
|
||||
<p className="text-sm text-default-600">
|
||||
The Optimistic Website Company - Amet minim mollit non deserunt
|
||||
ullamco est sit aliqua dolor do amet sint. Velit officia consequat
|
||||
duis enim velit mollit. Exercita -tion veniam consequat sunt
|
||||
nostrud amet.
|
||||
</p>
|
||||
<br />
|
||||
<p className="text-sm text-default-600">
|
||||
Amet minim mollit non deserunt ullamco est sit aliqua dolor do
|
||||
amet sint.The Optimistic Website Company - Amet minim mollit non
|
||||
deserunt ullamco est sit aliqua dolor do amet sint. Velit officia
|
||||
consequat duis enim velit mollit. Exercita -tion veniam consequat
|
||||
sunt nostrud amet.
|
||||
</p>
|
||||
<p className="text-sm text-default-600 mt-4">
|
||||
Amet minim mollit non deserunt ullamco est sit aliqua dolor do
|
||||
amet sint.The Optimistic Website Company.
|
||||
Amet minim mollit non deserunt ullamco est sit aliqua dolor do
|
||||
amet sint.The Optimistic Website Company. <br /> <br />
|
||||
Amet minim mollit non deserunt ullamco est sit aliqua dolor do
|
||||
amet sint.The Optimistic Website Company.
|
||||
</p>
|
||||
<div className="flex flex-wrap mt-8">
|
||||
<div className="xl:mr-8 mr-4 mb-3 space-y-1">
|
||||
<div className="font-semibold text-default-500 ">
|
||||
Existing website
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-xs font-normal text-primary">
|
||||
<Icon icon="heroicons:link" />
|
||||
<Link href="#">www.example.com</Link>
|
||||
</div>
|
||||
</div>
|
||||
<div className="xl:me-8 me-4 mb-3 space-y-1">
|
||||
<div className="font-semibold text-default-500">
|
||||
Project brief
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-xs font-normal text-primary-600 ">
|
||||
<Icon icon="heroicons:link" />
|
||||
<Link href="#">www.example.com</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-default-100 rounded px-4 pt-4 pb-1 flex flex-wrap justify-between mt-6">
|
||||
<div className="me-3 mb-3 space-y-2">
|
||||
<div className="text-xs font-medium text-default-600">
|
||||
Project owner
|
||||
</div>
|
||||
<div className="text-xs text-default-600">
|
||||
John Doe
|
||||
</div>
|
||||
</div>
|
||||
<div className="me-3 mb-3 space-y-2">
|
||||
<div className="text-xs font-medium text-default-600">
|
||||
Budget
|
||||
</div>
|
||||
<div className="text-xs text-default-600 ">
|
||||
$75,800
|
||||
</div>
|
||||
</div>
|
||||
<div className="me-3 mb-3 space-y-2">
|
||||
<div className="text-xs font-medium text-default-600">
|
||||
Start date
|
||||
</div>
|
||||
<div className="text-xs text-default-600">
|
||||
01/11/2021
|
||||
</div>
|
||||
</div>
|
||||
<div className="me-3 mb-3 space-y-2">
|
||||
<div className="text-xs font-medium text-default-600 ">
|
||||
Deadline
|
||||
</div>
|
||||
<div className="text-xs text-warning">01/11/2021</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="col-span-12 xl:col-span-4">
|
||||
<CardHeader>
|
||||
<CardTitle>Notes</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="px-2">
|
||||
<NotesCalendar />
|
||||
<ul className="divide-y divide-default-100">
|
||||
{meets.map((item, i) => (
|
||||
<li key={i} className=" py-2.5 px-3">
|
||||
<div className="flex gap-2">
|
||||
<div className="flex-1 flex gap-2.5">
|
||||
<div className="flex-none">
|
||||
<div className="h-8 w-8">
|
||||
<Image
|
||||
src={item.img}
|
||||
alt=""
|
||||
className="w-full h-full "
|
||||
width={32}
|
||||
height={32}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="text-default-600 text-sm mb-1 font-medium">
|
||||
{item.title}
|
||||
</div>
|
||||
<div className="flex gap-1 font-normal text-xs text-default-500">
|
||||
<div className="text-base">
|
||||
<Icon icon="heroicons-outline:video-camera" />
|
||||
</div>
|
||||
{item.meet}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-none text-xs text-default-600">
|
||||
{item.date}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-5">
|
||||
<Card>
|
||||
<CardHeader className="flex-row items-center justify-between">
|
||||
<CardTitle>Task List</CardTitle>
|
||||
<DashboardDropdown />
|
||||
</CardHeader>
|
||||
<CardContent className="p-0">
|
||||
<ul className="divide-y divide-default-100">
|
||||
{
|
||||
tasks.map((task, index) => <TaskItem
|
||||
key={index}
|
||||
task={task} />)
|
||||
}
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="flex-row items-center justify-between">
|
||||
<CardTitle>Messages</CardTitle>
|
||||
<DashboardDropdown />
|
||||
</CardHeader>
|
||||
<CardContent className="p-0">
|
||||
<ul className="divide-y divide-default-100">
|
||||
{
|
||||
messagesData.map((message, index) =>
|
||||
<MessageListItem
|
||||
message={message}
|
||||
key={index} />)
|
||||
}
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex-row items-center justify-between">
|
||||
<CardTitle>Activity</CardTitle>
|
||||
<DashboardDropdown />
|
||||
</CardHeader>
|
||||
<CardContent className="p-0">
|
||||
<ul className="relative before:absolute before:start-6 before:top-3.5 before:w-[1px] before:h-[80%] before:bg-default-200">
|
||||
{
|
||||
activityList.map((activity, index) =>
|
||||
<ActivityItem
|
||||
activity={activity}
|
||||
key={index}
|
||||
/>)
|
||||
}
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
<div className="grid grid-cols-12 gap-5">
|
||||
<div className="col-span-12 lg:col-span-8">
|
||||
<Card>
|
||||
<CardHeader className="flex-row items-center justify-between">
|
||||
<CardTitle>Team Members</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="p-0">
|
||||
<TeamTable data={teamData} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
<div className="col-span-12 lg:col-span-4">
|
||||
<Card>
|
||||
<CardHeader className="flex-row items-center justify-between">
|
||||
<CardTitle>Files</CardTitle>
|
||||
<DashboardDropdown />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ul className="divide-y divide-default-100">
|
||||
{files.map((item, i) => (
|
||||
<li key={i} className="py-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex-1 flex gap-2">
|
||||
<div className="flex-none">
|
||||
<div className="h-8 w-8">
|
||||
<Image
|
||||
src={item.img}
|
||||
alt=""
|
||||
width={32}
|
||||
height={32}
|
||||
className=" w-full h-full object-cover rounded-full border hover:border-white border-transparent"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="text-default-600 text-sm">
|
||||
{item.title}
|
||||
</div>
|
||||
<div className="font-normal text-xs text-default-500 mt-1">
|
||||
{item.date}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-none">
|
||||
<button
|
||||
type="button"
|
||||
className="text-xs text-slate-900 dark:text-white"
|
||||
>
|
||||
Download
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SinglePage
|
||||
|
|
@ -0,0 +1,224 @@
|
|||
"use client";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Calendar } from "@/components/ui/calendar";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { format } from "date-fns";
|
||||
import { CalendarIcon } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import { useState } from "react";
|
||||
import { useForm, SubmitHandler, Controller } from "react-hook-form"
|
||||
import Select, { MultiValue } from 'react-select'
|
||||
interface CreateTaskProps {
|
||||
open: boolean;
|
||||
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
}
|
||||
interface Option {
|
||||
value: string;
|
||||
label: string;
|
||||
image?: string;
|
||||
}
|
||||
type Inputs = {
|
||||
title: string;
|
||||
description: string;
|
||||
tags: MultiValue<Option>;
|
||||
assignee: MultiValue<Option>;
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
}
|
||||
const options: Option[] = [
|
||||
{
|
||||
value: "team",
|
||||
label: "team",
|
||||
},
|
||||
{
|
||||
value: "low",
|
||||
label: "low",
|
||||
},
|
||||
{
|
||||
value: "medium",
|
||||
label: "medium",
|
||||
},
|
||||
{
|
||||
value: "high",
|
||||
label: "high",
|
||||
},
|
||||
{
|
||||
value: "update",
|
||||
label: "update",
|
||||
}
|
||||
];
|
||||
const assigneeOptions: Option[] = [
|
||||
{ value: "mahedi", label: "Mahedi Amin", image: "/images/avatar/av-1.svg" },
|
||||
{ value: "sovo", label: "Sovo Haldar", image: "/images/avatar/av-2.svg" },
|
||||
{ value: "rakibul", label: "Rakibul Islam", image: "/images/avatar/av-3.svg" },
|
||||
{ value: "pritom", label: "Pritom Miha", image: "/images/avatar/av-4.svg" },
|
||||
];
|
||||
|
||||
|
||||
const CreateProject = ({ open, setOpen }: CreateTaskProps) => {
|
||||
const [startDate, setStartDate] = useState<Date>(new Date());
|
||||
const [endDate, setEndtDate] = useState<Date>(new Date());
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
control,
|
||||
formState: { errors },
|
||||
} = useForm<Inputs>()
|
||||
const onSubmit: SubmitHandler<Inputs> = (data) => {
|
||||
console.log(data)
|
||||
setOpen(false)
|
||||
}
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create Project</DialogTitle>
|
||||
</DialogHeader>
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="projectName">Project Name</Label>
|
||||
<Input
|
||||
id="projectName"
|
||||
placeholder="Project Name"
|
||||
{...register("title", { required: "Title is required." })}
|
||||
color={errors.title ? "destructive" : "default"}
|
||||
/>
|
||||
{errors.title && <p className="text-destructive text-sm font-medium">{errors.title.message}</p>}
|
||||
</div>
|
||||
<div className="grid lg:grid-cols-2 grid-cols-1 gap-5">
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="startDate">Start Date</Label>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full justify-start text-start font-normal border border-default-200 text-default-700 md:px-2.5 hover:bg-transparent hover:text-default-700"
|
||||
size="md"
|
||||
>
|
||||
<CalendarIcon className="me-2 h-3.5 w-3.5 text-default-500" />
|
||||
{format(startDate, "PPP")}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0">
|
||||
<Controller
|
||||
name="startDate"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={startDate}
|
||||
onSelect={(date) => {
|
||||
field.onChange(date);
|
||||
setStartDate(date as Date);
|
||||
}}
|
||||
initialFocus
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="endDate">End Date</Label>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full justify-start text-start font-normal border border-default-200 text-default-700 md:px-2.5 hover:bg-transparent hover:text-default-700"
|
||||
size="md"
|
||||
>
|
||||
<CalendarIcon className="me-2 h-3.5 w-3.5 text-default-500" />
|
||||
{format(endDate, "PPP")}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0">
|
||||
<Controller
|
||||
name="startDate"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={endDate}
|
||||
onSelect={(date) => {
|
||||
field.onChange(date);
|
||||
setEndtDate(date as Date);
|
||||
}}
|
||||
initialFocus
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="assignee">Assignee</Label>
|
||||
<Controller
|
||||
name="assignee"
|
||||
control={control}
|
||||
defaultValue={[]}
|
||||
render={({ field }) => (
|
||||
<Select
|
||||
{...field}
|
||||
options={assigneeOptions}
|
||||
isMulti
|
||||
onChange={(selectedOption) => field.onChange(selectedOption)}
|
||||
getOptionLabel={(option) => (
|
||||
<div className="flex items-center">
|
||||
<Image width={40} height={40} src={option.image as string} alt={option.label} className="w-8 h-8 rounded-full me-2" />
|
||||
<span className="text-sm text-default-600 font-medium">{option.label}</span>
|
||||
</div>
|
||||
) as unknown as string}
|
||||
placeholder="Select assignee"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="tag">Tags</Label>
|
||||
<Controller
|
||||
name="tags"
|
||||
control={control}
|
||||
defaultValue={[]}
|
||||
render={({ field }) => (
|
||||
<Select
|
||||
{...field}
|
||||
options={options}
|
||||
isMulti
|
||||
onChange={(selectedOption) => field.onChange(selectedOption)}
|
||||
placeholder="Select tags"
|
||||
getOptionLabel={(option) => <span className="text-sm text-default-600 font-medium capitalize">{option.label}</span> as unknown as string}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="description">Description</Label>
|
||||
<Textarea
|
||||
id="description"
|
||||
placeholder="Project Description"
|
||||
{...register("description")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button type="submit">Add</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateProject;
|
||||
|
|
@ -0,0 +1,158 @@
|
|||
import { faker } from "@faker-js/faker";
|
||||
|
||||
|
||||
export const defaultProjects = [
|
||||
{
|
||||
id: "c06d48bf-7f35-4789-b71e-d80fee5b430f",
|
||||
title: "CRM Dashboard ",
|
||||
projectLogo: "/images/project/p-2.png",
|
||||
desc: "Amet minim mollit non deserunt ullamco est sit aliqua dolor do amet sint.",
|
||||
startDate: "2022-10-03",
|
||||
endDate: "2022-10-06",
|
||||
progress: 90,
|
||||
assignee: [
|
||||
{
|
||||
image: "/images/avatar/av-1.svg",
|
||||
name: "Mahedi Amin",
|
||||
},
|
||||
{
|
||||
image: "/images/avatar/av-2.svg",
|
||||
name: "Sovo Haldar",
|
||||
},
|
||||
{
|
||||
image: "/images/avatar/av-3.svg",
|
||||
name: "Rakibul Islam",
|
||||
}
|
||||
],
|
||||
remainingDays: 3
|
||||
},
|
||||
{
|
||||
id: faker.string.uuid(),
|
||||
title: "Business Dashboard ",
|
||||
projectLogo: "/images/project/p-2.png",
|
||||
desc: "Amet minim mollit non deserunt ullamco est sit aliqua dolor do amet sint.",
|
||||
startDate: "2022-10-03",
|
||||
endDate: "2022-10-06",
|
||||
progress: 90,
|
||||
assignee: [
|
||||
{
|
||||
image: "/images/avatar/av-1.svg",
|
||||
name: "Mahedi Amin",
|
||||
},
|
||||
{
|
||||
image: "/images/avatar/av-2.svg",
|
||||
name: "Sovo Haldar",
|
||||
},
|
||||
{
|
||||
image: "/images/avatar/av-3.svg",
|
||||
name: "Rakibul Islam",
|
||||
}
|
||||
],
|
||||
remainingDays: 3
|
||||
},
|
||||
{
|
||||
id: faker.string.uuid(),
|
||||
title: "Management Dashboard ",
|
||||
projectLogo: "/images/project/p-2.png",
|
||||
desc: "Amet minim mollit non deserunt ullamco est sit aliqua dolor do amet sint.",
|
||||
startDate: "2022-10-03",
|
||||
endDate: "2022-10-06",
|
||||
progress: 90,
|
||||
assignee: [
|
||||
{
|
||||
image: "/images/avatar/av-1.svg",
|
||||
name: "Mahedi Amin",
|
||||
},
|
||||
{
|
||||
image: "/images/avatar/av-2.svg",
|
||||
name: "Sovo Haldar",
|
||||
},
|
||||
{
|
||||
image: "/images/avatar/av-3.svg",
|
||||
name: "Rakibul Islam",
|
||||
}
|
||||
],
|
||||
remainingDays: 3
|
||||
},
|
||||
{
|
||||
id: faker.string.uuid(),
|
||||
title: "Analytics Dashboard ",
|
||||
projectLogo: "/images/project/p-2.png",
|
||||
desc: "Amet minim mollit non deserunt ullamco est sit aliqua dolor do amet sint.",
|
||||
startDate: "2022-10-03",
|
||||
endDate: "2022-10-06",
|
||||
progress: 90,
|
||||
assignee: [
|
||||
{
|
||||
image: "/images/avatar/av-1.svg",
|
||||
name: "Mahedi Amin",
|
||||
},
|
||||
{
|
||||
image: "/images/avatar/av-2.svg",
|
||||
name: "Sovo Haldar",
|
||||
},
|
||||
{
|
||||
image: "/images/avatar/av-3.svg",
|
||||
name: "Rakibul Islam",
|
||||
}
|
||||
],
|
||||
remainingDays: 3
|
||||
},
|
||||
|
||||
{
|
||||
id: faker.string.uuid(),
|
||||
title: "Marketing Dashboard ",
|
||||
projectLogo: "/images/project/p-2.png",
|
||||
desc: "Amet minim mollit non deserunt ullamco est sit aliqua dolor do amet sint.",
|
||||
startDate: "2022-10-03",
|
||||
endDate: "2022-10-06",
|
||||
progress: 90,
|
||||
assignee: [
|
||||
{
|
||||
image: "/images/avatar/av-1.svg",
|
||||
name: "Mahedi Amin",
|
||||
},
|
||||
{
|
||||
image: "/images/avatar/av-2.svg",
|
||||
name: "Sovo Haldar",
|
||||
},
|
||||
{
|
||||
image: "/images/avatar/av-3.svg",
|
||||
name: "Rakibul Islam",
|
||||
}
|
||||
],
|
||||
remainingDays: 3
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
export const getProjects = async () => {
|
||||
return defaultProjects
|
||||
}
|
||||
|
||||
export const getProjectById = async (id: string) => {
|
||||
return defaultProjects.find(project => project.id === id)
|
||||
}
|
||||
|
||||
interface ProjectNav {
|
||||
label: string
|
||||
href: string
|
||||
active: boolean
|
||||
}
|
||||
|
||||
export function getProjectNav(pathname: string): ProjectNav[] {
|
||||
return [
|
||||
{
|
||||
label: 'grid view',
|
||||
href: "/app/projects/grid",
|
||||
active: pathname === "/app/projects/grid",
|
||||
},
|
||||
{
|
||||
label: 'list view',
|
||||
href: "/app/projects/list",
|
||||
active: pathname === "/app/projects/list",
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
export type Project = (typeof defaultProjects)[number]
|
||||
|
|
@ -0,0 +1,226 @@
|
|||
"use client";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Calendar } from "@/components/ui/calendar";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { format } from "date-fns";
|
||||
import { CalendarIcon } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import { useState } from "react";
|
||||
import { useForm, SubmitHandler, Controller } from "react-hook-form"
|
||||
import Select, { MultiValue } from 'react-select'
|
||||
interface EditProjectProps {
|
||||
open: boolean;
|
||||
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
}
|
||||
interface Option {
|
||||
value: string;
|
||||
label: string;
|
||||
image?: string;
|
||||
}
|
||||
type Inputs = {
|
||||
title: string;
|
||||
description: string;
|
||||
tags: MultiValue<Option>;
|
||||
assignee: MultiValue<Option>;
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
}
|
||||
const options: Option[] = [
|
||||
{
|
||||
value: "team",
|
||||
label: "team",
|
||||
},
|
||||
{
|
||||
value: "low",
|
||||
label: "low",
|
||||
},
|
||||
{
|
||||
value: "medium",
|
||||
label: "medium",
|
||||
},
|
||||
{
|
||||
value: "high",
|
||||
label: "high",
|
||||
},
|
||||
{
|
||||
value: "update",
|
||||
label: "update",
|
||||
}
|
||||
];
|
||||
const assigneeOptions: Option[] = [
|
||||
{ value: "mahedi", label: "Mahedi Amin", image: "/images/avatar/av-1.svg" },
|
||||
{ value: "sovo", label: "Sovo Haldar", image: "/images/avatar/av-2.svg" },
|
||||
{ value: "rakibul", label: "Rakibul Islam", image: "/images/avatar/av-3.svg" },
|
||||
{ value: "pritom", label: "Pritom Miha", image: "/images/avatar/av-4.svg" },
|
||||
];
|
||||
|
||||
|
||||
const EditProject = ({ open, setOpen }: EditProjectProps) => {
|
||||
const [startDate, setStartDate] = useState<Date>(new Date());
|
||||
const [endDate, setEndtDate] = useState<Date>(new Date());
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
control,
|
||||
formState: { errors },
|
||||
} = useForm<Inputs>()
|
||||
const onSubmit: SubmitHandler<Inputs> = (data) => {
|
||||
console.log(data)
|
||||
setOpen(false)
|
||||
}
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create Task</DialogTitle>
|
||||
</DialogHeader>
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="projectName">Project Name</Label>
|
||||
<Input
|
||||
id="projectName"
|
||||
placeholder="Project Name"
|
||||
defaultValue="Project Title"
|
||||
{...register("title", { required: "Title is required." })}
|
||||
color={errors.title ? "destructive" : "default"}
|
||||
/>
|
||||
{errors.title && <p className="text-destructive text-sm font-medium">{errors.title.message}</p>}
|
||||
</div>
|
||||
<div className="grid lg:grid-cols-2 grid-cols-1 gap-5">
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="startDate">Start Date</Label>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full justify-start text-start font-normal border border-default-200 text-default-700 md:px-2.5 hover:bg-transparent hover:text-default-700"
|
||||
size="md"
|
||||
>
|
||||
<CalendarIcon className="me-2 h-3.5 w-3.5 text-default-500" />
|
||||
{format(startDate, "PPP")}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0">
|
||||
<Controller
|
||||
name="startDate"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={startDate}
|
||||
onSelect={(date) => {
|
||||
field.onChange(date);
|
||||
setStartDate(date as Date);
|
||||
}}
|
||||
initialFocus
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="endDate">End Date</Label>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full justify-start text-start font-normal border border-default-200 text-default-700 md:px-2.5 hover:bg-transparent hover:text-default-700"
|
||||
size="md"
|
||||
>
|
||||
<CalendarIcon className="me-2 h-3.5 w-3.5 text-default-500" />
|
||||
{format(endDate, "PPP")}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0">
|
||||
<Controller
|
||||
name="startDate"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={endDate}
|
||||
onSelect={(date) => {
|
||||
field.onChange(date);
|
||||
setEndtDate(date as Date);
|
||||
}}
|
||||
initialFocus
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="assignee">Assignee</Label>
|
||||
<Controller
|
||||
name="assignee"
|
||||
control={control}
|
||||
defaultValue={[{ value: "mahedi", label: "Mahedi Amin", image: "/images/avatar/av-1.svg" }]}
|
||||
render={({ field }) => (
|
||||
<Select
|
||||
{...field}
|
||||
options={assigneeOptions}
|
||||
isMulti
|
||||
onChange={(selectedOption) => field.onChange(selectedOption)}
|
||||
getOptionLabel={(option) => (
|
||||
<div className="flex items-center">
|
||||
<Image width={40} height={40} src={option.image as string} alt={option.label} className="w-8 h-8 rounded-full me-2" />
|
||||
<span className="text-sm text-default-600 font-medium">{option.label}</span>
|
||||
</div>
|
||||
) as unknown as string}
|
||||
placeholder="Select assignee"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="tag">Tags</Label>
|
||||
<Controller
|
||||
name="tags"
|
||||
control={control}
|
||||
defaultValue={[{ value: "team", label: "Team" }]}
|
||||
render={({ field }) => (
|
||||
<Select
|
||||
{...field}
|
||||
options={options}
|
||||
isMulti
|
||||
onChange={(selectedOption) => field.onChange(selectedOption)}
|
||||
placeholder="Select tags"
|
||||
getOptionLabel={(option) => <span className="text-sm text-default-600 font-medium capitalize">{option.label}</span> as unknown as string}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="description">Description</Label>
|
||||
<Textarea
|
||||
id="description"
|
||||
placeholder="Project Description"
|
||||
{...register("description")}
|
||||
defaultValue="Project Description"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button type="submit">Add</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditProject;
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
|
||||
import { Rabbit } from 'lucide-react';
|
||||
|
||||
|
||||
const EmptyProject = () => {
|
||||
return (
|
||||
<div className="">
|
||||
<Rabbit className=" h-[200px] w-[200px] text-default-400 " strokeWidth={1} />
|
||||
<div className=' text-sm text-default-500 text-center'>No tasks</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmptyProject;
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
'use client'
|
||||
import React, { useState } from 'react'
|
||||
import { Button } from "@/components/ui/button";
|
||||
import EditProject from "../../edit-project";
|
||||
import DeleteConfirmationDialog from "@/components/delete-confirmation-dialog";
|
||||
import { Eye, MoreVertical, SquarePen, Trash2 } from "lucide-react";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu"
|
||||
import { defaultProjects } from '../../data';
|
||||
import { Link } from '@/i18n/routing'; const ProjectAction = () => {
|
||||
const [editTaskOpen, setEditTaskOpen] = useState<boolean>(false);
|
||||
const [deleteProject, setDeleteProject] = useState<boolean>(false)
|
||||
return (
|
||||
<>
|
||||
<EditProject
|
||||
open={editTaskOpen}
|
||||
setOpen={setEditTaskOpen}
|
||||
/>
|
||||
<DeleteConfirmationDialog
|
||||
open={deleteProject}
|
||||
onClose={() => setDeleteProject(false)}
|
||||
/>
|
||||
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
size="icon"
|
||||
className="flex-none bg-transparent ring-offset-transparent hover:bg-transparent hover:ring-0 hover:ring-transparent w-6"
|
||||
>
|
||||
<MoreVertical className="h-4 w-4 text-default-700" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="p-0 overflow-hidden" align="end" >
|
||||
<DropdownMenuItem
|
||||
|
||||
className="py-2 border-b border-default-200 text-default-600 focus:bg-default focus:text-default-foreground rounded-none cursor-pointer"
|
||||
|
||||
>
|
||||
<Link href={`/app/projects/${defaultProjects[0].id}`} className=' flex items-center w-full'>
|
||||
<Eye className="w-3.5 h-3.5 me-1" />
|
||||
View
|
||||
</Link>
|
||||
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => setEditTaskOpen(true)}
|
||||
className="py-2 border-b border-default-200 text-default-600 focus:bg-default focus:text-default-foreground rounded-none cursor-pointer"
|
||||
|
||||
>
|
||||
<SquarePen className="w-3.5 h-3.5 me-1" />
|
||||
Edit
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => setDeleteProject(true)}
|
||||
className="py-2 bg-destructive/30 focus:bg-destructive focus:text-destructive-foreground rounded-none cursor-pointer"
|
||||
|
||||
>
|
||||
<Trash2 className="w-3.5 h-3.5 me-1" />
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProjectAction
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
import React from 'react'
|
||||
import { getProjects } from '../data'
|
||||
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
import { Icon } from "@/components/ui/icon";
|
||||
import EmptyProject from './components/empty';
|
||||
import ProjectAction from './components/project-action';
|
||||
const ProjectGrid = async () => {
|
||||
const projects = await getProjects()
|
||||
if (projects.length === 0) return <EmptyProject />
|
||||
return (
|
||||
<div className='grid lg:grid-cols-3 md:grid-cols-2 grid-cols-1 gap-5'>
|
||||
{projects?.map(({ projectLogo, title, desc, startDate, endDate, progress, assignee, remainingDays }, index) => (
|
||||
<Card key={index}>
|
||||
<CardHeader className="flex-row gap-1 items-center space-y-0">
|
||||
<div className="flex-1 flex items-center gap-4">
|
||||
<Avatar className="flex-none h-10 w-10 rounded bg-default-200 text-default hover:bg-default-200">
|
||||
<AvatarImage src={projectLogo} />
|
||||
<AvatarFallback className="uppercase"> {title.charAt(0) + title.charAt(1)}</AvatarFallback>
|
||||
</Avatar>
|
||||
<h3 className="text-default-900 text-lg font-medium max-w-[210px] truncate text-center capitalize ">{title}</h3>
|
||||
</div>
|
||||
<ProjectAction />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-default-600 text-sm">{desc}</div>
|
||||
<div className="flex gap-4 mt-6">
|
||||
<div>
|
||||
<div className="text-xs text-default-400 mb-1">Start Date</div>
|
||||
<div className="text-xs text-default-600 font-medium">{startDate}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-default-400 mb-1">End Date</div>
|
||||
<div className="text-xs text-default-600 font-medium">{endDate}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-1">
|
||||
<div className="text-end text-xs text-default-600 mb-1.5 font-medium">{progress}%</div>
|
||||
<Progress value={progress} color="primary" size="sm" />
|
||||
</div>
|
||||
<div className="flex mt-5">
|
||||
<div className="flex-1">
|
||||
<div className="text-default-400 text-sm font-normal mb-3">Assigned to</div>
|
||||
<div className="flex items-center -space-x-1">
|
||||
{
|
||||
assignee?.map((user, index) => (
|
||||
<Avatar
|
||||
key={`user-${index}`}
|
||||
className="h-6 w-6 shadow-none border-none bg-transparent hover:bg-transparent"
|
||||
>
|
||||
<AvatarImage src={user.image} />
|
||||
<AvatarFallback> {user.name.charAt(0) + user.name.charAt(1)}</AvatarFallback>
|
||||
</Avatar>
|
||||
))
|
||||
}
|
||||
<div className="bg-card text-default-900 text-xs ring-2 ring-default-100 rounded-full h-6 w-6 flex flex-col justify-center items-center">
|
||||
+2
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-none">
|
||||
<div className="flex items-center gap-1 bg-destructive/10 text-destructive rounded-full px-2 py-0.5 text-xs mt-1">
|
||||
<Icon icon="heroicons-outline:clock" />
|
||||
{remainingDays} days left
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProjectGrid
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
import { Metadata } from "next";
|
||||
import ProjectWrapper from "./project-wrapper";
|
||||
export const metadata: Metadata = {
|
||||
title: 'Projects',
|
||||
description: 'Projects Page'
|
||||
}
|
||||
const Layout = ({ children }: { children: React.ReactNode }) => {
|
||||
return (
|
||||
<ProjectWrapper>
|
||||
{children}
|
||||
</ProjectWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Layout;
|
||||
|
|
@ -0,0 +1,310 @@
|
|||
"use client"
|
||||
import * as React from "react"
|
||||
import {
|
||||
ColumnDef,
|
||||
ColumnFiltersState,
|
||||
SortingState,
|
||||
VisibilityState,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
getFilteredRowModel,
|
||||
getPaginationRowModel,
|
||||
getSortedRowModel,
|
||||
useReactTable
|
||||
} from "@tanstack/react-table"
|
||||
import { defaultProjects, type Project } from "../../data";
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { ChevronLeft, ChevronRight, Eye, MoreVertical, SquarePen, Trash2 } from 'lucide-react';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu"
|
||||
import { Link } from '@/i18n/routing';
|
||||
import EditProject from "../../edit-project";
|
||||
import DeleteConfirmationDialog from "@/components/delete-confirmation-dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
|
||||
const ListTable = ({ projects }: { projects: Project[] }) => {
|
||||
const [sorting, setSorting] = React.useState<SortingState>([])
|
||||
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
|
||||
[]
|
||||
)
|
||||
const [columnVisibility, setColumnVisibility] =
|
||||
React.useState<VisibilityState>({})
|
||||
const [rowSelection, setRowSelection] = React.useState({})
|
||||
const [editTaskOpen, setEditTaskOpen] = React.useState<boolean>(false);
|
||||
const [deleteProject, setDeleteProject] = React.useState<boolean>(false)
|
||||
|
||||
const columns: ColumnDef<Project>[] = [
|
||||
{
|
||||
accessorKey: "title",
|
||||
header: "Name",
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<div className="flex items-center gap-3">
|
||||
<Avatar className="w-10 h-10 shadow-none border-none bg-transparent hover:bg-transparent">
|
||||
<AvatarImage src={row.original.projectLogo} />
|
||||
<AvatarFallback> DC</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="font-medium text-sm leading-4 whitespace-nowrap">
|
||||
{row.getValue("title")}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: "startDate",
|
||||
header: "Start Date",
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<span className="whitespace-nowrap">{row.getValue("startDate")}</span>
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: "endDate",
|
||||
header: "End Date",
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<span className="whitespace-nowrap">{row.getValue("endDate")}</span>
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: "assignee",
|
||||
header: "Assignee",
|
||||
cell: ({ row }: { row: { getValue: (key: string) => { image: string }[] } }) => {
|
||||
const assignees = row.getValue("assignee");
|
||||
return (
|
||||
<div className="flex -space-x-1 rtl:space-x-reverse">
|
||||
<div className="flex -space-x-1 rtl:space-x-reverse">
|
||||
{assignees.map((item, index) => (
|
||||
<Avatar
|
||||
key={`user-${index}`}
|
||||
className="h-6 w-6 shadow-none border-none bg-transparent hover:bg-transparent"
|
||||
>
|
||||
<AvatarImage src={item.image} />
|
||||
<AvatarFallback>SC</AvatarFallback>
|
||||
</Avatar>
|
||||
))}
|
||||
</div>
|
||||
<div className="bg-card text-default-900 text-xs ring-2 ring-default-100 rounded-full h-6 w-6 flex flex-col justify-center items-center">
|
||||
+2
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: "progress",
|
||||
header: "Status",
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<div>
|
||||
<Progress value={row.getValue("progress")} color="primary" size="sm" />
|
||||
<div className=" text-xs text-default-500 font-medium mt-3 whitespace-nowrap">
|
||||
12/15 Task Completed
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
accessorKey: "action",
|
||||
header: "Action",
|
||||
enableHiding: false,
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
size="icon"
|
||||
className="flex-none ring-offset-transparent bg-transparent hover:bg-transparent hover:ring-0 hover:ring-transparent w-6"
|
||||
>
|
||||
<MoreVertical className="h-4 w-4 text-default-700" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="p-0 overflow-hidden" align="end" >
|
||||
<DropdownMenuItem
|
||||
className="py-2 border-b border-default-200 text-default-600 focus:bg-default focus:text-default-foreground rounded-none cursor-pointer"
|
||||
asChild
|
||||
>
|
||||
<Link href={`/app/projects/${defaultProjects[0].id}`}>
|
||||
<Eye className="w-3.5 h-3.5 me-1" />
|
||||
View
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="py-2 border-b border-default-200 text-default-600 focus:bg-default focus:text-default-foreground rounded-none cursor-pointer"
|
||||
onClick={() => setEditTaskOpen(true)}
|
||||
>
|
||||
<SquarePen className="w-3.5 h-3.5 me-1" />
|
||||
Edit
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="py-2 bg-destructive/10 text-destructive focus:bg-destructive focus:text-destructive-foreground rounded-none cursor-pointer"
|
||||
onClick={() => setDeleteProject(true)}
|
||||
>
|
||||
<Trash2 className="w-3.5 h-3.5 me-1" />
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)
|
||||
}
|
||||
}
|
||||
]
|
||||
const table = useReactTable({
|
||||
data: projects,
|
||||
columns,
|
||||
onSortingChange: setSorting,
|
||||
onColumnFiltersChange: setColumnFilters,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
getFilteredRowModel: getFilteredRowModel(),
|
||||
onColumnVisibilityChange: setColumnVisibility,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
state: {
|
||||
sorting,
|
||||
columnFilters,
|
||||
columnVisibility,
|
||||
rowSelection
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<EditProject
|
||||
open={editTaskOpen}
|
||||
setOpen={setEditTaskOpen}
|
||||
/>
|
||||
<DeleteConfirmationDialog
|
||||
open={deleteProject}
|
||||
onClose={() => setDeleteProject(false)}
|
||||
/>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Project List</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="p-0">
|
||||
<Table>
|
||||
<TableHeader className="px-3 bg-default-100">
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => {
|
||||
return (
|
||||
<TableHead key={header.id}>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext()
|
||||
)}
|
||||
</TableHead>
|
||||
)
|
||||
})}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{table.getRowModel().rows?.length ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow
|
||||
key={row.id}
|
||||
data-state={row.getIsSelected() && "selected"}
|
||||
className="even:bg-default-100 px-6 h-20" >
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id}>
|
||||
{flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext()
|
||||
)}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={columns.length}
|
||||
className="h-24 text-center"
|
||||
>
|
||||
No results.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<div className="flex items-center justify-end py-4 px-10">
|
||||
|
||||
<div className="flex-1 flex items-center gap-3">
|
||||
<div className=" flex gap-2 items-center">
|
||||
<div className="text-sm font-medium text-default-60">Go </div>
|
||||
<Input
|
||||
type="number"
|
||||
className="w-16 px-2"
|
||||
defaultValue={table.getState().pagination.pageIndex + 1}
|
||||
onChange={(e) => {
|
||||
const pageNumber = e.target.value ? Number(e.target.value) - 1 : 0;
|
||||
table.setPageIndex(pageNumber);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-sm font-medium text-default-600">
|
||||
Page{" "} {table.getState().pagination.pageIndex + 1} of {table.getPageCount()}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 flex-none">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => table.previousPage()}
|
||||
disabled={!table.getCanPreviousPage()}
|
||||
className='w-8 h-8'
|
||||
>
|
||||
<ChevronLeft className='w-4 h-4' />
|
||||
</Button>
|
||||
{table.getPageOptions().map((page, pageIndex) => (
|
||||
<Button
|
||||
key={`basic-data-table-${pageIndex}`}
|
||||
onClick={() => table.setPageIndex(pageIndex)}
|
||||
size="icon"
|
||||
className={`w-8 h-8 ${table.getState().pagination.pageIndex === pageIndex ? 'bg-default' : 'bg-default-300 text-default'}`}
|
||||
>
|
||||
{page + 1}
|
||||
</Button>
|
||||
|
||||
))}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => table.nextPage()}
|
||||
disabled={!table.getCanNextPage()}
|
||||
className='w-8 h-8'
|
||||
>
|
||||
<ChevronRight className='w-4 h-4' />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default ListTable;
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
import React from 'react'
|
||||
import ListTable from './components/list-table'
|
||||
import { getProjects } from '../data'
|
||||
|
||||
const ProjectList = async () => {
|
||||
const projects = await getProjects()
|
||||
return (
|
||||
<div>
|
||||
<ListTable projects={projects} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProjectList
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
import React from 'react'
|
||||
|
||||
const Loading = () => {
|
||||
return (
|
||||
<div>loading..</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Loading
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
|
||||
import { redirect } from '@/components/navigation'
|
||||
|
||||
const ProjectPage = () => {
|
||||
redirect('/app/projects/grid')
|
||||
return null
|
||||
}
|
||||
|
||||
export default ProjectPage
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
"use client"
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Filter, List, Plus } from "lucide-react";
|
||||
import CreateProject from "./create-project";
|
||||
import { useState } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Link, usePathname } from "@/components/navigation";
|
||||
import { getProjectNav } from "./data";
|
||||
|
||||
const ProjectWrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
const [open, setOpen] = useState<boolean>(false);
|
||||
const pathname = usePathname();
|
||||
const menus = getProjectNav(pathname)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<CreateProject
|
||||
open={open}
|
||||
setOpen={setOpen}
|
||||
/>
|
||||
<div className="flex w-full flex-wrap items-center gap-4 mb-6">
|
||||
<h4 className="flex-1 font-medium lg:text-2xl text-xl capitalize text-default-900">
|
||||
Project
|
||||
</h4>
|
||||
<div className="flex items-center gap-4 flex-wrap">
|
||||
{menus?.map(({ label, href, active }, index) => (
|
||||
<Button
|
||||
key={index}
|
||||
asChild
|
||||
className={cn("flex-none capitalize bg-card text-default-600 hover:bg-card hover:text-default-600 hover:ring-0 hover:ring-transparent", {
|
||||
"bg-default text-default-foreground hover:bg-default hover:text-default-foreground": active,
|
||||
})}
|
||||
|
||||
>
|
||||
<Link href={href}>
|
||||
<List className="w-3.5 h-3.5 me-1" />
|
||||
<span>{label}</span>
|
||||
</Link>
|
||||
|
||||
</Button>
|
||||
))}
|
||||
|
||||
|
||||
<Button className="flex-none bg-card text-default-600 hover:ring-0 hover:ring-transparent hover:bg-default hover:text-default-foreground" >
|
||||
<Filter className="w-3.5 h-3.5 me-1" />
|
||||
<span>On Going</span>
|
||||
</Button>
|
||||
<Button
|
||||
className="flex-none"
|
||||
onClick={() => setOpen(true)}
|
||||
>
|
||||
<Plus className="w-4 h-4 me-1" />
|
||||
<span>Add Project</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProjectWrapper;
|
||||
|
|
@ -0,0 +1,228 @@
|
|||
"use client"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog"
|
||||
import { CalendarIcon, Plus } from "lucide-react";
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { z } from "zod"
|
||||
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { toast } from "@/components/ui/use-toast"
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import Image from "next/image";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { Calendar } from "@/components/ui/calendar";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { format } from "date-fns";
|
||||
import { useState } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
const assigneeOptions = [
|
||||
{ value: "mahedi", label: "Mahedi Amin", image: "/images/avatar/av-1.svg" },
|
||||
{ value: "sovo", label: "Sovo Haldar", image: "/images/avatar/av-2.svg" },
|
||||
{ value: "rakibul", label: "Rakibul Islam", image: "/images/avatar/av-3.svg" },
|
||||
{ value: "pritom", label: "Pritom Miha", image: "/images/avatar/av-4.svg" },
|
||||
];
|
||||
|
||||
|
||||
const FormSchema = z.object({
|
||||
title: z.string().min(2, {
|
||||
message: "Title must be at least 2 characters.",
|
||||
}),
|
||||
assign: z.string().optional(),
|
||||
tag: z.string().optional(),
|
||||
dob: z.date().optional(),
|
||||
description: z.string().optional()
|
||||
})
|
||||
|
||||
const CreateTodo = () => {
|
||||
const t = useTranslations("TodoApp")
|
||||
const [isDialogOpen, setIsDialogOpen] = useState<boolean>(false);
|
||||
const form = useForm<z.infer<typeof FormSchema>>({
|
||||
resolver: zodResolver(FormSchema),
|
||||
defaultValues: {
|
||||
title: "",
|
||||
},
|
||||
})
|
||||
|
||||
function onSubmit(data: z.infer<typeof FormSchema>) {
|
||||
toast({
|
||||
title: "Task created successfully.",
|
||||
})
|
||||
setIsDialogOpen(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button fullWidth size="lg" className="dark:bg-background dark:text-foreground">
|
||||
<Plus className="w-6 h-6 me-1.5" />
|
||||
{t("addTask")}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader className="mb-4">
|
||||
<DialogTitle> {t("addTask")}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="title"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="text-default-700">Title</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Enter Title" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="assign"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="text-default-700">Assign</FormLabel>
|
||||
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select..." />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{
|
||||
assigneeOptions.map((user, index) =>
|
||||
<SelectItem
|
||||
key={`user-${index}`}
|
||||
value={user.value}>
|
||||
<div className="flex items-center gap-2">
|
||||
<Image
|
||||
src={user.image}
|
||||
alt={user.label}
|
||||
width={20}
|
||||
height={20}
|
||||
className="w-5 h-5 rounded-full" />
|
||||
<span className="text-sm text-default-900">{user.label}</span>
|
||||
</div>
|
||||
</SelectItem>)
|
||||
}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="dob"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-col">
|
||||
<FormLabel className="text-default-700">Due Date</FormLabel>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<FormControl>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="md"
|
||||
fullWidth
|
||||
className={cn(
|
||||
"border-default md:px-3",
|
||||
!field.value && "text-muted-foreground border-default-200 md:px-3"
|
||||
)}
|
||||
>
|
||||
{field.value ? (
|
||||
format(field.value, "PPP")
|
||||
) : (
|
||||
<span>Pick a date</span>
|
||||
)}
|
||||
<CalendarIcon className={cn("ms-auto h-4 w-4 text-default-300",
|
||||
{ "text-default-900": field.value }
|
||||
)} />
|
||||
</Button>
|
||||
</FormControl>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0" align="start">
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={field.value}
|
||||
onSelect={field.onChange}
|
||||
disabled={(date) =>
|
||||
date > new Date() || date < new Date("1900-01-01")
|
||||
}
|
||||
initialFocus
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="tag"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="text-default-700">Tag</FormLabel>
|
||||
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select..." />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="team">Team</SelectItem>
|
||||
<SelectItem value="low">Low</SelectItem>
|
||||
<SelectItem value="medium">Medium</SelectItem>
|
||||
<SelectItem value="high">High</SelectItem>
|
||||
<SelectItem value="update">Update</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="description"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="text-default-700">Description</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
placeholder="Tell us a little bit about yourself"
|
||||
className="resize-none"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<div className="flex justify-end">
|
||||
<Button type="submit">Submit</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateTodo;
|
||||
|
|
@ -0,0 +1,147 @@
|
|||
import { faker } from "@faker-js/faker";
|
||||
|
||||
export const todos = [
|
||||
{
|
||||
id: faker.string.uuid(),
|
||||
image: [
|
||||
{
|
||||
image: "/images/avatar/avatar-4.png",
|
||||
label: "Mahedi Amin",
|
||||
value: "mahedi",
|
||||
},
|
||||
{
|
||||
image: "/images/avatar/avatar-2.png",
|
||||
label: "Sovo Haldar",
|
||||
value: "sovo",
|
||||
},
|
||||
{
|
||||
image: "/images/avatar/avatar-3.png",
|
||||
label: "Rakibul Islam",
|
||||
value: "rakibul",
|
||||
},
|
||||
],
|
||||
title: "laboriosam mollitia et enim quasi adipisci quia provident illum",
|
||||
isDone: false,
|
||||
isfav: false,
|
||||
isTrash: false,
|
||||
category: [
|
||||
{
|
||||
value: "team",
|
||||
label: "team",
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: faker.string.uuid(),
|
||||
image: [
|
||||
{
|
||||
image: "/images/avatar/avatar-2.png",
|
||||
label: "Rakibul Islam",
|
||||
value: "rakibul",
|
||||
}
|
||||
],
|
||||
title:
|
||||
"Amet minim mollit non deserunt ullamco est sit aliqua dolor do amet sint.",
|
||||
isDone: false,
|
||||
isfav: true,
|
||||
isTrash: false,
|
||||
category: [
|
||||
{
|
||||
value: "low",
|
||||
label: "low",
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: faker.string.uuid(),
|
||||
image: [
|
||||
{
|
||||
image: "/images/avatar/avatar-4.png",
|
||||
label: "Sovo Haldar",
|
||||
value: "sovo",
|
||||
},
|
||||
{
|
||||
image: "/images/avatar/avatar-1.png",
|
||||
label: "Rakibul Islam",
|
||||
value: "rakibul",
|
||||
},
|
||||
],
|
||||
title:
|
||||
"Amet minim mollit non deserunt ullamco est sit aliqua dolor do amet sint.",
|
||||
isDone: true,
|
||||
isfav: true,
|
||||
isTrash: false,
|
||||
category: [
|
||||
{
|
||||
value: "medium",
|
||||
label: "medium",
|
||||
},
|
||||
{
|
||||
value: "low",
|
||||
label: "low",
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id:faker.string.uuid(),
|
||||
image: [
|
||||
{
|
||||
image: "/images/avatar/avatar-3.png",
|
||||
label: "Mahedi Amin",
|
||||
value: "mahedi",
|
||||
},
|
||||
{
|
||||
image: "/images/avatar/avatar-4.png",
|
||||
label: "Sovo Haldar",
|
||||
value: "sovo",
|
||||
},
|
||||
{
|
||||
image: "/images/avatar/avatar-4.png",
|
||||
label: "Rakibul Islam",
|
||||
value: "rakibul",
|
||||
}
|
||||
],
|
||||
title: "illo expedita consequatur quia in",
|
||||
isDone: false,
|
||||
isfav: false,
|
||||
isTrash: false,
|
||||
category: [
|
||||
{
|
||||
value: "high",
|
||||
label: "high",
|
||||
},
|
||||
{
|
||||
value: "low",
|
||||
label: "low",
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id:faker.string.uuid(),
|
||||
image: [
|
||||
{
|
||||
image: "/images/avatar/avatar-5.png",
|
||||
label: "Rakibul Islam",
|
||||
value: "rakibul",
|
||||
},
|
||||
],
|
||||
title: "illo expedita consequatur quia in",
|
||||
isDone: false,
|
||||
isfav: false,
|
||||
isTrash: false,
|
||||
category: [
|
||||
{
|
||||
value: "update",
|
||||
label: "update",
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
export type Todo = ( typeof todos)[number]
|
||||
|
|
@ -0,0 +1,218 @@
|
|||
"use client"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog"
|
||||
import { CalendarIcon } from "lucide-react";
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { z } from "zod"
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { toast } from "@/components/ui/use-toast"
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import Image from "next/image";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { Calendar } from "@/components/ui/calendar";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { format } from "date-fns";
|
||||
|
||||
const assigneeOptions = [
|
||||
{ value: "mahedi", label: "Mahedi Amin", image: "/images/avatar/av-1.svg" },
|
||||
{ value: "sovo", label: "Sovo Haldar", image: "/images/avatar/av-2.svg" },
|
||||
{ value: "rakibul", label: "Rakibul Islam", image: "/images/avatar/av-3.svg" },
|
||||
{ value: "pritom", label: "Pritom Miha", image: "/images/avatar/av-4.svg" },
|
||||
];
|
||||
|
||||
|
||||
const FormSchema = z.object({
|
||||
title: z.string().min(2, {
|
||||
message: "Title must be at least 2 characters.",
|
||||
}),
|
||||
assign: z.string().optional(),
|
||||
tag: z.string().optional(),
|
||||
dob: z.date().optional(),
|
||||
description: z.string().optional()
|
||||
})
|
||||
|
||||
const EditTodo = ({ open, setOpen }: { open: boolean; setOpen: React.Dispatch<React.SetStateAction<boolean>> }) => {
|
||||
const form = useForm<z.infer<typeof FormSchema>>({
|
||||
resolver: zodResolver(FormSchema),
|
||||
defaultValues: {
|
||||
title: "This is title",
|
||||
assign: "mahedi",
|
||||
tag: "high",
|
||||
dob: new Date(),
|
||||
description: "This is description"
|
||||
}
|
||||
})
|
||||
|
||||
function onSubmit(data: z.infer<typeof FormSchema>) {
|
||||
toast({
|
||||
title: "Task Update successfully.",
|
||||
})
|
||||
setOpen(false);
|
||||
}
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen} >
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle> Edit Task</DialogTitle>
|
||||
</DialogHeader>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="title"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="text-default-700">Title</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Enter Title" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="assign"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="text-default-700">Assign</FormLabel>
|
||||
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select..." />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{
|
||||
assigneeOptions.map((user, index) =>
|
||||
<SelectItem
|
||||
key={`user-${index}`}
|
||||
value={user.value}>
|
||||
<div className="flex items-center gap-2">
|
||||
<Image
|
||||
src={user.image}
|
||||
alt={user.label}
|
||||
width={20}
|
||||
height={20}
|
||||
className="w-5 h-5 rounded-full" />
|
||||
<span className="text-sm text-default-900">{user.label}</span>
|
||||
</div>
|
||||
</SelectItem>)
|
||||
}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="dob"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-col">
|
||||
<FormLabel className="text-default-700">Due Date</FormLabel>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<FormControl>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="md"
|
||||
fullWidth
|
||||
className={cn(
|
||||
"border-default md:px-3",
|
||||
!field.value && "text-muted-foreground border-default-200 md:px-3"
|
||||
)}
|
||||
>
|
||||
{field.value ? (
|
||||
format(field.value, "PPP")
|
||||
) : (
|
||||
<span>Pick a date</span>
|
||||
)}
|
||||
<CalendarIcon className={cn("ms-auto h-4 w-4 text-default-300",
|
||||
{ "text-default-900": field.value }
|
||||
)} />
|
||||
</Button>
|
||||
</FormControl>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0" align="start">
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={field.value}
|
||||
onSelect={field.onChange}
|
||||
disabled={(date) =>
|
||||
date > new Date() || date < new Date("1900-01-01")
|
||||
}
|
||||
initialFocus
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="tag"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="text-default-700">Tag</FormLabel>
|
||||
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select..." />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="team">Team</SelectItem>
|
||||
<SelectItem value="low">Low</SelectItem>
|
||||
<SelectItem value="medium">Medium</SelectItem>
|
||||
<SelectItem value="high">High</SelectItem>
|
||||
<SelectItem value="update">Update</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="description"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="text-default-700">Description</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
placeholder="Tell us a little bit about yourself"
|
||||
className="resize-none"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button type="submit">Update</Button>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditTodo;
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
import { Metadata } from "next";
|
||||
import TodoWrapper from "./todo-wrapper";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Todo",
|
||||
description: "Todo Application"
|
||||
}
|
||||
const Layout = ({ children }: { children: React.ReactNode }) => {
|
||||
return (
|
||||
<TodoWrapper>
|
||||
{children}
|
||||
</TodoWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Layout;
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
||||
import CreateTodo from "./create-todo";
|
||||
import { todos } from "./data";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import TodoHeader from "./todo-header";
|
||||
import Todo from "./todo";
|
||||
import TodoSidebarWrapper from "./sidebar-wrapper";
|
||||
import Nav from "@/components/nav";
|
||||
import { useTranslations } from "next-intl";
|
||||
const TodoPage = () => {
|
||||
const t = useTranslations("TodoApp");
|
||||
return (
|
||||
<div className="flex gap-5 h-full ">
|
||||
<TodoSidebarWrapper >
|
||||
<Card className="h-full">
|
||||
<CardContent className="h-full p-0">
|
||||
<ScrollArea className="h-[calc(100%-30px)]" >
|
||||
<div className="mb-4 px-4 pt-5 m-1 sticky top-0">
|
||||
<CreateTodo />
|
||||
</div>
|
||||
<Nav links={[
|
||||
{
|
||||
title: "My Task",
|
||||
icon: "uil:image-v",
|
||||
active: true,
|
||||
},
|
||||
{
|
||||
|
||||
title: "Starred",
|
||||
icon: "heroicons:star",
|
||||
active: false,
|
||||
},
|
||||
{
|
||||
|
||||
title: "Completed",
|
||||
icon: "heroicons:document-check",
|
||||
active: false,
|
||||
},
|
||||
{
|
||||
|
||||
title: "Trash",
|
||||
icon: "heroicons:trash",
|
||||
active: false,
|
||||
}
|
||||
]} />
|
||||
<div className="py-4 px-5 text-default-800 font-semibold text-xs uppercase">
|
||||
{t("todos")}
|
||||
</div>
|
||||
<Nav dotStyle links={[
|
||||
{
|
||||
title: "Team",
|
||||
active: true,
|
||||
|
||||
},
|
||||
{
|
||||
title: "low",
|
||||
active: false,
|
||||
},
|
||||
{
|
||||
title: "medium",
|
||||
active: false,
|
||||
},
|
||||
{
|
||||
title: "high",
|
||||
active: false,
|
||||
},
|
||||
{
|
||||
title: "update",
|
||||
active: false,
|
||||
}
|
||||
|
||||
]} />
|
||||
</ScrollArea>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TodoSidebarWrapper>
|
||||
<div className="flex-1 w-full">
|
||||
<Card className="h-full overflow-hidden">
|
||||
<CardHeader className="border-b border-default-200 ">
|
||||
<TodoHeader />
|
||||
</CardHeader>
|
||||
<CardContent className="p-0 h-full">
|
||||
<div className="h-[calc(100%-60px)] overflow-y-auto no-scrollbar">
|
||||
{
|
||||
todos.map((todo, index) =>
|
||||
<Todo
|
||||
key={`todo-${index}`}
|
||||
todo={todo}
|
||||
/>)
|
||||
}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TodoPage;
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
'use client'
|
||||
import { useMediaQuery } from '@/hooks/use-media-query'
|
||||
import { useTodoConfig } from '@/hooks/use-todo'
|
||||
import { cn } from '@/lib/utils'
|
||||
import React from 'react'
|
||||
const TodoSidebarWrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
const [todoConfig] = useTodoConfig()
|
||||
const { isOpen } = todoConfig
|
||||
const isTablet = useMediaQuery("(min-width: 1024px)");
|
||||
if (!isTablet) {
|
||||
return (
|
||||
<div className={cn('absolute h-full start-0 w-[240px] z-50 ', {
|
||||
'-start-full': !isOpen
|
||||
})}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div className='flex-none md:w-[270px] w-[200px] '>{children}</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TodoSidebarWrapper
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
import { Input } from "@/components/ui/input";
|
||||
import { MoreVertical, Search } from "lucide-react";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu"
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Icon } from "@/components/ui/icon";
|
||||
import ToggleTodoSidebar from "./toggle-todo-sidebar";
|
||||
|
||||
|
||||
const TodoHeader = () => {
|
||||
const actions = [
|
||||
{
|
||||
name: "Reset Sort",
|
||||
icon: "heroicons-outline:sort-ascending",
|
||||
},
|
||||
{
|
||||
name: "Sort A-Z ",
|
||||
icon: "heroicons-outline:sort-ascending",
|
||||
},
|
||||
{
|
||||
name: "Sort Z-A ",
|
||||
icon: "heroicons-outline:sort-descending",
|
||||
}
|
||||
];
|
||||
return (
|
||||
<div className="flex gap-2">
|
||||
<div className="flex-1 flex items-center gap-1">
|
||||
<ToggleTodoSidebar />
|
||||
<Search className="w-4 h-4 text-default-400" />
|
||||
<Input placeholder="Search Task" className="max-w-[180px] border-none font-medium dark:bg-transparent" />
|
||||
</div>
|
||||
<div className="flex-none">
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button size="icon" className="w-8 h-8 rounded-full bg-default-100 group">
|
||||
<MoreVertical className="w-5 h-5 text-default group-hover:text-default-foreground" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="p-0 rounded-md overflow-hidden">
|
||||
{
|
||||
actions.map((action, index) => (
|
||||
<DropdownMenuItem
|
||||
key={`action-${index}`}
|
||||
className="flex items-center gap-1.5 p-2 border-b text-default-600 group focus:bg-default focus:text-primary-foreground rounded-none group"
|
||||
>
|
||||
<Icon icon={action.icon} className="group-hover:text-primary-foreground w-4 h-4" />
|
||||
<span className="text-default-700 group-hover:text-primary-foreground">{action.name}</span>
|
||||
</DropdownMenuItem>
|
||||
))
|
||||
}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TodoHeader;
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
'use client'
|
||||
import { useMediaQuery } from '@/hooks/use-media-query';
|
||||
import { useTodoConfig } from '@/hooks/use-todo'
|
||||
|
||||
const TodoWrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
const [todoConfig, setTodoConfig] = useTodoConfig();
|
||||
const { isOpen } = todoConfig
|
||||
const isTablet = useMediaQuery("(min-width: 1024px)");
|
||||
return (
|
||||
<div className='relative app-height'>
|
||||
{!isTablet && isOpen && (
|
||||
<div
|
||||
onClick={() => setTodoConfig({ ...todoConfig, isOpen: false })}
|
||||
className="overlay bg-default-900 dark:bg-default-900 dark:bg-opacity-60 bg-opacity-60 backdrop-filter
|
||||
backdrop-blur-sm absolute w-full flex-1 inset-0 z-20 rounded-md"
|
||||
|
||||
></div>
|
||||
)}
|
||||
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TodoWrapper
|
||||
|
|
@ -0,0 +1,110 @@
|
|||
"use client"
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Icon } from "@/components/ui/icon";
|
||||
import { Avatar, AvatarImage } from "@/components/ui/avatar";
|
||||
import { AvatarFallback } from "@radix-ui/react-avatar";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip"
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { cn } from "@/lib/utils";
|
||||
import EditTodo from "./edit-todo";
|
||||
import { Todo as TodoProps } from "./data";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { SquarePen, Trash2 } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import DeleteConfirmationDialog from "@/components/delete-confirmation-dialog";
|
||||
|
||||
const Todo = ({ todo }: { todo: TodoProps }) => {
|
||||
const { image, title, isfav, category } = todo;
|
||||
|
||||
const [confirmDelete, setConfirmDelete] = useState<boolean>(false);
|
||||
const [openEdit, setOpenEdit] = useState<boolean>(false);
|
||||
|
||||
const categoryClasses: Record<string, string> = {
|
||||
team: "bg-destructive/10 text-destructive",
|
||||
high: "bg-primary/10 text-primary",
|
||||
medium: "bg-warning/10 text-warning",
|
||||
low: "bg-success/10 text-success",
|
||||
update: "bg-info/10 text-info"
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<DeleteConfirmationDialog
|
||||
open={confirmDelete}
|
||||
onClose={() => setConfirmDelete(false)}
|
||||
/>
|
||||
<EditTodo
|
||||
open={openEdit}
|
||||
setOpen={setOpenEdit}
|
||||
/>
|
||||
<div className="flex items-center gap-4 border-b border-defualt-200 dark:border-default-300 last:border-none px-6 py-4 transition-all duration-300 hover:-translate-y-1">
|
||||
<div>
|
||||
<Checkbox className="mt-0.5 dark:bg-default-300"/>
|
||||
</div>
|
||||
<div className="ms-1">
|
||||
{isfav ? (
|
||||
<Icon
|
||||
icon="heroicons:star-20-solid"
|
||||
className="text-xl cursor-pointer text-[#FFCE30]"
|
||||
/>
|
||||
) : (
|
||||
<Icon
|
||||
icon="heroicons:star"
|
||||
className="text-xl cursor-pointer text-default-400"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<p className="flex-1 overflow-hidden text-sm text-default-600 truncate">{title}</p>
|
||||
|
||||
<div className="-space-x-1.5 rtl:space-x-reverse flex flex-nowrap">
|
||||
{
|
||||
image.map((item, index) =>
|
||||
<TooltipProvider key={`avatar-${index}`} >
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Avatar className="h-7 w-7 border-none shadow-none">
|
||||
<AvatarImage src={item.image} />
|
||||
<AvatarFallback>SA</AvatarFallback>
|
||||
</Avatar>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{item.label}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
<div className="flex gap-1 items-center">
|
||||
{category.map((item, index) => (
|
||||
<Badge
|
||||
key={`category-${index}`}
|
||||
className={cn("rounded-full px-3 py-1 font-medium", categoryClasses[item.value])}>
|
||||
{item.label}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
<Button
|
||||
className="bg-transparent text-default-400 ring-offset-transparent hover:bg-transparent hover:ring-0 hover:ring-transparent w-fit"
|
||||
size="icon"
|
||||
onClick={() => setOpenEdit(true)}
|
||||
>
|
||||
<SquarePen className=" w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
className="bg-transparent text-default-400 ring-offset-transparent hover:text-destructive border-none focus-visible:ring-transparent hover:bg-transparent hover:ring-0 hover:ring-transparent px-0 w-fit"
|
||||
size="icon"
|
||||
onClick={() => setConfirmDelete(true)}
|
||||
>
|
||||
<Trash2 className=" w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Todo;
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
'use client'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { useMediaQuery } from '@/hooks/use-media-query'
|
||||
import { useTodoConfig } from '@/hooks/use-todo'
|
||||
import { Menu } from 'lucide-react'
|
||||
import React from 'react'
|
||||
|
||||
const ToggleTodoSidebar = () => {
|
||||
const isTablet = useMediaQuery("(min-width: 1024px)");
|
||||
const [todoConfig, setTodoConfig] = useTodoConfig()
|
||||
if (isTablet) return null
|
||||
|
||||
|
||||
return (
|
||||
<Button size='icon' color='secondary' onClick={() => setTodoConfig({ ...todoConfig, isOpen: !todoConfig.isOpen })}>
|
||||
<Menu className=' h-5 w-5' />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
export default ToggleTodoSidebar
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
import { Metadata } from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Basic Widget',
|
||||
description: 'Basic Widget'
|
||||
}
|
||||
const BasicWidget = ({children}: {children: React.ReactNode}) => {
|
||||
return (
|
||||
<>
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default BasicWidget;
|
||||
|
|
@ -0,0 +1,532 @@
|
|||
|
||||
import { UpgradeBlock } from "@/components/blocks/upgrade-block";
|
||||
import { WelcomeBlock, BlockBadge } from "@/components/blocks/welcome-block";
|
||||
import SiteBreadcrumb from "@/components/site-breadcrumb";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Eye } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
|
||||
const WidgetPage = () => {
|
||||
return (
|
||||
<div>
|
||||
<SiteBreadcrumb />
|
||||
<div className="space-y-5" >
|
||||
<div className="grid lg:grid-cols-3 md:grid-cols-2 grid-cols-1 gap-5">
|
||||
{/* Block 1 */}
|
||||
<WelcomeBlock >
|
||||
<div className="max-w-[180px] relative z-10">
|
||||
<h4 className="text-xl font-medium text-primary-foreground dark:text-default-900 mb-2">
|
||||
Upgrade your Dashcode
|
||||
</h4>
|
||||
<p className="text-sm text-primary-foreground dark:text-default-800 text-opacity-80">
|
||||
Pro plan for better results
|
||||
</p>
|
||||
</div>
|
||||
<BlockBadge>Now</BlockBadge>
|
||||
<Image
|
||||
src="/images/all-img/widget-bg-3.png"
|
||||
width={400}
|
||||
height={150}
|
||||
priority
|
||||
alt="Description of the image"
|
||||
className="absolute top-0 start-0 w-full h-full object-cover rounded-md"
|
||||
/>
|
||||
</WelcomeBlock>
|
||||
{/* Block 2 */}
|
||||
<WelcomeBlock >
|
||||
<div className="max-w-[180px] relative z-10">
|
||||
<div className="text-xl font-medium text-default-900 dark:text-default-100 mb-2">
|
||||
Upgrade your Dashcode
|
||||
</div>
|
||||
<p className="text-sm text-default-800 dark:text-default-100">
|
||||
Pro plan for better results
|
||||
</p>
|
||||
</div>
|
||||
<BlockBadge>Now</BlockBadge>
|
||||
<Image
|
||||
src="/images/all-img/widget-bg-1.png"
|
||||
width={400}
|
||||
height={150}
|
||||
alt="Description of the image"
|
||||
className="absolute top-0 start-0 w-full h-full object-cover rounded-md"
|
||||
/>
|
||||
</WelcomeBlock>
|
||||
{/* Block 3 */}
|
||||
<WelcomeBlock >
|
||||
<div className="max-w-[180px] relative z-10">
|
||||
<div className="text-xl font-medium text-default-900 dark:text-default-100 mb-2">
|
||||
Upgrade your Dashcode
|
||||
</div>
|
||||
<p className="text-sm text-default-800 dark:text-default-100">
|
||||
Pro plan for better results
|
||||
</p>
|
||||
</div>
|
||||
<BlockBadge>Now</BlockBadge>
|
||||
<Image
|
||||
src="/images/all-img/widget-bg-3-1.png"
|
||||
width={400}
|
||||
height={150}
|
||||
alt="Description of the image"
|
||||
className="absolute top-0 start-0 w-full h-full object-cover rounded-md"
|
||||
/>
|
||||
</WelcomeBlock>
|
||||
{/* Block 4 */}
|
||||
<WelcomeBlock >
|
||||
<div className="max-w-[180px] relative z-10">
|
||||
<h4 className="text-xl font-medium text-primary-foreground dark:text-default-900 mb-2">
|
||||
<span className="block font-normal">Good evening,</span>
|
||||
<span className="block">Mr. Dianne Russell</span>
|
||||
</h4>
|
||||
<p className="text-sm text-primary-foreground dark:text-default-900 font-normal">
|
||||
Welcome to Dashcode
|
||||
</p>
|
||||
</div>
|
||||
<Image
|
||||
src="/images/all-img/widget-bg-2.png"
|
||||
width={400}
|
||||
height={150}
|
||||
alt="Description of the image"
|
||||
className="absolute top-0 start-0 w-full h-full object-cover rounded-md"
|
||||
/>
|
||||
</WelcomeBlock>
|
||||
{/* Block 5 */}
|
||||
<WelcomeBlock className="flex items-center" >
|
||||
<div className="flex-1 relative z-10">
|
||||
<div className="max-w-[180px]">
|
||||
<div className="text-xl font-medium text-default-900 dark:text-default-100 mb-2">
|
||||
<span className="block font-normal">Good evening,</span>
|
||||
<span className="block">Mr. Dianne Russell</span>
|
||||
</div>
|
||||
<p className="text-sm text-default-900 dark:text-default-100 font-normal">
|
||||
Welcome to Dashcode
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-none relative w-[120px] z-10">
|
||||
<Image
|
||||
src="/images/all-img/widgetvector.png"
|
||||
alt=""
|
||||
width={220}
|
||||
height={100}
|
||||
className="w-full h-full"
|
||||
/>
|
||||
</div>
|
||||
<Image
|
||||
src="/images/all-img/widget-bg-4.png"
|
||||
width={400}
|
||||
height={150}
|
||||
alt="Description of the image"
|
||||
className="absolute top-0 start-0 w-full h-full object-cover rounded-md"
|
||||
/>
|
||||
</WelcomeBlock>
|
||||
{/* block 6 */}
|
||||
<WelcomeBlock className="flex items-center" >
|
||||
<div className="flex-1 relative z-10">
|
||||
<div className="max-w-[180px]">
|
||||
<div className="text-xl font-medium text-primary-foreground mb-2">
|
||||
<span className="block font-normal">Good evening,</span>
|
||||
<span className="block">Mr. Dianne Russell</span>
|
||||
</div>
|
||||
<p className="text-sm text-wgite text-primary-foreground font-normal">
|
||||
Welcome to Dashcode
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-none relative z-10">
|
||||
<Image
|
||||
alt=""
|
||||
src="/images/all-img/widgetvector2.png"
|
||||
className="ms-auto w-[100px] h-[80px]"
|
||||
width={120}
|
||||
height={100}
|
||||
/>
|
||||
</div>
|
||||
<Image
|
||||
src="/images/all-img/widget-bg-5.png"
|
||||
width={400}
|
||||
height={150}
|
||||
priority
|
||||
alt="Description of the image"
|
||||
className="absolute top-0 start-0 w-full h-full object-cover rounded-md"
|
||||
/>
|
||||
</WelcomeBlock>
|
||||
{/* Block 7 */}
|
||||
<WelcomeBlock className="bg-default" >
|
||||
<div className="max-w-[180px] relative z-10">
|
||||
<h4 className="text-xl font-medium text-primary-foreground mb-2">
|
||||
Upgrade your Dashcode
|
||||
</h4>
|
||||
<p className="text-sm text-primary-foreground text-opacity-80">
|
||||
Pro plan for better results
|
||||
</p>
|
||||
</div>
|
||||
<BlockBadge className="bg-destructive text-destructive-foreground dark:bg-default-100 dark:text-default-900">Now</BlockBadge>
|
||||
|
||||
</WelcomeBlock>
|
||||
{/* Block 8 */}
|
||||
<WelcomeBlock className="bg-primary" >
|
||||
<div className="max-w-[180px] relative z-10">
|
||||
<h4 className="text-xl font-medium text-primary-foreground dark:text-default-900 mb-2">
|
||||
Upgrade your Dashcode
|
||||
</h4>
|
||||
<p className="text-sm text-primary-foreground dark:text-default-900 text-opacity-80">
|
||||
Pro plan for better results
|
||||
</p>
|
||||
</div>
|
||||
<BlockBadge className="bg-success text-success-foreground">Now</BlockBadge>
|
||||
</WelcomeBlock>
|
||||
{/* Block 9 */}
|
||||
<WelcomeBlock className="bg-gradient-to-r from-primary to-success" >
|
||||
<div className="max-w-[180px] relative z-10">
|
||||
<h4 className="text-xl font-medium text-primary-foreground dark:text-default-900 mb-2">
|
||||
Upgrade your Dashcode
|
||||
</h4>
|
||||
<p className="text-sm text-primary-foreground dark:text-default-900 text-opacity-80">
|
||||
Pro plan for better results
|
||||
</p>
|
||||
</div>
|
||||
<BlockBadge className="bg-warning text-warning-foreground">Now</BlockBadge>
|
||||
</WelcomeBlock>
|
||||
{/* Block 10 */}
|
||||
<WelcomeBlock className="flex items-center py-7" >
|
||||
<div className="flex-1 relative z-10">
|
||||
<div className="max-w-[180px]">
|
||||
<h4 className="text-2xl font-medium text-primary-foreground dark:text-default-900 mb-2">
|
||||
<span className="block text-sm">Current balance,</span>
|
||||
<span className="block">$34,564</span>
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-none relative z-10">
|
||||
<Button
|
||||
color="primary"
|
||||
className="hover:ring-0 hover:ring-offset-0 "
|
||||
>
|
||||
<Eye className="w-4 h-4 text-primary-foreground me-2" />
|
||||
View details
|
||||
</Button>
|
||||
</div>
|
||||
<Image
|
||||
src="/images/all-img/widget-bg-6.png"
|
||||
width={400}
|
||||
height={150}
|
||||
alt="Description of the image"
|
||||
className="absolute top-0 start-0 w-full h-full object-cover rounded-md"
|
||||
/>
|
||||
</WelcomeBlock>
|
||||
{/* Block 11 */}
|
||||
<WelcomeBlock className="flex items-center py-7" >
|
||||
<div className="flex-1 relative z-10">
|
||||
<div className="max-w-[180px]">
|
||||
<h4 className="text-2xl font-medium mb-2">
|
||||
<span className="block text-sm text-default-800 dark:text-default-100">Current balance,</span>
|
||||
<span className="block text-default-900 dark:text-default-100">$34,564</span>
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-none relative z-10">
|
||||
<Button
|
||||
color="primary"
|
||||
className="hover:ring-0 hover:ring-offset-0 "
|
||||
>
|
||||
<Eye className="w-4 h-4 text-primary-foreground me-2" />
|
||||
View details
|
||||
</Button>
|
||||
</div>
|
||||
<Image
|
||||
src="/images/all-img/widget-bg-7.png"
|
||||
width={400}
|
||||
height={150}
|
||||
priority
|
||||
alt="Description of the image"
|
||||
className="absolute top-0 start-0 w-full h-full object-cover rounded-md"
|
||||
/>
|
||||
</WelcomeBlock>
|
||||
{/* Block 12 */}
|
||||
<WelcomeBlock className="flex items-center py-7" >
|
||||
<div className="flex-1 relative z-10">
|
||||
<div className="max-w-[180px]">
|
||||
<h4 className="text-2xl font-medium mb-2">
|
||||
<span className="block text-sm text-default-800 dark:text-default-100">Current balance,</span>
|
||||
<span className="block text-default-900 dark:text-default-100">$34,564</span>
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-none relative z-10">
|
||||
<Button
|
||||
color="primary"
|
||||
className="hover:ring-0 hover:ring-offset-0 "
|
||||
>
|
||||
<Eye className="w-4 h-4 text-primary-foreground me-2" />
|
||||
View details
|
||||
</Button>
|
||||
</div>
|
||||
<Image
|
||||
src="/images/all-img/widget-bg-8.png"
|
||||
width={400}
|
||||
height={150}
|
||||
priority
|
||||
alt="Description of the image"
|
||||
className="absolute top-0 start-0 w-full h-full object-cover rounded-md"
|
||||
/>
|
||||
</WelcomeBlock>
|
||||
</div>
|
||||
{/* Upgrade Block */}
|
||||
<div className="grid xl:grid-cols-4 md:grid-cols-2 grid-cols-1 gap-5">
|
||||
<UpgradeBlock className="bg-default dark:bg-default-50">
|
||||
<div className="max-w-[168px] relative z-10">
|
||||
<div className="text-base font-medium text-default-foreground dark:text-default-900">Unlimited Access</div>
|
||||
<div className="text-xs font-normal text-default-foreground dark:text-default-800">
|
||||
Upgrade your system to business plan
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-6 mb-14 relative z-10">
|
||||
<Button
|
||||
size="md"
|
||||
className="bg-default-foreground text-default hover:bg-default-foreground hover:opacity-80 dark:bg-default dark:text-default-100 font-medium"
|
||||
>
|
||||
Upgrade
|
||||
</Button>
|
||||
</div>
|
||||
<div className="absolute bottom-0 start-0 z-10 w-full">
|
||||
<Image
|
||||
src="/images/svg/line.svg"
|
||||
width={500}
|
||||
height={200}
|
||||
alt="Line Image"
|
||||
draggable={false}
|
||||
/>
|
||||
</div>
|
||||
<div className="absolute -bottom-4 end-5">
|
||||
<Image
|
||||
src="/images/svg/rabit.svg"
|
||||
width={96}
|
||||
height={96}
|
||||
alt="Rabbit"
|
||||
draggable={false}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
</UpgradeBlock>
|
||||
<UpgradeBlock className="bg-primary">
|
||||
<div className="max-w-[168px] relative z-10">
|
||||
<div className="text-base font-medium text-default-foreground dark:text-default-900">Unlimited Access</div>
|
||||
<div className="text-xs font-normal text-default-foreground dark:text-default-800">
|
||||
Upgrade your system to business plan
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-6 mb-14 relative z-10">
|
||||
<Button
|
||||
size="md"
|
||||
className="bg-default-foreground text-default hover:bg-default-foreground hover:opacity-80 dark:bg-default dark:text-default-100 font-medium"
|
||||
>
|
||||
Upgrade
|
||||
</Button>
|
||||
</div>
|
||||
<div className="absolute bottom-0 start-0 z-10 w-full">
|
||||
<Image
|
||||
src="/images/svg/line.svg"
|
||||
width={500}
|
||||
height={200}
|
||||
alt="Line Image"
|
||||
draggable={false}
|
||||
/>
|
||||
</div>
|
||||
<div className="absolute -bottom-4 end-5">
|
||||
<Image
|
||||
src="/images/svg/rabit.svg"
|
||||
width={96}
|
||||
height={96}
|
||||
alt="Rabbit"
|
||||
draggable={false}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
</UpgradeBlock>
|
||||
<UpgradeBlock className="bg-success">
|
||||
<div className="max-w-[168px] relative z-10">
|
||||
<div className="text-base font-medium text-default-foreground dark:text-default-900">Unlimited Access</div>
|
||||
<div className="text-xs font-normal text-default-foreground dark:text-default-800">
|
||||
Upgrade your system to business plan
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-6 mb-14 relative z-10">
|
||||
<Button
|
||||
size="md"
|
||||
className="bg-default-foreground text-default hover:bg-default-foreground hover:opacity-80 dark:bg-default dark:text-default-100 font-medium"
|
||||
>
|
||||
Upgrade
|
||||
</Button>
|
||||
</div>
|
||||
<div className="absolute bottom-0 start-0 z-10 w-full">
|
||||
<Image
|
||||
src="/images/svg/line.svg"
|
||||
width={500}
|
||||
height={200}
|
||||
alt="Line Image"
|
||||
draggable={false}
|
||||
/>
|
||||
</div>
|
||||
<div className="absolute -bottom-4 end-5">
|
||||
<Image
|
||||
src="/images/svg/rabit.svg"
|
||||
width={96}
|
||||
height={96}
|
||||
alt="Rabbit"
|
||||
draggable={false}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
</UpgradeBlock>
|
||||
<UpgradeBlock className="bg-info">
|
||||
<div className="max-w-[168px] relative z-10">
|
||||
<div className="text-base font-medium text-default-foreground dark:text-default-900">Unlimited Access</div>
|
||||
<div className="text-xs font-normal text-default-foreground dark:text-default-800">
|
||||
Upgrade your system to business plan
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-6 mb-14 relative z-10">
|
||||
<Button
|
||||
size="md"
|
||||
className="bg-default-foreground text-default hover:bg-default-foreground hover:opacity-80 dark:bg-default dark:text-default-100 font-medium"
|
||||
>
|
||||
Upgrade
|
||||
</Button>
|
||||
</div>
|
||||
<div className="absolute bottom-0 start-0 z-10 w-full">
|
||||
<Image
|
||||
src="/images/svg/line.svg"
|
||||
width={500}
|
||||
height={200}
|
||||
alt="Line Image"
|
||||
draggable={false}
|
||||
/>
|
||||
</div>
|
||||
<div className="absolute -bottom-4 end-5">
|
||||
<Image
|
||||
src="/images/svg/rabit.svg"
|
||||
width={96}
|
||||
height={96}
|
||||
alt="Rabbit"
|
||||
draggable={false}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
</UpgradeBlock>
|
||||
|
||||
</div>
|
||||
{/* gift block */}
|
||||
|
||||
<div className="grid xl:grid-cols-6 lg:grid-cols-4 md:grid-cols-2 grid-cols-1 gap-5">
|
||||
<UpgradeBlock
|
||||
className="bg-default dark:bg-default-200 mt-14">
|
||||
<div className="absolute start-1/2 top-0 -translate-x-1/2 -translate-y-1/2">
|
||||
<Image
|
||||
src="/images/svg/gift.svg"
|
||||
alt="image"
|
||||
height={72}
|
||||
width={72}
|
||||
className="h-16 w-16"
|
||||
/>
|
||||
</div>
|
||||
<div className="max-w-[160px] mx-auto mt-10">
|
||||
<div className="text-base font-medium text-default-foreground dark:text-default-900">Unlimited Access</div>
|
||||
<div className="text-xs font-normal text-default-foreground dark:text-default-900 mt-1">
|
||||
Upgrade your system to business plan
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
<Button fullWidth size="md"
|
||||
className="bg-primary-foreground dark:bg-white hover:ring-0 hover:ring-offset-0 ring-offset-transparent dark:text-default-100 hover:bg-primary-foreground hover:text-default hover:opacity-90 text-default">
|
||||
Upgrade
|
||||
</Button>
|
||||
</div>
|
||||
</UpgradeBlock>
|
||||
{
|
||||
["primary", "success", "info", "destructive", "warning"].map((item, index) => (
|
||||
<UpgradeBlock
|
||||
key={index}
|
||||
className={`bg-${item} mt-14`}>
|
||||
<div className="absolute start-1/2 top-0 -translate-x-1/2 -translate-y-1/2">
|
||||
<Image
|
||||
src="/images/svg/gift.svg"
|
||||
alt="image"
|
||||
height={72}
|
||||
width={72}
|
||||
className="h-16 w-16"
|
||||
/>
|
||||
</div>
|
||||
<div className="max-w-[160px] mx-auto mt-10">
|
||||
<div className="text-base font-medium text-default-foreground dark:text-default-900">Unlimited Access</div>
|
||||
<div className="text-xs font-normal text-default-foreground dark:text-default-900 mt-1">
|
||||
Upgrade your system to business plan
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
<Button fullWidth size="md" className="bg-primary-foreground hover:ring-0 hover:ring-offset-0 ring-offset-transparent dark:bg-white dark:text-default-100 hover:bg-primary-foreground hover:text-default hover:opacity-90 text-default">
|
||||
Upgrade
|
||||
</Button>
|
||||
</div>
|
||||
</UpgradeBlock>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
{/* rabbit block */}
|
||||
<div className="grid grid-cols-6 gap-5">
|
||||
<UpgradeBlock
|
||||
className="bg-default dark:bg-default-200 mt-14">
|
||||
<div className="absolute start-1/2 top-0 -translate-x-1/2 -translate-y-1/2">
|
||||
<Image
|
||||
src="/images/svg/rabit.svg"
|
||||
alt="image"
|
||||
height={100}
|
||||
width={100}
|
||||
className="h-24 w-24"
|
||||
/>
|
||||
</div>
|
||||
<div className="max-w-[160px] mx-auto mt-14">
|
||||
<div className="text-base font-medium text-default-foreground dark:text-default-900">Unlimited Access</div>
|
||||
<div className="text-xs font-normal text-default-foreground mt-1 dark:text-default-900">
|
||||
Upgrade your system to business plan
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
<Button fullWidth size="md" className="bg-primary-foreground hover:ring-0 hover:ring-offset-0 ring-offset-transparent dark:bg-white dark:text-default-100 hover:bg-primary-foreground hover:text-default hover:opacity-90 text-default">
|
||||
Upgrade
|
||||
</Button>
|
||||
</div>
|
||||
</UpgradeBlock>
|
||||
{
|
||||
["primary", "success", "info", "destructive", "warning"].map((item, index) => (
|
||||
<UpgradeBlock key={index} className={`bg-${item} mt-14`}>
|
||||
<div className="absolute start-1/2 top-0 -translate-x-1/2 -translate-y-1/2">
|
||||
<Image
|
||||
src="/images/svg/rabit.svg"
|
||||
alt="image"
|
||||
height={100}
|
||||
width={100}
|
||||
className="h-24 w-24"
|
||||
/>
|
||||
</div>
|
||||
<div className="max-w-[160px] mx-auto mt-14">
|
||||
<div className="text-base font-medium text-default-foreground dark:text-default-900">Unlimited Access</div>
|
||||
<div className="text-xs font-normal text-default-foreground dark:text-default-900 mt-1">
|
||||
Upgrade your system to business plan
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
<Button fullWidth size="md" className="bg-primary-foreground hover:ring-0 hover:ring-offset-0 ring-offset-transparent dark:bg-white dark:text-default-100 hover:bg-primary-foreground hover:text-default hover:opacity-90 text-default">
|
||||
Upgrade
|
||||
</Button>
|
||||
</div>
|
||||
</UpgradeBlock>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default WidgetPage;
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
import { Metadata } from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Dashcode Next Js",
|
||||
description: "Dashcode is a popular dashboard template.",
|
||||
};
|
||||
const Layout = ({ children }: { children: React.ReactNode }) => {
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
export default Layout;
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
import { Metadata } from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Statistic Widget',
|
||||
description: 'Statistic Widget Description'
|
||||
}
|
||||
const StatisticWidget = ({children}: {children: React.ReactNode}) => {
|
||||
return (
|
||||
<>
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default StatisticWidget;
|
||||
|
|
@ -0,0 +1,340 @@
|
|||
import Image from "next/image";
|
||||
import OrdersBlock from "@/components/blocks/orders-block";
|
||||
import ProgressBlock from "@/components/blocks/progress-block";
|
||||
import { StatisticsBlock } from "@/components/blocks/statistics-block";
|
||||
import { StatusBlock } from "@/components/blocks/status-block";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import { Icon } from "@/components/ui/icon";
|
||||
import { Box, TrendingDown, TrendingUp, ShoppingCart } from "lucide-react";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { BarChart } from "lucide-react";
|
||||
import EarningBlock from "@/components/blocks/earning-block";
|
||||
import SiteBreadcrumb from "@/components/site-breadcrumb";
|
||||
|
||||
const StatisticPage = () => {
|
||||
return (
|
||||
<div>
|
||||
<SiteBreadcrumb />
|
||||
<div className="space-y-6">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
|
||||
<StatusBlock
|
||||
title="Total Revenue"
|
||||
total="3,564"
|
||||
iconWrapperClass="bg-info/10"
|
||||
chartColor="#00EBFF"
|
||||
icon={<ShoppingCart className="w-5 h-5 text-info" />}
|
||||
/>
|
||||
<StatusBlock
|
||||
title="Products sold"
|
||||
total="564"
|
||||
icon={<Box className="w-5 h-5 text-warning" />}
|
||||
iconWrapperClass="bg-warning/10"
|
||||
chartColor="#FB8F65"
|
||||
/>
|
||||
|
||||
<StatusBlock
|
||||
title="Growth"
|
||||
total="+5.0%"
|
||||
icon={<TrendingUp className="w-5 h-5 text-primary" />}
|
||||
iconWrapperClass="bg-primary/10"
|
||||
chartColor="#2563eb"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
|
||||
<StatisticsBlock
|
||||
title="Total Revenue"
|
||||
total="3,564"
|
||||
className="bg-info/10"
|
||||
/>
|
||||
<StatisticsBlock
|
||||
title="Products sold"
|
||||
total="564"
|
||||
className="bg-warning/10"
|
||||
chartColor="#FB8F65"
|
||||
/>
|
||||
<StatisticsBlock
|
||||
title="Growth"
|
||||
total="+5.0%"
|
||||
className="bg-primary/10"
|
||||
chartColor="#2563eb"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Card>
|
||||
<CardContent className=" p-6">
|
||||
<div className="grid xl:grid-cols-4 lg:grid-cols-2 md:grid-cols-2 grid-cols-1 gap-5 place-content-center">
|
||||
<div className="flex space-x-4 h-full items-center rtl:space-x-reverse">
|
||||
<div className="flex-none">
|
||||
<Avatar className="h-20 w-20 bg-transparent hover:bg-transparent">
|
||||
<AvatarImage src="/images/all-img/main-user.png" />
|
||||
<AvatarFallback>SA</AvatarFallback>
|
||||
</Avatar>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h4 className="text-xl font-medium mb-2">
|
||||
<span className="block font-light text-default-800">Good evening,</span>
|
||||
<span className="block text-default-900">Mr. Jone Doe</span>
|
||||
</h4>
|
||||
<p className="text-sm text-default-600">Welcome to Dashcode</p>
|
||||
</div>
|
||||
</div>
|
||||
{/* status blocks */}
|
||||
<StatusBlock
|
||||
title="Current balance"
|
||||
total="$34,564"
|
||||
chartType="bar"
|
||||
className="bg-secondary/10 shadow-none rounded"
|
||||
opacity={1}
|
||||
/>
|
||||
<StatusBlock
|
||||
title="Credit"
|
||||
total="$3,564"
|
||||
chartColor="#80fac1"
|
||||
className="bg-secondary/10 shadow-none rounded"
|
||||
series={[40, 70, 45, 100, 75, 40, 80, 90]}
|
||||
chartType="bar"
|
||||
opacity={1}
|
||||
/>
|
||||
<StatusBlock
|
||||
title="Debit"
|
||||
total="$3,564"
|
||||
chartColor="#ffbf99"
|
||||
className="bg-secondary/10 shadow-none rounded"
|
||||
chartType="bar"
|
||||
series={[40, 70, 45, 100, 75, 40, 80, 90]}
|
||||
opacity={1}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 ">
|
||||
<Card className="bg-warning/20 relative">
|
||||
<CardContent className="p-4 ">
|
||||
<Image
|
||||
src="/images/all-img/shade-1.png"
|
||||
alt="images"
|
||||
draggable="false"
|
||||
className="absolute top-0 start-0 w-full h-full object-contain"
|
||||
width={300}
|
||||
height={200}
|
||||
priority
|
||||
/>
|
||||
<div className="mb-6 text-sm text-default-900 font-medium">Sales </div>
|
||||
<div className=" text-2xl text-default-900 font-medium mb-6"> 354</div>
|
||||
<div className="flex gap-2">
|
||||
<div className="flex-none text-xl">
|
||||
<TrendingUp className="w-4 h-4 text-primary" />
|
||||
</div>
|
||||
<div className="flex-1 text-sm">
|
||||
<span className="block mb-0.2">25.67% </span>
|
||||
<span className="block mb-1 text-default-600">
|
||||
From last week
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-info/20 relative">
|
||||
<CardContent className="p-4">
|
||||
<Image
|
||||
src="/images/all-img/shade-2.png"
|
||||
alt="images"
|
||||
draggable="false"
|
||||
className="absolute top-0 start-0 w-full h-full object-contain"
|
||||
width={300}
|
||||
height={200}
|
||||
priority
|
||||
/>
|
||||
<div className="mb-6 text-sm text-default-900 font-medium">Revenue </div>
|
||||
<div className=" text-2xl text-default-900 font-medium mb-6">$86,954</div>
|
||||
<div className="flex gap-2">
|
||||
<div className="flex-none text-xl">
|
||||
<TrendingUp className="w-4 h-4 text-primary" />
|
||||
</div>
|
||||
<div className="flex-1 text-sm">
|
||||
<span className="block mb-0.2">8.67% </span>
|
||||
<span className="block mb-1 text-default-600">
|
||||
From last week
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-primary/20 relative">
|
||||
<CardContent className=" p-4">
|
||||
<Image
|
||||
src="/images/all-img/shade-3.png"
|
||||
alt="images"
|
||||
draggable="false"
|
||||
className="absolute top-0 start-0 w-full h-full object-contain"
|
||||
width={300}
|
||||
height={200}
|
||||
priority
|
||||
/>
|
||||
<div className="mb-6 text-sm text-default-900 font-medium">Conversion </div>
|
||||
<div className=" text-2xl text-default-900 font-medium mb-6">15%</div>
|
||||
<div className="flex gap-2">
|
||||
<div className="flex-none text-xl">
|
||||
<TrendingDown className="w-4 h-4 text-destructive" />
|
||||
</div>
|
||||
<div className="flex-1 text-sm">
|
||||
<span className="block mb-0.2 text-destructive">1.67% </span>
|
||||
<span className="block mb-1 text-default-600">
|
||||
From last week
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-success/20 relative">
|
||||
<CardContent className=" p-4">
|
||||
<Image
|
||||
src="/images/all-img/shade-4.png"
|
||||
alt="images"
|
||||
draggable="false"
|
||||
className="absolute top-0 start-0 w-full h-full object-contain"
|
||||
width={300}
|
||||
height={200}
|
||||
priority
|
||||
/>
|
||||
<div className="mb-6 text-sm text-default-900 font-medium">Leads </div>
|
||||
<div className=" text-2xl text-default-900 font-medium mb-6">654</div>
|
||||
<div className="flex gap-2">
|
||||
<div className="flex-none text-xl">
|
||||
<TrendingDown className="w-4 h-4 text-primary" />
|
||||
</div>
|
||||
<div className="flex-1 text-sm">
|
||||
<span className="block mb-0.2 text-primary">1.67% </span>
|
||||
<span className="block mb-1 text-default-600">
|
||||
From last week
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 ">
|
||||
<Card className="bg-info/20">
|
||||
<CardContent className=" p-4 text-center">
|
||||
<div className="mx-auto h-10 w-10 rounded-full flex items-center justify-center bg-white mb-4">
|
||||
<BarChart className=" h-6 w-6 text-info" />
|
||||
</div>
|
||||
<div className="block text-sm text-default-600 font-medium mb-1.5"> Total Task</div>
|
||||
<div className="text-2xl text-default-900 font-medium"> 64</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-warning/20">
|
||||
<CardContent className=" p-4 text-center">
|
||||
<div
|
||||
className="mx-auto h-10 w-10 rounded-full flex items-center justify-center bg-white mb-4"
|
||||
>
|
||||
<Icon className="w-6 h-6 text-warning" icon="heroicons:chart-pie" />
|
||||
</div>
|
||||
<div className="block text-sm text-default-600 font-medium mb-1.5">Completed</div>
|
||||
<div className="text-2xl text-default-900 font-medium">45</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-primary/20">
|
||||
<CardContent className=" p-4 text-center">
|
||||
<div
|
||||
className="mx-auto h-10 w-10 rounded-full flex items-center justify-center bg-white mb-4"
|
||||
>
|
||||
<Icon className="w-6 h-6 text-primary" icon="heroicons:clock" />
|
||||
</div>
|
||||
<div className="block text-sm text-default-600 font-medium mb-1.5">Hours</div>
|
||||
<div className="text-2xl text-default-900 font-medium">190</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-success/20">
|
||||
<CardContent className="p-4 text-center">
|
||||
<div
|
||||
className="mx-auto h-10 w-10 rounded-full flex items-center justify-center bg-white mb-4"
|
||||
>
|
||||
<Icon className="w-6 h-6 text-success" icon="heroicons:calculator" />
|
||||
</div>
|
||||
<div className="block text-sm text-default-600 font-medium mb-1.5">Spendings</div>
|
||||
<div className="text-2xl text-default-900 font-medium">$3,564</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
{/* progress */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 ">
|
||||
<ProgressBlock
|
||||
title="Progress"
|
||||
/>
|
||||
<ProgressBlock
|
||||
title="Progress"
|
||||
colors={["#F1595C", "#F9E1E5"]}
|
||||
labels={["Completed", "In Progress"]}
|
||||
/>
|
||||
<ProgressBlock
|
||||
title="Progress"
|
||||
colors={["#50C793", "#E5F3E5"]}
|
||||
labels={["Success", "Failure"]}
|
||||
/>
|
||||
<ProgressBlock
|
||||
title="Progress"
|
||||
colors={["#FA916B", "#fcc8b5"]}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-6 gap-4">
|
||||
<OrdersBlock
|
||||
title="Orders"
|
||||
total="123k"
|
||||
/>
|
||||
<OrdersBlock
|
||||
title="Orders"
|
||||
total="123k"
|
||||
chartColor="#f1595c"
|
||||
/>
|
||||
<OrdersBlock
|
||||
title="Orders"
|
||||
total="123k"
|
||||
chartColor="#50c893"
|
||||
/>
|
||||
<OrdersBlock
|
||||
title="Profit"
|
||||
total="123k"
|
||||
chartColor="#4669fa"
|
||||
chartType="line"
|
||||
percentageContent={<span className="text-primary">+2.5%</span>}
|
||||
/>
|
||||
<OrdersBlock
|
||||
title="Profit"
|
||||
total="123k"
|
||||
chartColor="#f1595c"
|
||||
chartType="line"
|
||||
percentageContent={<span className="text-destructive">+2.5%</span>}
|
||||
/>
|
||||
<OrdersBlock
|
||||
title="Profit"
|
||||
total="123k"
|
||||
chartColor="#50c893"
|
||||
chartType="line"
|
||||
percentageContent={<span className="text-success">+2.5%</span>}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<EarningBlock
|
||||
title="Earnings"
|
||||
total="$12,335.00"
|
||||
percentage="+08%"
|
||||
/>
|
||||
<EarningBlock
|
||||
title="Earnings"
|
||||
total="$12,335.00"
|
||||
percentage="+08%"
|
||||
/>
|
||||
<EarningBlock
|
||||
title="Earnings"
|
||||
total="$12,335.00"
|
||||
percentage="+08%"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default StatisticPage;
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
import { Metadata } from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Dashcode Next Js",
|
||||
description: "Dashcode is a popular dashboard template.",
|
||||
};
|
||||
const Layout = ({ children }: { children: React.ReactNode }) => {
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
export default Layout;
|
||||
|
|
@ -0,0 +1,177 @@
|
|||
import { Badge } from "@/components/ui/badge";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "@/components/ui/accordion"
|
||||
import { useTranslations } from "next-intl";
|
||||
interface item {
|
||||
version: string;
|
||||
date: string;
|
||||
changes: { name: string; tag?: string }[];
|
||||
}
|
||||
const ChangeLogPage = () => {
|
||||
const t = useTranslations("Changelog")
|
||||
const items: item[] = [
|
||||
{
|
||||
version: "Version 2.0.1",
|
||||
date: "1 February 2023",
|
||||
changes: [
|
||||
{
|
||||
name: "Monochrome mode",
|
||||
tag: "added",
|
||||
},
|
||||
{
|
||||
name: "Axios configuration",
|
||||
tag: "fixed",
|
||||
},
|
||||
{
|
||||
name: "Other minor issues",
|
||||
tag: "fixed",
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
version: "Version 2.0.0",
|
||||
date: "24 January 2023",
|
||||
changes: [
|
||||
{
|
||||
name: "Change log page added.",
|
||||
tag: "added",
|
||||
},
|
||||
{
|
||||
name: "Badge added in sidebar.",
|
||||
tag: "added",
|
||||
},
|
||||
{
|
||||
name: "Vuex replaced with pinia",
|
||||
tag: "update",
|
||||
},
|
||||
{
|
||||
name: "Webpack replaced with Vite.",
|
||||
tag: "update",
|
||||
},
|
||||
{
|
||||
name: "Other minor issues",
|
||||
tag: "fixed",
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
version: "Version 1.0.1 ",
|
||||
date: "3 January 2023",
|
||||
changes: [
|
||||
{
|
||||
name: "RTL version added.",
|
||||
tag: "added",
|
||||
},
|
||||
{
|
||||
name: "Sidebar updated.",
|
||||
tag: "update",
|
||||
},
|
||||
{
|
||||
name: "Other minor issues",
|
||||
tag: "fixed",
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
version: "Version 1.0.0 ",
|
||||
date: "29 December 2022",
|
||||
changes: [
|
||||
{
|
||||
name: "Initial Release",
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
return (
|
||||
<div className="grid grid-cols-12 gap-5">
|
||||
<div className="lg:col-span-8 col-span-12">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{t("version")}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Badge color="primary" className="dark:text-white">New</Badge>
|
||||
<div className="mt-6">
|
||||
<Accordion type="single" collapsible className="w-full">
|
||||
{
|
||||
items.map((item, index) => (
|
||||
<AccordionItem
|
||||
value={`value-${index + 1}`}
|
||||
key={`changelog-${index}`}
|
||||
className="border-default-100"
|
||||
>
|
||||
<AccordionTrigger>
|
||||
<div>
|
||||
{item.version}
|
||||
<span className="font-semibold text-xs text-default-400">
|
||||
- Published on {item.date}
|
||||
</span>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
{item.changes.map((data, j) => (
|
||||
<div key={j}>
|
||||
<div className="flex gap-3 items-center mt-2 text-default-600 text-sm">
|
||||
<span className="h-2 w-2 bg-primary rounded-full"></span>
|
||||
<span>{data.name}</span>
|
||||
<span
|
||||
className={` px-2 rounded-full text-xs capitalize
|
||||
${data.tag === "added"
|
||||
? "bg-primary/10 text-primary"
|
||||
: data.tag === "update"
|
||||
? "bg-yellow-100 text-yellow-500"
|
||||
: data.tag === "fixed"
|
||||
? "bg-destructive/10 text-destructive"
|
||||
: ""
|
||||
}
|
||||
`}
|
||||
>
|
||||
{data.tag}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
))
|
||||
}
|
||||
</Accordion>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
<div className="lg:col-span-4 col-span-12">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
{t("changelog")}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<h5 className="text-xs font-medium">
|
||||
{t("versionHistory")}
|
||||
</h5>
|
||||
<ul className="space-y-3 mt-6 divide-y divide-default-100 dark:divide-default-300">
|
||||
{items.map((item, i) => (
|
||||
<li
|
||||
className="flex justify-between items-center text-xs text-default-600 pt-3"
|
||||
key={i}
|
||||
>
|
||||
<span>{item.version} </span>
|
||||
<span>{item.date}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChangeLogPage;
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
"use client";
|
||||
import dynamic from "next/dynamic";
|
||||
const Chart = dynamic(() => import("react-apexcharts"), { ssr: false });
|
||||
|
||||
import { useTheme } from "next-themes";
|
||||
import { hexToRGB } from "@/lib/utils";
|
||||
import { useConfig } from "@/hooks/use-config";
|
||||
import {
|
||||
getGridConfig,
|
||||
getXAxisConfig,
|
||||
getYAxisConfig,
|
||||
} from "@/lib/appex-chart-options";
|
||||
import { colors } from "@/lib/colors";
|
||||
|
||||
|
||||
const BasicArea = ({ height = 300 }) => {
|
||||
const [config] = useConfig();
|
||||
|
||||
const { theme: mode } = useTheme();
|
||||
|
||||
const series = [
|
||||
{
|
||||
data: [90, 70, 85, 60, 80, 70, 90, 75, 60, 80],
|
||||
},
|
||||
];
|
||||
const options: any = {
|
||||
chart: {
|
||||
toolbar: {
|
||||
show: false,
|
||||
},
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: false,
|
||||
},
|
||||
stroke: {
|
||||
curve: "smooth",
|
||||
width: 4,
|
||||
},
|
||||
colors: [colors.primary],
|
||||
tooltip: {
|
||||
theme: mode === "dark" ? "dark" : "light",
|
||||
},
|
||||
grid: getGridConfig(),
|
||||
|
||||
fill: {
|
||||
type: "gradient",
|
||||
colors: [colors.primary],
|
||||
gradient: {
|
||||
shadeIntensity: 1,
|
||||
opacityFrom: 0.2,
|
||||
opacityTo: 0.1,
|
||||
stops: [50, 100, 0],
|
||||
},
|
||||
},
|
||||
yaxis: getYAxisConfig(mode === 'light' ? colors["default-600"] : colors["default-300"]),
|
||||
xaxis: getXAxisConfig(
|
||||
mode === 'light' ? colors["default-600"] : colors["default-300"]
|
||||
),
|
||||
padding: {
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
},
|
||||
};
|
||||
return (
|
||||
<Chart
|
||||
options={options}
|
||||
series={series}
|
||||
type="area"
|
||||
height={height}
|
||||
width={"100%"}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default BasicArea;
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,108 @@
|
|||
"use client";
|
||||
import dynamic from "next/dynamic";
|
||||
const Chart = dynamic(() => import("react-apexcharts"), { ssr: false });
|
||||
import { colors } from "@/lib/colors";
|
||||
import { useTheme } from "next-themes";
|
||||
import { hexToRGB } from "@/lib/utils";
|
||||
import { useConfig } from "@/hooks/use-config";
|
||||
import {
|
||||
getGridConfig,
|
||||
getXAxisConfig,
|
||||
getYAxisConfig,
|
||||
} from "@/lib/appex-chart-options";
|
||||
|
||||
const IrregularTimeSeries = ({ height = 300 }) => {
|
||||
const [config] = useConfig();
|
||||
const { theme: mode } = useTheme();
|
||||
|
||||
const series = [
|
||||
{
|
||||
name: "product A",
|
||||
data: [100, 200, 300, 400, 500, 200, 100],
|
||||
},
|
||||
{
|
||||
name: "product B",
|
||||
data: [600, 700, 300, 500],
|
||||
},
|
||||
{
|
||||
name: "product C",
|
||||
data: [500, 100, 400, 700, 400, 700],
|
||||
},
|
||||
];
|
||||
const options: any = {
|
||||
chart: {
|
||||
toolbar: {
|
||||
show: false,
|
||||
},
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: false,
|
||||
},
|
||||
stroke: {
|
||||
curve: "smooth",
|
||||
width: 4,
|
||||
},
|
||||
colors: [
|
||||
colors.primary,
|
||||
colors.success,
|
||||
colors.info,
|
||||
],
|
||||
tooltip: {
|
||||
theme: mode === "dark" ? "dark" : "light",
|
||||
},
|
||||
grid: getGridConfig(),
|
||||
|
||||
fill: {
|
||||
type: "gradient",
|
||||
colors: [
|
||||
colors.primary,
|
||||
colors.success,
|
||||
colors.info,
|
||||
],
|
||||
gradient: {
|
||||
shadeIntensity: 1,
|
||||
opacityFrom: 0.2,
|
||||
opacityTo: 0.1,
|
||||
stops: [20, 100, 100, 100],
|
||||
},
|
||||
},
|
||||
yaxis: getYAxisConfig(
|
||||
mode === 'light' ? colors["default-600"] : colors["default-300"]
|
||||
),
|
||||
xaxis: getXAxisConfig(
|
||||
mode === 'light' ? colors["default-600"] : colors["default-300"]
|
||||
),
|
||||
padding: {
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
},
|
||||
legend: {
|
||||
labels: {
|
||||
colors: mode === 'light' ? colors["default-600"] : colors["default-300"],
|
||||
},
|
||||
itemMargin: {
|
||||
horizontal: 5,
|
||||
vertical: 5,
|
||||
},
|
||||
markers: {
|
||||
width: 10,
|
||||
height: 10,
|
||||
radius: 10,
|
||||
offsetX: config.isRtl ? 5 : -5
|
||||
}
|
||||
},
|
||||
};
|
||||
return (
|
||||
<Chart
|
||||
options={options}
|
||||
series={series}
|
||||
type="area"
|
||||
height={height}
|
||||
width={"100%"}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default IrregularTimeSeries;
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
|
||||
export const metadata = {
|
||||
title: "Appex Area Chart",
|
||||
};
|
||||
|
||||
const Layout = ({ children }: { children: React.ReactNode }) => {
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
export default Layout;
|
||||
|
|
@ -0,0 +1,265 @@
|
|||
"use client";
|
||||
import dynamic from "next/dynamic";
|
||||
const Chart = dynamic(() => import("react-apexcharts"), { ssr: false });
|
||||
import { colors } from "@/lib/colors";
|
||||
import { useTheme } from "next-themes";
|
||||
import { hexToRGB } from "@/lib/utils";
|
||||
import { useConfig } from "@/hooks/use-config";
|
||||
import {
|
||||
getGridConfig,
|
||||
getXAxisConfig,
|
||||
getYAxisConfig,
|
||||
} from "@/lib/appex-chart-options";
|
||||
|
||||
const NegativeAreaChart = ({ height = 320 }) => {
|
||||
const [config] = useConfig();
|
||||
const { theme: mode } = useTheme();
|
||||
|
||||
|
||||
const series = [
|
||||
{
|
||||
name: "north",
|
||||
data: [
|
||||
{
|
||||
x: 1996,
|
||||
y: 322,
|
||||
},
|
||||
{
|
||||
x: 1997,
|
||||
y: 324,
|
||||
},
|
||||
{
|
||||
x: 1998,
|
||||
y: 329,
|
||||
},
|
||||
{
|
||||
x: 1999,
|
||||
y: 342,
|
||||
},
|
||||
{
|
||||
x: 2000,
|
||||
y: 348,
|
||||
},
|
||||
{
|
||||
x: 2001,
|
||||
y: 334,
|
||||
},
|
||||
{
|
||||
x: 2002,
|
||||
y: 325,
|
||||
},
|
||||
{
|
||||
x: 2003,
|
||||
y: 316,
|
||||
},
|
||||
{
|
||||
x: 2004,
|
||||
y: 318,
|
||||
},
|
||||
{
|
||||
x: 2005,
|
||||
y: 330,
|
||||
},
|
||||
{
|
||||
x: 2006,
|
||||
y: 355,
|
||||
},
|
||||
{
|
||||
x: 2007,
|
||||
y: 366,
|
||||
},
|
||||
{
|
||||
x: 2008,
|
||||
y: 337,
|
||||
},
|
||||
{
|
||||
x: 2009,
|
||||
y: 352,
|
||||
},
|
||||
{
|
||||
x: 2010,
|
||||
y: 377,
|
||||
},
|
||||
{
|
||||
x: 2011,
|
||||
y: 383,
|
||||
},
|
||||
{
|
||||
x: 2012,
|
||||
y: 344,
|
||||
},
|
||||
{
|
||||
x: 2013,
|
||||
y: 366,
|
||||
},
|
||||
{
|
||||
x: 2014,
|
||||
y: 389,
|
||||
},
|
||||
{
|
||||
x: 2015,
|
||||
y: 334,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "south",
|
||||
data: [
|
||||
{
|
||||
x: 1996,
|
||||
y: 162,
|
||||
},
|
||||
{
|
||||
x: 1997,
|
||||
y: 90,
|
||||
},
|
||||
{
|
||||
x: 1998,
|
||||
y: 50,
|
||||
},
|
||||
{
|
||||
x: 1999,
|
||||
y: 77,
|
||||
},
|
||||
{
|
||||
x: 2000,
|
||||
y: 35,
|
||||
},
|
||||
{
|
||||
x: 2001,
|
||||
y: -45,
|
||||
},
|
||||
{
|
||||
x: 2002,
|
||||
y: -88,
|
||||
},
|
||||
{
|
||||
x: 2003,
|
||||
y: -120,
|
||||
},
|
||||
{
|
||||
x: 2004,
|
||||
y: -156,
|
||||
},
|
||||
{
|
||||
x: 2005,
|
||||
y: -123,
|
||||
},
|
||||
{
|
||||
x: 2006,
|
||||
y: -88,
|
||||
},
|
||||
{
|
||||
x: 2007,
|
||||
y: -66,
|
||||
},
|
||||
{
|
||||
x: 2008,
|
||||
y: -45,
|
||||
},
|
||||
{
|
||||
x: 2009,
|
||||
y: -29,
|
||||
},
|
||||
{
|
||||
x: 2010,
|
||||
y: -45,
|
||||
},
|
||||
{
|
||||
x: 2011,
|
||||
y: -88,
|
||||
},
|
||||
{
|
||||
x: 2012,
|
||||
y: -132,
|
||||
},
|
||||
{
|
||||
x: 2013,
|
||||
y: -146,
|
||||
},
|
||||
{
|
||||
x: 2014,
|
||||
y: -169,
|
||||
},
|
||||
{
|
||||
x: 2015,
|
||||
y: -184,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
const options: any = {
|
||||
chart: {
|
||||
toolbar: {
|
||||
show: false,
|
||||
},
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: false,
|
||||
},
|
||||
stroke: {
|
||||
curve: "smooth",
|
||||
width: 4,
|
||||
},
|
||||
colors: [
|
||||
colors.primary,
|
||||
colors.info,
|
||||
],
|
||||
tooltip: {
|
||||
theme: mode === "dark" ? "dark" : "light",
|
||||
},
|
||||
grid: getGridConfig(),
|
||||
|
||||
fill: {
|
||||
type: "gradient",
|
||||
colors: [
|
||||
colors.primary,
|
||||
colors.info,
|
||||
],
|
||||
gradient: {
|
||||
shadeIntensity: 1,
|
||||
opacityFrom: 0.2,
|
||||
opacityTo: 0.1,
|
||||
stops: [50, 100, 0],
|
||||
},
|
||||
},
|
||||
yaxis: getYAxisConfig(
|
||||
mode === 'light' ? colors["default-600"] : colors["default-300"]
|
||||
),
|
||||
xaxis: getXAxisConfig(
|
||||
mode === 'light' ? colors["default-600"] : colors["default-300"]
|
||||
),
|
||||
padding: {
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
},
|
||||
legend: {
|
||||
labels: {
|
||||
colors: mode === 'light' ? colors["default-600"] : colors["default-300"],
|
||||
},
|
||||
itemMargin: {
|
||||
horizontal: 5,
|
||||
vertical: 5,
|
||||
},
|
||||
markers: {
|
||||
width: 10,
|
||||
height: 10,
|
||||
radius: 10,
|
||||
offsetX: config.isRtl ? 5 : -5
|
||||
}
|
||||
},
|
||||
};
|
||||
return (
|
||||
<Chart
|
||||
options={options}
|
||||
series={series}
|
||||
type="area"
|
||||
height={height}
|
||||
width={"100%"}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default NegativeAreaChart;
|
||||
|
|
@ -0,0 +1,183 @@
|
|||
"use client";
|
||||
import dynamic from "next/dynamic";
|
||||
const Chart = dynamic(() => import("react-apexcharts"), { ssr: false });
|
||||
import { colors } from "@/lib/colors";
|
||||
import { useTheme } from "next-themes";
|
||||
import { hexToRGB } from "@/lib/utils";
|
||||
import { useConfig } from "@/hooks/use-config";
|
||||
import {
|
||||
getGridConfig,
|
||||
getYAxisConfig,
|
||||
getLabel,
|
||||
} from "@/lib/appex-chart-options";
|
||||
|
||||
const NullValueAreaChart = ({ height = 300 }) => {
|
||||
const [config] = useConfig();
|
||||
const { theme: mode } = useTheme();
|
||||
|
||||
|
||||
const series = [
|
||||
{
|
||||
name: "Network",
|
||||
data: [
|
||||
{
|
||||
x: "Dec 23 2017",
|
||||
y: null,
|
||||
},
|
||||
{
|
||||
x: "Dec 24 2017",
|
||||
y: 44,
|
||||
},
|
||||
{
|
||||
x: "Dec 25 2017",
|
||||
y: 31,
|
||||
},
|
||||
{
|
||||
x: "Dec 26 2017",
|
||||
y: 38,
|
||||
},
|
||||
{
|
||||
x: "Dec 27 2017",
|
||||
y: null,
|
||||
},
|
||||
{
|
||||
x: "Dec 28 2017",
|
||||
y: 32,
|
||||
},
|
||||
{
|
||||
x: "Dec 29 2017",
|
||||
y: 55,
|
||||
},
|
||||
{
|
||||
x: "Dec 30 2017",
|
||||
y: 51,
|
||||
},
|
||||
{
|
||||
x: "Dec 31 2017",
|
||||
y: 67,
|
||||
},
|
||||
{
|
||||
x: "Jan 01 2018",
|
||||
y: 22,
|
||||
},
|
||||
{
|
||||
x: "Jan 02 2018",
|
||||
y: 34,
|
||||
},
|
||||
{
|
||||
x: "Jan 03 2018",
|
||||
y: null,
|
||||
},
|
||||
{
|
||||
x: "Jan 04 2018",
|
||||
y: null,
|
||||
},
|
||||
{
|
||||
x: "Jan 05 2018",
|
||||
y: 11,
|
||||
},
|
||||
{
|
||||
x: "Jan 06 2018",
|
||||
y: 4,
|
||||
},
|
||||
{
|
||||
x: "Jan 07 2018",
|
||||
y: 15,
|
||||
},
|
||||
{
|
||||
x: "Jan 08 2018",
|
||||
y: null,
|
||||
},
|
||||
{
|
||||
x: "Jan 09 2018",
|
||||
y: 9,
|
||||
},
|
||||
{
|
||||
x: "Jan 10 2018",
|
||||
y: 34,
|
||||
},
|
||||
{
|
||||
x: "Jan 11 2018",
|
||||
y: null,
|
||||
},
|
||||
{
|
||||
x: "Jan 12 2018",
|
||||
y: null,
|
||||
},
|
||||
{
|
||||
x: "Jan 13 2018",
|
||||
y: 13,
|
||||
},
|
||||
{
|
||||
x: "Jan 14 2018",
|
||||
y: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const options: any = {
|
||||
chart: {
|
||||
toolbar: {
|
||||
show: false,
|
||||
},
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: false,
|
||||
},
|
||||
stroke: {
|
||||
curve: "straight",
|
||||
},
|
||||
colors: [
|
||||
colors.primary,
|
||||
colors.success,
|
||||
],
|
||||
tooltip: {
|
||||
theme: mode === "dark" ? "dark" : "light",
|
||||
},
|
||||
grid: getGridConfig(),
|
||||
|
||||
fill: {
|
||||
type: "pattern",
|
||||
colors: [
|
||||
colors.primary,
|
||||
colors.success,
|
||||
],
|
||||
pattern: {
|
||||
style: ["verticalLines", "horizontalLines"],
|
||||
width: 5,
|
||||
height: 6,
|
||||
},
|
||||
},
|
||||
yaxis: getYAxisConfig(
|
||||
mode === 'light' ? colors["default-600"] : colors["default-300"]
|
||||
),
|
||||
xaxis: {
|
||||
type: "datetime",
|
||||
labels: getLabel(mode === 'light' ? colors["default-600"] : colors["default-300"]),
|
||||
axisBorder: {
|
||||
show: false,
|
||||
},
|
||||
axisTicks: {
|
||||
show: false,
|
||||
},
|
||||
},
|
||||
padding: {
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
},
|
||||
};
|
||||
return (
|
||||
<Chart
|
||||
options={options}
|
||||
series={series}
|
||||
type="area"
|
||||
height={height}
|
||||
width={"100%"}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default NullValueAreaChart;
|
||||
|
|
@ -0,0 +1,107 @@
|
|||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import BasicArea from "./basic-area";
|
||||
import SplineArea from "./spline-area";
|
||||
import IrregularTimeSeries from "./irregular-time-series";
|
||||
import StackedAreaChart from "./stacked-area";
|
||||
import NullValueAreaChart from "./null-value-area";
|
||||
import GithubStyleChart from "./github-style-chart";
|
||||
import { Link } from '@/i18n/routing';
|
||||
import GithubStyleCharts1 from "./github-styles-charts1";
|
||||
import NegativeAreaChart from "./negative-areachart";
|
||||
import Image from "next/image";
|
||||
import avatar7 from "@/public/images/avatar/avatar-7.png";
|
||||
|
||||
|
||||
const AreaChartPage = () => {
|
||||
return (
|
||||
<div className=" grid xl:grid-cols-2 grid-cols-1 gap-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Basic Area Chart</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<BasicArea />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Spline Area Chart</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<SplineArea />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Area with Negative Values Chart</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<NegativeAreaChart />
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Area Chart - Github Style</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<GithubStyleChart />
|
||||
<div className="flex items-center space-x-2 rtl:space-x-reverse mt-2">
|
||||
<div className="w-12 h-12">
|
||||
<Image
|
||||
className="w-full h-full object-cover"
|
||||
src={avatar7}
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
<div className="">
|
||||
<Link
|
||||
href="#"
|
||||
className="text-base capitalize font-medium text-card-foreground"
|
||||
>
|
||||
coder
|
||||
</Link>
|
||||
<div className="cmeta space-x-1 rtl:space-x-reverse">
|
||||
<span className="text-base font-bold text-card-foreground commits">
|
||||
110
|
||||
</span>
|
||||
<span className="text-base text-card-foreground font-medium ">
|
||||
commits
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<GithubStyleCharts1 />
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Irregular Timeseries Chart</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<IrregularTimeSeries />
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Stacked Area Chart</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<StackedAreaChart />
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Area Chart With Null Values </CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<NullValueAreaChart />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AreaChartPage;
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
"use client";
|
||||
import dynamic from "next/dynamic";
|
||||
const Chart = dynamic(() => import("react-apexcharts"), { ssr: false });
|
||||
import { colors } from "@/lib/colors";
|
||||
import { useTheme } from "next-themes";
|
||||
import { hexToRGB } from "@/lib/utils";
|
||||
import { useConfig } from "@/hooks/use-config";
|
||||
import {
|
||||
getGridConfig,
|
||||
getXAxisConfig,
|
||||
getYAxisConfig,
|
||||
} from "@/lib/appex-chart-options";
|
||||
|
||||
const SplineArea = ({ height = 300 }) => {
|
||||
const [config] = useConfig();
|
||||
const { theme: mode } = useTheme();
|
||||
|
||||
|
||||
const series = [
|
||||
{
|
||||
name: "series1",
|
||||
data: [31, 40, 28, 51, 42, 109, 100],
|
||||
},
|
||||
{
|
||||
name: "series2",
|
||||
data: [11, 32, 45, 32, 34, 52, 41],
|
||||
},
|
||||
];
|
||||
|
||||
const options: any = {
|
||||
chart: {
|
||||
toolbar: {
|
||||
show: false,
|
||||
},
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: false,
|
||||
},
|
||||
stroke: {
|
||||
curve: "smooth",
|
||||
width: 4,
|
||||
},
|
||||
colors: [
|
||||
colors.primary,
|
||||
colors.success,
|
||||
],
|
||||
tooltip: {
|
||||
theme: mode === "dark" ? "dark" : "light",
|
||||
},
|
||||
grid: getGridConfig(),
|
||||
|
||||
fill: {
|
||||
type: "gradient",
|
||||
colors: [
|
||||
colors.primary,
|
||||
colors.success,
|
||||
],
|
||||
gradient: {
|
||||
shadeIntensity: 1,
|
||||
opacityFrom: 0.2,
|
||||
opacityTo: 0.1,
|
||||
stops: [50, 100, 0],
|
||||
},
|
||||
},
|
||||
yaxis: getYAxisConfig(
|
||||
mode === 'light' ? colors["default-600"] : colors["default-300"]
|
||||
),
|
||||
xaxis: getXAxisConfig(
|
||||
mode === 'light' ? colors["default-600"] : colors["default-300"]
|
||||
),
|
||||
padding: {
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
},
|
||||
legend: {
|
||||
labels: {
|
||||
colors: mode === 'light' ? colors["default-600"] : colors["default-300"],
|
||||
},
|
||||
itemMargin: {
|
||||
horizontal: 5,
|
||||
vertical: 5,
|
||||
},
|
||||
markers: {
|
||||
width: 10,
|
||||
height: 10,
|
||||
radius: 10,
|
||||
offsetX: config.isRtl ? 5 : -5
|
||||
}
|
||||
},
|
||||
};
|
||||
return (
|
||||
<Chart
|
||||
options={options}
|
||||
series={series}
|
||||
type="area"
|
||||
height={height}
|
||||
width={"100%"}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default SplineArea;
|
||||
|
|
@ -0,0 +1,157 @@
|
|||
"use client";
|
||||
import dynamic from "next/dynamic";
|
||||
const Chart = dynamic(() => import("react-apexcharts"), { ssr: false });
|
||||
import { colors } from "@/lib/colors";
|
||||
import { useTheme } from "next-themes";
|
||||
import { hexToRGB } from "@/lib/utils";
|
||||
import { useConfig } from "@/hooks/use-config";
|
||||
import {
|
||||
getGridConfig,
|
||||
getYAxisConfig,
|
||||
getLabel,
|
||||
} from "@/lib/appex-chart-options";
|
||||
|
||||
interface DataRange {
|
||||
min: number;
|
||||
max: number;
|
||||
}
|
||||
const StackedAreaChart = ({ height = 300 }) => {
|
||||
const [config] = useConfig();
|
||||
const { theme: mode } = useTheme();
|
||||
|
||||
|
||||
function generateDayWiseTimeSeries(baseval: number, count: number, yrange: DataRange) {
|
||||
var i = 0;
|
||||
var series: [number, number][] = [];
|
||||
while (i < count) {
|
||||
var x = baseval;
|
||||
var y =
|
||||
Math.floor(Math.random() * (yrange.max - yrange.min + 1)) + yrange.min;
|
||||
|
||||
series.push([x, y]);
|
||||
baseval += 86400000;
|
||||
i++;
|
||||
}
|
||||
return series;
|
||||
}
|
||||
|
||||
const series = [
|
||||
{
|
||||
name: "South",
|
||||
data: generateDayWiseTimeSeries(
|
||||
new Date("11 Feb 2017 GMT").getTime(),
|
||||
20,
|
||||
{
|
||||
min: 10,
|
||||
max: 60,
|
||||
}
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "North",
|
||||
data: generateDayWiseTimeSeries(
|
||||
new Date("11 Feb 2017 GMT").getTime(),
|
||||
20,
|
||||
{
|
||||
min: 10,
|
||||
max: 40,
|
||||
}
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "Central",
|
||||
data: generateDayWiseTimeSeries(
|
||||
new Date("11 Feb 2017 GMT").getTime(),
|
||||
20,
|
||||
{
|
||||
min: 10,
|
||||
max: 30,
|
||||
}
|
||||
),
|
||||
},
|
||||
];
|
||||
const options: any = {
|
||||
chart: {
|
||||
toolbar: {
|
||||
show: false,
|
||||
},
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: false,
|
||||
},
|
||||
stroke: {
|
||||
curve: "smooth",
|
||||
width: 4,
|
||||
},
|
||||
colors: [
|
||||
colors.primary,
|
||||
colors.primary,
|
||||
colors.info,
|
||||
],
|
||||
tooltip: {
|
||||
theme: mode === "dark" ? "dark" : "light",
|
||||
},
|
||||
grid: getGridConfig(),
|
||||
|
||||
fill: {
|
||||
type: "gradient",
|
||||
colors: [
|
||||
colors.primary,
|
||||
colors.primary,
|
||||
colors.info,
|
||||
],
|
||||
gradient: {
|
||||
shadeIntensity: 1,
|
||||
opacityFrom: 0.4,
|
||||
opacityTo: 0.1,
|
||||
stops: [50, 100, 0],
|
||||
},
|
||||
},
|
||||
yaxis: getYAxisConfig(
|
||||
mode === 'light' ? colors["default-600"] : colors["default-300"]
|
||||
),
|
||||
|
||||
xaxis: {
|
||||
type: "datetime",
|
||||
axisBorder: {
|
||||
show: false,
|
||||
},
|
||||
axisTicks: {
|
||||
show: false,
|
||||
},
|
||||
labels: getLabel(mode === 'light' ? colors["default-600"] : colors["default-300"]),
|
||||
},
|
||||
padding: {
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
},
|
||||
legend: {
|
||||
labels: {
|
||||
colors: mode === 'light' ? colors["default-600"] : colors["default-300"],
|
||||
},
|
||||
itemMargin: {
|
||||
horizontal: 5,
|
||||
vertical: 5,
|
||||
},
|
||||
markers: {
|
||||
width: 10,
|
||||
height: 10,
|
||||
radius: 10,
|
||||
offsetX: config.isRtl ? 5 : -5
|
||||
}
|
||||
},
|
||||
};
|
||||
return (
|
||||
<Chart
|
||||
options={options}
|
||||
series={series}
|
||||
type="area"
|
||||
height={height}
|
||||
width={"100%"}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default StackedAreaChart;
|
||||
|
|
@ -0,0 +1,132 @@
|
|||
"use client";
|
||||
import dynamic from "next/dynamic";
|
||||
const Chart = dynamic(() => import("react-apexcharts"), { ssr: false });
|
||||
import { colors } from "@/lib/colors";
|
||||
import { useTheme } from "next-themes";
|
||||
import { hexToRGB } from "@/lib/utils";
|
||||
import { useConfig } from "@/hooks/use-config";
|
||||
|
||||
const BarImageChart = ({ height = 350 }) => {
|
||||
const [config] = useConfig();
|
||||
const { theme: mode } = useTheme();
|
||||
|
||||
|
||||
const series = [
|
||||
{
|
||||
name: "coins",
|
||||
data: [
|
||||
2, 4, 3, 4, 3, 5, 5, 6.5, 6, 5, 4, 5, 8, 7, 7, 8, 8, 10, 9, 9, 12, 12,
|
||||
11, 12, 13, 14, 16, 14, 15, 17, 19, 21,
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const labels: number[] = Array.from({ length: 39 }, (_, index) => index + 1);
|
||||
|
||||
const options: any = {
|
||||
chart: {
|
||||
toolbar: {
|
||||
show: false,
|
||||
},
|
||||
},
|
||||
plotOptions: {
|
||||
bar: {
|
||||
horizontal: true,
|
||||
barHeight: "100%",
|
||||
},
|
||||
},
|
||||
labels: labels,
|
||||
dataLabels: {
|
||||
enabled: false,
|
||||
},
|
||||
stroke: {
|
||||
show: false,
|
||||
width: 1,
|
||||
colors: [
|
||||
mode === 'light' ? colors["default-600"] : colors["default-300"],
|
||||
],
|
||||
},
|
||||
colors: [
|
||||
colors.primary,
|
||||
],
|
||||
tooltip: {
|
||||
shared: false,
|
||||
theme: mode === "dark" ? "dark" : "light",
|
||||
y: {
|
||||
formatter: function (val: number) {
|
||||
return val + "K";
|
||||
},
|
||||
},
|
||||
},
|
||||
grid: {
|
||||
position: "back",
|
||||
},
|
||||
|
||||
fill: {
|
||||
type: "image",
|
||||
opacity: 0.87,
|
||||
image: {
|
||||
src: [
|
||||
"https://apexcharts.com/wp-content/uploads/2018/10/bar-chart-image-fill.png",
|
||||
],
|
||||
width: 466,
|
||||
height: 406,
|
||||
},
|
||||
},
|
||||
yaxis: {
|
||||
show: false,
|
||||
axisTicks: {
|
||||
show: true,
|
||||
},
|
||||
labels: {
|
||||
style: {
|
||||
colors: [
|
||||
mode === 'light' ? colors["default-600"] : colors["default-300"]
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
xaxis: {
|
||||
categories: [2008, 2009, 2010, 2011, 2012, 2013, 2014],
|
||||
axisBorder: {
|
||||
show: false,
|
||||
},
|
||||
axisTicks: {
|
||||
show: false,
|
||||
},
|
||||
labels: {
|
||||
style: {
|
||||
colors: [
|
||||
mode === 'light' ? colors["default-600"] : colors["default-300"],
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
states: {
|
||||
hover: {
|
||||
filter: "none",
|
||||
}
|
||||
},
|
||||
padding: {
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
},
|
||||
legend: {
|
||||
position: "right",
|
||||
offsetY: 40,
|
||||
},
|
||||
};
|
||||
return (
|
||||
<Chart
|
||||
options={options}
|
||||
series={series}
|
||||
type="bar"
|
||||
height={height}
|
||||
width={"100%"}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default BarImageChart;
|
||||
|
|
@ -0,0 +1,203 @@
|
|||
"use client";
|
||||
import dynamic from "next/dynamic";
|
||||
const Chart = dynamic(() => import("react-apexcharts"), { ssr: false });
|
||||
import { colors } from "@/lib/colors";
|
||||
import { useTheme } from "next-themes";
|
||||
import { hexToRGB } from "@/lib/utils";
|
||||
import { useConfig } from "@/hooks/use-config";
|
||||
import {
|
||||
getGridConfig,
|
||||
getYAxisConfig,
|
||||
} from "@/lib/appex-chart-options";
|
||||
|
||||
const BarsWithMarkes = ({ height = 350 }) => {
|
||||
const [config] = useConfig();
|
||||
const { theme: mode } = useTheme();
|
||||
|
||||
|
||||
|
||||
const series = [
|
||||
{
|
||||
name: "Actual",
|
||||
data: [
|
||||
{
|
||||
x: "2011",
|
||||
y: 12,
|
||||
goals: [
|
||||
{
|
||||
name: "Expected",
|
||||
value: 14,
|
||||
strokeWidth: 2,
|
||||
strokeDashArray: 2,
|
||||
strokeColor: "#775DD0",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
x: "2012",
|
||||
y: 44,
|
||||
goals: [
|
||||
{
|
||||
name: "Expected",
|
||||
value: 54,
|
||||
strokeWidth: 5,
|
||||
strokeHeight: 10,
|
||||
strokeColor: "#775DD0",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
x: "2013",
|
||||
y: 54,
|
||||
goals: [
|
||||
{
|
||||
name: "Expected",
|
||||
value: 52,
|
||||
strokeWidth: 10,
|
||||
strokeHeight: 0,
|
||||
strokeLineCap: "round",
|
||||
strokeColor: "#775DD0",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
x: "2014",
|
||||
y: 66,
|
||||
goals: [
|
||||
{
|
||||
name: "Expected",
|
||||
value: 61,
|
||||
strokeWidth: 10,
|
||||
strokeHeight: 0,
|
||||
strokeLineCap: "round",
|
||||
strokeColor: "#775DD0",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
x: "2015",
|
||||
y: 81,
|
||||
goals: [
|
||||
{
|
||||
name: "Expected",
|
||||
value: 66,
|
||||
strokeWidth: 10,
|
||||
strokeHeight: 0,
|
||||
strokeLineCap: "round",
|
||||
strokeColor: "#775DD0",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
x: "2016",
|
||||
y: 67,
|
||||
goals: [
|
||||
{
|
||||
name: "Expected",
|
||||
value: 70,
|
||||
strokeWidth: 5,
|
||||
strokeHeight: 10,
|
||||
strokeColor: "#775DD0",
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
const options: any = {
|
||||
chart: {
|
||||
toolbar: {
|
||||
show: false,
|
||||
},
|
||||
},
|
||||
plotOptions: {
|
||||
bar: {
|
||||
horizontal: true,
|
||||
},
|
||||
},
|
||||
dataLabels: {
|
||||
formatter: function (val: number, opt: any) {
|
||||
const goals =
|
||||
opt.w.config.series[opt.seriesIndex].data[opt.dataPointIndex].goals;
|
||||
|
||||
if (goals && goals.length) {
|
||||
return `${val} / ${goals[0].value}`;
|
||||
}
|
||||
return val;
|
||||
},
|
||||
},
|
||||
stroke: {
|
||||
show: false,
|
||||
width: 1,
|
||||
colors: [
|
||||
mode === 'light' ? colors["default-600"] : colors["default-300"],
|
||||
],
|
||||
},
|
||||
colors: [
|
||||
colors.success,
|
||||
],
|
||||
tooltip: {
|
||||
theme: mode === "dark" ? "dark" : "light",
|
||||
},
|
||||
grid: getGridConfig(),
|
||||
yaxis: getYAxisConfig(
|
||||
mode === 'light' ? colors["default-600"] : colors["default-300"]
|
||||
),
|
||||
xaxis: {
|
||||
axisBorder: {
|
||||
show: false,
|
||||
},
|
||||
axisTicks: {
|
||||
show: false,
|
||||
},
|
||||
labels: {
|
||||
style: {
|
||||
colors: [
|
||||
mode === 'light' ? colors["default-600"] : colors["default-300"],
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
padding: {
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
},
|
||||
|
||||
legend: {
|
||||
show: true,
|
||||
showForSingleSeries: true,
|
||||
customLegendItems: ["Actual", "Expected"],
|
||||
labels: {
|
||||
colors: mode === 'light' ? colors["default-600"] : colors["default-300"],
|
||||
},
|
||||
itemMargin: {
|
||||
horizontal: 5,
|
||||
vertical: 5,
|
||||
},
|
||||
markers: {
|
||||
width: 10,
|
||||
height: 10,
|
||||
radius: 10,
|
||||
offsetX: config.isRtl ? 5 : -5,
|
||||
fillColors: [
|
||||
colors.success,
|
||||
colors.primary,
|
||||
]
|
||||
}
|
||||
},
|
||||
};
|
||||
return (
|
||||
<Chart
|
||||
options={options}
|
||||
series={series}
|
||||
type="bar"
|
||||
height={height}
|
||||
width={"100%"}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default BarsWithMarkes;
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
"use client";
|
||||
import dynamic from "next/dynamic";
|
||||
const Chart = dynamic(() => import("react-apexcharts"), { ssr: false });
|
||||
import { colors } from "@/lib/colors";
|
||||
import { useTheme } from "next-themes";
|
||||
import { hexToRGB } from "@/lib/utils";
|
||||
import { useConfig } from "@/hooks/use-config";
|
||||
import {
|
||||
getGridConfig,
|
||||
getLabel,
|
||||
getYAxisConfig,
|
||||
} from "@/lib/appex-chart-options";
|
||||
|
||||
const BasicBar = ({ height = 350 }) => {
|
||||
const [config] = useConfig();
|
||||
const { theme: mode } = useTheme();
|
||||
|
||||
|
||||
|
||||
const series = [
|
||||
{
|
||||
data: [400, 430, 448, 470, 540, 580, 690, 1100, 1200, 1380],
|
||||
}
|
||||
];
|
||||
const options: any = {
|
||||
chart: {
|
||||
toolbar: {
|
||||
show: false,
|
||||
},
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: false,
|
||||
},
|
||||
stroke: {
|
||||
curve: "smooth",
|
||||
width: 4,
|
||||
},
|
||||
colors: [colors.info],
|
||||
tooltip: {
|
||||
theme: mode === "dark" ? "dark" : "light",
|
||||
},
|
||||
grid: getGridConfig(),
|
||||
yaxis: getYAxisConfig(
|
||||
mode === 'light' ? colors["default-600"] : colors["default-300"]
|
||||
),
|
||||
xaxis: {
|
||||
categories: [
|
||||
"South Korea",
|
||||
"Canada",
|
||||
"United Kingdom",
|
||||
"Netherlands",
|
||||
"Italy",
|
||||
"France",
|
||||
"Japan",
|
||||
"United States",
|
||||
"China",
|
||||
"Germany",
|
||||
],
|
||||
labels: getLabel(mode === 'light' ? colors["default-600"] : colors["default-300"]),
|
||||
axisBorder: {
|
||||
show: false,
|
||||
},
|
||||
axisTicks: {
|
||||
show: false,
|
||||
}
|
||||
},
|
||||
plotOptions: {
|
||||
bar: {
|
||||
horizontal: true,
|
||||
}
|
||||
},
|
||||
padding: {
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
}
|
||||
};
|
||||
return (
|
||||
<Chart
|
||||
options={options}
|
||||
series={series}
|
||||
type="bar"
|
||||
height={height}
|
||||
width={"100%"}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default BasicBar;
|
||||
|
|
@ -0,0 +1,136 @@
|
|||
"use client";
|
||||
import dynamic from "next/dynamic";
|
||||
const Chart = dynamic(() => import("react-apexcharts"), { ssr: false });
|
||||
import { colors } from "@/lib/colors";
|
||||
import { useTheme } from "next-themes";
|
||||
import { hexToRGB } from "@/lib/utils";
|
||||
import { useConfig } from "@/hooks/use-config";
|
||||
import { getGridConfig, getLabel } from "@/lib/appex-chart-options";
|
||||
|
||||
const CustomLabelBar = ({ height = 350 }) => {
|
||||
const [config] = useConfig();
|
||||
const { theme: mode } = useTheme();
|
||||
|
||||
|
||||
|
||||
const series = [
|
||||
{
|
||||
data: [400, 430, 448, 470, 540, 580, 690, 1100, 1200, 1380],
|
||||
},
|
||||
];
|
||||
|
||||
const options: any = {
|
||||
chart: {
|
||||
toolbar: {
|
||||
show: false,
|
||||
},
|
||||
},
|
||||
plotOptions: {
|
||||
bar: {
|
||||
barHeight: "100%",
|
||||
distributed: true,
|
||||
horizontal: true,
|
||||
dataLabels: {
|
||||
position: "bottom",
|
||||
}
|
||||
}
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: true,
|
||||
textAnchor: "start",
|
||||
style: {
|
||||
colors: ["#fff"]
|
||||
},
|
||||
formatter: function (val: number, opt: any) {
|
||||
return opt.w.globals.labels[opt.dataPointIndex] + ": " + val;
|
||||
},
|
||||
offsetX: 0,
|
||||
dropShadow: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
stroke: {
|
||||
show: false,
|
||||
width: 1,
|
||||
colors: [
|
||||
mode === 'light' ? colors["default-600"] : colors["default-300"]
|
||||
]
|
||||
},
|
||||
colors: [
|
||||
colors.primary,
|
||||
colors.info,
|
||||
colors.success,
|
||||
colors.primary
|
||||
],
|
||||
tooltip: {
|
||||
theme: mode === "dark" ? "dark" : "light",
|
||||
},
|
||||
grid: getGridConfig(),
|
||||
yaxis: {
|
||||
show: false,
|
||||
axisTicks: {
|
||||
show: true,
|
||||
},
|
||||
labels: {
|
||||
style: {
|
||||
colors: [
|
||||
mode === 'light' ? colors["default-600"] : colors["default-300"],
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
xaxis: {
|
||||
categories: [
|
||||
"South Korea",
|
||||
"Canada",
|
||||
"United Kingdom",
|
||||
"Netherlands",
|
||||
"Italy",
|
||||
"France",
|
||||
"Japan",
|
||||
"United States",
|
||||
"China",
|
||||
"India",
|
||||
],
|
||||
axisBorder: {
|
||||
show: false,
|
||||
},
|
||||
axisTicks: {
|
||||
show: false,
|
||||
},
|
||||
labels: getLabel(mode === 'light' ? colors["default-600"] : colors["default-300"]),
|
||||
},
|
||||
legend: {
|
||||
labels: {
|
||||
colors: mode === 'light' ? colors["default-600"] : colors["default-300"]
|
||||
},
|
||||
itemMargin: {
|
||||
horizontal: 5,
|
||||
vertical: 5,
|
||||
},
|
||||
markers: {
|
||||
width: 10,
|
||||
height: 10,
|
||||
radius: 10,
|
||||
offsetX: config.isRtl ? 5 : -5
|
||||
}
|
||||
},
|
||||
padding: {
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
}
|
||||
};
|
||||
return (
|
||||
<Chart
|
||||
options={options}
|
||||
series={series}
|
||||
type="bar"
|
||||
height={height}
|
||||
width={"100%"}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomLabelBar;
|
||||
|
|
@ -0,0 +1,117 @@
|
|||
"use client";
|
||||
import dynamic from "next/dynamic";
|
||||
const Chart = dynamic(() => import("react-apexcharts"), { ssr: false });
|
||||
import { colors } from "@/lib/colors";
|
||||
import { useTheme } from "next-themes";
|
||||
import { hexToRGB } from "@/lib/utils";
|
||||
import { useConfig } from "@/hooks/use-config";
|
||||
import {
|
||||
getGridConfig,
|
||||
getLabel,
|
||||
getYAxisConfig,
|
||||
} from "@/lib/appex-chart-options";
|
||||
|
||||
const GroupedBar = ({ height = 350 }) => {
|
||||
const [config] = useConfig();
|
||||
const { theme: mode } = useTheme();
|
||||
|
||||
|
||||
|
||||
const series = [
|
||||
{
|
||||
data: [44, 55, 41, 64, 22, 43],
|
||||
},
|
||||
{
|
||||
data: [53, 32, 33, 52, 13, 44],
|
||||
},
|
||||
];
|
||||
const options: any = {
|
||||
chart: {
|
||||
toolbar: {
|
||||
show: false,
|
||||
},
|
||||
},
|
||||
plotOptions: {
|
||||
bar: {
|
||||
horizontal: true,
|
||||
dataLabels: {
|
||||
position: "top",
|
||||
},
|
||||
},
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: true,
|
||||
offsetX: -10,
|
||||
style: {
|
||||
fontSize: "12px",
|
||||
fontWeight: 700,
|
||||
colors: [
|
||||
mode === 'light' ? colors["default-600"] : colors["default-300"],
|
||||
],
|
||||
},
|
||||
},
|
||||
stroke: {
|
||||
show: false,
|
||||
width: 1,
|
||||
colors: [
|
||||
mode === 'light' ? colors["default-600"] : colors["default-300"],
|
||||
],
|
||||
},
|
||||
colors: [
|
||||
colors.primary,
|
||||
colors.info,
|
||||
],
|
||||
tooltip: {
|
||||
theme: mode === "dark" ? "dark" : "light",
|
||||
},
|
||||
grid: getGridConfig(),
|
||||
yaxis: getYAxisConfig(
|
||||
mode === 'light' ? colors["default-600"] : colors["default-300"]
|
||||
),
|
||||
xaxis: {
|
||||
categories: [2001, 2002, 2003, 2004, 2005, 2006],
|
||||
axisBorder: {
|
||||
show: false,
|
||||
},
|
||||
axisTicks: {
|
||||
show: false,
|
||||
},
|
||||
labels: getLabel(
|
||||
mode === 'light' ? colors["default-600"] : colors["default-300"]
|
||||
),
|
||||
},
|
||||
padding: {
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
},
|
||||
|
||||
legend: {
|
||||
labels: {
|
||||
colors: mode === 'light' ? colors["default-600"] : colors["default-300"],
|
||||
},
|
||||
itemMargin: {
|
||||
horizontal: 5,
|
||||
vertical: 5,
|
||||
},
|
||||
markers: {
|
||||
width: 10,
|
||||
height: 10,
|
||||
radius: 10,
|
||||
offsetX: config.isRtl ? 5 : -5
|
||||
}
|
||||
},
|
||||
};
|
||||
return (
|
||||
<Chart
|
||||
options={options}
|
||||
series={series}
|
||||
type="bar"
|
||||
height={height}
|
||||
width={"100%"}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default GroupedBar;
|
||||
|
|
@ -0,0 +1,159 @@
|
|||
"use client";
|
||||
import dynamic from "next/dynamic";
|
||||
const Chart = dynamic(() => import("react-apexcharts"), { ssr: false });
|
||||
import { colors } from "@/lib/colors";
|
||||
import { useTheme } from "next-themes";
|
||||
import { hexToRGB } from "@/lib/utils";
|
||||
import { useConfig } from "@/hooks/use-config";
|
||||
import { getGridConfig, getYAxisConfig } from "@/lib/appex-chart-options";
|
||||
|
||||
const GroupedStackBar = ({ height = 350 }) => {
|
||||
const [config] = useConfig();
|
||||
const { theme: mode } = useTheme();
|
||||
|
||||
|
||||
|
||||
const series = [
|
||||
{
|
||||
name: "Q1 Budget",
|
||||
group: "budget",
|
||||
data: [44000, 55000, 41000, 67000, 22000],
|
||||
},
|
||||
{
|
||||
name: "Q1 Actual",
|
||||
group: "actual",
|
||||
data: [48000, 50000, 40000, 65000, 25000],
|
||||
},
|
||||
{
|
||||
name: "Q2 Budget",
|
||||
group: "budget",
|
||||
data: [13000, 36000, 20000, 8000, 13000],
|
||||
},
|
||||
{
|
||||
name: "Q2 Actual",
|
||||
group: "actual",
|
||||
data: [20000, 40000, 25000, 10000, 12000],
|
||||
},
|
||||
];
|
||||
|
||||
const options: any = {
|
||||
chart: {
|
||||
toolbar: {
|
||||
show: false,
|
||||
},
|
||||
stacked: true,
|
||||
},
|
||||
plotOptions: {
|
||||
bar: {
|
||||
horizontal: true,
|
||||
dataLabels: {
|
||||
total: {
|
||||
enabled: false,
|
||||
offsetX: 0,
|
||||
style: {
|
||||
colors: [
|
||||
mode === 'light' ? colors["default-600"] : colors["default-300"],
|
||||
],
|
||||
fontSize: "13px",
|
||||
fontWeight: 800,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: true,
|
||||
offsetX: 0,
|
||||
style: {
|
||||
fontSize: "12px",
|
||||
colors: [
|
||||
mode === 'light' ? colors["default-600"] : colors["default-300"],
|
||||
],
|
||||
},
|
||||
formatter: (val: number) => {
|
||||
return val / 1000 + "K";
|
||||
},
|
||||
},
|
||||
stroke: {
|
||||
show: false,
|
||||
width: 1,
|
||||
colors: [
|
||||
mode === 'light' ? colors["default-600"] : colors["default-300"],
|
||||
]
|
||||
},
|
||||
colors: [
|
||||
colors.primary,
|
||||
colors.info,
|
||||
colors.success,
|
||||
colors.primary
|
||||
],
|
||||
tooltip: {
|
||||
theme: mode === "dark" ? "dark" : "light",
|
||||
},
|
||||
grid: getGridConfig(),
|
||||
yaxis: getYAxisConfig(
|
||||
mode === 'light' ? colors["default-600"] : colors["default-300"]
|
||||
),
|
||||
xaxis: {
|
||||
categories: [
|
||||
"Online advertising",
|
||||
"Sales Training",
|
||||
"Print advertising",
|
||||
"Catalogs",
|
||||
"Meetings",
|
||||
],
|
||||
axisBorder: {
|
||||
show: false,
|
||||
},
|
||||
axisTicks: {
|
||||
show: false,
|
||||
},
|
||||
labels: {
|
||||
formatter: function (val: number) {
|
||||
return val + "K";
|
||||
},
|
||||
style: {
|
||||
colors: [
|
||||
mode === 'light' ? colors["default-600"] : colors["default-300"],
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
padding: {
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
},
|
||||
|
||||
legend: {
|
||||
position: "top",
|
||||
horizontalAlign: "left",
|
||||
offsetX: 40,
|
||||
labels: {
|
||||
colors: mode === 'light' ? colors["default-600"] : colors["default-300"],
|
||||
},
|
||||
itemMargin: {
|
||||
horizontal: 5,
|
||||
vertical: 5,
|
||||
},
|
||||
markers: {
|
||||
width: 12,
|
||||
height: 12,
|
||||
radius: 2,
|
||||
offsetX: config.isRtl ? 5 : -5
|
||||
}
|
||||
}
|
||||
};
|
||||
return (
|
||||
<Chart
|
||||
options={options}
|
||||
series={series}
|
||||
type="bar"
|
||||
height={height}
|
||||
width={"100%"}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default GroupedStackBar;
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
export const metadata = {
|
||||
title: "Appex Bar Chart",
|
||||
};
|
||||
|
||||
const Layout = ({ children }: { children: React.ReactNode }) => {
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
export default Layout;
|
||||
|
|
@ -0,0 +1,150 @@
|
|||
"use client";
|
||||
import dynamic from "next/dynamic";
|
||||
const Chart = dynamic(() => import("react-apexcharts"), { ssr: false });
|
||||
import { colors } from "@/lib/colors";
|
||||
import { useTheme } from "next-themes";
|
||||
import { hexToRGB } from "@/lib/utils";
|
||||
import { useConfig } from "@/hooks/use-config";
|
||||
import {
|
||||
getGridConfig,
|
||||
getYAxisConfig,
|
||||
} from "@/lib/appex-chart-options";
|
||||
|
||||
const NegativeValuesBar = ({ height = 350 }) => {
|
||||
const [config] = useConfig();
|
||||
const { theme: mode } = useTheme();
|
||||
|
||||
|
||||
|
||||
const series = [
|
||||
{
|
||||
name: "Males",
|
||||
data: [
|
||||
0.4, 0.65, 0.76, 0.88, 1.5, 2.1, 2.9, 3.8, 3.9, 4.2, 4, 4.3, 4.1, 4.2,
|
||||
4.5, 3.9, 3.5, 3,
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Females",
|
||||
data: [
|
||||
-0.8, -1.05, -1.06, -1.18, -1.4, -2.2, -2.85, -3.7, -3.96, -4.22, -4.3,
|
||||
-4.4, -4.1, -4, -4.1, -3.4, -3.1, -2.8,
|
||||
],
|
||||
},
|
||||
];
|
||||
const options: any = {
|
||||
chart: {
|
||||
toolbar: {
|
||||
show: false,
|
||||
},
|
||||
stacked: true,
|
||||
},
|
||||
plotOptions: {
|
||||
bar: {
|
||||
horizontal: true,
|
||||
}
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: false,
|
||||
offsetX: 0,
|
||||
style: {
|
||||
fontSize: "12px",
|
||||
colors: [
|
||||
mode === 'light' ? colors["default-600"] : colors["default-300"],
|
||||
],
|
||||
},
|
||||
},
|
||||
stroke: {
|
||||
show: false,
|
||||
width: 1,
|
||||
colors: [
|
||||
mode === 'light' ? colors["default-600"] : colors["default-300"],
|
||||
],
|
||||
},
|
||||
colors: [
|
||||
colors.primary,
|
||||
colors.info,
|
||||
],
|
||||
tooltip: {
|
||||
theme: mode === "dark" ? "dark" : "light",
|
||||
},
|
||||
grid: getGridConfig(),
|
||||
yaxis: getYAxisConfig(
|
||||
mode === 'light' ? colors["default-600"] : colors["default-300"]
|
||||
),
|
||||
xaxis: {
|
||||
categories: [
|
||||
"85+",
|
||||
"80-84",
|
||||
"75-79",
|
||||
"70-74",
|
||||
"65-69",
|
||||
"60-64",
|
||||
"55-59",
|
||||
"50-54",
|
||||
"45-49",
|
||||
"40-44",
|
||||
"35-39",
|
||||
"30-34",
|
||||
"25-29",
|
||||
"20-24",
|
||||
"15-19",
|
||||
"10-14",
|
||||
"5-9",
|
||||
"0-4",
|
||||
],
|
||||
axisBorder: {
|
||||
show: false,
|
||||
},
|
||||
axisTicks: {
|
||||
show: false,
|
||||
},
|
||||
labels: {
|
||||
formatter: function (val: number) {
|
||||
return Math.abs(Math.round(val)) + "%";
|
||||
},
|
||||
style: {
|
||||
colors: [
|
||||
mode === 'light' ? colors["default-600"] : colors["default-300"],
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
padding: {
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
},
|
||||
|
||||
legend: {
|
||||
position: "top",
|
||||
horizontalAlign: "end",
|
||||
offsetX: 40,
|
||||
labels: {
|
||||
colors: mode === 'light' ? colors["default-600"] : colors["default-300"],
|
||||
},
|
||||
itemMargin: {
|
||||
horizontal: 5,
|
||||
vertical: 5,
|
||||
},
|
||||
markers: {
|
||||
width: 12,
|
||||
height: 12,
|
||||
radius: 2,
|
||||
offsetX: config.isRtl ? 5 : -5
|
||||
}
|
||||
}
|
||||
};
|
||||
return (
|
||||
<Chart
|
||||
options={options}
|
||||
series={series}
|
||||
type="bar"
|
||||
height={height}
|
||||
width={"100%"}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default NegativeValuesBar;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue