Merge remote-tracking branch 'origin/main'

This commit is contained in:
marzban-dev
2024-12-28 21:07:17 +03:30
18 changed files with 562 additions and 45 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
+33 -5
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()
message = f"کد یک بار مصرف : {otp}"
print(message)
# send otp
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:
+34
View File
@@ -14,3 +14,37 @@ sms_api = Ghasedak(api_key=os.getenv("SMS_API_KEY"))
### ارسال پیام otp
#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)
+18 -10
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"
networks:
- default
volumes:
postgres_data:
media_data:
networks:
default:
+46
View File
@@ -120,14 +120,60 @@
--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: translateX(50%);
}
}
@keyframes slideDown {
from {
height: 0;
}
to {
height: var(--reka-accordion-content-height);
}
}
@keyframes slideUp {
from {
height: var(--reka-accordion-content-height);
}
to {
height: 0;
}
}
@keyframes overlayShow {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes contentShow {
from {
opacity: 0;
transform: translate(-50%, -48%) scale(0.96);
}
to {
opacity: 1;
transform: translate(-50%, -50%) scale(1);
}
}
}
/* CONTAINER */
@@ -0,0 +1,61 @@
<script setup lang="ts">
// types
type Highlight = {
icon: string;
title: string;
description: string;
};
// state
const highlights = ref<Highlight[]>([
{
icon: "ci:headset",
title: "خدمات مشتری",
description: "پشتیبانی استثنایی، راه‌حل‌های پایدار",
},
{
icon: "ci:delivery",
title: "ارسال سریع و رایگان",
description: "ارسال رایگان برای سفارش‌های بالای ۱۵۰ دلار",
},
{
icon: "ci:users",
title: "معرفی به دوستان",
description: "دوستان خود را معرفی کنید و هر دو ۱۵٪ تخفیف بگیرید",
},
{
icon: "ci:shield-done",
title: "پرداخت امن",
description: "اطلاعات پرداخت شما به‌صورت امن پردازش می‌شود",
},
]);
</script>
<template>
<section class="w-full flex-center py-[5rem] gap-[1.25rem] container">
<template v-for="(highlight, index) in highlights" :key="index">
<div class="flex flex-col-center gap-[.75rem] w-1/4 px-5">
<div class="size-[1.5rem] flex-center">
<Icon :name="highlight.icon" />
</div>
<div class="w-full flex-col-center gap-[.25rem]">
<span class="typo-sub-h-md text-black text-center">
{{ highlight.title }}
</span>
<p class="text-slate-500 typo-p-md text-center">
{{ highlight.description }}
</p>
</div>
</div>
<div
class="w-[1px] h-[5rem] bg-slate-200"
v-if="index + 1 != highlights.length"
/>
</template>
</section>
</template>
<style scoped></style>
@@ -27,7 +27,7 @@ const onSwiper = (swiper: SwiperClass) => {
</script>
<template>
<div class="w-full flex flex-col gap-[4rem] py-[5rem] container">
<section class="w-full flex flex-col gap-[4rem] py-[5rem] container">
<div class="w-full flex justify-between items-center">
<span class="text-black typo-h-3">
{{ title }}
@@ -90,7 +90,7 @@ const onSwiper = (swiper: SwiperClass) => {
</SwiperSlide>
</Swiper>
</div>
</div>
</section>
</template>
<style scoped></style>
@@ -0,0 +1,132 @@
<template>
<div class="w-full flex flex-col">
<AccordionRoot
class="w-full last:border-b last:border-slate-200"
default-value="item-1"
type="single"
:collapsible="true"
>
<AccordionItem value="item-1" class="overflow-hidden">
<AccordionHeader
class="border-t border-slate-200 py-[1.5rem] flex justify-between items-center"
>
<span class="typo-sub-h-md text-black">مشخصات</span>
<AccordionTrigger class="group">
<Icon
name="ci:plus"
size="24"
class="group-data-[state=open]:rotate-45 transition-transform"
/>
</AccordionTrigger>
</AccordionHeader>
<AccordionContent
class="data-[state=open]:animate-slide-down pb-[1.5rem] data-[state=closed]:animate-slide-up overflow-hidden"
>
<div
class="w-full grid grid-cols-2 gap-y-[1.5rem] gap-x-[1rem]"
>
<div
v-for="i in 4"
class="flex flex-col gap-y-[1.5rem]"
>
<span
class="typo-sub-h-lg text-black w-full pt-[1.5rem]"
>صفحه نمایش</span
>
<ul class="list-disc w-full ps-5">
<li class="text-slate-500 typo-p-md">
روشنایی :3000mn
</li>
<li class="text-slate-500 typo-p-md">
روشنایی :3000mn
</li>
</ul>
</div>
</div>
</AccordionContent>
</AccordionItem>
<AccordionItem value="item-2" class="overflow-hidden">
<AccordionHeader
class="border-t border-slate-200 py-[1.5rem] flex justify-between items-center"
>
<span class="typo-sub-h-md text-black">مشخصات</span>
<AccordionTrigger class="group">
<Icon
name="ci:plus"
size="24"
class="group-data-[state=open]:rotate-45 transition-transform"
/>
</AccordionTrigger>
</AccordionHeader>
<AccordionContent
class="data-[state=open]:animate-slide-down pb-[1.5rem] data-[state=closed]:animate-slide-up overflow-hidden"
>
<div
class="w-full grid grid-cols-2 gap-y-[1.5rem] gap-x-[1rem]"
>
<div
v-for="i in 4"
class="flex flex-col gap-y-[1.5rem]"
>
<span
class="typo-sub-h-lg text-black w-full pt-[1.5rem]"
>صفحه نمایش</span
>
<ul class="list-disc w-full ps-5">
<li class="text-slate-500 typo-p-md">
روشنایی :3000mn
</li>
<li class="text-slate-500 typo-p-md">
روشنایی :3000mn
</li>
</ul>
</div>
</div>
</AccordionContent>
</AccordionItem>
<AccordionItem value="item-3" class="overflow-hidden">
<AccordionHeader
class="border-t border-slate-200 py-[1.5rem] flex justify-between items-center"
>
<span class="typo-sub-h-md text-black">مشخصات</span>
<AccordionTrigger class="group">
<Icon
name="ci:plus"
size="24"
class="group-data-[state=open]:rotate-45 transition-transform"
/>
</AccordionTrigger>
</AccordionHeader>
<AccordionContent
class="data-[state=open]:animate-slide-down pb-[1.5rem] data-[state=closed]:animate-slide-up overflow-hidden"
>
<div
class="w-full grid grid-cols-2 gap-y-[1.5rem] gap-x-[1rem]"
>
<div
v-for="i in 4"
class="flex flex-col gap-y-[1.5rem]"
>
<span
class="typo-sub-h-lg text-black w-full pt-[1.5rem]"
>صفحه نمایش</span
>
<ul class="list-disc w-full ps-5">
<li class="text-slate-500 typo-p-md">
روشنایی :3000mn
</li>
<li class="text-slate-500 typo-p-md">
روشنایی :3000mn
</li>
</ul>
</div>
</div>
</AccordionContent>
</AccordionItem>
</AccordionRoot>
</div>
</template>
<script setup lang="ts"></script>
<style scoped></style>
@@ -0,0 +1,38 @@
<template>
<section class="w-full p-[5rem] flex flex-col gap-y-[1.5rem]">
<div class="w-full flex">
<span class="text-black typo-h-3"> جزيات محصول </span>
</div>
<div class="w-full flex items-start justify-between gap-[3rem]">
<div class="w-8/12">
<Accordion />
</div>
<div class="w-4/12">
<div
class="w-full bg-slate-50 rounded-xl flex-col-center p-[5rem] gap-[1.5rem]"
>
<span>داخل جعبه چیه؟</span>
<div
class="w-full grid grid-cols-2 gap-y-[1.5rem] gap-x-[3rem]"
>
<div
v-for="i in 4"
class="w-full flex-col-center gap-[.75rem]"
>
<div
class="size-[6.25rem] rounded-full border-slate-200 bg-white flex-center"
>
<Icon name="ci:flag" size="44" />
</div>
<span class="text-black typo-p-md">Headphones</span>
</div>
</div>
</div>
</div>
</div>
</section>
</template>
<script setup lang="ts"></script>
<style scoped></style>
@@ -1,7 +1,7 @@
<script lang="ts" setup></script>
<template>
<div class="h-[95svh] w-full relative bg-black mt-[5rem]">
<section class="h-[95svh] w-full relative bg-black mt-[5rem]">
<img src="/img/product-3.jpg" class="object-cover absolute size-full" />
<div class="size-full absolute inset-0 bg-black/60" />
<StickyCard
@@ -11,5 +11,5 @@
title="نام محصول"
class="absolute right-6 bottom-6"
/>
</div>
</section>
</template>
@@ -0,0 +1,78 @@
<script setup lang="ts"></script>
<template>
<DialogRoot modal>
<DialogTrigger>
<Button
end-icon="ci:filter"
variant="secondary"
class="rounded-full"
>
فیلتر محصولات
</Button>
</DialogTrigger>
<DialogPortal to="#teleports">
<DialogOverlay
class="bg-black/60 backdrop-blur-sm data-[state=open]:animate-overlay-show fixed inset-0 z-30"
/>
<DialogContent
class="data-[state=open]:animate-content-show absolute top-1/2 left-1/2 max-h-[85vh] w-[90vw] max-w-[450px] -translate-x-1/2 -translate-y-1/2 rounded-[6px] bg-white p-[25px] shadow-[hsl(206_22%_7%_/_35%)_0px_10px_38px_-10px,_hsl(206_22%_7%_/_20%)_0px_10px_20px_-15px] focus:outline-none z-[100]"
>
<DialogTitle
class="flex items-center typo-h-4 text-black font-semibold"
>
Edit profile
</DialogTitle>
<DialogDescription
class="text-mauve11 mt-[10px] mb-5 text-sm leading-normal"
>
Make changes to your profile here. Click save when you're
done.
</DialogDescription>
<fieldset class="mb-[15px] flex items-center gap-5">
<label
class="text-black w-[90px] text-right text-sm"
for="name"
>
Name
</label>
<input
id="name"
class="text-black bg-stone-50 shadow-green7 focus:shadow-gray-100 inline-flex h-[35px] w-full flex-1 items-center justify-center rounded-lg px-[10px] text-sm leading-none shadow-[0_0_0_1px] outline-none focus:shadow-[0_0_0_2px]"
defaultValue="Pedro Duarte"
/>
</fieldset>
<fieldset class="mb-[15px] flex items-center gap-5">
<label
class="text-black w-[90px] text-right text-sm"
for="username"
>
Username
</label>
<input
id="username"
class="text-black bg-stone-50 shadow-green7 focus:shadow-gray-100 inline-flex h-[35px] w-full flex-1 items-center justify-center rounded-lg px-[10px] text-sm leading-none shadow-[0_0_0_1px] outline-none focus:shadow-[0_0_0_2px]"
defaultValue="@peduarte"
/>
</fieldset>
<div class="mt-[25px] flex justify-end">
<DialogClose as-child>
<button
class="bg-green4 text-green11 text-sm hover:bg-green5 focus:shadow-green7 inline-flex h-[35px] items-center justify-center rounded-lg px-[15px] font-semibold leading-none focus:shadow-[0_0_0_2px] focus:outline-none"
>
Save changes
</button>
</DialogClose>
</div>
<DialogClose
class="text-black hover:bg-green4 focus:shadow-green7 absolute top-[10px] right-[10px] inline-flex h-[25px] w-[25px] appearance-none items-center justify-center rounded-full focus:shadow-[0_0_0_2px] focus:outline-none"
aria-label="Close"
>
<Icon name="ci:close" />
</DialogClose>
</DialogContent>
</DialogPortal>
</DialogRoot>
</template>
<style lang="scss" scoped></style>
+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"]
+7 -1
View File
@@ -11,11 +11,17 @@
</script>
<template>
<div class="w-full flex flex-col-center persian-number font-yekan-bakh" dir="rtl">
<div
class="w-full flex flex-col-center persian-number font-yekan-bakh"
dir="rtl"
>
<Header />
<main class="w-full overflow-x-hidden">
<slot />
</main>
<div class="w-full flex-col flex">
<ServiceHighlights />
<Footer />
</div>
</div>
</template>
+5 -2
View File
@@ -1,8 +1,11 @@
<script lang="ts" setup></script>
<template>
<div class="w-full flex flex-col">
<ProductHero />
<Video />
<Comments />
<ProductVideo />
<ProductComments />
<ProductDetails />
<RelatedProducts title="محصولات مشابه" />
</div>
</template>
+35
View File
@@ -0,0 +1,35 @@
<script setup lang="ts"></script>
<template>
<div class="w-full container flex flex-col">
<div class="w-full flex justify-between items-center py-[5rem]">
<div class="flex flex-col items-start gap-[1.5rem] text-black">
<div class="flex-center gap-[.75rem]">
<span>خانه</span>
<span>/</span>
<span>محصولات</span>
<span>/</span>
<span>همه</span>
</div>
<h1 class="typo-hero-2">همه محصولات</h1>
</div>
<FilterButton />
</div>
<ul class="w-full grid grid-cols-3 gap-[1.5rem]">
<li v-for="i in 9" :key="i">
<ProductCard
brand="Samsung"
title="Galaxy S20 Ultra"
picture="/assets/img/product-1.jpg"
:colors="['#0000ff', '#00ff00', 'red']"
:price="599"
:rate="2.4"
tag="New"
/>
</li>
</ul>
</div>
</template>
<style scoped></style>