diff --git a/frontend/.gitignore b/frontend/.gitignore index 4a7f73a..5379696 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -10,7 +10,7 @@ dist node_modules # Logs -logs +.logs *.log # Misc diff --git a/frontend/components/global/CategoryCard.vue b/frontend/components/global/CategoryCard.vue index 3e26b2c..7f7176d 100644 --- a/frontend/components/global/CategoryCard.vue +++ b/frontend/components/global/CategoryCard.vue @@ -47,7 +47,7 @@ const { colorObject } = useImageColor(`#category-image-${id.value}`); class="flex flex-col gap-2" >
- تمام دسته ها + {{ category }} 24 diff --git a/frontend/components/global/ComboBox.vue b/frontend/components/global/ComboBox.vue index 176f192..0fbcd4f 100644 --- a/frontend/components/global/ComboBox.vue +++ b/frontend/components/global/ComboBox.vue @@ -46,7 +46,7 @@ watch( diff --git a/frontend/components/product/ChatBox/ChatInput.vue b/frontend/components/product/ChatBox/ChatInput.vue index 206b075..69cfea4 100644 --- a/frontend/components/product/ChatBox/ChatInput.vue +++ b/frontend/components/product/ChatBox/ChatInput.vue @@ -4,6 +4,7 @@ import AiLoading from "~/components/product/ChatBox/AiLoading.vue"; import useCreateChatMessage from "~/composables/api/chat/useCreateChatMessage"; +import { useToast } from "~/composables/global/useToast"; // state diff --git a/frontend/composables/api/auth/useAuth.ts b/frontend/composables/api/auth/useAuth.ts index 2fa8493..e74d967 100644 --- a/frontend/composables/api/auth/useAuth.ts +++ b/frontend/composables/api/auth/useAuth.ts @@ -3,6 +3,7 @@ export const useAuth = () => { // state const token = useCookie("token"); + const refreshToken = useCookie("refresh-token"); // method @@ -10,11 +11,20 @@ export const useAuth = () => { token.value = newToken; }; + const updateRefreshToken = (newToken: string) => { + refreshToken.value = newToken; + }; + const logout = (reload ?: boolean) => { token.value = undefined; + refreshToken.value = undefined; if (reload) window.location.reload(); }; - return { token, updateToken, logout }; + // computed + + const isLoggedIn = computed(() => !!token.value); + + return { token, refreshToken, updateRefreshToken, updateToken, logout, isLoggedIn }; }; \ No newline at end of file diff --git a/frontend/composables/api/auth/useRefreshAuth.ts b/frontend/composables/api/auth/useRefreshAuth.ts new file mode 100644 index 0000000..6540427 --- /dev/null +++ b/frontend/composables/api/auth/useRefreshAuth.ts @@ -0,0 +1,36 @@ +// imports + +import { useMutation } from "@tanstack/vue-query"; +import { API_ENDPOINTS } from "~/constants"; + +// types + +export type RefreshAuthRequest = { + refresh: string, +}; + +export type RefreshAuthResponse = { + access: string, + refresh: string, +}; + + +const useRefreshAuth = () => { + + // state + + const { $axios: axios } = useNuxtApp(); + + // methods + + const handleRefreshAuth = async (variables: RefreshAuthRequest) => { + const { data } = await axios.post(`${API_ENDPOINTS.auth.refresh}/`, variables); + return data; + }; + + return useMutation({ + mutationFn: (variables: RefreshAuthRequest) => handleRefreshAuth(variables) + }); +}; + +export default useRefreshAuth; diff --git a/frontend/composables/api/auth/useVerify.ts b/frontend/composables/api/auth/useVerify.ts new file mode 100644 index 0000000..44c6c6d --- /dev/null +++ b/frontend/composables/api/auth/useVerify.ts @@ -0,0 +1,30 @@ +// imports + +import { useMutation } from "@tanstack/vue-query"; +import { API_ENDPOINTS } from "~/constants"; + +// types + +export type VerifyRequest = { + token: string, +}; + +const useVerify = () => { + + // state + + const { $axios: axios } = useNuxtApp(); + + // methods + + const handleVerify = async (variables: VerifyRequest) => { + const { data } = await axios.post(`${API_ENDPOINTS.auth.verify}`, variables); + return data; + }; + + return useMutation({ + mutationFn: (variables: VerifyRequest) => handleVerify(variables) + }); +}; + +export default useVerify; diff --git a/frontend/composables/api/chat/useGetChat.ts b/frontend/composables/api/chat/useGetChat.ts index 32a6da1..8209937 100644 --- a/frontend/composables/api/chat/useGetChat.ts +++ b/frontend/composables/api/chat/useGetChat.ts @@ -2,6 +2,7 @@ import { useInfiniteQuery } from "@tanstack/vue-query"; import { API_ENDPOINTS, QUERY_KEYS } from "~/constants"; +import { useAuth } from "~/composables/api/auth/useAuth"; // types @@ -16,6 +17,8 @@ const useGetBranch = ( const { $axios: axios } = useNuxtApp(); + const { isLoggedIn } = useAuth(); + // method const handleGetChat = async ({ productId, limit, offset }: { @@ -37,7 +40,7 @@ const useGetBranch = ( }; return useInfiniteQuery({ - enabled, + enabled: isLoggedIn, queryKey: [QUERY_KEYS.chat], initialPageParam: { limit: 10, diff --git a/frontend/composables/api/product/useGetCategories.ts b/frontend/composables/api/product/useGetCategories.ts new file mode 100644 index 0000000..7170bc2 --- /dev/null +++ b/frontend/composables/api/product/useGetCategories.ts @@ -0,0 +1,29 @@ +// imports + +import { useQuery } from "@tanstack/vue-query"; +import { API_ENDPOINTS, QUERY_KEYS } from "~/constants"; + +// types + +export type GetCategoriesResponse = { categories: Category[] }; + +const useGetCategories = () => { + + // state + + const { $axios: axios } = useNuxtApp(); + + // methods + + const handleGetCategories = async () => { + const { data } = await axios.get(`${API_ENDPOINTS.products.categories}`); + return data.categories; + }; + + return useQuery({ + queryKey: [QUERY_KEYS.categories], + queryFn: () => handleGetCategories() + }); +}; + +export default useGetCategories; diff --git a/frontend/composables/api/products/useGetProducts.ts b/frontend/composables/api/products/useGetProducts.ts index 7828df4..adfa9a1 100644 --- a/frontend/composables/api/products/useGetProducts.ts +++ b/frontend/composables/api/products/useGetProducts.ts @@ -5,44 +5,53 @@ import { API_ENDPOINTS, QUERY_KEYS } from "~/constants"; // types -export type GetProductsResponse = Product[]; +export type GetProductsResponse = ApiPaginated; export type GetProductsFilters = { - search: string | undefined; - sort: string | undefined; - categories: string[] | undefined; - price_range: number[] | undefined; - has_discount: boolean | undefined; - in_stock: boolean | undefined; + search?: string | undefined; + sort?: string | undefined; + category?: string | undefined; + price_gte: number; + price_lte: number; + has_discount?: boolean | undefined; + in_stock?: boolean | undefined; + page: number; }; // composable -const useGetProducts = ( - filters: GetProductsFilters, - page: ComputedRef -) => { +const useGetProducts = (params?: GetProductsFilters) => { // state const { $axios: axios } = useNuxtApp(); + const { + search, + sort, + in_stock, + has_discount, + category, + price_gte, + price_lte, + page, + } = toRefs(params as GetProductsFilters); + // methods - const handleGetProducts = async ({ - filters, - page, - }: { - filters: GetProductsFilters; - page: number; - }) => { + const handleGetProducts = async (params?: GetProductsFilters) => { const { data } = await axios.get( `${API_ENDPOINTS.products.get_all}`, { params: { - ...filters, - page, - offest: page * 10 - 10, - limit: 10, + sort: params?.sort, + in_stock: params?.in_stock, + search: params?.search, + has_discount: params?.has_discount, + category: params?.category, + price_gte: params?.price_gte, + price_lte: params?.price_lte, + offest: params?.page! * 9 - 9, + limit: 9, }, } ); @@ -52,8 +61,18 @@ const useGetProducts = ( return useQuery({ staleTime: 60 * 1000, - queryKey: [QUERY_KEYS.products, filters, page], - queryFn: () => handleGetProducts({ filters, page: page.value }), + queryKey: [ + QUERY_KEYS.products, + search, + sort, + in_stock, + has_discount, + category, + price_gte, + price_lte, + page, + ], + queryFn: () => handleGetProducts(params), }); }; diff --git a/frontend/constants/index.ts b/frontend/constants/index.ts index 7a3cd9f..0049a18 100644 --- a/frontend/constants/index.ts +++ b/frontend/constants/index.ts @@ -7,6 +7,8 @@ export const API_ENDPOINTS = { get: "/products", }, auth: { + refresh: "/token/refresh", + verify: "/accounts/verify", signin: "/token", logout: "/accounts/logout", }, @@ -16,6 +18,7 @@ export const API_ENDPOINTS = { }, products: { get_all: "/products", + categories: "/products/categories", }, }; @@ -24,6 +27,7 @@ export const QUERY_KEYS = { product: "product", products: "products", account: "account", + categories: "categories", }; export const MUTATION_KEYS = { diff --git a/frontend/layouts/Default.vue b/frontend/layouts/Default.vue index cb94ffa..3d5c39c 100644 --- a/frontend/layouts/Default.vue +++ b/frontend/layouts/Default.vue @@ -1,3 +1,70 @@ + + \ No newline at end of file diff --git a/frontend/pages/products.vue b/frontend/pages/products.vue index 443a4c2..4561e8b 100644 --- a/frontend/pages/products.vue +++ b/frontend/pages/products.vue @@ -8,31 +8,48 @@ import { PRODUCT_RANGE } from "~/constants"; // state -const route = useRoute(); +const params: GetProductsFilters = useUrlSearchParams("history", { + initialValue: { + search: "", + sort: "newest", + price_gte: PRODUCT_RANGE.min, + price_lte: PRODUCT_RANGE.max, + in_stock: false, + has_discount: false, + category: "", + page: "1", + }, +}); -const params: GetProductsFilters & { page: number } = - useUrlSearchParams("history"); +const search = ref(params.search ?? ""); +const searchDebounced = refDebounced(search, 1000); -// computed +// provide / inject -const page = computed(() => (route.query["page"] ? +route.query["page"] : 1)); +provide("params", params); // queries -const { data: products, isLoading: productsIsLoading } = useGetProducts( - params, - page -); +const { data, isLoading: productsIsLoading } = useGetProducts(params); -// life-cycle - -onMounted(() => { - if (!("range" in params)) { - params.range = []; - params.range[0] = PRODUCT_RANGE.min; - params.range[1] = PRODUCT_RANGE.max; - } +const products = computed(() => { + return data.value?.results.flat(); }); + +const paginationData = computed(() => { + return data!.value?.results.map((_, i: number) => { + return { type: "page", value: i }; + }); +}); + +// watch + +watch( + () => searchDebounced.value, + (newValue) => { + params.search = newValue; + } +); diff --git a/frontend/pages/signin/index.vue b/frontend/pages/signin/index.vue index b201195..ada01c8 100644 --- a/frontend/pages/signin/index.vue +++ b/frontend/pages/signin/index.vue @@ -28,11 +28,10 @@ definePageMeta({ const { addToast } = useToast(); -const { updateToken } = useAuth(); +const { updateToken, updateRefreshToken } = useAuth(); const { refetch: refetchAccount } = useGetAccount(); - const showOtp = ref(false); const otpCode = ref([]); @@ -107,6 +106,8 @@ const handleLogin = async () => { }); updateToken(response.access); + updateRefreshToken(response.refresh); + await new Promise(resolve => setTimeout(resolve, 1000)); await refetchAccount(); addToast({ @@ -116,9 +117,7 @@ const handleLogin = async () => { } }); - setTimeout(() => { - navigateTo("/"); - }, 2000); + navigateTo("/"); } catch (e) { otpCode.value = []; addToast({ message: "مشکلی پیش آمده" }); diff --git a/frontend/plugins/axios.ts b/frontend/plugins/axios.ts index 2a63f37..16ac61c 100644 --- a/frontend/plugins/axios.ts +++ b/frontend/plugins/axios.ts @@ -4,10 +4,10 @@ import { API_ENDPOINTS } from "~/constants"; export default defineNuxtPlugin(() => { const config = useRuntimeConfig(); - const { token } = useAuth(); + const { token, logout } = useAuth(); const axios = axiosOriginal.create({ - baseURL: config.public.API_BASE_URL, + baseURL: config.public.API_BASE_URL }); axios.interceptors.request.use((config) => { @@ -25,9 +25,9 @@ export default defineNuxtPlugin(() => { return response; }, function(error) { - if (error.status === 401) { - logout(); - } + // if (error.status === 401) { + // logout(); + // } return Promise.reject(error); }); diff --git a/frontend/plugins/skeleton.client.ts b/frontend/plugins/skeleton.client.ts new file mode 100644 index 0000000..998e6b3 --- /dev/null +++ b/frontend/plugins/skeleton.client.ts @@ -0,0 +1,6 @@ +import { Skeletor } from 'vue-skeletor'; +import 'vue-skeletor/dist/vue-skeletor.css'; + +export default defineNuxtPlugin((nuxtApp) => { + nuxtApp.vueApp.component("Skeleton", Skeletor) +}) \ No newline at end of file diff --git a/frontend/swagger.yaml b/frontend/swagger.yaml new file mode 100644 index 0000000..ccd8b14 --- /dev/null +++ b/frontend/swagger.yaml @@ -0,0 +1,1213 @@ +openapi: 3.0.3 +info: + title: '' + version: 1.0.0 +paths: + /accounts/address/{id}: + get: + operationId: accounts_address_retrieve + parameters: + - in: path + name: id + schema: + type: integer + required: true + tags: + - accounts + security: + - jwtAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/UserAddress' + description: '' + /accounts/address/create: + post: + operationId: accounts_address_create_create + tags: + - accounts + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/UserAddressRequest' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/UserAddressRequest' + multipart/form-data: + schema: + $ref: '#/components/schemas/UserAddressRequest' + required: true + security: + - jwtAuth: [] + responses: + '201': + content: + application/json: + schema: + $ref: '#/components/schemas/UserAddress' + description: '' + /accounts/address/delete/{id}: + delete: + operationId: accounts_address_delete_destroy + parameters: + - in: path + name: id + schema: + type: integer + required: true + tags: + - accounts + security: + - jwtAuth: [] + responses: + '204': + description: No response body + /accounts/address/edit/{id}: + put: + operationId: accounts_address_edit_update + parameters: + - in: path + name: id + schema: + type: integer + required: true + tags: + - accounts + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/UserAddressRequest' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/UserAddressRequest' + multipart/form-data: + schema: + $ref: '#/components/schemas/UserAddressRequest' + required: true + security: + - jwtAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/UserAddress' + description: '' + patch: + operationId: accounts_address_edit_partial_update + parameters: + - in: path + name: id + schema: + type: integer + required: true + tags: + - accounts + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedUserAddressRequest' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedUserAddressRequest' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedUserAddressRequest' + security: + - jwtAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/UserAddress' + description: '' + /accounts/address/list: + get: + operationId: accounts_address_list_list + tags: + - accounts + security: + - jwtAuth: [] + responses: + '200': + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/UserAddress' + description: '' + /accounts/profile: + get: + operationId: accounts_profile_retrieve + tags: + - accounts + security: + - jwtAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Profile' + description: '' + patch: + operationId: accounts_profile_partial_update + tags: + - accounts + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedProfileRequest' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedProfileRequest' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedProfileRequest' + security: + - jwtAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Profile' + description: '' + /accounts/send_otp: + post: + operationId: accounts_send_otp_create + tags: + - Authentication + requestBody: + content: + application/json: + schema: + type: object + properties: + phone: + type: string + example: 09123456789 + required: + - phone + security: + - jwtAuth: [] + - {} + responses: + '200': + description: No response body + /accounts/verify: + post: + operationId: accounts_verify_create + description: |- + Takes a token and indicates if it is valid. This view provides no + information about a token's fitness for a particular use. + tags: + - accounts + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/TokenVerifyRequest' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/TokenVerifyRequest' + multipart/form-data: + schema: + $ref: '#/components/schemas/TokenVerifyRequest' + required: true + responses: + '200': + description: No response body + /chat/product/{id}: + get: + operationId: chat_product_retrieve + description: Retrieve all messages for a product chat. + summary: Retrieve messages for a product chat + parameters: + - in: path + name: id + schema: + type: integer + required: true + - in: query + name: limit + schema: + type: integer + default: 10 + description: Number of results to return per page. + - in: query + name: offset + schema: + type: integer + default: 0 + description: The starting position of the results. + tags: + - chat + security: + - jwtAuth: [] + responses: + '200': + content: + application/json: + schema: + type: object + additionalProperties: {} + description: Unspecified response body + description: '' + post: + operationId: chat_product_create + description: Send a new message in the product chat. + summary: Send a new message in the product chat + parameters: + - in: path + name: id + schema: + type: integer + required: true + tags: + - chat + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/NewMessageRequest' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/NewMessageRequest' + multipart/form-data: + schema: + $ref: '#/components/schemas/NewMessageRequest' + required: true + security: + - jwtAuth: [] + responses: + '200': + content: + application/json: + schema: + type: object + additionalProperties: {} + description: Unspecified response body + description: '' + '400': + content: + application/json: + schema: + type: object + additionalProperties: {} + description: Unspecified response body + description: '' + /products/: + get: + operationId: products_list + description: Retrieve products with optional filters and sorting. Provide a + list of category IDs to filter products by those categories and their subcategories. + parameters: + - in: query + name: category + schema: + type: array + items: + type: number + explode: false + style: form + - in: query + name: has_discount + schema: + type: boolean + description: Filter products that have a discount. + - in: query + name: in_stock + schema: + type: boolean + description: Filter products that are in stock (positive stock). + - in: query + name: limit + schema: + type: integer + description: Number of results to return per page (pagination). + - in: query + name: offset + schema: + type: integer + description: The starting position of the results (pagination). + - in: query + name: price_gte + schema: + type: number + format: float + description: Filter products with price greater than or equal to this value. + - in: query + name: price_lte + schema: + type: number + format: float + description: Filter products with price less than or equal to this value. + - in: query + name: search + schema: + type: string + description: Search by product name or description. + - in: query + name: sort + schema: + type: string + description: |- + Sort results by one of the following fields: + `name`, `-name`, `price`, `-price`, `discount`, `-discount`, `created_at`, `-created_at`. + Prefix with `-` for descending order. + tags: + - products + security: + - {} + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PaginatedProductList' + description: '' + '404': + content: + application/json: + schema: + type: object + additionalProperties: {} + description: '' + /products/{id}: + get: + operationId: products_retrieve + parameters: + - in: path + name: id + schema: + type: integer + required: true + tags: + - products + security: + - {} + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Product' + description: '' + /products/{id}/comments: + get: + operationId: products_comments_retrieve_2 + parameters: + - in: path + name: id + schema: + type: integer + required: true + tags: + - products + security: + - jwtAuth: [] + - {} + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Comment' + description: '' + post: + operationId: products_comments_create_2 + parameters: + - in: path + name: id + schema: + type: integer + required: true + tags: + - products + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/CommentRequest' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/CommentRequest' + multipart/form-data: + schema: + $ref: '#/components/schemas/CommentRequest' + required: true + security: + - jwtAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Comment' + description: '' + delete: + operationId: products_comments_destroy_2 + parameters: + - in: path + name: id + schema: + type: integer + required: true + tags: + - products + security: + - jwtAuth: [] + responses: + '204': + description: No response body + /products/categories: + get: + operationId: products_categories_list + parameters: + - in: query + name: search + schema: + type: string + description: Search by category name or description. + tags: + - products + security: + - {} + responses: + '200': + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Category' + description: '' + '404': + content: + application/json: + schema: + type: object + additionalProperties: {} + description: '' + /products/comments/{id}: + get: + operationId: products_comments_retrieve + parameters: + - in: path + name: id + schema: + type: integer + required: true + tags: + - products + security: + - jwtAuth: [] + - {} + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Comment' + description: '' + post: + operationId: products_comments_create + parameters: + - in: path + name: id + schema: + type: integer + required: true + tags: + - products + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/CommentRequest' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/CommentRequest' + multipart/form-data: + schema: + $ref: '#/components/schemas/CommentRequest' + required: true + security: + - jwtAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Comment' + description: '' + delete: + operationId: products_comments_destroy + parameters: + - in: path + name: id + schema: + type: integer + required: true + tags: + - products + security: + - jwtAuth: [] + responses: + '204': + description: No response body + /tickets/: + get: + operationId: tickets_list + tags: + - tickets + security: + - jwtAuth: [] + responses: + '200': + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Ticket' + description: '' + /tickets/{id}: + get: + operationId: tickets_retrieve + parameters: + - in: path + name: id + schema: + type: integer + required: true + tags: + - tickets + security: + - jwtAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Ticket' + description: '' + /tickets/{id}/messages: + post: + operationId: tickets_messages_create + parameters: + - in: path + name: id + schema: + type: integer + required: true + tags: + - tickets + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/MessageRequest' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/MessageRequest' + multipart/form-data: + schema: + $ref: '#/components/schemas/MessageRequest' + required: true + security: + - jwtAuth: [] + responses: + '201': + content: + application/json: + schema: + $ref: '#/components/schemas/Message' + description: '' + /tickets/{id}/update-status: + post: + operationId: tickets_update_status_create + parameters: + - in: path + name: id + schema: + type: integer + required: true + tags: + - tickets + security: + - jwtAuth: [] + responses: + '200': + description: No response body + /tickets/create: + post: + operationId: tickets_create_create + tags: + - tickets + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/TicketRequest' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/TicketRequest' + multipart/form-data: + schema: + $ref: '#/components/schemas/TicketRequest' + required: true + security: + - jwtAuth: [] + responses: + '201': + content: + application/json: + schema: + $ref: '#/components/schemas/Ticket' + description: '' + /token/: + post: + operationId: token_create + description: |- + Takes a set of user credentials and returns an access and refresh JSON web + token pair to prove the authentication of those credentials. + tags: + - Authentication + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/CustomTokenObtainPairRequest' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/CustomTokenObtainPairRequest' + multipart/form-data: + schema: + $ref: '#/components/schemas/CustomTokenObtainPairRequest' + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/CustomTokenObtainPair' + description: '' + /token/refresh/: + post: + operationId: token_refresh_create + description: |- + Takes a refresh type JSON web token and returns an access type JSON web + token if the refresh token is valid. + tags: + - token + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/TokenRefreshRequest' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/TokenRefreshRequest' + multipart/form-data: + schema: + $ref: '#/components/schemas/TokenRefreshRequest' + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/TokenRefresh' + description: '' +components: + schemas: + Category: + type: object + properties: + id: + type: integer + readOnly: true + name: + type: string + title: نام دسته‌بندی + maxLength: 50 + slug: + type: string + description: اسم دسته را برای مسیر به انگلیسی و بدون فاصله وارد کنید + maxLength: 50 + pattern: ^[-a-zA-Z0-9_]+$ + icon: + type: string + nullable: true + title: آیکون دسته‌بندی + maxLength: 100 + meta_title: + type: string + nullable: true + title: عنوان متا + description: عنوان متا برای SEO + maxLength: 60 + meta_description: + type: string + nullable: true + title: توضیحات متا + description: توضیحات متا برای SEO + maxLength: 160 + parent: + type: integer + nullable: true + title: دسته‌بندی والد + children: + type: string + readOnly: true + required: + - children + - id + - name + - slug + Comment: + type: object + properties: + id: + type: integer + readOnly: true + content: + type: string + title: محتوای نظر + timestamp: + type: string + format: date-time + readOnly: true + title: زمان ثبت کامنت + show: + type: boolean + readOnly: true + title: نشان دادن کامنت + product: + type: integer + readOnly: true + title: محصول + user: + type: integer + required: + - content + - id + - product + - show + - timestamp + - user + CommentRequest: + type: object + properties: + content: + type: string + minLength: 1 + title: محتوای نظر + user: + type: integer + required: + - content + - user + CurrencyEnum: + enum: + - dollor + - toman + - derham + type: string + description: |- + * `dollor` - دلار + * `toman` - تومان + * `derham` - درهم + CustomTokenObtainPair: + type: object + properties: + otp: + type: string + CustomTokenObtainPairRequest: + type: object + properties: + otp: + type: string + minLength: 1 + phone: + type: string + writeOnly: true + minLength: 1 + required: + - phone + Message: + type: object + properties: + id: + type: integer + readOnly: true + content: + type: string + created_at: + type: string + format: date-time + readOnly: true + ticket: + type: integer + sender: + type: integer + required: + - content + - created_at + - id + - sender + - ticket + MessageRequest: + type: object + properties: + content: + type: string + minLength: 1 + ticket: + type: integer + sender: + type: integer + required: + - content + - sender + - ticket + NewMessageRequest: + type: object + properties: + new_message: + type: string + minLength: 1 + description: The message content to send to the chat. + required: + - new_message + PaginatedProductList: + type: object + required: + - count + - results + properties: + count: + type: integer + example: 123 + next: + type: string + nullable: true + format: uri + example: http://api.example.org/accounts/?offset=400&limit=100 + previous: + type: string + nullable: true + format: uri + example: http://api.example.org/accounts/?offset=200&limit=100 + results: + type: array + items: + $ref: '#/components/schemas/Product' + PatchedProfileRequest: + type: object + properties: + first_name: + type: string + title: نام + maxLength: 50 + last_name: + type: string + title: نام خانوادگی + maxLength: 50 + email: + type: string + format: email + nullable: true + title: ایمیل + maxLength: 255 + profile_photo: + type: string + format: binary + nullable: true + title: عکس پروفایل + PatchedUserAddressRequest: + type: object + properties: + name: + type: string + minLength: 1 + maxLength: 30 + address: + type: string + minLength: 1 + postal_code: + type: string + minLength: 1 + maxLength: 10 + phone: + type: string + minLength: 1 + maxLength: 11 + Product: + type: object + properties: + id: + type: integer + readOnly: true + price: + type: string + readOnly: true + is_new: + type: string + readOnly: true + name: + type: string + maxLength: 255 + description: + type: string + currency: + allOf: + - $ref: '#/components/schemas/CurrencyEnum' + title: نوع ارز + image: + type: string + format: uri + rating: + type: integer + maximum: 2147483647 + minimum: 0 + view: + type: integer + maximum: 2147483647 + minimum: -2147483648 + title: بازدید + sell: + type: integer + maximum: 2147483647 + minimum: -2147483648 + title: فروش + in_stock: + type: integer + maximum: 2147483647 + minimum: -2147483648 + title: تعداد موجود + discount: + type: integer + maximum: 32767 + minimum: -32768 + title: تخفیف + slug: + type: string + nullable: true + title: نام یکتا + description: این فیلد را خالی بگذارید + pattern: ^[-\w]+$ + maxLength: 50 + meta_description: + type: string + nullable: true + description: این فیلد را حتما پر کنید + maxLength: 300 + meta_keywords: + type: string + nullable: true + description: این فیلد را حتما پر کنید + maxLength: 300 + meta_rating: + type: number + format: double + description: امتیاز محصول + created_at: + type: string + format: date-time + readOnly: true + title: زمان ثبت محصول + category: + type: integer + nullable: true + required: + - created_at + - currency + - description + - id + - image + - is_new + - name + - price + Profile: + type: object + properties: + first_name: + type: string + title: نام + maxLength: 50 + last_name: + type: string + title: نام خانوادگی + maxLength: 50 + email: + type: string + format: email + nullable: true + title: ایمیل + maxLength: 255 + profile_photo: + type: string + format: uri + nullable: true + title: عکس پروفایل + phone: + type: string + readOnly: true + title: شماره تماس + required: + - phone + StatusEnum: + enum: + - open + - in_progress + - resolved + - closed + type: string + description: |- + * `open` - Open + * `in_progress` - In Progress + * `resolved` - Resolved + * `closed` - Closed + Ticket: + type: object + properties: + id: + type: integer + readOnly: true + messages: + type: array + items: + $ref: '#/components/schemas/Message' + readOnly: true + subject: + type: string + maxLength: 255 + status: + $ref: '#/components/schemas/StatusEnum' + created_at: + type: string + format: date-time + readOnly: true + updated_at: + type: string + format: date-time + readOnly: true + customer: + type: integer + admin: + type: integer + nullable: true + required: + - created_at + - customer + - id + - messages + - subject + - updated_at + TicketRequest: + type: object + properties: + subject: + type: string + minLength: 1 + maxLength: 255 + status: + $ref: '#/components/schemas/StatusEnum' + customer: + type: integer + admin: + type: integer + nullable: true + required: + - customer + - subject + TokenRefresh: + type: object + properties: + access: + type: string + readOnly: true + refresh: + type: string + required: + - access + - refresh + TokenRefreshRequest: + type: object + properties: + refresh: + type: string + minLength: 1 + required: + - refresh + TokenVerifyRequest: + type: object + properties: + token: + type: string + writeOnly: true + minLength: 1 + required: + - token + UserAddress: + type: object + properties: + id: + type: integer + readOnly: true + name: + type: string + maxLength: 30 + address: + type: string + postal_code: + type: string + maxLength: 10 + phone: + type: string + maxLength: 11 + required: + - address + - id + - name + - phone + - postal_code + UserAddressRequest: + type: object + properties: + name: + type: string + minLength: 1 + maxLength: 30 + address: + type: string + minLength: 1 + postal_code: + type: string + minLength: 1 + maxLength: 10 + phone: + type: string + minLength: 1 + maxLength: 11 + required: + - address + - name + - phone + - postal_code + securitySchemes: + jwtAuth: + type: http + scheme: bearer + bearerFormat: JWT diff --git a/frontend/tools/logger.ts b/frontend/tools/logger.ts new file mode 100644 index 0000000..95a6f83 --- /dev/null +++ b/frontend/tools/logger.ts @@ -0,0 +1,77 @@ +import fs from "fs/promises"; + +type LogType = { + title: string; + status?: "success" | "error" | "info" | "warning"; + message?: string, + details?: any +} + +class Logger { + private static formatToMarkdown(log: LogType) { + const date = new Date(); + let month = "" + (date.getMonth() + 1); + let day = "" + date.getDate(); + let year = date.getFullYear(); + let hour = date.getHours(); + let minutes = date.getMinutes(); + let seconds = date.getSeconds(); + + if (month.length < 2) { + month = "0" + month; + } + + if (day.length < 2) { + day = "0" + day; + } + + let markdownContent = ""; + + let icon = "ℹ️"; + + switch (log.status) { + case "info": + icon = "ℹ️"; + break; + case "error": + icon = "‼️"; + break; + case "warning": + icon = "⚠️"; + break; + case "success": + icon = "✅"; + break; + default : + icon = "ℹ️"; + break; + } + + markdownContent += `# ${icon} ${log.title} \n`; + markdownContent += `## ${[year, month, day].join("-")} ${hour}:${minutes}:${seconds} \n`; + + if (log.message) { + markdownContent += `**Message:** ${log.message}\n`; + } + if (log.details) { + markdownContent += `**Details:**\n\n\`\`\`json\n${JSON.stringify(log.details, null, 2)}\n\`\`\`\n\n`; + } + + markdownContent += "

\n\n"; + markdownContent += "---\n"; + + return markdownContent; + } + + public static async log(info: LogType) { + const formattedLog = this.formatToMarkdown(info); + + try { + await fs.appendFile(".logs/log.md", formattedLog); + } catch (e) { + console.error(e); + } + } +} + +export default Logger; \ No newline at end of file diff --git a/frontend/types/global.d.ts b/frontend/types/global.d.ts index ac13093..c0e1e48 100644 --- a/frontend/types/global.d.ts +++ b/frontend/types/global.d.ts @@ -32,4 +32,15 @@ declare global { "meta_rating": number | null } + type Category = { + "id": number, + "name": string, + "slug": string, + "icon": string, + "meta_title": string, + "meta_description": string, + "parent": number, + "children": "string" + } + }