This commit is contained in:
Mamalizz
2024-12-26 21:04:18 +03:30
22 changed files with 863 additions and 57 deletions
+35
View File
@@ -0,0 +1,35 @@
name: Deploy to Server
on:
push:
branches:
- main
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Copy files to server
uses: appleboy/scp-action@v0.1.6
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SSH_USER }}
password: ${{ secrets.SSH_PASSWORD }}
source: "."
target: "/root/hshop/"
- name: SSH command to build and start Docker
uses: appleboy/ssh-action@v0.1.6
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SSH_USER }}
password: ${{ secrets.SSH_PASSWORD }}
script: |
cd /root/hshop/
docker compose down
docker compose build
docker compose up -d
+7 -7
View File
@@ -2,11 +2,11 @@
DEBUG = True # keep debug true to set the database to sqlite
# postgres database info
DB_NAME = ''
DB_USER = ''
DB_PASSWORD = ''
DB_HOST = ''
DB_PORT = ''
DB_NAME = 'hshop'
DB_USER = 'byeto'
DB_PASSWORD = 'vuhbyq-cypMu0-sirbon'
DB_HOST = 'db'
DB_PORT = 5434
SECRET_KEY = '2h&gmi54wqauwqht48l-9c)r6_67_(oe_$ll%(+xz%u#)+of@d'
# email stuff
EMAIL_BACKEND = ''
@@ -17,9 +17,9 @@ DEFAULT_FROM_EMAIL = ''
# telegram bot toekn
TELEGRAM_BOT_TOKEN = ''
# domain for allowed host and allowed cors
DOMAIN = ''
DOMAIN = '38.60.202.91'
# domain for api (the domain that django will use)
API_DOMAIN = ''
API_DOMAIN = '38.60.202.91'
SITE_TITLE = ''
SITE_HEADER = ''
# jwt token configs
+35 -7
View File
@@ -9,6 +9,7 @@ from drf_spectacular.utils import extend_schema, OpenApiParameter
from rest_framework_simplejwt.views import TokenObtainPairView
from django.shortcuts import get_object_or_404
from rest_framework_simplejwt.tokens import RefreshToken
import ghasedak_sms
class SendOTPView(APIView):
permission_classes = [AllowAny]
@extend_schema(
@@ -24,18 +25,45 @@ class SendOTPView(APIView):
)
def post(self, request):
phone = request.data.get('phone')
if not phone:
return Response({'detail': 'Phone number is required'}, status=status.HTTP_400_BAD_REQUEST)
try:
user, created = User.objects.get_or_create(phone=phone)
print(created)
print(user.phone)
otp = user.set_otp()
otp = user.set_otp()
message = f"کد یک بار مصرف : {otp}"
print(message)
# send otp
return Response({'detail': 'OTP sent successfully'}, status=status.HTTP_200_OK)
sms_api = ghasedak_sms.Ghasedak(api_key="4dc844abd4409fe247ec73831aed2498ad3749c1945660cc252654371756b966vafe5d9LGgMbnfGn")
# response = sms_api.send_single_sms(ghasedak_sms.SendSingleSmsInput(message=message, receptor=phone, line_number='30005006006908', send_date='', client_reference_id=''))
# print(response)
response = sms_api.send_single_sms(
ghasedak_sms.SendSingleSmsInput(
message=message,
receptor=phone,
line_number='90002930',
send_date='',
client_reference_id=''
)
)
# response = sms_api.send_otp_sms(otp_input)
if response['statusCode'] == 200:
return Response({'detail': 'OTP sent successfully'}, status=status.HTTP_200_OK)
else:
print('remmber to remove #TODO')
return Response({'detail': f'OTP sent successfully {otp}'}, status=status.HTTP_200_OK)
# return Response({'detail': response, 'otp_code': otp}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
except User.DoesNotExist:
return Response({'detail': 'User not found'}, status=status.HTTP_404_NOT_FOUND)
return Response({'detail': 'user not found'}, status=status.HTTP_404_NOT_FOUND)
except Exception as e:
return Response({'detail': f'An error occurred: {response}'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
class CustomTokenObtainPairView(TokenObtainPairView):
+9 -9
View File
@@ -27,7 +27,7 @@ EMAIL_HOST_PASSWORD = os.getenv("EMAIL_HOST_PASSWORD")
DEFAULT_FROM_EMAIL = os.getenv("SECRET_KEY")
SECRET_KEY = os.getenv("SECRET_KEY")
DEBUG = os.getenv("DEBUG")
DEBUG = False
# in production lists of allowed hosts and allowed orgins will genrate
# in development every host and orgin will be true
# in prodcution it will use the postgres info you enterd in .env.local
@@ -39,18 +39,18 @@ if not DEBUG:
f"https://{DOMAIN}",
f"http://{DOMAIN}",
]
CORS_ALLOWED_ORIGINS = [f"https://{API_DOMAIN}", f"http://{API_DOMAIN}",
f"http://{DOMAIN}", f"https://{DOMAIN}", ]
# CORS_ALLOWED_ORIGINS = [f"https://{API_DOMAIN}", f"http://{API_DOMAIN}",
# f"http://{DOMAIN}", f"https://{DOMAIN}", ]
CORS_ALLOW_ALL_ORIGINS = True
# database postgres
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': os.getenv("DB_NAME"),
'USER': os.getenv("DB_USER"),
'PASSWORD': os.getenv("DB_PASSWORD"),
'HOST': os.getenv("DB_HOST"),
'PORT': os.getenv("DB_PORT"),
'NAME': "hshop",
'USER': "byeto",
'PASSWORD': "vuhbyq-cypMu0-sirbon",
'HOST': 'db',
'PORT': 5432,
}
}
else:
+35 -1
View File
@@ -13,4 +13,38 @@ sms_api = Ghasedak(api_key=os.getenv("SMS_API_KEY"))
### ارسال پیام otp
#response = sms_api.send_otp_sms(receptor='09359****', message='OTP message')
#response = sms_api.send_otp_sms(receptor='09359****', message='OTP message')
import ghasedak_sms
# Initialize the Ghasedak API client with your API key
sms_api = ghasedak_sms.Ghasedak(api_key="Your_API_KEY")
# Define the OTP code and the recipient's phone number
otp_code = "123456" # Replace with the generated OTP code
phone_number = "09xxxxxxxxx" # Replace with the recipient's phone number
# Create the OTP input object
otp_input = ghasedak_sms.SendOtpInput(
send_date=None, # Immediate send; use a specific datetime for scheduled send
receptors=[
ghasedak_sms.SendOtpReceptorDto(
mobile=phone_number,
# client_reference_id='optional_client_ref_id' # Optional: Add if you have a client reference ID
)
],
template_name="YourTemplateName", # Replace with your OTP template name
inputs=[
ghasedak_sms.SendOtpInput.OtpInput(param="Code", value=otp_code),
# Add more parameters if your template requires them
],
udh=False # Set to True if you need User Data Header; typically False for standard SMS
)
# Send the OTP SMS
response = sms_api.send_otp_sms(otp_input)
# Print the response to check the result
print(response)
+19 -11
View File
@@ -1,11 +1,13 @@
services:
# frontend:
# build:
# context: ./frontend
# ports:
# - "80:3000"
# depends_on:
# - django
frontend:
build:
context: ./frontend
ports:
- "80:3000"
depends_on:
- django
networks:
- default
django:
build:
@@ -18,22 +20,28 @@ services:
- ./backend:/app
- media_data:/app/media
command: ["sh", "-c", "python manage.py migrate && python manage.py runserver 0.0.0.0:8000"]
networks:
- default
db:
image: postgres:16
environment:
POSTGRES_DB: shop_db
POSTGRES_DB: hshop
POSTGRES_USER: byeto
POSTGRES_PASSWORD: vuhbyq-cypMu0-sirbon
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
- "5434:5432"
- "5434:5432"
networks:
- default
volumes:
postgres_data:
media_data:
networks:
default:
+6 -1
View File
@@ -120,15 +120,20 @@
--breakpoint-xs: 480px;
/* ANIMATIONS */
<<<<<<< HEAD
--animate-marquee: marquee 3s linear infinite;
--animate-slide-down: slideDown 300ms ease-out;
--animate-slide-up: slideUp 300ms ease-out;
--animate-overlay-show: overlayShow 150ms ease-in;
--animate-content-show: contentShow 150ms ease-in;
=======
--animate-marquee: marquee 25s linear infinite;
--animate-marquee-reverse: marquee 25s linear infinite reverse;
>>>>>>> be4fa509843c81855f5ffc118150196c94a7b17b
@keyframes marquee {
to {
transform: translateY(-50%);
transform: translateX(50%);
}
}
+117
View File
@@ -0,0 +1,117 @@
<script setup lang="ts">
// types
type Props = {
tag: string;
date: string;
comments: number;
title: string;
description: string;
link: string;
variant?: "sm" | "lg";
}
// props
const props = withDefaults(defineProps<Props>(), {
variant: "lg"
});
const {} = toRefs(props);
</script>
<template>
<div
:class="variant === 'lg' ? 'rounded-150 overflow-hidden' : ''"
class="max-h-[700px] h-[700px] relative"
>
<Tag
v-if="variant === 'lg'"
class="bg-success-500 absolute left-10 top-10 z-20"
>
اسپیکر
</Tag>
<div
v-if="variant === 'sm'"
class="h-[350px] rounded-150 overflow-hidden relative"
>
<Tag
class="bg-success-500 absolute z-20 left-6 top-6"
>
اسپیکر
</Tag>
<img
src="/img/hero-bg.jpg"
class="absolute size-full object-cover z-10"
alt=""
/>
</div>
<div
:class="variant === 'lg' ? 'absolute px-10' : 'invert mt-8'"
class="bottom-10 flex flex-col gap-6 z-20"
>
<div class="flex items-center gap-6">
<div class="flex items-center gap-2">
<Icon
name="ci:comment"
size="18"
class="**:stroke-white"
/>
<span class="typo-p-sm text-white">
۰ نظر
</span>
</div>
<div class="flex items-center gap-2">
<Icon
name="ci:calendar"
size="18"
class="**:stroke-white"
/>
<span class="typo-p-sm text-white">
۳۱ مهر ۱۴۰۳
</span>
</div>
</div>
<div class="flex gap-4 flex-col">
<span
:class="variant === 'lg' ? 'typo-h-4' : 'typo-h-6'"
class="text-white"
>
برسی آیفون ۱۶ پرومکس
</span>
<p
:class="variant === 'lg' ? 'typo-h-4' : 'typo-h-6 text-slate-500'"
class="typo-p-md text-white text-justify"
>
نیاز و کاربردهای متنوع با هدف بهبود ابزارهای کاربردی می باشد.
نیاز و کاربردهای متنوع با هدف بهبود ابزارهای کاربردی می باشد.
کتابهای زیادی در شصت و سه درصد گذشته.
</p>
</div>
<span class="underline text-white typo-p-md">
بیشتر بخوانید...
</span>
</div>
<img
v-if="variant === 'lg'"
src="/img/hero-bg.jpg"
class="absolute size-full object-cover z-10"
alt=""
/>
<div
v-if="variant === 'lg'"
class="w-full h-full bg-linear-to-t from-black to-transparent absolute inset-0 z-15"
/>
</div>
</template>
+73
View File
@@ -0,0 +1,73 @@
<script setup lang="ts">
// types
type Props = {
// brands: string[];
}
// props
const props = defineProps<Props>();
const {} = toRefs(props);
// state
const { $gsap: gsap } = useNuxtApp();
// lifecycle
onMounted(() => {
gsap.to("#marquee-text-container", {
xPercent: -50, // Adjust based on content width
duration: 10, // Adjust for desired speed
ease: "none",
repeat: -1
});
});
</script>
<template>
<div class="relative w-full flex flex-col justify-center h-[350px]">
<div class="-rotate-z-2 z-20">
<div
class="bg-warning-500 flex pr-20 gap-20 py-2 w-max animate-marquee-reverse"
>
<span
v-for="i in 10"
class="text-[50px] text-white whitespace-nowrap font-semibold"
>
TEST {{ i }}
</span>
<span
v-for="i in 10"
class="text-[50px] text-white whitespace-nowrap font-semibold"
>
TEST {{ i }}
</span>
</div>
</div>
<div class="rotate-z-2 z-10">
<div
class="bg-slate-50 flex pr-20 gap-20 py-2 w-max animate-marquee"
>
<span
v-for="i in 10"
class="text-[50px] text-slate-300 whitespace-nowrap font-semibold"
>
TEST {{ i }}
</span>
<span
v-for="i in 10"
class="text-[50px] text-slate-300 whitespace-nowrap font-semibold"
>
TEST {{ i }}
</span>
</div>
</div>
</div>
</template>
@@ -0,0 +1,47 @@
<script setup lang="ts">
// types
type Props = {
category: string;
count: number;
description: string;
picture: string;
}
// props
defineProps<Props>();
</script>
<template>
<div class="relative rounded-150 overflow-hidden w-full h-[500px]">
<img
class="absolute object-cover size-full"
:src="picture"
alt=""
/>
<div class="bg-linear-to-t from-black/80 to-transparent absolute z-10 size-full" />
<div class="absolute z-20 bottom-0 p-6 flex items-end justify-between w-full">
<div class="flex flex-col gap-2 text-white">
<div class="typo-s-h-md">
تمام دسته ها
<span class="typo-p-xs -translate-y-1 inline-block mr-1">
24
</span>
</div>
<span class="typo-p-md">محصولات ما را مشاهده کنید</span>
</div>
<Icon
size="24"
name="ci:arrow-left"
class="**:stroke-white mb-1"
/>
</div>
</div>
</template>
@@ -3,6 +3,7 @@
import { Swiper, SwiperSlide } from "swiper/vue";
import type { SwiperClass } from "swiper/react";
// types
type Props = {
@@ -22,6 +23,7 @@ const swiper_instance = ref<SwiperClass | null>(null);
const onSwiper = (swiper: SwiperClass) => {
swiper_instance.value = swiper;
};
</script>
<template>
@@ -86,7 +88,6 @@ const onSwiper = (swiper: SwiperClass) => {
tag="New"
/>
</SwiperSlide>
...
</Swiper>
</div>
</section>
+96
View File
@@ -0,0 +1,96 @@
<script setup lang="ts">
// types
import { Swiper, SwiperSlide } from "swiper/vue";
import type { SwiperClass } from "swiper/react";
type Props = {}
// props
const props = defineProps<Props>();
const {} = toRefs(props);
// state
const swiper_instance = ref<SwiperClass | null>(null);
const slides = [
{
id: 0,
title: "TEST"
},
{
id: 1,
title: "TEST"
},
{
id: 2,
title: "TEST"
},
{
id: 3,
title: "TEST"
},
{
id: 4,
title: "TEST"
}
];
// methods
const onSwiper = (swiper: SwiperClass) => {
swiper_instance.value = swiper;
};
</script>
<template>
<div class="w-full my-20 relative">
<Swiper
:slides-per-view="3.65"
:space-between="20"
:slides-offset-after="125"
:slides-offset-before="125"
@swiper="onSwiper"
>
<SwiperSlide
v-for="slide in slides"
:key="slide.id"
>
<CategoryCard
category="یک دسته بندی تست"
picture="/img/product-1.jpg"
:count="20"
description="یک دسته بندی تستasdasd"
/>
</SwiperSlide>
</Swiper>
<div
v-if="!swiper_instance?.isBeginning"
@click="swiper_instance?.slidePrev()"
class="absolute z-20 right-20 shadow-lg cursor-pointer shadow-black/25 bottom-[50%] bg-white rounded-full size-11.5 flex justify-center items-center"
>
<Icon
name="ci:arrow-right"
class="**:stroke-black"
size="24"
/>
</div>
<div
v-if="!swiper_instance?.isEnd"
@click="swiper_instance?.slideNext()"
class="absolute z-20 left-20 shadow-lg cursor-pointer shadow-black/25 bottom-[50%] bg-white rounded-full size-11.5 flex justify-center items-center"
>
<Icon
name="ci:arrow-left"
class="**:stroke-black"
size="24"
/>
</div>
</div>
</template>
+121
View File
@@ -0,0 +1,121 @@
<script setup lang="ts">
// import
import { Swiper, SwiperSlide } from "swiper/vue";
import type { SwiperClass } from "swiper/react";
// state
const swiper_instance = ref<SwiperClass | null>(null);
const slides = [
{
id: 0,
title: "TEST"
},
{
id: 1,
title: "TEST"
},
{
id: 2,
title: "TEST"
},
{
id: 3,
title: "TEST"
},
{
id: 4,
title: "TEST"
}
];
// methods
const onSwiper = (swiper: SwiperClass) => {
swiper_instance.value = swiper;
};
const onChange = (swiper: SwiperClass) => {
console.log(swiper.activeIndex, swiper.realIndex, swiper.snapIndex);
};
</script>
<template>
<div class="w-full mb-20">
<div
class="relative"
>
<Swiper
:slides-per-view="1.2"
:loop="true"
:space-between="40"
:centered-slides="true"
@swiper="onSwiper"
@slide-change="onChange"
>
<SwiperSlide
v-for="slide in slides"
:key="slide.id"
>
<div class="relative w-full rounded-200 h-[80svh] overflow-hidden">
<img
class="absolute inset-0 size-full object-cover"
src="/img/hero-bg.jpg"
alt=""
/>
<div class="size-full absolute z-10 bg-linear-to-t from-black to-transparent" />
<div class="px-20 absolute z-10 w-full bottom-36">
<div class="border-b border-white/10 pb-6">
<h3 class="typo-hero-1 text-white">
Samsung {{ slide.id }}
</h3>
<div class="flex justify-between items-end">
<span class="typo-p-lg text-white">
توضیحات درمورد این محصول خاص
</span>
<Button class="invert rounded-full hover:bg-transparent">
خرید Samsung
</Button>
</div>
</div>
</div>
</div>
</SwiperSlide>
</Swiper>
<div class="absolute w-full bottom-20 left-[50%] translate-x-[-50%] z-100">
<div class="container h-full">
<div class="h-full flex items-center justify-between px-20">
<button @click="swiper_instance?.slidePrev()">
<Icon
class="**:stroke-white cursor-pointer"
name="ci:arrow-right"
size="24"
/>
</button>
<div class="flex items-center justify-center gap-3 text-white">
<div
v-for="(slide, index) in slides"
:class="swiper_instance?.realIndex === index ? 'bg-white' : 'bg-transparent'"
class="border border-white size-3 rounded-full transition-all duration-200"
@click="swiper_instance?.slideTo(index)"
>
</div>
</div>
<button>
<Icon
@click="swiper_instance?.slideNext()"
class="**:stroke-white cursor-pointer"
name="ci:arrow-left"
size="24"
/>
</button>
</div>
</div>
</div>
</div>
</div>
</template>
@@ -0,0 +1,65 @@
<script setup lang="ts">
// types
type Props = {}
// props
const props = defineProps<Props>();
const {} = toRefs(props);
</script>
<template>
<section class="h-[100svh] mt-20 px-20">
<div class="flex items-center justify-between mb-20">
<span class="typo-h-4 text-black">
مقالات اخیر سایت
</span>
<Button variant="outlined" class="rounded-full" start-icon="ci:paper">
نمایش همه
</Button>
</div>
<div class="flex gap-12">
<div class="flex-1 flex flex-col gap-12">
<BlogPost
description="aaasd"
title="asd"
:comments="2"
link="#"
date="2020-06-10"
tag="asdsa"
/>
<BlogPost
description="aaasd"
title="asd"
:comments="2"
link="#"
date="2020-06-10"
tag="asdsa"
/>
</div>
<div class="flex-[0.8] flex flex-col">
<BlogPost
description="aaasd"
title="asd"
:comments="2"
link="#"
date="2020-06-10"
tag="asdsa"
variant="sm"
/>
<BlogPost
description="aaasd"
title="asd"
:comments="2"
link="#"
date="2020-06-10"
tag="asdsa"
variant="sm"
/>
</div>
</div>
</section>
</template>
@@ -0,0 +1,78 @@
<script setup lang="ts">
// import
import { Swiper, SwiperSlide } from "swiper/vue";
import type { SwiperClass } from "swiper/react";
// types
type Props = {}
// props
const props = defineProps<Props>();
const {} = toRefs(props);
// state
const swiper_instance = ref<SwiperClass | null>(null);
// methods
const onSwiper = (swiper: SwiperClass) => {
swiper_instance.value = swiper;
};
</script>
<template>
<div class="relative max-h-[700px] flex justify-center items-center h-svh w-full">
<img
class="absolute size-full object-cover"
src="/img/hero-bg.jpg"
alt=""
/>
<div class="absolute bg-black/60 size-full z-10" />
<div class="w-full relative z-10">
<Swiper
:slides-per-view="1"
:loop="true"
:space-between="40"
:centered-slides="true"
:grab-cursor="true"
@swiper="onSwiper"
>
<SwiperSlide
v-for="i in 6"
:key="i"
>
<div class="flex justify-center items-center">
<div class="max-w-[900px] px-4 text-white flex flex-col items-center gap-4">
<Icon name="ci:instagram" size="28" class="**:stroke-white" />
<p class="typo-h-5 leading-[150%] text-center">
لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ و با
استفاده از طراحان گرافیک است. چاپگرها و متون بلکه روزنامه و مجله
در ستون و سطرآنچنان که لازم.
</p>
<span class="typo-p-xl text-center">
- منصور مرزبان
</span>
</div>
</div>
</SwiperSlide>
</Swiper>
<div class="flex items-center justify-center gap-3 text-white mt-6 relative z-10">
<div
v-for="(i, index) in 6"
:class="swiper_instance?.realIndex === index ? 'bg-white' : 'bg-transparent'"
class="border border-white size-2 rounded-full transition-all duration-200"
>
</div>
</div>
</div>
</div>
</template>
+93
View File
@@ -0,0 +1,93 @@
<script setup lang="ts">
// import
import { useDraggable } from "@vueuse/core";
// state
const clipPathPercent = ref(49);
const draggableEl = ref<HTMLElement | null>(null);
const previewContainerEl = ref<HTMLElement | null>(null);
const { x: dragAxisX } = useDraggable(draggableEl, {
initialValue: { x: 0, y: 0 },
axis: "x"
});
// watch
watch(() => dragAxisX.value, (newValue) => {
const clientRect = previewContainerEl.value?.getBoundingClientRect()!;
const percent = clientRect.width / 100;
const clipPercent = (newValue - clientRect.x - 8) / percent;
if (clipPercent >= 5 && clipPercent <= 95) {
clipPathPercent.value = clipPercent;
}
});
</script>
<template>
<div class="container">
<div class="flex flex-col items-center gap-3 mb-16">
<span class="typo-p-md text-slate-500">یک متن تست لورم</span>
<span class="typo-h-3 text-black">تفاوت محصول را ببینید در اینجا</span>
</div>
<div ref="previewContainerEl" class="rounded-200 overflow-hidden h-[90svh] relative">
<img
src="/img/hero-bg.jpg"
class="select-none absolute size-full object-cover"
alt=""
/>
<div
class="absolute size-full right-0 w-full"
>
<img
src="/img/hero-bg.jpg"
class="overlay-image select-none absolute object-cover size-full hue-rotate-200 brightness-35"
alt=""
/>
<div
:style="{
left: `${clipPathPercent}%`
}"
ref="draggableEl"
class="select-none w-2 h-full bg-white absolute left-0 flex items-center justify-center"
>
<div
class="cursor-grab hover:scale-115 transition-transform rounded-full absolute bg-white size-11 flex items-center justify-center"
>
<Icon name="ci:arrows" size="24" class="**:stroke-black" />
</div>
</div>
</div>
<div class="absolute bottom-0 p-10 w-full flex justify-between items-end bg-linear-to-t from-black/55 to-transparent">
<div class="flex flex-col gap-2 text-white">
<span class="typo-p-md">رنگ محصول</span>
<span class="typo-h-3">نارنجی</span>
</div>
<div class="flex flex-col justify-start gap-2 text-white">
<span class="typo-p-md">رنگ محصول</span>
<span class="typo-h-3">سفید</span>
</div>
</div>
</div>
</div>
</template>
<style>
.overlay-image {
clip-path: polygon(
v-bind('clipPathPercent + "%"') 0,
100% 0,
100% 100%,
v-bind('clipPathPercent + "%"') 100%
);
}
</style>
@@ -1,6 +1,6 @@
<script setup lang="ts">
// types
import Tooltip from "~/components/Tooltip.vue";
import Tooltip from "~/components/ui/Tooltip.vue";
type Props = {
variant?: "solid" | "outlined";
+13
View File
@@ -0,0 +1,13 @@
FROM node:20-alpine as build-stage
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
FROM node:20-alpine as production-stage
WORKDIR /app
COPY --from=build-stage /app /app
EXPOSE 3000
ENV NODE_ENV=production
CMD ["npm", "run", "start"]
+11 -19
View File
@@ -1,26 +1,18 @@
<script setup lang="ts">
import Categories from "~/components/home/Categories.vue";
</script>
<template>
<div class="w-full h-screen p-8 flex gap-8 justify-start items-start container">
<Product
brand="Samsung"
title="Galaxy S20 Ultra"
picture="/assets/img/product-1.jpg"
:colors="['#0000ff', '#00ff00','red']"
:price="599"
:rate="2.4"
tag="New"
/>
<Product
brand="Samsung"
title="Galaxy S20 Ultra"
picture="/assets/img/product-1.jpg"
:colors="['#0000ff', '#00ff00','red']"
:price="599"
:rate="2.4"
tag="New"
/>
<div class="w-full">
<Hero />
<Preview />
<Categories />
<ProductsSlider title="یک عنوان تستی" />
<Brands />
<ProductHero />
<MostRecentComments />
<LatestStories />
</div>
</template>
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 MiB