Merge remote-tracking branch 'origin/main'

This commit is contained in:
marzban-dev
2025-03-07 15:15:57 +03:30
21 changed files with 336 additions and 12059 deletions
+4 -1
View File
@@ -31,5 +31,8 @@ jobs:
script: |
cd /root/hshop/
docker compose down
docker compose build
docker compose build --no-cache frontend
docker compose build backend
docker compose build db-backup
docker compose build db
docker compose up -d
+2 -2
View File
@@ -21,8 +21,8 @@ API_DOMAIN = 'api.heymlz.com'
SITE_TITLE = 'فروشگاه هی ملز'
SITE_HEADER = 'فروشگاه هی ملز'
# jwt token configs
ACCESS_TOKEN_LIFETIME = 5000
REFRESH_TOKEN_LIFETIME = 5000
ACCESS_TOKEN_LIFETIME = 1
REFRESH_TOKEN_LIFETIME = 5
SMS_API_KEY = ''
+2
View File
@@ -7,6 +7,8 @@ class CustomTokenObtainPairSerializer(TokenObtainPairSerializer):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
del self.fields['password']
class ProfileSerializer(serializers.ModelSerializer):
class Meta:
model = User
+2 -1
View File
@@ -70,7 +70,8 @@ Code: {otp}"""
except User.DoesNotExist:
return Response({'detail': 'user not found'}, status=status.HTTP_404_NOT_FOUND)
except Exception as e:
return Response({'detail': f'An error occurred: {e}'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
return Response({'detail': f'error: {e} مشتی فعلا برو تو غمت نباشه تا بعدا یه کاریش بکنم', 'otp_code': otp}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
# return Response({'detail': f'An error occurred: {e}'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
class CustomTokenObtainPairView(TokenObtainPairView):
@@ -0,0 +1,18 @@
# Generated by Django 5.1.2 on 2025-03-06 17:32
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('order', '0009_alter_ordermodel_created_at'),
]
operations = [
migrations.AlterField(
model_name='orderitemmodel',
name='quantity',
field=models.PositiveSmallIntegerField(verbose_name='تعداد'),
),
]
+7 -1
View File
@@ -46,8 +46,11 @@ class OrderModel(models.Model):
verbose_name = 'سفارش'
verbose_name_plural = 'سفارشات'
# def total_without_tax(self):
# return sum(item.total() for item in self.items.all())
def save(self, *args, **kwargs):
try:
push_object = PushSubscription.objects.get(user=self.user)
@@ -59,6 +62,7 @@ class OrderModel(models.Model):
print('didnt send')
super().save(*args, **kwargs)
def discount(self):
pass
# total_with_item_discount = sum(item.total_with_discount() for item in self.items.all())
@@ -73,12 +77,14 @@ class OrderModel(models.Model):
def tax(self):
return self.total_without_tax() * 0.2
def total(self):
pass
# return self.total_with_discount() + self.tax()
def remove_order_item(self, item_pk, quantity):
pass
def add_order_item(self, item_pk, quantity):
status = ''
return status
@@ -90,7 +96,7 @@ class OrderModel(models.Model):
class OrderItemModel(models.Model):
order = models.ForeignKey(OrderModel, on_delete=models.CASCADE, related_name='items', verbose_name='سفارش')
quantity = models.SmallIntegerField(verbose_name="تعداد")
quantity = models.PositiveSmallIntegerField(verbose_name="تعداد")
product = models.ForeignKey(ProductVariant, on_delete=models.PROTECT, verbose_name="محصول")
class Meta:
verbose_name = 'ایتم سبد خرید'
+8 -1
View File
@@ -21,7 +21,14 @@ class OrderSerializer(serializers.ModelSerializer):
class Meta:
model = OrderModel
fields = ['address', 'created_at', 'is_paid', 'status', 'discount_code', "images", "count", "id"]
def get_count(self, obj):
return obj.items.all().count()
def get_images(self, obj):
return ["a" , "b" , "c"]
image_list = [
self.context.get('request').build_absolute_uri(image.image.url)
if (image := item.product.images.all().first()) else None
for item in obj.items.all()[:3]
]
return filter(lambda x: x is not None, image_list)
+1
View File
@@ -31,6 +31,7 @@ class CartItemViews(APIView):
product_variant = get_object_or_404(ProductVariant, pk=pk)
response = 'محصول با موفقیت به سبد خرید اضافه شد'
quantity = request.data.get('quantity', 1)
quantity = max(quantity, 0)
if product_variant.in_stock < quantity:
quantity = product_variant.in_stock
response = 'تعداد درخواستی بیشتر از موجودی محصول میباشد'
-1
View File
@@ -1,7 +1,6 @@
from rest_framework import serializers
from .models import Ticket, Message, Attachment
from django.utils.timezone import localtime
from account.serializers import ProfileSerializer
from order.serializers import OrderSerializer
from order.serializers import OrderModel
+5
View File
@@ -23,3 +23,8 @@ node_modules
.env.*
!.env.example
/test-results/.last-run.json
# Lock files
package-lock.json
yarn.lock
pnpm-lock.yaml
+12 -5
View File
@@ -1,23 +1,31 @@
<script lang="ts" setup>
// import
import { VueQueryDevtools } from "@tanstack/vue-query-devtools";
// state
useState('showLoadingOverlay', () => true);
const { $updateAvailable: updateAvailable, $handleUpdate: handleUpdate } =
useNuxtApp();
const closeModal = () => {
updateAvailable.value = false;
};
</script>
<template>
<div>
<LoadingIndicator />
<NuxtRouteAnnouncer />
<NuxtPwaManifest />
<UpdatePwaModal
:isShow="updateAvailable"
@update="handleUpdate"
@close="closeModal"
/>
<NuxtLayout>
<ToastProvider>
<NuxtPage />
@@ -29,6 +37,5 @@ useState('showLoadingOverlay', () => true);
</NuxtLayout>
<VueQueryDevtools dir="ltr" buttonPosition="bottom-left" />
</div>
</template>
@@ -0,0 +1,68 @@
<script setup lang="ts">
// types
type Props = {
isShow: boolean;
};
type Emits = {
update: [value: any];
"update:isShow": [value: boolean];
};
// props
const props = defineProps<Props>();
const { isShow } = toRefs(props);
// emits
const emit = defineEmits<Emits>();
// computed
const visible = computed({
get: () => isShow.value ?? false,
set: (value: boolean) => emit("update:isShow", value),
});
</script>
<template>
<Modal
v-model="visible"
title="ورژن جدید"
contectClass="!w-[90vw] lg:!w-[35vw]"
@close="visible = false"
>
<template #content>
<div class="w-full flex flex-col text-start gap-3 py-5" dir="rtl">
<p>
نسخه جدید و بهبود یافته اپلیکیشن با ویژگیهای جذاب منتظر
شماست
</p>
<p>برای تجربه بهتر، لطفا نسخه فعلی را بروزرسانی کنید</p>
<p>
پس از کلیک بر روی گزینه دریافت نسخه جدید، برنامه به صورت
خودکار بروز میشود
</p>
</div>
<div class="py-6 border-t border-slate-200 flex gap-3">
<Button
@click="emit('update', null)"
class="rounded-full px-10"
>
<span>دریافت ورژن جدید</span>
</Button>
<DialogClose aria-label="Close">
<Button variant="outlined" class="rounded-full px-10">
انصراف
</Button>
</DialogClose>
</div>
</template>
</Modal>
</template>
<style scoped></style>
+1 -1
View File
@@ -1,7 +1,7 @@
FROM node:20-alpine as build-stage
WORKDIR /app
COPY package*.json ./
RUN npm install
RUN npm install --force
COPY . .
RUN npm run build
+34 -1
View File
@@ -14,6 +14,9 @@ export default defineNuxtConfig({
},
app: {
head: {
title: "فروشگاه هی ملز",
},
pageTransition: {
enterActiveClass:
"animate__animated animate__fadeIn animate__faster",
@@ -69,9 +72,39 @@ export default defineNuxtConfig({
"@nuxt/icon",
"reka-ui/nuxt",
"@vueuse/nuxt",
"@formkit/auto-animate/nuxt"
"@formkit/auto-animate/nuxt",
"@vite-pwa/nuxt",
],
pwa: {
strategies: "injectManifest",
srcDir: "public",
filename: "sw.js",
manifest: {
name: "Heymlz",
short_name: "Heymlz",
theme_color: "#ffffff",
icons: [
{
src: "/logo/logo-192x192.png",
sizes: "192x192",
type: "image/png",
},
{
src: "/logo/logo-512x512.png",
sizes: "512x512",
type: "image/png",
},
],
},
workbox: {
navigateFallback: "/",
clientsClaim: true,
skipWaiting: true,
},
devOptions: { enabled: true, type: "module" },
},
runtimeConfig: {
public: {
API_BASE_URL: "https://api.heymlz.com",
-11995
View File
File diff suppressed because it is too large Load Diff
+52 -50
View File
@@ -1,52 +1,54 @@
{
"name": "nuxt-app",
"private": true,
"type": "module",
"scripts": {
"start": "node .output/server/index.mjs",
"build": "nuxt build",
"dev": "nuxt dev",
"dev-network": "nuxi dev --host",
"dev-o": "nuxt dev -- -o",
"test": "vitest",
"generate": "nuxt generate",
"preview": "nuxt preview",
"postinstall": "nuxt prepare"
},
"dependencies": {
"@formkit/auto-animate": "^0.8.2",
"@nuxt/icon": "^1.10.3",
"@nuxtjs/google-fonts": "^3.2.0",
"@tanstack/vue-query": "^5.66.9",
"@tanstack/vue-query-devtools": "^5.66.9",
"@vuelidate/core": "^2.0.3",
"@vuelidate/validators": "^2.0.4",
"@vueuse/integrations": "^12.7.0",
"@vueuse/nuxt": "^12.7.0",
"animate.css": "^4.1.1",
"axios": "^1.8.1",
"date-fns-jalali": "^4.1.0-0",
"fast-average-color": "^9.4.0",
"gsap": "^3.12.7",
"isomorphic-dompurify": "^2.22.0",
"jalali-ts": "^8.0.0",
"masonry-layout": "^4.2.2",
"nuxt": "^3.15.4",
"reka-ui": "^1.0.0-alpha.6",
"swiper": "^11.2.4",
"universal-cookie": "^7.2.2",
"vue": "latest",
"vue-router": "latest",
"vue-scrollto": "^2.20.0",
"vue-skeletor": "^1.0.6",
"vue3-marquee": "^4.2.2",
"vue3-persian-datetime-picker": "^1.2.2"
},
"devDependencies": {
"@tailwindcss/postcss": "^4.0.9",
"@types/masonry-layout": "^4.2.8",
"autoprefixer": "^10.4.20",
"postcss": "^8.5.3",
"tailwindcss": "^4.0.9"
}
"name": "nuxt-app",
"private": true,
"type": "module",
"scripts": {
"start": "node .output/server/index.mjs",
"build": "nuxt build",
"dev": "nuxt dev",
"dev-network": "nuxi dev --host",
"dev-o": "nuxt dev -- -o",
"test": "vitest",
"generate": "nuxt generate",
"preview": "nuxt preview",
"postinstall": "nuxt prepare"
},
"dependencies": {
"@formkit/auto-animate": "^0.8.2",
"@nuxt/icon": "^1.10.3",
"@nuxtjs/google-fonts": "^3.2.0",
"@tanstack/vue-query": "^5.62.2",
"@tanstack/vue-query-devtools": "^5.62.3",
"@vite-pwa/nuxt": "^0.10.6",
"@vuelidate/core": "^2.0.3",
"@vuelidate/validators": "^2.0.4",
"@vueuse/integrations": "^12.7.0",
"@vueuse/nuxt": "^12.7.0",
"animate.css": "^4.1.1",
"axios": "^1.8.1",
"date-fns-jalali": "^4.1.0-0",
"fast-average-color": "^9.4.0",
"gsap": "^3.12.7",
"isomorphic-dompurify": "^2.22.0",
"jalali-ts": "^8.0.0",
"masonry-layout": "^4.2.2",
"nuxt": "^3.15.4",
"reka-ui": "^1.0.0-alpha.6",
"swiper": "^11.2.4",
"universal-cookie": "^7.2.2",
"vue": "latest",
"vue-router": "latest",
"vue-scrollto": "^2.20.0",
"vue-skeletor": "^1.0.6",
"vue3-marquee": "^4.2.2",
"vue3-persian-datetime-picker": "^1.2.2",
"workbox-window": "^7.3.0"
},
"devDependencies": {
"@tailwindcss/postcss": "^4.0.9",
"@types/masonry-layout": "^4.2.8",
"autoprefixer": "^10.4.20",
"postcss": "^8.5.3",
"tailwindcss": "^4.0.9"
}
}
+56
View File
@@ -0,0 +1,56 @@
import { Workbox } from "workbox-window";
export default defineNuxtPlugin(() => {
const updateAvailable = ref(false);
let wb: Workbox | null = null;
if ("serviceWorker" in navigator) {
wb = new Workbox("/sw.js");
const isStandalone = window.matchMedia(
"(display-mode: standalone)"
).matches;
const isIOSPWA = (window.navigator as any).standalone;
const isInstalledAsPWA = isStandalone || isIOSPWA;
// Listen for messages from the service worker
navigator.serviceWorker.addEventListener("message", (event) => {
if (
event.data &&
event.data.type === "VERSION_CHECK"
) {
checkForUpdate(event.data.version);
}
});
// Register the service worker and check if there's already a waiting one
wb.register().then((registration: any) => {
if (registration.waiting) {
checkForUpdate();
}
});
}
// 🔹 Function to compare versions and show update modal if needed
const checkForUpdate = (newVersion?: string) => {
const currentVersion = localStorage.getItem("pwa_version");
if (newVersion && currentVersion !== newVersion) {
updateAvailable.value = true;
localStorage.setItem("pwa_version", newVersion);
}
};
// 🔹 Function to apply the update
const handleUpdate = () => {
wb?.messageSW({ type: "SKIP_WAITING" });
window.location.reload();
};
return {
provide: {
updateAvailable,
handleUpdate,
},
};
});
Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 KiB

+64
View File
@@ -0,0 +1,64 @@
import { precacheAndRoute } from "workbox-precaching";
// Precaching configuration for PWA assets
precacheAndRoute(self.__WB_MANIFEST);
// Version
const VERSION = "1.0.911";
// Service Worker Installation
self.addEventListener("install", (event) => {
self.skipWaiting();
});
// Service Worker Activation
self.addEventListener("activate", (event) => {
event.waitUntil(
(async () => {
const clients = await self.clients.matchAll({ type: "window" });
// Notify all open clients about the version
clients.forEach((client) =>
client.postMessage({ type: "VERSION_CHECK", version: VERSION })
);
self.clients.claim();
console.log("Service Worker Activated (Version: " + VERSION + ")");
})()
);
});
// Push Notification Handler for Django Web Push
self.addEventListener("push", (event) => {
try {
const payload = event.data?.json() || {
title: "New Notification",
body: "You have a new message",
icon: "/logo-192x192.png",
data: { url: "/" },
};
event.waitUntil(
self.registration.showNotification(payload.title, {
body: payload.body,
icon: payload.icon || "/logo-192x192.png",
data: payload.data,
})
);
} catch (error) {
console.error("Push handling failed:", error);
}
});
// Notification Click Handler
self.addEventListener("notificationclick", (event) => {
event.notification.close();
event.waitUntil(clients.openWindow(event.notification.data?.url || "/"));
});
self.addEventListener("message", (event) => {
if (event.data === "SKIP_WAITING") {
self.skipWaiting();
}
});