diff --git a/frontend/assets/css/tailwind.css b/frontend/assets/css/tailwind.css index 5696404..e1f23fb 100644 --- a/frontend/assets/css/tailwind.css +++ b/frontend/assets/css/tailwind.css @@ -273,9 +273,9 @@ /* CONTAINER */ -* { +/* * { scroll-behavior: smooth !important; -} +} */ @utility container { @apply mx-auto px-[var(--app-container-padding)] w-full max-sm:max-w-[var(--breakpoint-xs)] max-md:max-w-[var(--breakpoint-sm)] max-lg:max-w-[var(--breakpoint-md)] max-xl:max-w-[var(--breakpoint-lg)] max-w-[var(--breakpoint-2xl)]; diff --git a/frontend/components/global/Brands.vue b/frontend/components/global/Brands.vue index d570827..aef0faa 100644 --- a/frontend/components/global/Brands.vue +++ b/frontend/components/global/Brands.vue @@ -29,20 +29,20 @@ const brands = ref([ متون بلکه روزنامه و مجله در ستون و سطرآنچنان که

-
+
-
+
HEYMLZ
@@ -63,7 +63,7 @@ const brands = ref([ >
diff --git a/frontend/components/global/ComboBox.vue b/frontend/components/global/ComboBox.vue index 19622a0..6c06e4a 100644 --- a/frontend/components/global/ComboBox.vue +++ b/frontend/components/global/ComboBox.vue @@ -33,7 +33,7 @@ const emit = defineEmits(["update:modelValue"]); // state -const value = ref(); +const value = ref(); // watch @@ -49,7 +49,10 @@ watch( watch( () => modelValue.value, (newValue) => { - const target = options.value.flatMap((option) => option.children).find((child) => child.id == newValue); + let target; + target = newValue?.toString().startsWith("category") + ? options.value.find((child) => child.slug == newValue) + : options.value.flatMap((option) => option.children).find((child) => child.slug == newValue); value.value = target || undefined; }, diff --git a/frontend/components/global/LoadingOverlay.vue b/frontend/components/global/LoadingOverlay.vue index 4b10c09..6196932 100644 --- a/frontend/components/global/LoadingOverlay.vue +++ b/frontend/components/global/LoadingOverlay.vue @@ -35,14 +35,16 @@ const progressStyle = computed(() => { // }; const onAssetFinished = () => { - gsap.to("#loading-overlay", { - opacity: 0, - onComplete: () => { - shouldRenderLoadingOverlay.value = false; - isWindowScrollLocked.value = false; - isSiteLoadingDisabled.value = true; - }, - }); + if (!isSiteLoadingDisabled.value) { + gsap.to("#loading-overlay", { + opacity: 0, + onComplete: () => { + shouldRenderLoadingOverlay.value = false; + isWindowScrollLocked.value = false; + isSiteLoadingDisabled.value = true; + }, + }); + } }; // watch diff --git a/frontend/components/global/Pagination.vue b/frontend/components/global/Pagination.vue index 90665f1..9283789 100644 --- a/frontend/components/global/Pagination.vue +++ b/frontend/components/global/Pagination.vue @@ -11,6 +11,7 @@ type Props = { type: string; value: number; }[]; + perPage: number; }; // props @@ -21,6 +22,9 @@ defineProps(); const params: any = inject("params"); +const router = useRouter(); +const route = useRoute(); + const { isMobile } = useRatio(); const { y } = useWindowScroll({ behavior: "smooth" }); @@ -28,9 +32,12 @@ const { y } = useWindowScroll({ behavior: "smooth" }); // computed const page = computed({ - get: () => (params?.page ? Number(params.page) : 1), + get: () => (route.query["page"] ? Number(route.query["page"]) : 1), set: (value: number) => { params.page = value; + router.push({ + query: { ...route.query, page: value }, + }); y.value = 0; }, }); @@ -40,24 +47,30 @@ const page = computed({ - برو اول + @@ -67,7 +80,7 @@ const page = computed({ {{ page.value }} @@ -83,19 +96,23 @@ const page = computed({ - برو آخر + diff --git a/frontend/components/global/ProductsGrid.vue b/frontend/components/global/ProductsGrid.vue index 0f8f026..730414a 100644 --- a/frontend/components/global/ProductsGrid.vue +++ b/frontend/components/global/ProductsGrid.vue @@ -34,18 +34,22 @@ withDefaults(defineProps(), {
diff --git a/frontend/components/global/product-detail/Slider.vue b/frontend/components/global/product-detail/Slider.vue index 1cb2b26..273d5e5 100644 --- a/frontend/components/global/product-detail/Slider.vue +++ b/frontend/components/global/product-detail/Slider.vue @@ -57,12 +57,17 @@ const changeSlide = (id: number) => { name="zoom" mode="out-in" > - +
+ +
diff --git a/frontend/components/global/product/ProductCard.vue b/frontend/components/global/product/ProductCard.vue index b927d2d..ac19fb1 100644 --- a/frontend/components/global/product/ProductCard.vue +++ b/frontend/components/global/product/ProductCard.vue @@ -4,7 +4,14 @@ import Tag from "~/components/global/Tag.vue"; import Rate from "~/components/global/Rate.vue"; import ColorCircle from "~/components/global/ColorCircle.vue"; -import { useImageColor } from "~/composables/global/useImageColor"; +import { useRatio } from "~/composables/global/useRatio"; + +// state + +const containerEl = ref(null); +const { isOutside: isMouseOutsideContainer } = useMouseInElement(containerEl); +const parallax = reactive(useParallax(containerEl)); +const { isMobile } = useRatio(); // types @@ -25,22 +32,36 @@ type Props = { const props = defineProps(); const { id, colors } = toRefs(props); -// state - -const { colorObject } = useImageColor(`#product-image-${id.value}`); - // computed const limitedColors = computed(() => { return colors.value.slice(0, 3); }); + +const parallaxStyle = computed(() => { + if (isMobile.value || isMouseOutsideContainer.value) { + return { + transform: `rotateX(0deg) rotateY(0deg)`, + transition: "0.3s ease-out all", + }; + } + + return { + transform: `rotateX(${parallax.roll * 20}deg) rotateY(${parallax.tilt * 20}deg)`, + transition: "0.3s ease-out all", + }; +}); diff --git a/frontend/components/home/ProductsShowcase.vue b/frontend/components/home/ProductsShowcase.vue index 788d81e..55c346a 100644 --- a/frontend/components/home/ProductsShowcase.vue +++ b/frontend/components/home/ProductsShowcase.vue @@ -2,135 +2,239 @@ // import import useHomeData from "~/composables/api/home/useHomeData"; +import { motion } from "motion-v"; +import useSlider from "~/composables/global/useSlider"; // state const { data: homeData } = useHomeData(); -const { $gsap: gsap, $ScrollTrigger: ScrollTrigger } = useNuxtApp(); - -let gsapTimeline: gsap.core.Timeline; -let scrollTrigger: ScrollTrigger; - -// lifecycle - -onMounted(() => { - gsapTimeline = gsap.timeline(); - const showcaseElements = gsap.utils.toArray(".showcase-slide"); - - setTimeout(() => { - showcaseElements.forEach((element, index) => { - gsapTimeline.fromTo( - element, - index === 0 - ? { - opacity: 1, - scale: 1, - // rotateX: -25, - zIndex: 1, - top: 0, - ease: "none", - } - : { - opacity: 0, - scale: 1, - // rotateX: -25, - zIndex: 1, - top: 20, - ease: "none", - }, - { - opacity: 1, - scale: 1, - // rotateX: 0, - zIndex: 5, - top: 0, - ease: "none", - }, - index === 0 ? "-=0%" : undefined - ); - - if (index < showcaseElements.length - 1) { - gsapTimeline.to(element, { - opacity: 0, - scale: 1.03, - // rotateX: 25, - top: -20, - ease: "none", - }); - } - }); - - scrollTrigger = ScrollTrigger.create({ - trigger: "#products-showcase-container", - animation: gsapTimeline, - scrub: 1, - pin: true, - start: "top top", - anticipatePin: 1, - // markers: true, - end: "bottom top", - }); - - setTimeout(() => { - scrollTrigger.update(); - scrollTrigger.refresh(); - }, 1000); - }, 1000); +const { nextSlide, prevSlide, slideTo, activeSlide, progress } = useSlider({ + duration: 10_000, + count: homeData.value!.show_case_slider.length, }); -onUnmounted(() => { - gsapTimeline.progress(1).pause(); - gsapTimeline.kill(); -}); +const variants = { + hide: { opacity: 0, y: -200 }, + show: { + opacity: 1, + y: 0, + transition: { + when: "beforeChildren", + staggerChildren: 0.15, + }, + }, + exit: (slidesCount: number) => { + return { + opacity: 0, + y: 200, + transition: { + when: "afterChildren", + delay: slidesCount * 0.21, + staggerChildren: 0.1, + staggerDirection: 1, + }, + }; + }, +}; + +const childContentVariants = { + hide: { + filter: "blur(20px)", + opacity: 0, + }, + show: { + filter: "blur(0px)", + opacity: 1, + }, + exit: { + filter: "blur(20px)", + opacity: 0, + }, +}; + +const childImageVariants = { + hide: { + filter: "blur(20px)", + y: 70, + scale: 0.65, + opacity: 0, + }, + show: { + filter: "blur(0px)", + y: 0, + scale: 1, + opacity: 1, + transition: { + type: "spring", + damping: 20, + }, + }, + exit: { + filter: "blur(20px)", + y: 70, + scale: 0.65, + opacity: 0, + transition: { + default: { + type: "spring", + damping: 20, + }, + opacity: { + duration: 0.1, + }, + }, + }, +}; diff --git a/frontend/components/product/ProductHero/ProductDescription.vue b/frontend/components/product/ProductHero/ProductDescription.vue new file mode 100644 index 0000000..3f33eec --- /dev/null +++ b/frontend/components/product/ProductHero/ProductDescription.vue @@ -0,0 +1,67 @@ + + + diff --git a/frontend/components/product/ProductHero.vue b/frontend/components/product/ProductHero/index.vue similarity index 97% rename from frontend/components/product/ProductHero.vue rename to frontend/components/product/ProductHero/index.vue index 3f0e3f0..a3a989a 100644 --- a/frontend/components/product/ProductHero.vue +++ b/frontend/components/product/ProductHero/index.vue @@ -73,8 +73,8 @@ watch( to="#" class="typo-label-sm" > - {{ product!.category.name }} + {{ product!.category.name }} +

{{ product!.name }}

@@ -161,10 +161,7 @@ watch( /> -
+
تنوع رنگی : diff --git a/frontend/components/products/FilterProducts.vue b/frontend/components/products/FilterProducts.vue index 92c41ae..f21c9e4 100644 --- a/frontend/components/products/FilterProducts.vue +++ b/frontend/components/products/FilterProducts.vue @@ -83,8 +83,9 @@ const resetFilters = () => { sliderValue.value = [PRODUCT_RANGE.min, PRODUCT_RANGE.max]; has_discount.value = false; in_stock.value = false; + params.page = 1; - router.push({ path: `/products/`, query: { ...route.query } }); + router.push({ path: `/products/`, query: { ...route.query, page: 1 } }); }; watch( diff --git a/frontend/composables/api/home/useHomeData.ts b/frontend/composables/api/home/useHomeData.ts index a7fec35..e06c00f 100644 --- a/frontend/composables/api/home/useHomeData.ts +++ b/frontend/composables/api/home/useHomeData.ts @@ -7,37 +7,39 @@ import type { GetArticleResponse } from "~/composables/api/blog/useGetArticle"; // types export type GetHomeDataResponse = { - "sliders": { - "id": number, - "link": string, - "title": string, - "description": string, - "image": string | null, - "video": string | null - }[], - "main_categories": Category[], - "products": ProductListItem[], - "difreance_section": { - "image1": string, - "image2": string, - "title1": string, - "title2": string, - "description1": string, - "description2": string, - "link1": string, - "link2": string - }, - "show_case_slider" : { - "id": number, - "title": string, - "description": string, - "link": string, - "image": string, - }[] + sliders: { + id: number; + link: string; + title: string; + description: string; + image: string | null; + video: string | null; + }[]; + main_categories: Category[]; + products: ProductListItem[]; + difreance_section: { + image1: string; + image2: string; + title1: string; + title2: string; + description1: string; + description2: string; + link1: string; + link2: string; + }; + show_case_slider: { + id: number; + title: string; + description: string; + link: string; + image1: string; + image2: string; + image3: string; + background_image: string; + }[]; }; const useHomeData = () => { - // state const { $axios: axios } = useNuxtApp(); @@ -51,7 +53,7 @@ const useHomeData = () => { return useQuery({ queryKey: [QUERY_KEYS.home], - queryFn: () => handleHomeData() + queryFn: () => handleHomeData(), }); }; diff --git a/frontend/composables/api/orders/useGetCartOrders.ts b/frontend/composables/api/orders/useGetCartOrders.ts index 03ebe0a..c5c35f2 100644 --- a/frontend/composables/api/orders/useGetCartOrders.ts +++ b/frontend/composables/api/orders/useGetCartOrders.ts @@ -2,6 +2,7 @@ import { useQuery } from "@tanstack/vue-query"; import { API_ENDPOINTS, QUERY_KEYS } from "~/constants"; +import { useAuth } from "../auth/useAuth"; // types @@ -12,17 +13,18 @@ const useGetCartOrders = () => { const { $axios: axios } = useNuxtApp(); + const { token } = useAuth(); + // methods const handleGetCartOrders = async () => { - const { data } = await axios.get( - API_ENDPOINTS.orders.cart.get_all - ); + const { data } = await axios.get(API_ENDPOINTS.orders.cart.get_all); return data; }; return useQuery({ queryKey: [QUERY_KEYS.cart], + enabled: !!token.value, queryFn: () => handleGetCartOrders(), }); }; diff --git a/frontend/composables/api/tickets/useGetAllTickets.ts b/frontend/composables/api/tickets/useGetAllTickets.ts index 2fb48d4..1ef9e67 100644 --- a/frontend/composables/api/tickets/useGetAllTickets.ts +++ b/frontend/composables/api/tickets/useGetAllTickets.ts @@ -21,17 +21,14 @@ const useGetAllTickets = (params: ComputedRef) => { // methods const handleGetAllTickets = async (params: GetAllTicketsRequest) => { - const { data } = await axios.get( - API_ENDPOINTS.tickets.get_all, - { - params: { - sort: params.sort, - filter: params.status, - offset: Number(params.page) * 10 - 10, - limit: 10, - }, - } - ); + const { data } = await axios.get(API_ENDPOINTS.tickets.get_all, { + params: { + sort: params.sort, + filter: params.status, + offset: Number(params.page) * 7 - 7, + limit: 7, + }, + }); return data; }; diff --git a/frontend/composables/global/useSlider.ts b/frontend/composables/global/useSlider.ts new file mode 100644 index 0000000..6e11af2 --- /dev/null +++ b/frontend/composables/global/useSlider.ts @@ -0,0 +1,91 @@ +type Props = { + duration?: number; + count: number; +}; + +const useSlider = ({ duration = 0, count }: Props) => { + // states + + const sliderTimer = ref(null); + const progressTimer = ref(null); + + const progress = ref(0); + const activeSlide = ref(0); + + // methods + + const slideTo = (index: number) => { + activeSlide.value = index; + restartSliderTimer(); + }; + + const nextSlide = () => { + if (activeSlide.value > count - 2) { + activeSlide.value = 0; + } else { + activeSlide.value = activeSlide.value + 1; + } + }; + + const prevSlide = () => { + if (activeSlide.value < 1) { + activeSlide.value = count - 1; + } else { + activeSlide.value = activeSlide.value - 1; + } + }; + + const prevSlideHandler = () => { + restartSliderTimer(); + runProgress(); + prevSlide(); + }; + + const nextSlideHandler = () => { + restartSliderTimer(); + runProgress(); + nextSlide(); + }; + + const runProgress = () => { + const delay = duration / 100; + + if (progressTimer.value) clearInterval(progressTimer.value); + progress.value = 0; + + progressTimer.value = setInterval(() => { + if (progress.value < 100) { + progress.value = progress.value + 1; + } + }, delay); + }; + + const restartSliderTimer = () => { + if (sliderTimer.value) clearInterval(sliderTimer.value); + runProgress(); + + if (duration > 0) { + sliderTimer.value = setInterval(() => { + runProgress(); + nextSlide(); + }, duration); + } + }; + + onMounted(() => { + restartSliderTimer(); + }); + + onUnmounted(() => {}); + + return { + activeSlide, + progress, + slideTo, + nextSlide: nextSlideHandler, + prevSlide: prevSlideHandler, + restart: restartSliderTimer, + }; +}; + +export default useSlider; diff --git a/frontend/composables/global/useToast.ts b/frontend/composables/global/useToast.ts index 4649bac..a68c0b0 100644 --- a/frontend/composables/global/useToast.ts +++ b/frontend/composables/global/useToast.ts @@ -1,14 +1,14 @@ export type ToastOptions = { description?: string; duration?: number; - status?: "success" | "error" | "info" | "warning", -} + status?: "success" | "error" | "info" | "warning"; +}; type Toast = { id: number; message: string; - options?: ToastOptions -} + options?: ToastOptions; +}; const toasts = ref([]); @@ -20,12 +20,12 @@ export function useToast() { }; const destroyToast = (id: number) => { - toasts.value = toasts.value.filter(toast => toast.id !== id); + toasts.value = toasts.value.filter((toast) => toast.id !== id); }; return { toasts, addToast, - destroyToast + destroyToast, }; -} \ No newline at end of file +} diff --git a/frontend/nuxt.config.ts b/frontend/nuxt.config.ts index b3d1b9f..fd0cb10 100644 --- a/frontend/nuxt.config.ts +++ b/frontend/nuxt.config.ts @@ -16,6 +16,20 @@ export default defineNuxtConfig({ name: "fade", mode: "out-in", }, + head: { + meta: [ + { + name: "mobile-web-app-capable", + content: "yes", + }, + ], + link: [ + { + rel: "apple-touch-icon", + href: "/logo/apple-icon-180.png", + }, + ], + }, // layoutTransition: { // name: "fade", // mode: "out-in", @@ -83,14 +97,28 @@ export default defineNuxtConfig({ theme_color: "#ffffff", icons: [ { - src: "/logo/logo-192x192.png", + src: "/logo/manifest-icon-192.maskable.png", sizes: "192x192", type: "image/png", + purpose: "any", }, { - src: "/logo/logo-512x512.png", + src: "/logo/manifest-icon-192.maskable.png", + sizes: "192x192", + type: "image/png", + purpose: "maskable", + }, + { + src: "/logo/manifest-icon-512.maskable.png", sizes: "512x512", type: "image/png", + purpose: "any", + }, + { + src: "/logo/manifest-icon-512.maskable.png", + sizes: "512x512", + type: "image/png", + purpose: "maskable", }, ], }, diff --git a/frontend/package.json b/frontend/package.json index 70881a0..587c266 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -27,7 +27,7 @@ "@vuelidate/core": "^2.0.3", "@vuelidate/validators": "^2.0.4", "@vueuse/integrations": "^12.7.0", - "@vueuse/nuxt": "^12.7.0", + "@vueuse/nuxt": "^13.3.0", "animate.css": "^4.1.1", "axios": "^1.8.1", "date-fns-jalali": "^4.1.0-0", @@ -42,6 +42,7 @@ "swiper": "^11.2.6", "universal-cookie": "^7.2.2", "vue": "latest", + "vue-image-zoomer": "^2.4.4", "vue-router": "latest", "vue-scrollto": "^2.20.0", "vue-skeletor": "^1.0.6", diff --git a/frontend/pages/products/[...slug].vue b/frontend/pages/products/[...slug].vue index 6bc1adc..ba9a558 100644 --- a/frontend/pages/products/[...slug].vue +++ b/frontend/pages/products/[...slug].vue @@ -36,7 +36,7 @@ const filters = computed(() => { in_stock: params.in_stock ?? false, has_discount: params.has_discount ?? false, category: Array.isArray(route.params.slug) ? route.params.slug[1] ?? undefined : undefined, - page: params.page ?? 1, + page: route.query["page"] ?? 1, }; }); @@ -153,6 +153,7 @@ watch(
diff --git a/frontend/pages/profile/tickets/index.vue b/frontend/pages/profile/tickets/index.vue index bc375f4..2a653af 100644 --- a/frontend/pages/profile/tickets/index.vue +++ b/frontend/pages/profile/tickets/index.vue @@ -16,7 +16,12 @@ definePageMeta({ // state -const params = useUrlSearchParams("history") as GetAllTicketsRequest; +const params: GetAllTicketsRequest = useUrlSearchParams("history", { + initialValue: { + page: 1, + }, + writeMode: "push", +}); const filters = computed(() => { return { @@ -236,6 +241,7 @@ const clearFilters = () => { diff --git a/frontend/plugins/imageZoomer.client.ts b/frontend/plugins/imageZoomer.client.ts new file mode 100644 index 0000000..a024333 --- /dev/null +++ b/frontend/plugins/imageZoomer.client.ts @@ -0,0 +1,7 @@ +// @ts-ignore +import VueImageZoomer from 'vue-image-zoomer'; +import 'vue-image-zoomer/dist/style.css'; + +export default defineNuxtPlugin((nuxtApp) => { + nuxtApp.vueApp.use(VueImageZoomer, { name: "ImageZoomer" }); +}); diff --git a/frontend/plugins/scrollToTop.client.ts b/frontend/plugins/scrollToTop.client.ts index ada8613..db662ce 100644 --- a/frontend/plugins/scrollToTop.client.ts +++ b/frontend/plugins/scrollToTop.client.ts @@ -1,11 +1,18 @@ export default defineNuxtPlugin((nuxtApp) => { let previousPath: string | null = null; + let isFirstLoad = true; nuxtApp.hook("page:finish", () => { - const currentPath = useRoute().fullPath; + const { fullPath: currentPath } = useRoute(); + + if (isFirstLoad) { + previousPath = currentPath; + isFirstLoad = false; + return; + } if (previousPath !== currentPath) { - window.scrollTo(0, 0); + window.scrollTo({ top: 0, behavior: "auto" }); previousPath = currentPath; } }); diff --git a/frontend/public/logo/apple-icon-180.png b/frontend/public/logo/apple-icon-180.png new file mode 100644 index 0000000..ce85009 Binary files /dev/null and b/frontend/public/logo/apple-icon-180.png differ diff --git a/frontend/public/logo/apple-touch-icon-light.png b/frontend/public/logo/apple-touch-icon-light.png deleted file mode 100644 index 48cbb2a..0000000 Binary files a/frontend/public/logo/apple-touch-icon-light.png and /dev/null differ diff --git a/frontend/public/logo/apple-touch-icon.png b/frontend/public/logo/apple-touch-icon.png deleted file mode 100644 index 172eed5..0000000 Binary files a/frontend/public/logo/apple-touch-icon.png and /dev/null differ diff --git a/frontend/public/logo/logo-192x192.png b/frontend/public/logo/logo-192x192.png deleted file mode 100644 index 81e65bd..0000000 Binary files a/frontend/public/logo/logo-192x192.png and /dev/null differ diff --git a/frontend/public/logo/logo-char-192x192.png b/frontend/public/logo/logo-char-192x192.png deleted file mode 100644 index 234986c..0000000 Binary files a/frontend/public/logo/logo-char-192x192.png and /dev/null differ diff --git a/frontend/public/logo/logo-char-512x512.png b/frontend/public/logo/logo-char-512x512.png deleted file mode 100644 index 9427a20..0000000 Binary files a/frontend/public/logo/logo-char-512x512.png and /dev/null differ diff --git a/frontend/public/logo/logo-col.png b/frontend/public/logo/logo-col.png deleted file mode 100644 index 8377635..0000000 Binary files a/frontend/public/logo/logo-col.png and /dev/null differ diff --git a/frontend/public/logo/logo-light-192x192.png b/frontend/public/logo/logo-light-192x192.png deleted file mode 100644 index 2770fe6..0000000 Binary files a/frontend/public/logo/logo-light-192x192.png and /dev/null differ diff --git a/frontend/public/logo/logo-light-512x512.png b/frontend/public/logo/logo-light-512x512.png deleted file mode 100644 index e0037d6..0000000 Binary files a/frontend/public/logo/logo-light-512x512.png and /dev/null differ diff --git a/frontend/public/logo/logo-512x512.png b/frontend/public/logo/logo.png similarity index 100% rename from frontend/public/logo/logo-512x512.png rename to frontend/public/logo/logo.png diff --git a/frontend/public/logo/manifest-icon-192.maskable.png b/frontend/public/logo/manifest-icon-192.maskable.png new file mode 100644 index 0000000..422ff4d Binary files /dev/null and b/frontend/public/logo/manifest-icon-192.maskable.png differ diff --git a/frontend/public/logo/manifest-icon-512.maskable.png b/frontend/public/logo/manifest-icon-512.maskable.png new file mode 100644 index 0000000..9f6907d Binary files /dev/null and b/frontend/public/logo/manifest-icon-512.maskable.png differ