Tests page and test element
This commit is contained in:
parent
a5533e914d
commit
59298e1581
@ -1,47 +1,103 @@
|
||||
import axiosInstance from "./_axiosInstance";
|
||||
import { AuthLoginResponse, CategoryResponse, Pagination, QuestionResponse, UserResponse } from "./types";
|
||||
import {
|
||||
AuthLoginResponse,
|
||||
CategoryResponse,
|
||||
Pagination,
|
||||
QuestionResponse,
|
||||
TestResponse,
|
||||
UserResponse,
|
||||
UserTestResponse,
|
||||
} from "./types";
|
||||
|
||||
export const get_questions = async (page: number, test_id?: number, category_id?: number) => {
|
||||
const response = await axiosInstance.get<Pagination<QuestionResponse>>("/api/questions/", {
|
||||
export const get_questions = async (
|
||||
page: number,
|
||||
test_id?: number,
|
||||
category_id?: number
|
||||
) => {
|
||||
const response = await axiosInstance.get<Pagination<QuestionResponse>>(
|
||||
"/api/questions/",
|
||||
{
|
||||
params: {
|
||||
page,
|
||||
test_id,
|
||||
category_id
|
||||
category_id,
|
||||
},
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
return response.data;
|
||||
}
|
||||
};
|
||||
|
||||
export const get_question = async (id: number) => {
|
||||
const response = await axiosInstance.get<{
|
||||
data: QuestionResponse
|
||||
data: QuestionResponse;
|
||||
}>(`/api/questions/${id}`);
|
||||
|
||||
return response.data;
|
||||
}
|
||||
};
|
||||
|
||||
export const get_current_user = async () => {
|
||||
const response = await axiosInstance.get<{
|
||||
user: UserResponse
|
||||
user: UserResponse;
|
||||
}>("/api/auth/me/");
|
||||
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const get_categories = async () => {
|
||||
const response = await axiosInstance.get<{
|
||||
data: CategoryResponse[];
|
||||
}>("/api/categories/");
|
||||
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const get_tests = async (page: number, category_id?: number) => {
|
||||
const response = await axiosInstance.get<Pagination<TestResponse>>(
|
||||
"/api/tests/",
|
||||
{
|
||||
params: {
|
||||
page,
|
||||
category_id,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const get_test = async (id: number) => {
|
||||
const response = await axiosInstance.get<{
|
||||
data: TestResponse;
|
||||
}>(`/api/tests/${id}`);
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export const get_categories = async () => {
|
||||
export const get_user_tests = async () => {
|
||||
const response = await axiosInstance.get<{
|
||||
data: CategoryResponse[]
|
||||
}>("/api/categories/");
|
||||
data: UserTestResponse[];
|
||||
}>("/api/user-tests/");
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export const get_user_test = async (id: number) => {
|
||||
const response = await axiosInstance.get<{
|
||||
data: UserTestResponse;
|
||||
}>(`/api/user-tests/${id}`);
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export const post_login = async (email: string, password: string) => {
|
||||
const response = await axiosInstance.post<AuthLoginResponse>("/api/auth/login/", {
|
||||
const response = await axiosInstance.post<AuthLoginResponse>(
|
||||
"/api/auth/login/",
|
||||
{
|
||||
email,
|
||||
password
|
||||
});
|
||||
password,
|
||||
}
|
||||
);
|
||||
|
||||
return response.data;
|
||||
}
|
||||
};
|
||||
|
||||
24
api/tests.ts
Normal file
24
api/tests.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { get_test, get_tests } from "./_client";
|
||||
|
||||
interface useTestsAttr {
|
||||
page?: number;
|
||||
category_id?: number;
|
||||
}
|
||||
|
||||
export const useTests = ({ page = 1, category_id }: useTestsAttr = { }) => {
|
||||
return useQuery({
|
||||
queryKey: ['tests', page, category_id],
|
||||
queryFn: () => get_tests(page, category_id)
|
||||
})
|
||||
}
|
||||
|
||||
export const useTest = (id: number) => {
|
||||
return useQuery({
|
||||
queryKey: ['test', id],
|
||||
queryFn: async () => {
|
||||
const response = await get_test(id);
|
||||
return response.data;
|
||||
}
|
||||
})
|
||||
}
|
||||
53
api/types.ts
53
api/types.ts
@ -50,6 +50,59 @@ export interface CategoryResponse {
|
||||
|
||||
created_at: string;
|
||||
}
|
||||
export interface TestResponse {
|
||||
id: number;
|
||||
|
||||
title: string;
|
||||
description: string;
|
||||
|
||||
category_id: number;
|
||||
category: CategoryResponse;
|
||||
|
||||
is_available: boolean;
|
||||
|
||||
author_id: number;
|
||||
author: UserResponse;
|
||||
questions: QuestionResponse[];
|
||||
|
||||
closed_at: string;
|
||||
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
export interface AnswerResponse {
|
||||
id: number;
|
||||
question_id: number;
|
||||
user_test_id: number;
|
||||
|
||||
question: QuestionResponse;
|
||||
answer?: number[] | string[];
|
||||
|
||||
user_id: number;
|
||||
|
||||
is_correct: boolean;
|
||||
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface UserTestResponse {
|
||||
id: number;
|
||||
test_id: number;
|
||||
user_id: number;
|
||||
user: UserResponse;
|
||||
closed_at: string;
|
||||
is_completed: boolean;
|
||||
score: number;
|
||||
is_available: boolean;
|
||||
|
||||
answers: AnswerResponse[];
|
||||
|
||||
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export enum UserTypes {
|
||||
Admin = "admin",
|
||||
Creator = "creator",
|
||||
|
||||
22
api/userTests.ts
Normal file
22
api/userTests.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { get_user_test, get_user_tests } from "./_client";
|
||||
|
||||
export const useTests = () => {
|
||||
return useQuery({
|
||||
queryKey: ['user-tests'],
|
||||
queryFn: async () => {
|
||||
const response = await get_user_tests();
|
||||
return response.data;
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const useTest = (id: number) => {
|
||||
return useQuery({
|
||||
queryKey: ['test', id],
|
||||
queryFn: async () => {
|
||||
const response = await get_user_test(id);
|
||||
return response.data;
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -41,6 +41,7 @@ export default function HomeScreen() {
|
||||
key={question.id}
|
||||
question={question}
|
||||
onPress={() => router.push(`/questions/${question.id}`)}
|
||||
withCategory={true}
|
||||
/>
|
||||
))}
|
||||
</ParallaxScrollView>
|
||||
|
||||
@ -8,6 +8,7 @@ import Content from "@/components/ui/content";
|
||||
import CustomSelect from "@/components/ui/custom-select";
|
||||
import PaginationList from "@/components/ui/pagination-list";
|
||||
import { ThemedText } from "@/components/ui/themed-text";
|
||||
import getCategoryOptions from "@/utils/get-category-options";
|
||||
import { router } from "expo-router";
|
||||
import { useRef, useState } from "react";
|
||||
|
||||
@ -21,12 +22,8 @@ export default function QuestionsScreen() {
|
||||
category_id: category
|
||||
});
|
||||
|
||||
const categoryOptions = categories.map((cat) => {
|
||||
return {
|
||||
label: cat.name,
|
||||
value: String(cat.id),
|
||||
};
|
||||
});
|
||||
const categoryOptions = getCategoryOptions(categories);
|
||||
|
||||
const handleChangeCategory = (value: string) => {
|
||||
setPage(1);
|
||||
if(value === "all")
|
||||
@ -53,7 +50,7 @@ export default function QuestionsScreen() {
|
||||
<PaginationList<QuestionResponse>
|
||||
pagination={questionsPagination}
|
||||
renderItem={(item) => (
|
||||
<Question question={item} onPress={() => router.push(`/questions/${item.id}`)} />
|
||||
<Question question={item} onPress={() => router.push(`/questions/${item.id}`)} withCategory={true} />
|
||||
)}
|
||||
skeleton={<Question />}
|
||||
currentPage={page}
|
||||
|
||||
@ -1,11 +1,64 @@
|
||||
|
||||
import { useTests } from '@/api/tests';
|
||||
import { TestResponse } from '@/api/types';
|
||||
import useTaxonomyContext from '@/components/providers/taxonomy-provider/hook';
|
||||
import Test from '@/components/test';
|
||||
import Content from '@/components/ui/content';
|
||||
import CustomSelect from '@/components/ui/custom-select';
|
||||
import PaginationList from '@/components/ui/pagination-list';
|
||||
import { ThemedText } from '@/components/ui/themed-text';
|
||||
import getCategoryOptions from '@/utils/get-category-options';
|
||||
import { router } from 'expo-router';
|
||||
import { useRef, useState } from 'react';
|
||||
import { ScrollView, View } from 'react-native';
|
||||
|
||||
export default function TestsScreen() {
|
||||
const { categories } = useTaxonomyContext();
|
||||
|
||||
const [category, setCategory] = useState<undefined|number>(undefined);
|
||||
const [page, setPage] = useState<number>(1);
|
||||
|
||||
const { data: testsPagination, isLoading } = useTests({
|
||||
page: page,
|
||||
category_id: category
|
||||
});
|
||||
|
||||
const scrollRef = useRef<ScrollView>(null);
|
||||
|
||||
const categoryOptions = getCategoryOptions(categories);
|
||||
|
||||
const handleChangeCategory = (value: string) => {
|
||||
setPage(1);
|
||||
if(value === "all")
|
||||
setCategory(undefined);
|
||||
else
|
||||
setCategory(+value);
|
||||
}
|
||||
|
||||
return (
|
||||
<Content>
|
||||
<Content ref={scrollRef}>
|
||||
<ThemedText type="title" className='mb-3'>Tests</ThemedText>
|
||||
|
||||
<View className="mb-4">
|
||||
<CustomSelect
|
||||
options={categoryOptions}
|
||||
noneOption={{ label: "All categories", value: "all" }}
|
||||
selectedValue={category ? `${category}` : undefined}
|
||||
onValueChange={handleChangeCategory}
|
||||
placeholder="Select category" />
|
||||
</View>
|
||||
|
||||
<PaginationList<TestResponse>
|
||||
pagination={testsPagination}
|
||||
isLoadingPage={isLoading}
|
||||
renderItem={(item) => (
|
||||
<Test test={item} onPress={() => router.push(`/tests/${item.id}`)} />
|
||||
)}
|
||||
skeleton={<Test />}
|
||||
currentPage={page}
|
||||
setCurrentPage={setPage}
|
||||
scrollView={scrollRef}
|
||||
/>
|
||||
</Content>
|
||||
);
|
||||
}
|
||||
|
||||
@ -42,7 +42,7 @@ const QuestionScreen = () => {
|
||||
<>
|
||||
<Stack.Screen options={{ title: `Question #${id}` }} />
|
||||
<Content>
|
||||
<Question question={data} withMeta={true} />
|
||||
<Question question={data} withMeta={true} withCategory={true} />
|
||||
|
||||
<Divider className="mt-5 mb-5" />
|
||||
|
||||
|
||||
46
app/tests/[id].tsx
Normal file
46
app/tests/[id].tsx
Normal file
@ -0,0 +1,46 @@
|
||||
import { useTest } from "@/api/tests";
|
||||
import Question from "@/components/question";
|
||||
import Test from "@/components/test";
|
||||
import { Button, ButtonText } from "@/components/ui/button";
|
||||
import Content from "@/components/ui/content";
|
||||
import { Stack, useLocalSearchParams } from "expo-router";
|
||||
import { View } from "react-native";
|
||||
|
||||
const TestScreen = () => {
|
||||
const { id: idParam } = useLocalSearchParams<{ id: string }>();
|
||||
const id = +idParam;
|
||||
const { data, isLoading } = useTest(id);
|
||||
|
||||
if (!data)
|
||||
return (
|
||||
<>
|
||||
<Stack.Screen options={{ title: `Test #${id}` }} />
|
||||
<Content>{isLoading && <Test />}</Content>
|
||||
</>
|
||||
);
|
||||
|
||||
const handleStartTest = () => {};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack.Screen options={{ title: `Test #${id}` }} />
|
||||
<Content>
|
||||
<View className="mb-3">
|
||||
<Test test={data} />
|
||||
</View>
|
||||
|
||||
{data.is_available && <Button className="mb-3" onPress={handleStartTest}>
|
||||
<ButtonText>Start Test</ButtonText>
|
||||
</Button>}
|
||||
|
||||
<View className="gap-4">
|
||||
{data.questions.map((question) => (
|
||||
<Question key={question.id} question={question} />
|
||||
))}
|
||||
</View>
|
||||
</Content>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default TestScreen;
|
||||
@ -11,17 +11,20 @@ interface QuestionProps {
|
||||
question?: QuestionResponse;
|
||||
onPress?: () => void;
|
||||
withMeta?: boolean;
|
||||
withCategory?: boolean;
|
||||
}
|
||||
|
||||
const Question = ({ question, onPress, withMeta = false }: QuestionProps) => {
|
||||
const Question = ({ question, onPress, withCategory = false, withMeta = false }: QuestionProps) => {
|
||||
if (!question) return <Skeleton className="w-full h-20" />;
|
||||
|
||||
return (
|
||||
<Pressable onPress={onPress}>
|
||||
<Panel>
|
||||
{withCategory && (
|
||||
<ThemedText type="meta" className="mb-1">
|
||||
{question.category.name}
|
||||
</ThemedText>
|
||||
)}
|
||||
<ThemedText style={styles.questionTitle}>{question.title}</ThemedText>
|
||||
<ThemedText>{question.description}</ThemedText>
|
||||
|
||||
|
||||
61
components/test.tsx
Normal file
61
components/test.tsx
Normal file
@ -0,0 +1,61 @@
|
||||
import { TestResponse } from "@/api/types";
|
||||
import formatISODate from "@/utils/format-iso-date";
|
||||
import { FontAwesome5 } from "@expo/vector-icons";
|
||||
import { Pressable, StyleSheet, View } from "react-native";
|
||||
import { Badge, BadgeText } from "./ui/badge";
|
||||
import { Divider } from "./ui/divider";
|
||||
import Panel from "./ui/panel";
|
||||
import { Skeleton } from "./ui/skeleton";
|
||||
import { ThemedText } from "./ui/themed-text";
|
||||
|
||||
interface TestProps {
|
||||
test?: TestResponse;
|
||||
onPress?: (test: TestResponse) => void;
|
||||
}
|
||||
|
||||
const Test = ({ test, onPress }: TestProps) => {
|
||||
if (!test) return <Skeleton className="w-full h-24" />;
|
||||
|
||||
const availableText = test.is_available ? `Available: ${formatISODate(test.closed_at)}` : "Closed";
|
||||
|
||||
return (
|
||||
<Pressable onPress={() => onPress?.(test)}>
|
||||
<Panel>
|
||||
<ThemedText type="meta" className="mb-1">
|
||||
{test.category.name}
|
||||
</ThemedText>
|
||||
<View className="mb-1 flex-row">
|
||||
<Badge action={test.is_available ? "success" : "error"}>
|
||||
<BadgeText>
|
||||
<FontAwesome5
|
||||
name={test.is_available ? "check-circle" : "times-circle"}
|
||||
/>{" "}
|
||||
{availableText}
|
||||
</BadgeText>
|
||||
</Badge>
|
||||
</View>
|
||||
<ThemedText style={styles.testTitle}>{test.title}</ThemedText>
|
||||
<ThemedText>{test.description}</ThemedText>
|
||||
|
||||
<Divider className="mt-2 mb-2" />
|
||||
|
||||
<View className="justify-between flex-row">
|
||||
<ThemedText type="meta">
|
||||
<FontAwesome5 name="user" /> {test.author.username}
|
||||
</ThemedText>
|
||||
<ThemedText type="meta">{test.questions.length} Questions</ThemedText>
|
||||
</View>
|
||||
</Panel>
|
||||
</Pressable>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
testTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: "600",
|
||||
marginBottom: 10,
|
||||
},
|
||||
});
|
||||
|
||||
export default Test;
|
||||
216
components/ui/badge/index.tsx
Normal file
216
components/ui/badge/index.tsx
Normal file
@ -0,0 +1,216 @@
|
||||
'use client';
|
||||
import React, { useMemo } from 'react';
|
||||
import { Text, View } from 'react-native';
|
||||
import { PrimitiveIcon, UIIcon } from '@gluestack-ui/core/icon/creator';
|
||||
import { tva } from '@gluestack-ui/utils/nativewind-utils';
|
||||
import {
|
||||
withStyleContext,
|
||||
useStyleContext,
|
||||
} from '@gluestack-ui/utils/nativewind-utils';
|
||||
import { cssInterop } from 'nativewind';
|
||||
import type { VariantProps } from '@gluestack-ui/utils/nativewind-utils';
|
||||
|
||||
import { Svg } from 'react-native-svg';
|
||||
const SCOPE = 'BADGE';
|
||||
|
||||
const badgeStyle = tva({
|
||||
base: 'flex-row items-center rounded-sm data-[disabled=true]:opacity-50 px-2 py-1',
|
||||
variants: {
|
||||
action: {
|
||||
error: 'bg-background-error border-error-300',
|
||||
warning: 'bg-background-warning border-warning-300',
|
||||
success: 'bg-background-success border-success-300',
|
||||
info: 'bg-background-info border-info-300',
|
||||
muted: 'bg-background-muted border-background-300',
|
||||
},
|
||||
variant: {
|
||||
solid: '',
|
||||
outline: 'border',
|
||||
},
|
||||
size: {
|
||||
sm: '',
|
||||
md: '',
|
||||
lg: '',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const badgeTextStyle = tva({
|
||||
base: 'text-typography-700 font-body font-normal tracking-normal uppercase',
|
||||
|
||||
parentVariants: {
|
||||
action: {
|
||||
error: 'text-error-600',
|
||||
warning: 'text-warning-600',
|
||||
success: 'text-success-600',
|
||||
info: 'text-info-600',
|
||||
muted: 'text-background-800',
|
||||
},
|
||||
size: {
|
||||
sm: 'text-2xs',
|
||||
md: 'text-xs',
|
||||
lg: 'text-sm',
|
||||
},
|
||||
},
|
||||
variants: {
|
||||
isTruncated: {
|
||||
true: 'web:truncate',
|
||||
},
|
||||
bold: {
|
||||
true: 'font-bold',
|
||||
},
|
||||
underline: {
|
||||
true: 'underline',
|
||||
},
|
||||
strikeThrough: {
|
||||
true: 'line-through',
|
||||
},
|
||||
sub: {
|
||||
true: 'text-xs',
|
||||
},
|
||||
italic: {
|
||||
true: 'italic',
|
||||
},
|
||||
highlight: {
|
||||
true: 'bg-yellow-500',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const badgeIconStyle = tva({
|
||||
base: 'fill-none',
|
||||
parentVariants: {
|
||||
action: {
|
||||
error: 'text-error-600',
|
||||
warning: 'text-warning-600',
|
||||
success: 'text-success-600',
|
||||
info: 'text-info-600',
|
||||
muted: 'text-background-800',
|
||||
},
|
||||
size: {
|
||||
sm: 'h-3 w-3',
|
||||
md: 'h-3.5 w-3.5',
|
||||
lg: 'h-4 w-4',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const ContextView = withStyleContext(View, SCOPE);
|
||||
|
||||
cssInterop(PrimitiveIcon, {
|
||||
className: {
|
||||
target: 'style',
|
||||
nativeStyleToProp: {
|
||||
height: true,
|
||||
width: true,
|
||||
fill: true,
|
||||
color: 'classNameColor',
|
||||
stroke: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
type IBadgeProps = React.ComponentPropsWithoutRef<typeof ContextView> &
|
||||
VariantProps<typeof badgeStyle>;
|
||||
function Badge({
|
||||
children,
|
||||
action = 'muted',
|
||||
variant = 'solid',
|
||||
size = 'md',
|
||||
className,
|
||||
...props
|
||||
}: { className?: string } & IBadgeProps) {
|
||||
|
||||
const contextValue = useMemo(
|
||||
() => ({ action, variant, size }),
|
||||
[action, variant, size]
|
||||
);
|
||||
|
||||
return (
|
||||
<ContextView
|
||||
className={badgeStyle({ action, variant, class: className })}
|
||||
{...props}
|
||||
context={contextValue}
|
||||
>
|
||||
{children}
|
||||
</ContextView>
|
||||
);
|
||||
}
|
||||
|
||||
type IBadgeTextProps = React.ComponentPropsWithoutRef<typeof Text> &
|
||||
VariantProps<typeof badgeTextStyle>;
|
||||
|
||||
const BadgeText = React.forwardRef<
|
||||
React.ComponentRef<typeof Text>,
|
||||
IBadgeTextProps
|
||||
>(function BadgeText({ children, className, size, ...props }, ref) {
|
||||
const { size: parentSize, action: parentAction } = useStyleContext(SCOPE);
|
||||
return (
|
||||
<Text
|
||||
ref={ref}
|
||||
className={badgeTextStyle({
|
||||
parentVariants: {
|
||||
size: parentSize,
|
||||
action: parentAction,
|
||||
},
|
||||
size,
|
||||
class: className,
|
||||
})}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Text>
|
||||
);
|
||||
});
|
||||
|
||||
type IBadgeIconProps = React.ComponentPropsWithoutRef<typeof PrimitiveIcon> &
|
||||
VariantProps<typeof badgeIconStyle>;
|
||||
|
||||
const BadgeIcon = React.forwardRef<
|
||||
React.ComponentRef<typeof Svg>,
|
||||
IBadgeIconProps
|
||||
>(function BadgeIcon({ className, size, ...props }, ref) {
|
||||
const { size: parentSize, action: parentAction } = useStyleContext(SCOPE);
|
||||
|
||||
if (typeof size === 'number') {
|
||||
return (
|
||||
<UIIcon
|
||||
ref={ref}
|
||||
{...props}
|
||||
className={badgeIconStyle({ class: className })}
|
||||
size={size}
|
||||
/>
|
||||
);
|
||||
} else if (
|
||||
(props?.height !== undefined || props?.width !== undefined) &&
|
||||
size === undefined
|
||||
) {
|
||||
return (
|
||||
<UIIcon
|
||||
ref={ref}
|
||||
{...props}
|
||||
className={badgeIconStyle({ class: className })}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<UIIcon
|
||||
className={badgeIconStyle({
|
||||
parentVariants: {
|
||||
size: parentSize,
|
||||
action: parentAction,
|
||||
},
|
||||
size,
|
||||
class: className,
|
||||
})}
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
Badge.displayName = 'Badge';
|
||||
BadgeText.displayName = 'BadgeText';
|
||||
BadgeIcon.displayName = 'BadgeIcon';
|
||||
|
||||
export { Badge, BadgeIcon, BadgeText };
|
||||
12
utils/format-iso-date.ts
Normal file
12
utils/format-iso-date.ts
Normal file
@ -0,0 +1,12 @@
|
||||
const formatISODate = (isoDateString: string): string => {
|
||||
const date = new Date(isoDateString);
|
||||
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, "0");
|
||||
const day = String(date.getDate()).padStart(2, "0");
|
||||
const hours = String(date.getHours()).padStart(2, "0");
|
||||
const minutes = String(date.getMinutes()).padStart(2, "0");
|
||||
|
||||
return `${year}.${month}.${day} ${hours}:${minutes}`;
|
||||
}
|
||||
export default formatISODate;
|
||||
12
utils/get-category-options.ts
Normal file
12
utils/get-category-options.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { CategoryResponse } from "@/api/types";
|
||||
|
||||
const getCategoryOptions = (categories: CategoryResponse[]) => {
|
||||
return categories.map((cat) => {
|
||||
return {
|
||||
label: cat.name,
|
||||
value: String(cat.id),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export default getCategoryOptions;
|
||||
Loading…
x
Reference in New Issue
Block a user