first commit

This commit is contained in:
2024-09-17 12:11:39 +08:00
commit 42fbcc5d89
236 changed files with 27582 additions and 0 deletions

View File

@@ -0,0 +1,73 @@
import {
Container,
Heading,
Tab,
TabList,
TabPanel,
TabPanels,
Tabs,
} from "@chakra-ui/react"
import { useQueryClient, useQuery } from "@tanstack/react-query"
import { createFileRoute } from "@tanstack/react-router"
import { UserPublic, CoursesService } from "../../../client"
import CourseDetails from "../../../components/Courses/CourseDetails"
import CourseImages from "../../../components/Courses/CourseImages"
import CourseInfoImages from "../../../components/Courses/CourseInfoImages"
import Sechedule from "../../../components/Courses/Sechedule"
const tabsConfig = [
{ title: "Course Details", component: CourseDetails },
{ title: "Course Images", component: CourseImages },
{ title: "Course Info Images", component: CourseInfoImages },
{ title: "Schedule", component: Sechedule },
]
export const Route = createFileRoute('/_layout/Courses/$id/EditCourse')({
component: EditCourse,
})
function getcoursesQueryOptions(id: string) {
return {
queryFn: () =>
CoursesService.readCourse({ id: id }),
queryKey: ["course"],
}
}
function EditCourse() {
const {
data: course,
} = useQuery(
{
...getcoursesQueryOptions(Route.useParams().id),
// placeholderData: (prevData) => prevData,
}
)
console.log(course)
const finalTabs = tabsConfig.slice(0, 4)
return (
<Container maxW="full">
<Heading size="lg" textAlign={{ base: "center", md: "left" }} py={12}>
User Settings
</Heading>
<Tabs variant="enclosed">
<TabList>
{finalTabs.map((tab, index) => (
<Tab key={index}>{tab.title}</Tab>
))}
</TabList>
<TabPanels>
{finalTabs.map((tab, index) => (
<TabPanel key={index}>
{<tab.component />}
</TabPanel>
))}
</TabPanels>
</Tabs>
</Container>
)
}

View File

@@ -0,0 +1,234 @@
import {
Button,
FormControl,
FormErrorMessage,
FormLabel,
Input,
Textarea,
Modal,
ModalBody,
ModalCloseButton,
ModalContent,
ModalFooter,
ModalHeader,
ModalOverlay,
Container
} from "@chakra-ui/react"
import { useQuery, useQueryClient, useMutation } from "@tanstack/react-query"
import { createFileRoute, useNavigate, Await, useRouter } from "@tanstack/react-router"
import { useEffect, useState } from "react"
import useCustomToast from "../../../hooks/useCustomToast"
import { CoursesService, type ApiError, CourseCreate, } from "../../../client"
import { handleError } from "../../../utils"
import { type SubmitHandler, useForm } from "react-hook-form"
import { EditorState, ContentState, convertToRaw } from 'draft-js';
import { Editor } from "react-draft-wysiwyg";
import draftToHtml from 'draftjs-to-html';
import htmlToDraft from 'html-to-draftjs';
import "react-draft-wysiwyg/dist/react-draft-wysiwyg.css";
export const Route = createFileRoute("/_layout/Courses/AddCourse")({
component: AddCourse,
})
function AddCourseForms() {
const toolbar = {
options: ['inline', 'blockType', 'fontSize', 'list', 'textAlign', 'history', 'embedded', 'emoji', 'image'],
inline: { inDropdown: true },
list: { inDropdown: true },
textAlign: { inDropdown: true },
link: { inDropdown: true },
history: { inDropdown: true },
}
const showToast = useCustomToast()
const queryClient = useQueryClient()
const [contentEditorState, setContentEditorState] = useState<EditorState>(EditorState.createEmpty());
const [contents, setContent] = useState<string>('');
const [infoEditorState, setInfoEditorState] = useState<EditorState>(EditorState.createEmpty());
const [info, setInfo] = useState<string>('');
const [longDescriptionEditorState, setLongDescriptionEditorState] = useState<EditorState>(EditorState.createEmpty());
const [longDescription, setlongDescription] = useState<string>('');
const [remarksEditorState, setRemarksEditorState] = useState<EditorState>(EditorState.createEmpty());
const [remarks, setRemarks] = useState<string>('');
const {
register,
handleSubmit,
reset,
getValues,
setValue,
formState: { isSubmitting, errors, isDirty },
} = useForm<CourseCreate>({
mode: "onBlur",
criteriaMode: "all",
defaultValues: {
title: "",
sort_description: "",
long_description: "",
information: "",
contant: "",
remark: "",
}
})
const mutation = useMutation({
mutationFn: (data: CourseCreate) =>
CoursesService.createCourse({ requestBody: data }),
onSuccess: () => {
showToast("Success!", "Course create successfully.", "success")
},
onError: (err: ApiError) => {
handleError(err, showToast)
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ["courses"] })
history.go(-1)
},
})
const onSubmit: SubmitHandler<CourseCreate> = async (data) => {
mutation.mutate(data)
}
return (
<Container maxW="full" mt="20" as="form" onSubmit={handleSubmit(onSubmit)}>
<FormControl isInvalid={!!errors.title}>
<FormLabel htmlFor="title">Title</FormLabel>
<Input
id="address"
type="text"
{...register("title", {
required: "title is required",
})}
/>
{errors.title && (
<FormErrorMessage>{errors.title.message}</FormErrorMessage>
)}
</FormControl>
<FormControl mt={4}></FormControl>
<FormControl >
<FormLabel htmlFor="sort_description">Short Description</FormLabel>
<Textarea
id="sort_description"
{...register("sort_description", {
required: "sort_description is required",
})}
/>
</FormControl>
<FormControl mt={4}></FormControl>
<FormControl >
<FormLabel htmlFor="long_description">Long Description</FormLabel>
<Editor
editorState={longDescriptionEditorState}
wrapperClassName="wrapper-class"
editorClassName="demo-editor"
onEditorStateChange={newState => {
setLongDescriptionEditorState(newState);
setlongDescription(draftToHtml(convertToRaw(newState.getCurrentContent())));
setValue("long_description", longDescription);
}}
toolbar={toolbar}
/>
</FormControl>
<FormControl mt={4}></FormControl>
<FormControl >
<FormLabel htmlFor="information">Information</FormLabel>
<Editor
editorState={infoEditorState}
wrapperClassName="wrapper-class"
editorClassName="demo-editor"
onEditorStateChange={newState => {
setInfoEditorState(newState);
setInfo(draftToHtml(convertToRaw(newState.getCurrentContent())));
setValue("information", info);
}}
toolbar={toolbar}
/>
</FormControl>
<FormControl mt={4}></FormControl>
<FormControl >
<FormLabel htmlFor="contant">Content</FormLabel>
<Editor
editorState={contentEditorState}
wrapperClassName="wrapper-class"
editorClassName="demo-editor"
onEditorStateChange={newState => {
setContentEditorState(newState);
setContent(draftToHtml(convertToRaw(newState.getCurrentContent())));
setValue("contant", contents);
}}
toolbar={toolbar}
/>
</FormControl>
<FormControl mt={4}></FormControl>
<FormControl >
<FormLabel htmlFor="remark">Remark</FormLabel>
<Editor
editorState={remarksEditorState}
wrapperClassName="wrapper-class"
editorClassName="demo-editor"
onEditorStateChange={newState => {
setRemarksEditorState(newState);
setRemarks(draftToHtml(convertToRaw(newState.getCurrentContent())));
setValue("remark", remarks);
}}
toolbar={toolbar}
/>
</FormControl>
<FormControl mt={4}></FormControl>
{/* <button
type="button"
onClick={() => {
const values = getValues()
console.log(values)
// history.go(-1)// { test: "test-input", test1: "test1-input" }
}}
>
Get Values
</button> */}
<Button
variant="primary"
type="submit"
isLoading={isSubmitting}
isDisabled={!isDirty}
>
Save
</Button>
<FormControl mt={20}></FormControl>
</Container>
)
}
function AddCourse() {
return (
<Container maxW="full">
<AddCourseForms />
</Container>
)
}

View File

@@ -0,0 +1,193 @@
import {
Button,
Container,
Flex,
Heading,
SkeletonText,
Table,
TableContainer,
Tbody,
Td,
Th,
Thead,
Tr,
Icon,
useDisclosure
} from "@chakra-ui/react"
import { useQuery, useQueryClient } from "@tanstack/react-query"
import { createFileRoute, useNavigate } from "@tanstack/react-router"
import { useEffect, useState } from "react"
import { z } from "zod"
import { FaPlus, FaPen, FaTrashAlt } from "react-icons/fa"
import { CoursesService } from "../../../client"
import Delete from "../../../components/Common/DeleteAlert"
import ActionsMenu from "../../../components/Common/ActionsMenu"
import Navbar from "../../../components/Common/Navbar"
//import Addcourse from "../../components/courses/Addcourse"
import { Link } from "@tanstack/react-router"
const CoursesSearchSchema = z.object({
page: z.number().catch(1),
})
export const Route = createFileRoute("/_layout/Courses/Courses")({
component: Courses,
validateSearch: (search) => CoursesSearchSchema.parse(search),
})
const PER_PAGE = 100
function getcoursesQueryOptions({ page }: { page: number }) {
return {
queryFn: () =>
CoursesService.readCourses({ skip: (page - 1) * PER_PAGE, limit: PER_PAGE }),
queryKey: ["courses", { page }],
}
}
function CoursesTable() {
const [id, setId] = useState<string>('');
const deleteModal = useDisclosure()
const queryClient = useQueryClient()
const { page } = Route.useSearch()
const navigate = useNavigate({ from: Route.fullPath })
const setPage = (page: number) =>
navigate({ search: (prev) => ({ ...prev, page }) })
const {
data: courses,
isPending,
isPlaceholderData,
} = useQuery({
...getcoursesQueryOptions({ page }),
placeholderData: (prevData) => prevData,
})
const hasNextPage = !isPlaceholderData && courses?.data.length === PER_PAGE
const hasPreviousPage = page > 1
useEffect(() => {
if (hasNextPage) {
queryClient.prefetchQuery(getcoursesQueryOptions({ page: page + 1 }))
}
}, [page, queryClient, hasNextPage])
return (
<>
<TableContainer>
<Table size={{ base: "sm", md: "md" }}>
<Thead>
<Tr>
<Th>ID</Th>
<Th>Title</Th>
<Th>Short description</Th>
<Th>Actions</Th>
</Tr>
</Thead>
{isPending ? (
<Tbody>
<Tr>
{new Array(4).fill(null).map((_, index) => (
<Td key={index}>
<SkeletonText noOfLines={1} paddingBlock="16px" />
</Td>
))}
</Tr>
</Tbody>
) : (
<Tbody>
{courses?.data.map((course) => (
<Tr key={course.id} opacity={isPlaceholderData ? 0.5 : 1}>
<Td isTruncated maxWidth="20px">{course.id}</Td>
<Td isTruncated maxWidth="50px">
{course.title}
</Td>
<Td
isTruncated
maxWidth="250px"
>
{course.sort_description || "N/A"}
</Td>
<Td>
<Button
variant="primary"
gap={1}
fontSize={{ base: "sm", md: "inherit" }}
as={Link}
to={`/Courses/${course.id}/EditCourse`}
>
<Icon as={FaPen} />
</Button>
<Button
marginLeft={2}
variant="primary"
gap={1}
fontSize={{ base: "sm" }}
onClick={() => {
setId(course.id)
deleteModal.onOpen()
}}
>
<Icon as={FaTrashAlt} />
</Button>
</Td>
</Tr>
))}
</Tbody>
)}
</Table>
</TableContainer>
<Delete
type={'Course'}
id={id}
isOpen={deleteModal.isOpen}
onClose={deleteModal.onClose}
/>
<Flex
gap={4}
//aligncourses="center"
mt={4}
direction="row"
justifyContent="flex-end"
>
<Button onClick={() => setPage(page - 1)} isDisabled={!hasPreviousPage}>
Previous
</Button>
<span>Page {page}</span>
<Button isDisabled={!hasNextPage} onClick={() => setPage(page + 1)}>
Next
</Button>
</Flex>
</>
)
}
function Courses() {
return (
<Container maxW="full">
<Heading size="lg" textAlign={{ base: "center", md: "left" }} pt={12}>
Courses Management
</Heading>
<Flex py={8} gap={4}>
{/* TODO: Complete search functionality */}
{/* <InputGroup w={{ base: '100%', md: 'auto' }}>
<InputLeftElement pointerEvents='none'>
<Icon as={FaSearch} color='ui.dim' />
</InputLeftElement>
<Input type='text' placeholder='Search' fontSize={{ base: 'sm', md: 'inherit' }} borderRadius='8px' />
</InputGroup> */}
<Button
variant="primary"
gap={1}
fontSize={{ base: "sm", md: "inherit" }}
as={Link} to="/Courses/AddCourse"
>
<Icon as={FaPlus} /> Add Course
</Button>
</Flex>
<CoursesTable />
</Container>
)
}

View File

@@ -0,0 +1,155 @@
import {
Button,
Container,
Flex,
Heading,
SkeletonText,
Table,
TableContainer,
Tbody,
Td,
Th,
Thead,
Tr,
} from "@chakra-ui/react"
import { useQuery, useQueryClient } from "@tanstack/react-query"
import { createFileRoute, useNavigate } from "@tanstack/react-router"
import { useEffect, useState } from "react"
import { z } from "zod"
import parse from 'html-react-parser';
import { AboutUsService } from "../../client"
import ActionsMenu from "../../components/Common/ActionsMenu"
import Navbar from "../../components/Common/Navbar"
import AddAboutUs from "../../components/AboutUs/AddAboutUs"
const aboutUsSearchSchema = z.object({
page: z.number().catch(1),
})
export const Route = createFileRoute("/_layout/aboutUs")({
component: AboutUs,
validateSearch: (search) => aboutUsSearchSchema.parse(search),
})
const PER_PAGE = 100
function getItemsQueryOptions({ page }: { page: number }) {
return {
queryFn: () =>
AboutUsService.readAboutUs({ skip: (page - 1) * PER_PAGE, limit: PER_PAGE }),
queryKey: ["aboutUs", { page }],
}
}
function AboutUsTable() {
const queryClient = useQueryClient()
const { page } = Route.useSearch()
const navigate = useNavigate({ from: Route.fullPath })
const setPage = (page: number) =>
navigate({ search: (prev) => ({ ...prev, page }) })
const {
data: aboutUs,
isPending,
isPlaceholderData,
} = useQuery({
...getItemsQueryOptions({ page }),
placeholderData: (prevData) => prevData,
})
aboutUs?.data.sort((a, b) => a.index - b.index)
const hasNextPage = !isPlaceholderData && aboutUs?.data.length === PER_PAGE
const hasPreviousPage = page > 1
useEffect(() => {
if (hasNextPage) {
queryClient.prefetchQuery(getItemsQueryOptions({ page: page + 1 }))
}
}, [page, queryClient, hasNextPage])
return (
<>
<TableContainer>
<Table size={{ base: "sm", md: "md" }}>
<Thead>
<Tr>
<Th>ID</Th>
<Th>Description</Th>
<Th>Image</Th>
<Th>Index</Th>
<Th>Actions</Th>
</Tr>
</Thead>
{isPending ? (
<Tbody>
<Tr>
{new Array(4).fill(null).map((_, index) => (
<Td key={index}>
<SkeletonText noOfLines={1} paddingBlock="16px" />
</Td>
))}
</Tr>
</Tbody>
) : (
<Tbody>
{aboutUs?.data.map((aboutUs) => (
<Tr key={aboutUs.id} opacity={isPlaceholderData ? 0.5 : 1}>
<Td isTruncated maxWidth="50">{aboutUs.id}</Td>
<Td
whiteSpace="pre-line"
maxWidth="350px"
>
{parse(aboutUs.description) || "N/A"}
</Td>
<Td>
<img src={import.meta.env.VITE_API_URL+"/"+aboutUs.image} width="100px" height="100px" />
</Td>
<Td isTruncated maxWidth="10px">
{aboutUs.index}
</Td>
<Td>
<ActionsMenu type={"AboutUs"} value={aboutUs} />
</Td>
</Tr>
))}
</Tbody>
)}
</Table>
</TableContainer>
<Flex
gap={4}
alignItems="center"
mt={4}
direction="row"
justifyContent="flex-end"
>
<Button onClick={() => setPage(page - 1)} isDisabled={!hasPreviousPage}>
Previous
</Button>
<span>Page {page}</span>
<Button isDisabled={!hasNextPage} onClick={() => setPage(page + 1)}>
Next
</Button>
</Flex>
</>
)
}
function AboutUs() {
return (
<Container maxW="full">
<Heading size="lg" textAlign={{ base: "center", md: "left" }} pt={12}>
About Us Management
</Heading>
<Navbar type={"AboutUs"} addModalAs={AddAboutUs} />
<AboutUsTable />
</Container>
)
}

View File

@@ -0,0 +1,170 @@
import {
Badge,
Box,
Button,
Container,
Flex,
Heading,
SkeletonText,
Table,
TableContainer,
Tbody,
Td,
Th,
Thead,
Tr,
} from "@chakra-ui/react"
import { useQuery, useQueryClient } from "@tanstack/react-query"
import { createFileRoute, useNavigate } from "@tanstack/react-router"
import { useEffect } from "react"
import { z } from "zod"
import { type UserPublic, UsersService } from "../../client"
import AddUser from "../../components/Admin/AddUser"
import ActionsMenu from "../../components/Common/ActionsMenu"
import Navbar from "../../components/Common/Navbar"
const usersSearchSchema = z.object({
page: z.number().catch(1),
})
export const Route = createFileRoute("/_layout/admin")({
component: Admin,
validateSearch: (search) => usersSearchSchema.parse(search),
})
const PER_PAGE = 5
function getUsersQueryOptions({ page }: { page: number }) {
return {
queryFn: () =>
UsersService.readUsers({ skip: (page - 1) * PER_PAGE, limit: PER_PAGE }),
queryKey: ["users", { page }],
}
}
function UsersTable() {
const queryClient = useQueryClient()
const currentUser = queryClient.getQueryData<UserPublic>(["currentUser"])
const { page } = Route.useSearch()
const navigate = useNavigate({ from: Route.fullPath })
const setPage = (page: number) =>
navigate({ search: (prev) => ({ ...prev, page }) })
const {
data: users,
isPending,
isPlaceholderData,
} = useQuery({
...getUsersQueryOptions({ page }),
placeholderData: (prevData) => prevData,
})
const hasNextPage = !isPlaceholderData && users?.data.length === PER_PAGE
const hasPreviousPage = page > 1
useEffect(() => {
if (hasNextPage) {
queryClient.prefetchQuery(getUsersQueryOptions({ page: page + 1 }))
}
}, [page, queryClient, hasNextPage])
return (
<>
<TableContainer>
<Table size={{ base: "sm", md: "md" }}>
<Thead>
<Tr>
<Th width="20%">Full name</Th>
<Th width="50%">Email</Th>
<Th width="10%">Role</Th>
<Th width="10%">Status</Th>
<Th width="10%">Actions</Th>
</Tr>
</Thead>
{isPending ? (
<Tbody>
<Tr>
{new Array(4).fill(null).map((_, index) => (
<Td key={index}>
<SkeletonText noOfLines={1} paddingBlock="16px" />
</Td>
))}
</Tr>
</Tbody>
) : (
<Tbody>
{users?.data.map((user) => (
<Tr key={user.id}>
<Td
color={!user.full_name ? "ui.dim" : "inherit"}
isTruncated
maxWidth="150px"
>
{user.full_name || "N/A"}
{currentUser?.id === user.id && (
<Badge ml="1" colorScheme="teal">
You
</Badge>
)}
</Td>
<Td isTruncated maxWidth="150px">
{user.email}
</Td>
<Td>{user.is_superuser ? "Superuser" : "User"}</Td>
<Td>
<Flex gap={2}>
<Box
w="2"
h="2"
borderRadius="50%"
bg={user.is_active ? "ui.success" : "ui.danger"}
alignSelf="center"
/>
{user.is_active ? "Active" : "Inactive"}
</Flex>
</Td>
<Td>
<ActionsMenu
type="User"
value={user}
disabled={currentUser?.id === user.id ? true : false}
/>
</Td>
</Tr>
))}
</Tbody>
)}
</Table>
</TableContainer>
<Flex
gap={4}
alignItems="center"
mt={4}
direction="row"
justifyContent="flex-end"
>
<Button onClick={() => setPage(page - 1)} isDisabled={!hasPreviousPage}>
Previous
</Button>
<span>Page {page}</span>
<Button isDisabled={!hasNextPage} onClick={() => setPage(page + 1)}>
Next
</Button>
</Flex>
</>
)
}
function Admin() {
return (
<Container maxW="full">
<Heading size="lg" textAlign={{ base: "center", md: "left" }} pt={12}>
Users Management
</Heading>
<Navbar type={"User"} addModalAs={AddUser} />
<UsersTable />
</Container>
)
}

View File

@@ -0,0 +1,150 @@
import {
Button,
Container,
Flex,
Heading,
SkeletonText,
Table,
TableContainer,
Tbody,
Td,
Th,
Thead,
Tr,
} from "@chakra-ui/react"
import { useQuery, useQueryClient } from "@tanstack/react-query"
import { createFileRoute, useNavigate } from "@tanstack/react-router"
import { useEffect } from "react"
import { z } from "zod"
import moment from 'moment';
import { ClientMessagesService } from "../../client"
import ActionsMenu from "../../components/Common/ActionsMenu"
const messagesSearchSchema = z.object({
page: z.number().catch(1),
})
export const Route = createFileRoute("/_layout/clientMessages")({
component: Messages,
validateSearch: (search) => messagesSearchSchema.parse(search),
})
const PER_PAGE = 100
function getMessagesQueryOptions({ page }: { page: number }) {
return {
queryFn: () =>
ClientMessagesService.readMessages({ skip: (page - 1) * PER_PAGE, limit: PER_PAGE }),
queryKey: ["messages", { page }],
}
}
function MessagesTable() {
const queryClient = useQueryClient()
const { page } = Route.useSearch()
const navigate = useNavigate({ from: Route.fullPath })
const setPage = (page: number) =>
navigate({ search: (prev) => ({ ...prev, page }) })
const {
data: messages,
isPending,
isPlaceholderData,
} = useQuery({
...getMessagesQueryOptions({ page }),
placeholderData: (prevData) => prevData,
})
const hasNextPage = !isPlaceholderData && messages?.data.length === PER_PAGE
const hasPreviousPage = page > 1
messages?.data.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())
useEffect(() => {
if (hasNextPage) {
queryClient.prefetchQuery(getMessagesQueryOptions({ page: page + 1 }))
}
}, [page, queryClient, hasNextPage])
return (
<>
<TableContainer>
<Table size={{ base: "sm", md: "md" }}>
<Thead>
<Tr>
<Th>id</Th>
<Th>Name</Th>
<Th>Phone</Th>
<Th>Email</Th>
<Th>Message</Th>
<Th>Created</Th>
<Th>Actions</Th>
</Tr>
</Thead>
{isPending ? (
<Tbody>
<Tr>
{new Array(4).fill(null).map((_, index) => (
<Td key={index}>
<SkeletonText noOfLines={1} paddingBlock="16px" />
</Td>
))}
</Tr>
</Tbody>
) : (
<Tbody>
{messages?.data.map((message) => (
<Tr key={message.id} opacity={isPlaceholderData ? 0.5 : 1}>
<Td isTruncated maxWidth="50">{message.id}</Td>
<Td maxWidth="150px">
{message.name}
</Td>
<Td maxWidth="150px">
{message.phone}
</Td>
<Td maxWidth="150px">
{message.email}
</Td>
<Td whiteSpace="pre-line" maxWidth="250px">
{message.message}
</Td>
<Td maxWidth="150px">
{moment(message.created_at).utcOffset("+08:00").format('DD-MM-YYYY HH:mm')}
</Td>
<Td>
<ActionsMenu type={"Message"} value={message} />
</Td>
</Tr>
))}
</Tbody>
)}
</Table>
</TableContainer>
<Flex
gap={4}
alignItems="center"
mt={4}
direction="row"
justifyContent="flex-end"
>
<Button onClick={() => setPage(page - 1)} isDisabled={!hasPreviousPage}>
Previous
</Button>
<span>Page {page}</span>
<Button isDisabled={!hasNextPage} onClick={() => setPage(page + 1)}>
Next
</Button>
</Flex>
</>
)
}
function Messages() {
return (
<Container maxW="full">
<Heading size="lg" textAlign={{ base: "center", md: "left" }} pt={12}>
Messages Management
</Heading>
<MessagesTable />
</Container>
)
}

View File

@@ -0,0 +1,25 @@
import { Box, Container, Text } from "@chakra-ui/react"
import { createFileRoute } from "@tanstack/react-router"
import useAuth from "../../hooks/useAuth"
export const Route = createFileRoute("/_layout/")({
component: Dashboard,
})
function Dashboard() {
const { user: currentUser } = useAuth()
return (
<>
<Container maxW="full">
<Box pt={12} m={4}>
<Text fontSize="2xl">
Hi, {currentUser?.full_name || currentUser?.email} 👋🏼
</Text>
<Text>Welcome back, nice to see you again!</Text>
</Box>
</Container>
</>
)
}

View File

@@ -0,0 +1,145 @@
import {
Button,
Container,
Flex,
Heading,
SkeletonText,
Table,
TableContainer,
Tbody,
Td,
Th,
Thead,
Tr,
} from "@chakra-ui/react"
import { useQuery, useQueryClient } from "@tanstack/react-query"
import { createFileRoute, useNavigate } from "@tanstack/react-router"
import { useEffect } from "react"
import { z } from "zod"
import { ItemsService } from "../../client"
import ActionsMenu from "../../components/Common/ActionsMenu"
import Navbar from "../../components/Common/Navbar"
import AddItem from "../../components/Items/AddItem"
const itemsSearchSchema = z.object({
page: z.number().catch(1),
})
export const Route = createFileRoute("/_layout/items")({
component: Items,
validateSearch: (search) => itemsSearchSchema.parse(search),
})
const PER_PAGE = 5
function getItemsQueryOptions({ page }: { page: number }) {
return {
queryFn: () =>
ItemsService.readItems({ skip: (page - 1) * PER_PAGE, limit: PER_PAGE }),
queryKey: ["items", { page }],
}
}
function ItemsTable() {
const queryClient = useQueryClient()
const { page } = Route.useSearch()
const navigate = useNavigate({ from: Route.fullPath })
const setPage = (page: number) =>
navigate({ search: (prev) => ({ ...prev, page }) })
const {
data: items,
isPending,
isPlaceholderData,
} = useQuery({
...getItemsQueryOptions({ page }),
placeholderData: (prevData) => prevData,
})
const hasNextPage = !isPlaceholderData && items?.data.length === PER_PAGE
const hasPreviousPage = page > 1
useEffect(() => {
if (hasNextPage) {
queryClient.prefetchQuery(getItemsQueryOptions({ page: page + 1 }))
}
}, [page, queryClient, hasNextPage])
return (
<>
<TableContainer>
<Table size={{ base: "sm", md: "md" }}>
<Thead>
<Tr>
<Th>ID</Th>
<Th>Title</Th>
<Th>Description</Th>
<Th>Actions</Th>
</Tr>
</Thead>
{isPending ? (
<Tbody>
<Tr>
{new Array(4).fill(null).map((_, index) => (
<Td key={index}>
<SkeletonText noOfLines={1} paddingBlock="16px" />
</Td>
))}
</Tr>
</Tbody>
) : (
<Tbody>
{items?.data.map((item) => (
<Tr key={item.id} opacity={isPlaceholderData ? 0.5 : 1}>
<Td>{item.id}</Td>
<Td isTruncated maxWidth="150px">
{item.title}
</Td>
<Td
color={!item.description ? "ui.dim" : "inherit"}
isTruncated
maxWidth="150px"
>
{item.description || "N/A"}
</Td>
<Td>
<ActionsMenu type={"Item"} value={item} />
</Td>
</Tr>
))}
</Tbody>
)}
</Table>
</TableContainer>
<Flex
gap={4}
alignItems="center"
mt={4}
direction="row"
justifyContent="flex-end"
>
<Button onClick={() => setPage(page - 1)} isDisabled={!hasPreviousPage}>
Previous
</Button>
<span>Page {page}</span>
<Button isDisabled={!hasNextPage} onClick={() => setPage(page + 1)}>
Next
</Button>
</Flex>
</>
)
}
function Items() {
return (
<Container maxW="full">
<Heading size="lg" textAlign={{ base: "center", md: "left" }} pt={12}>
Items Management
</Heading>
<Navbar type={"Item"} addModalAs={AddItem} />
<ItemsTable />
</Container>
)
}

View File

@@ -0,0 +1,58 @@
import {
Container,
Heading,
Tab,
TabList,
TabPanel,
TabPanels,
Tabs,
} from "@chakra-ui/react"
import { useQueryClient } from "@tanstack/react-query"
import { createFileRoute } from "@tanstack/react-router"
import type { UserPublic } from "../../client"
import Appearance from "../../components/UserSettings/Appearance"
import ChangePassword from "../../components/UserSettings/ChangePassword"
import DeleteAccount from "../../components/UserSettings/DeleteAccount"
import UserInformation from "../../components/UserSettings/UserInformation"
const tabsConfig = [
{ title: "My profile", component: UserInformation },
{ title: "Password", component: ChangePassword },
{ title: "Appearance", component: Appearance },
{ title: "Danger zone", component: DeleteAccount },
]
export const Route = createFileRoute("/_layout/settings")({
component: UserSettings,
})
function UserSettings() {
const queryClient = useQueryClient()
const currentUser = queryClient.getQueryData<UserPublic>(["currentUser"])
const finalTabs = currentUser?.is_superuser
? tabsConfig.slice(0, 3)
: tabsConfig
return (
<Container maxW="full">
<Heading size="lg" textAlign={{ base: "center", md: "left" }} py={12}>
User Settings
</Heading>
<Tabs variant="enclosed">
<TabList>
{finalTabs.map((tab, index) => (
<Tab key={index}>{tab.title}</Tab>
))}
</TabList>
<TabPanels>
{finalTabs.map((tab, index) => (
<TabPanel key={index}>
<tab.component />
</TabPanel>
))}
</TabPanels>
</Tabs>
</Container>
)
}

View File

@@ -0,0 +1,269 @@
import {
Button,
FormControl,
FormErrorMessage,
FormLabel,
Input,
Modal,
ModalBody,
ModalCloseButton,
ModalContent,
ModalFooter,
ModalHeader,
ModalOverlay,
Container
} from "@chakra-ui/react"
import { useQuery, useQueryClient, useMutation } from "@tanstack/react-query"
import { createFileRoute, useNavigate, Await } from "@tanstack/react-router"
import { useEffect, useState } from "react"
import useCustomToast from "../../hooks/useCustomToast"
import { WebSettingsService, type WebSettingUpdate, type ApiError, } from "../../client"
import { handleError } from "../../utils"
import { type SubmitHandler, useForm } from "react-hook-form"
export const Route = createFileRoute("/_layout/webSetting")({
component: WebSetting,
})
function getWebSettingQuery() {
return {
queryFn: () =>
WebSettingsService.readWebSetting(),
queryKey: ["webSetting"],
}
}
function WebSettingForms() {
const showToast = useCustomToast()
const queryClient = useQueryClient()
// const {
// data: webSetting,
// isPending,
// isPlaceholderData,
// } = useQuery({
// ...getWebSettingQuery(),
// placeholderData: (prevData) => prevData,
// })
const {
register,
handleSubmit,
reset,
formState: { isSubmitting, errors, isDirty },
} = useForm<WebSettingUpdate>({
mode: "onBlur",
criteriaMode: "all",
defaultValues: async() => WebSettingsService.readWebSetting()
})
const mutation = useMutation({
mutationFn: (data: WebSettingUpdate) =>
WebSettingsService.updateWebSetting({ requestBody: data }),
onSuccess: () => {
showToast("Success!", "Settings updated successfully.", "success")
},
onError: (err: ApiError) => {
handleError(err, showToast)
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ["webSetting"] })
},
})
const onSubmit: SubmitHandler<WebSettingUpdate> = async (data) => {
mutation.mutate(data)
}
return (
<Container maxW="full" mt="20" as="form" onSubmit={handleSubmit(onSubmit)}>
<FormControl isInvalid={!!errors.address}>
<FormLabel htmlFor="address">Address</FormLabel>
<Input
id="address"
type="text"
{...register("address", {
required: "address is required",
})}
/>
{errors.address && (
<FormErrorMessage>{errors.address.message}</FormErrorMessage>
)}
</FormControl>
<FormControl mt={4}></FormControl>
<FormControl >
<FormLabel htmlFor="google_map_api_key">Google Map Api Key</FormLabel>
<Input
id="google_map_api_key"
type="text"
{...register("google_map_api_key", {
required: "google_map_api_key is required",
})}
/>
</FormControl>
<FormControl mt={4}></FormControl>
<FormControl >
<FormLabel htmlFor="latitude">Latitude</FormLabel>
<Input
id="latitude"
type="float"
{...register("latitude", {
required: "latitude is required",
})}
/>
</FormControl>
<FormControl mt={4}></FormControl>
<FormControl >
<FormLabel htmlFor="longitude">Longitude</FormLabel>
<Input
id="longitude"
type="float"
{...register("longitude", {
required: "longitude is required",
})}
/>
</FormControl>
<FormControl mt={4}></FormControl>
<FormControl >
<FormLabel htmlFor="phone">Phone</FormLabel>
<Input
id="phone"
type="text"
{...register("phone", {
required: "phone is required",
})}
/>
</FormControl>
<FormControl mt={4}></FormControl>
<FormControl >
<FormLabel htmlFor="email">Email</FormLabel>
<Input
id="email"
type="text"
{...register("email", {
required: "email is required",
})}
/>
</FormControl>
<FormControl mt={4}></FormControl>
<FormControl >
<FormLabel htmlFor="facebook">Facebook</FormLabel>
<Input
id="facebook"
type="text"
{...register("facebook", {
required: "facebook is required",
})}
/>
</FormControl>
<FormControl mt={4}></FormControl>
<FormControl >
<FormLabel htmlFor="instagram">Instagram</FormLabel>
<Input
id="instagram"
type="text"
{...register("instagram", {
required: "instagram is required",
})}
/>
</FormControl>
<FormControl mt={4}></FormControl>
<FormControl >
<FormLabel htmlFor="youtube">Youtube Channel</FormLabel>
<Input
id="youtube"
type="text"
{...register("youtube", {
required: "youtube is required",
})}
/>
</FormControl>
<FormControl mt={4}></FormControl>
<FormControl >
<FormLabel htmlFor="youtube_link">Youtube Video Link</FormLabel>
<Input
id="youtube_link"
type="text"
{...register("youtube_link", {
required: "youtube_link is required",
})}
/>
</FormControl>
<FormControl mt={4}></FormControl>
<FormControl >
<FormLabel htmlFor="whatsapp">Whatsapp</FormLabel>
<Input
id="whatsapp"
type="text"
{...register("whatsapp", {
required: "whatsapp is required",
})}
/>
</FormControl>
<FormControl mt={4}></FormControl>
<Button
variant="primary"
type="submit"
isLoading={isSubmitting}
isDisabled={!isDirty}
>
Save
</Button>
<FormControl mt={20}></FormControl>
</Container>
)
}
function WebSetting() {
return (
<Container maxW="full">
<WebSettingForms />
</Container>
)
}