Tests page and test element
This commit is contained in:
parent
a5533e914d
commit
59298e1581
@ -1,47 +1,103 @@
|
|||||||
import axiosInstance from "./_axiosInstance";
|
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) => {
|
export const get_questions = async (
|
||||||
const response = await axiosInstance.get<Pagination<QuestionResponse>>("/api/questions/", {
|
page: number,
|
||||||
params: {
|
test_id?: number,
|
||||||
page,
|
category_id?: number
|
||||||
test_id,
|
) => {
|
||||||
category_id
|
const response = await axiosInstance.get<Pagination<QuestionResponse>>(
|
||||||
|
"/api/questions/",
|
||||||
|
{
|
||||||
|
params: {
|
||||||
|
page,
|
||||||
|
test_id,
|
||||||
|
category_id,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
})
|
);
|
||||||
|
|
||||||
return response.data;
|
return response.data;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const get_question = async (id: number) => {
|
export const get_question = async (id: number) => {
|
||||||
const response = await axiosInstance.get<{
|
const response = await axiosInstance.get<{
|
||||||
data: QuestionResponse
|
data: QuestionResponse;
|
||||||
}>(`/api/questions/${id}`);
|
}>(`/api/questions/${id}`);
|
||||||
|
|
||||||
return response.data;
|
return response.data;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const get_current_user = async () => {
|
export const get_current_user = async () => {
|
||||||
const response = await axiosInstance.get<{
|
const response = await axiosInstance.get<{
|
||||||
user: UserResponse
|
user: UserResponse;
|
||||||
}>("/api/auth/me/");
|
}>("/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;
|
return response.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const get_categories = async () => {
|
export const get_user_tests = async () => {
|
||||||
const response = await axiosInstance.get<{
|
const response = await axiosInstance.get<{
|
||||||
data: CategoryResponse[]
|
data: UserTestResponse[];
|
||||||
}>("/api/categories/");
|
}>("/api/user-tests/");
|
||||||
|
|
||||||
return response.data;
|
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) => {
|
export const post_login = async (email: string, password: string) => {
|
||||||
const response = await axiosInstance.post<AuthLoginResponse>("/api/auth/login/", {
|
const response = await axiosInstance.post<AuthLoginResponse>(
|
||||||
email,
|
"/api/auth/login/",
|
||||||
password
|
{
|
||||||
});
|
email,
|
||||||
|
password,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return response.data;
|
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;
|
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 {
|
export enum UserTypes {
|
||||||
Admin = "admin",
|
Admin = "admin",
|
||||||
Creator = "creator",
|
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}
|
key={question.id}
|
||||||
question={question}
|
question={question}
|
||||||
onPress={() => router.push(`/questions/${question.id}`)}
|
onPress={() => router.push(`/questions/${question.id}`)}
|
||||||
|
withCategory={true}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</ParallaxScrollView>
|
</ParallaxScrollView>
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import Content from "@/components/ui/content";
|
|||||||
import CustomSelect from "@/components/ui/custom-select";
|
import CustomSelect from "@/components/ui/custom-select";
|
||||||
import PaginationList from "@/components/ui/pagination-list";
|
import PaginationList from "@/components/ui/pagination-list";
|
||||||
import { ThemedText } from "@/components/ui/themed-text";
|
import { ThemedText } from "@/components/ui/themed-text";
|
||||||
|
import getCategoryOptions from "@/utils/get-category-options";
|
||||||
import { router } from "expo-router";
|
import { router } from "expo-router";
|
||||||
import { useRef, useState } from "react";
|
import { useRef, useState } from "react";
|
||||||
|
|
||||||
@ -21,12 +22,8 @@ export default function QuestionsScreen() {
|
|||||||
category_id: category
|
category_id: category
|
||||||
});
|
});
|
||||||
|
|
||||||
const categoryOptions = categories.map((cat) => {
|
const categoryOptions = getCategoryOptions(categories);
|
||||||
return {
|
|
||||||
label: cat.name,
|
|
||||||
value: String(cat.id),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
const handleChangeCategory = (value: string) => {
|
const handleChangeCategory = (value: string) => {
|
||||||
setPage(1);
|
setPage(1);
|
||||||
if(value === "all")
|
if(value === "all")
|
||||||
@ -53,7 +50,7 @@ export default function QuestionsScreen() {
|
|||||||
<PaginationList<QuestionResponse>
|
<PaginationList<QuestionResponse>
|
||||||
pagination={questionsPagination}
|
pagination={questionsPagination}
|
||||||
renderItem={(item) => (
|
renderItem={(item) => (
|
||||||
<Question question={item} onPress={() => router.push(`/questions/${item.id}`)} />
|
<Question question={item} onPress={() => router.push(`/questions/${item.id}`)} withCategory={true} />
|
||||||
)}
|
)}
|
||||||
skeleton={<Question />}
|
skeleton={<Question />}
|
||||||
currentPage={page}
|
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 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 { 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() {
|
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 (
|
return (
|
||||||
<Content>
|
<Content ref={scrollRef}>
|
||||||
<ThemedText type="title" className='mb-3'>Tests</ThemedText>
|
<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>
|
</Content>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -42,7 +42,7 @@ const QuestionScreen = () => {
|
|||||||
<>
|
<>
|
||||||
<Stack.Screen options={{ title: `Question #${id}` }} />
|
<Stack.Screen options={{ title: `Question #${id}` }} />
|
||||||
<Content>
|
<Content>
|
||||||
<Question question={data} withMeta={true} />
|
<Question question={data} withMeta={true} withCategory={true} />
|
||||||
|
|
||||||
<Divider className="mt-5 mb-5" />
|
<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;
|
question?: QuestionResponse;
|
||||||
onPress?: () => void;
|
onPress?: () => void;
|
||||||
withMeta?: boolean;
|
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" />;
|
if (!question) return <Skeleton className="w-full h-20" />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Pressable onPress={onPress}>
|
<Pressable onPress={onPress}>
|
||||||
<Panel>
|
<Panel>
|
||||||
<ThemedText type="meta" className="mb-1">
|
{withCategory && (
|
||||||
{question.category.name}
|
<ThemedText type="meta" className="mb-1">
|
||||||
</ThemedText>
|
{question.category.name}
|
||||||
|
</ThemedText>
|
||||||
|
)}
|
||||||
<ThemedText style={styles.questionTitle}>{question.title}</ThemedText>
|
<ThemedText style={styles.questionTitle}>{question.title}</ThemedText>
|
||||||
<ThemedText>{question.description}</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