Merge remote-tracking branch 'origin/main'

This commit is contained in:
marzban-dev
2025-02-02 21:23:35 +03:30
47 changed files with 1101 additions and 212 deletions
+2 -2
View File
@@ -20,8 +20,8 @@ TELEGRAM_BOT_TOKEN = ''
DOMAIN = 'heymlz.com'
# domain for api (the domain that django will use)
API_DOMAIN = 'api.heymlz.com'
SITE_TITLE = ''
SITE_HEADER = ''
SITE_TITLE = 'Heymlz Shop'
SITE_HEADER = 'Heymlz Shop'
# jwt token configs
ACCESS_TOKEN_LIFETIME = 5000
REFRESH_TOKEN_LIFETIME = 5000
+36 -7
View File
@@ -1,29 +1,39 @@
from django.contrib import admin
from .models import *
from unfold.admin import ModelAdmin
from unfold.admin import ModelAdmin, TabularInline
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from import_export.admin import ImportExportModelAdmin
from unfold.contrib.import_export.forms import ExportForm, ImportForm, SelectableFieldsExportForm
from unfold.contrib.forms.widgets import ArrayWidget, WysiwygWidget
from django.contrib.postgres.fields import ArrayField
from django.contrib.auth.models import Group
from unfold.forms import AdminPasswordChangeForm
from unfold.forms import AdminPasswordChangeForm, UserChangeForm, UserCreationForm
class UserAddressInLine(TabularInline):
model = UserAddressModel
extra = 0
tab = True
@admin.register(User)
class UserAdmin(BaseUserAdmin, ModelAdmin, ImportExportModelAdmin):
form = UserChangeForm
add_form = UserCreationForm
change_password_form = AdminPasswordChangeForm
filter_horizontal = []
ordering = []
inlines = [UserAddressInLine]
list_filter = []
search_fields = ['phone', 'first_name', 'last_name', ]
list_display = ['phone', 'email', 'is_superuser']
readonly_fields = []
exclude = ('otp_hash', 'otp_expiry', 'is_active', 'is_staff', 'password', 'last_login')
import_form_class = ImportForm
export_form_class = ExportForm
fieldsets = (
('Personal info', {'fields': ('first_name', 'last_name', 'profile_photo')}),
('contact', {'fields': ('phone', 'email')}),
('Personal info', {'fields': ('first_name', 'last_name', 'profile_photo', 'password'),}),
('contact', {'fields': ('phone', 'email'),}),
)
add_fieldsets = (
@@ -49,4 +59,23 @@ from django.contrib import admin
from rest_framework_simplejwt.token_blacklist.models import BlacklistedToken, OutstandingToken
# Unregister the BlacklistedToken and OutstandingToken models
admin.site.unregister(BlacklistedToken)
admin.site.unregister(OutstandingToken)
admin.site.unregister(OutstandingToken)
@admin.register(UserAddressModel)
class AddressAdmin(ModelAdmin, ImportExportModelAdmin):
import_form_class = ImportForm
export_form_class = ExportForm
compressed_fields = True
warn_unsaved_form = True
formfield_overrides = {
models.TextField: {
"widget": WysiwygWidget,
},
ArrayField: {
"widget": ArrayWidget,
}
}
@@ -0,0 +1,17 @@
# Generated by Django 5.1.2 on 2025-02-01 15:15
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('account', '0001_initial'),
]
operations = [
migrations.AlterModelOptions(
name='user',
options={'verbose_name': 'کاربر', 'verbose_name_plural': 'کاربران'},
),
]
@@ -0,0 +1,25 @@
# Generated by Django 5.1.2 on 2025-02-02 14:55
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('account', '0002_alter_user_options'),
]
operations = [
migrations.AddField(
model_name='useraddressmodel',
name='city',
field=models.CharField(default='', max_length=30),
preserve_default=False,
),
migrations.AddField(
model_name='useraddressmodel',
name='province',
field=models.CharField(default='', max_length=30),
preserve_default=False,
),
]
@@ -0,0 +1,18 @@
# Generated by Django 5.1.2 on 2025-02-02 15:52
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('account', '0003_useraddressmodel_city_useraddressmodel_province'),
]
operations = [
migrations.AddField(
model_name='useraddressmodel',
name='for_me',
field=models.BooleanField(default=False),
),
]
+3 -1
View File
@@ -110,6 +110,8 @@ class UserAddressModel(models.Model):
address = models.TextField()
postal_code = models.CharField(max_length=10)
phone = models.CharField(max_length=11)
city = models.CharField(max_length=30)
province = models.CharField(max_length=30)
for_me = models.BooleanField(default=False)
def __str__(self):
return f"{self.user.phone}, {self.name}"
+2 -2
View File
@@ -17,8 +17,8 @@ class ProfileSerializer(serializers.ModelSerializer):
class UserAddressSerializer(serializers.ModelSerializer):
class Meta:
model = UserAddressModel
fields = ['id', 'name', 'address', 'postal_code', 'phone']
fields = ['id', 'name', 'address', 'postal_code', 'phone', 'city', 'province', 'for_me']
read_only_fields = ('id',)
def validate(self, data):
user = self.context['request'].user
if not user.is_authenticated:
@@ -0,0 +1,22 @@
# Generated by Django 5.1.2 on 2025-02-01 15:15
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('blog', '0001_initial'),
]
operations = [
migrations.AlterModelOptions(
name='blogmodel',
options={'verbose_name': 'بلاگ', 'verbose_name_plural': 'بلاگ ها'},
),
migrations.AlterField(
model_name='blogmodel',
name='cover_image',
field=models.ImageField(blank=True, upload_to='blog_covers/'),
),
]
+3
View File
@@ -26,6 +26,9 @@ class BlogModel(models.Model):
def __str__(self):
return self.title
class Meta:
verbose_name = 'بلاگ'
verbose_name_plural = 'بلاگ ها'
# class Comment(models.Model):
+16 -2
View File
@@ -1,13 +1,27 @@
from rest_framework import serializers
from .models import BlogModel
from account.models import User
class AuthorSerializer(serializers.ModelSerializer):
full_name = serializers.SerializerMethodField()
class Meta:
model = User
fields = ['full_name', 'profile_photo']
def get_full_name(self, obj):
if obj.first_name and obj.last_name:
return obj.first_name + ' ' + obj.last_name
else:
return 'ادمین وبسایت'
class BlogSerilizer(serializers.ModelSerializer):
author = AuthorSerializer()
class Meta:
model = BlogModel
fields = ['title','author', 'slug', 'category', 'created_at', 'updated_at', 'cover_image', 'views']
exclude = ('is_published',)
class AllBlogSerilizer(serializers.ModelSerializer):
author = AuthorSerializer()
class Meta:
model = BlogModel
exclude = ('is_published',)
exclude = ('is_published', 'content', 'summery', )
@@ -0,0 +1,17 @@
# Generated by Django 5.1.2 on 2025-02-01 15:15
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('chat', '0001_initial'),
]
operations = [
migrations.AlterModelOptions(
name='productchatmodel',
options={'verbose_name': 'جت محصلول و کاربر', 'verbose_name_plural': 'چت های محصلول کاربر'},
),
]
+4 -2
View File
@@ -4,7 +4,7 @@ from product.models import ProductModel, DollorModel
from django.conf import settings
import openai
from time import sleep
from product.serializers import ProductChatSerializer
from product.serializers import DynamicProductSerializer
ASSISTANT_ID = 'asst_1wOnCKncEHkOfp0FjOIz4Xkp'
@@ -15,6 +15,8 @@ class ProductChatModel(models.Model):
class Meta:
unique_together = ['user', 'product']
verbose_name = 'جت محصلول و کاربر'
verbose_name_plural = 'چت های محصلول کاربر'
def __str__(self):
return f'{self.user} - {self.product}'
@@ -24,7 +26,7 @@ class ProductChatModel(models.Model):
client = openai.OpenAI(api_key=settings.OPENAI_API_KEY)
dollor_object, _ = DollorModel.objects.get_or_create(unique_filed='unique')
dollor_price = dollor_object.price
product_json = ProductChatSerializer(instance=self.product, context={'dollor_price': dollor_price}).data
product_json = DynamicProductSerializer(instance=self.product, context={'dollor_price': dollor_price, 'view_type': 'chat'}).data
try:
thread = client.beta.threads.create(
+108 -46
View File
@@ -131,7 +131,7 @@ ROOT_URLCONF = 'core.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'DIRS': [BASE_DIR / "templates"],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
@@ -178,6 +178,7 @@ STATIC_ROOT = '/app/static'
STATICFILES_DIRS = [
os.path.join(BASE_DIR, 'custom_static'),
# BASE_DIR / "core" / "static"
]
STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
@@ -225,6 +226,7 @@ UNFOLD = {
"SITE_HEADER": os.getenv("SITE_HEADER"),
"SITE_URL": DOMAIN,
"SITE_SYMBOL": "shield_person",
"DASHBOARD_CALLBACK": "core.views.dashboard_callback",
"SITE_FAVICONS": [
{
"rel": "icon",
@@ -233,31 +235,50 @@ UNFOLD = {
"href": lambda request: static("favicon.svg"),
},
],
# "LOGIN": {
# "image": lambda request: static("robot.png"),
# },
"BORDER_RADIUS": "15px",
"SHOW_HISTORY": True,
"SHOW_VIEW_ON_SITE": True,
"ENVIRONMENT": "core.settings.environment_callback",
"COLORS": {
"COLORS": {
"base": {
"50": "249 250 251",
"100": "243 244 246",
"200": "229 231 235",
"300": "209 213 219",
"400": "156 163 175",
"500": "107 114 128",
"600": "75 85 99",
"700": "55 65 81",
"800": "31 41 55",
"900": "17 24 39",
"950": "3 7 18"
},
"primary": {
"50": "255 241 242",
"100": "255 228 230",
"200": "254 205 211",
"300": "253 164 175",
"400": "251 113 133",
"500": "244 63 94",
"600": "225 29 72",
"700": "190 18 60",
"800": "159 18 57",
"900": "136 19 55",
"950": "76 5 25"
},
"font": {
"subtle-light": "107 114 128",
"subtle-dark": "156 163 175",
"default-light": "75 85 99",
"default-dark": "209 213 219",
"important-light": "17 24 39",
"important-dark": "243 244 246",
},
"primary": {
"50": "245 250 255",
"100": "230 243 254",
"200": "180 218 253",
"300": "131 193 252",
"400": "81 168 251",
"500": "31 144 249",
"600": "6 118 224",
"700": "4 92 174",
"800": "3 66 124",
"900": "2 39 75",
"950": "1 13 25"
"subtle-light": "var(--color-base-500)", # text-base-500
"subtle-dark": "var(--color-base-400)", # text-base-400
"default-light": "var(--color-base-600)", # text-base-600
"default-dark": "var(--color-base-300)", # text-base-300
"important-light": "var(--color-base-900)", # text-base-900
"important-dark": "var(--color-base-100)", # text-base-100
},
},
"EXTENSIONS": {
@@ -269,7 +290,7 @@ UNFOLD = {
},
"SIDEBAR": {
"show_search": True,
"show_search": False,
"show_all_applications": False,
"navigation": [
{
@@ -283,9 +304,10 @@ UNFOLD = {
"link": reverse_lazy("admin:index"),
},
{
"title": _("اسلایدر"),
"icon": "home",
"link": reverse_lazy("admin:home_slidermodel_changelist"),
"title": _("سفارشات"),
"icon": "shopping_cart",
"link": reverse_lazy("admin:order_ordermodel_changelist"),
"badge": "utils.admin.admin_pending_count",
},
],
},
@@ -293,9 +315,9 @@ UNFOLD = {
{
"title": _("پنل فروش محصولات وبسایت"),
"title": _("Shop Products"),
"separator": True,
"collapsible": True,
"collapsible": False,
"items": [
{
"title": _("محصولات"),
@@ -303,7 +325,27 @@ UNFOLD = {
"link": reverse_lazy("admin:product_productmodel_changelist"),
},
# esm category model ro lower case bezar inja amir
{
"title": _("نظرات"),
"icon": "chat",
"link": reverse_lazy("admin:product_commentmodel_changelist"),
},
{
"title": _("قیمت دلار"),
"icon": "payments",
"link": reverse_lazy("admin:product_dollormodel_changelist"),
"badge": "utils.admin.dollor_price",
},
],
},
{
"title": _("Categories section"),
"separator": True,
"collapsible": False,
"items": [
{
"title": _("دسته بندی"),
@@ -314,23 +356,38 @@ UNFOLD = {
"title": _("زیر دسته بندی"),
"icon": "category",
"link": reverse_lazy("admin:product_subcategorymodel_changelist"),
}
],
},
{
"title": _("Visual Sections "),
"separator": True,
"collapsible": True,
"items": [
{
"title": _("اسلاید ها"),
"icon": "slide_library",
"link": reverse_lazy("admin:home_slidermodel_changelist"),
},
{
"title": _("نظرات"),
"icon": "chat",
"link": reverse_lazy("admin:product_commentmodel_changelist"),
},
"title": _("عکس مقایسه"),
"icon": "compare",
"link": reverse_lazy("admin:home_homeimagemodel_changelist"),
}
,
{
"title": _("قیمت دلار"),
"icon": "payments",
"link": reverse_lazy("admin:product_dollormodel_changelist"),
},
"title": _("مقالات و بلاگ ها"),
"icon": "newsmode",
"link": reverse_lazy("admin:blog_blogmodel_changelist"),
}
],
},
{
"title": _("بخش کاربران و مشتریان"),
"title": _("Users and Customers"),
"separator": True,
"collapsible": True,
"items": [
@@ -339,30 +396,35 @@ UNFOLD = {
"title": _("کاربران"),
"icon": "person",
"link": reverse_lazy("admin:account_user_changelist"),
},{
"title": _("چت محصول"),
"icon": "chat",
"link": reverse_lazy("admin:chat_productchatmodel_changelist"),
},
{
"title": _("ادرس ها"),
"icon": "contact_mail",
"link": reverse_lazy("admin:account_useraddressmodel_changelist"),
},
],
},
{
"title": _("بخش هوش مصنوعی"),
"title": _("Ticket and Support"),
"separator": True,
"collapsible": True,
"items": [
{
"title": _("چت محصول"),
"icon": "chat",
"link": reverse_lazy("admin:chat_productchatmodel_changelist"),
"title": _("تیکت"),
"icon": "confirmation_number",
"link": reverse_lazy("admin:ticket_ticket_changelist"),
},
],
},
],
},
@@ -370,7 +432,7 @@ UNFOLD = {
AUTH_USER_MODEL = 'account.User'
def environment_callback(request):
return ["Development", "warning"]
return ["Development", "danger"]
def badge_callback(request):
+190
View File
@@ -0,0 +1,190 @@
import json
import random
from functools import lru_cache
from django.contrib.humanize.templatetags.humanize import intcomma
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _
from django.views.generic import RedirectView, TemplateView
from unfold.views import UnfoldModelAdminViewMixin
class HomeView(RedirectView):
pattern_name = "admin:index"
def dashboard_callback(request, context):
context.update(random_data())
return context
@lru_cache
def random_data():
WEEKDAYS = [
"Mon",
"Tue",
"Wed",
"Thu",
"Fri",
"Sat",
"Sun",
]
positive = [[1, random.randrange(8, 28)] for i in range(1, 28)]
negative = [[-1, -random.randrange(8, 28)] for i in range(1, 28)]
average = [r[1] - random.randint(3, 5) for r in positive]
performance_positive = [[1, random.randrange(8, 28)] for i in range(1, 28)]
performance_negative = [[-1, -random.randrange(8, 28)] for i in range(1, 28)]
response = {
"navigation": [
{"title": _("Dashboard"), "link": "/", "active": True},
{"title": _("Products"), "link": "/admin/product/productmodel/"},
{"title": _("Orders"), "link": "/admin/order/ordermodel/"},
],
"kpi": [
{
"title": "Product A Performance",
"metric": f"${intcomma(f"{random.uniform(1000, 9999):.02f}")}",
"footer": mark_safe(
f'<strong class="text-green-700 font-semibold dark:text-green-400">+{intcomma(f"{random.uniform(1, 9):.02f}")}%</strong>&nbsp;progress from last week'
),
"chart": json.dumps(
{
"labels": [WEEKDAYS[day % 7] for day in range(1, 28)],
"datasets": [{"data": average, "borderColor": "#9333ea"}],
}
),
},
{
"title": "Product B Performance",
"metric": f"${intcomma(f"{random.uniform(1000, 9999):.02f}")}",
"footer": mark_safe(
f'<strong class="text-green-700 font-semibold dark:text-green-400">+{intcomma(f"{random.uniform(1, 9):.02f}")}%</strong>&nbsp;progress from last week'
),
},
{
"title": "Product C Performance",
"metric": f"${intcomma(f"{random.uniform(1000, 9999):.02f}")}",
"footer": mark_safe(
f'<strong class="text-green-700 font-semibold dark:text-green-400">+{intcomma(f"{random.uniform(1, 9):.02f}")}%</strong>&nbsp;progress from last week'
),
},
],
"progress": [
{
"title": "🦆 Social marketing e-book",
"description": f"${intcomma(f"{random.uniform(1000, 9999):.02f}")}",
"value": random.randint(10, 90),
},
{
"title": "🦍 Freelancing tasks",
"description": f"${intcomma(f"{random.uniform(1000, 9999):.02f}")}",
"value": random.randint(10, 90),
},
{
"title": "🐋 Development coaching",
"description": f"${intcomma(f"{random.uniform(1000, 9999):.02f}")}",
"value": random.randint(10, 90),
},
{
"title": "🦑 Product consulting",
"description": f"${intcomma(f"{random.uniform(1000, 9999):.02f}")}",
"value": random.randint(10, 90),
},
{
"title": "🐨 Other income",
"description": f"${intcomma(f"{random.uniform(1000, 9999):.02f}")}",
"value": random.randint(10, 90),
},
{
"title": "🐶 Course sales",
"description": f"${intcomma(f"{random.uniform(1000, 9999):.02f}")}",
"value": random.randint(10, 90),
},
{
"title": "🐻‍❄️ Ads revenue",
"description": f"${intcomma(f"{random.uniform(1000, 9999):.02f}")}",
"value": random.randint(10, 90),
},
{
"title": "🦩 Customer Retention Rate",
"description": f"${intcomma(f"{random.uniform(1000, 9999):.02f}")}",
"value": random.randint(10, 90),
},
{
"title": "🦊 Marketing ROI",
"description": f"${intcomma(f"{random.uniform(1000, 9999):.02f}")}",
"value": random.randint(10, 90),
},
{
"title": "🦁 Affiliate partnerships",
"description": f"${intcomma(f"{random.uniform(1000, 9999):.02f}")}",
"value": random.randint(10, 90),
},
],
"chart": json.dumps(
{
"labels": [WEEKDAYS[day % 7] for day in range(1, 28)],
"datasets": [
{
"label": "Example 1",
"type": "line",
"data": average,
"borderColor": "var(--color-primary-500)",
},
{
"label": "Example 2",
"data": positive,
"backgroundColor": "var(--color-primary-700)",
},
{
"label": "Example 3",
"data": negative,
"backgroundColor": "var(--color-primary-300)",
},
],
}
),
"performance": [
{
"title": _("Last week revenue"),
"metric": "$1,234.56",
"footer": mark_safe(
'<strong class="text-green-600 font-medium">+3.14%</strong>&nbsp;progress from last week'
),
"chart": json.dumps(
{
"labels": [WEEKDAYS[day % 7] for day in range(1, 28)],
"datasets": [
{
"data": performance_positive,
"borderColor": "var(--color-primary-700)",
}
],
}
),
},
{
"title": _("Last week expenses"),
"metric": "$1,234.56",
"footer": mark_safe(
'<strong class="text-green-600 font-medium">+3.14%</strong>&nbsp;progress from last week'
),
"chart": json.dumps(
{
"labels": [WEEKDAYS[day % 7] for day in range(1, 28)],
"datasets": [
{
"data": performance_negative,
"borderColor": "var(--color-primary-300)",
},
],
}
),
},
],
}
return response
@@ -0,0 +1,17 @@
# Generated by Django 5.1.2 on 2025-02-01 15:15
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('home', '0001_initial'),
]
operations = [
migrations.AlterModelOptions(
name='homeimagemodel',
options={'verbose_name': 'مدل عکس تفاوت خانه', 'verbose_name_plural': 'مدل عکس تفاوت خانه'},
),
]
+3
View File
@@ -30,3 +30,6 @@ class HomeImageModel(models.Model):
unique_filed = models.CharField(max_length=20, choices=unique, unique=True, default='unique')
def __str__(self):
return f'{self.title1} - {self.title2}'
class Meta:
verbose_name = 'مدل عکس تفاوت خانه'
verbose_name_plural = 'مدل عکس تفاوت خانه'
+2 -2
View File
@@ -1,7 +1,7 @@
from django.shortcuts import render
from rest_framework.views import APIView, Response
from product.models import ProductModel, SubCategoryModel, DollorModel
from product.serializers import SubCategorySerializer, ProductSerializer
from product.serializers import SubCategorySerializer, DynamicProductSerializer
from .serializers import SliderSerializer, HomeImageSerializer
from .models import SliderModel, HomeImageModel
from rest_framework import status
@@ -21,7 +21,7 @@ class HomeView(APIView):
sub_category_ser = SubCategorySerializer(instance=sub_categories, many=True, context={'request': request})
products_to_show = ProductModel.objects.filter(show=True)
product_ser = ProductSerializer(instance=products_to_show, many=True, context={'request': request, 'dollor_price': dollor_price})
product_ser = DynamicProductSerializer(instance=products_to_show, many=True, context={'request': request, 'dollor_price': dollor_price, 'view_type': 'list'})
home_image = HomeImageModel.objects.all().first()
home_image_ser = HomeImageSerializer(instance=home_image, context={'request': request})
+48 -5
View File
@@ -1,23 +1,40 @@
from django.contrib import admin
from .models import *
from unfold.admin import ModelAdmin
from unfold.admin import ModelAdmin, TabularInline
from import_export.admin import ImportExportModelAdmin
from unfold.contrib.import_export.forms import ExportForm, ImportForm, SelectableFieldsExportForm
from unfold.contrib.forms.widgets import ArrayWidget, WysiwygWidget
from django.contrib.postgres.fields import ArrayField
from unfold.widgets import (
UnfoldAdminColorInputWidget,
)
from unfold.decorators import action, display
class InStuckColorsInLine(TabularInline):
model = InStuckColors
extra = 0
tab = True
formfield_overrides = {
models.CharField: {"widget": UnfoldAdminColorInputWidget()},
}
@admin.register(ProductModel)
class ProductModelAdmin(ModelAdmin, ImportExportModelAdmin):
import_form_class = ImportForm
export_form_class = ExportForm
inlines = [InStuckColorsInLine]
readonly_fields = ('slug', )
compressed_fields = True
search_fields = ['name']
autocomplete_fields = ['related_products']
# compressed_fields = True
warn_unsaved_form = True
list_display = ['display_image', 'price',]
fieldsets = (
('Main Fileds', {'fields': ('name', 'description', 'price', 'currency', 'discount', 'category', 'related_products', 'show',), "classes": ["tab"],}),
('SEO Fileds', {'fields': ('meta_description', 'meta_keywords', 'meta_rating', 'slug'), "classes": ["tab"],}),
('Users Fileds', {'fields': ('rating', 'view', 'sell', ), "classes": ["tab"],})
)
formfield_overrides = {
models.TextField: {
"widget": WysiwygWidget,
@@ -26,6 +43,32 @@ class ProductModelAdmin(ModelAdmin, ImportExportModelAdmin):
"widget": ArrayWidget,
}
}
@display(description='محصول', header=True)
def display_image(self, instance):
if instance.image1:
return [
instance.name,
None,
None,
{
"path": instance.image1.url,
"height": 30,
"width": 30,
"borderless": True,
# "squared": True,
},
]
return ('خالی',)
# @display(
# description=("نمایش در صفحه ی اصلی"),
# label={
# True: "danger",
# False: "success",
# },
# )
# def display_show(self, instance):
# return instance.show
@admin.register(MainCategoryModel)
class MainCategoryModelAdmin(ModelAdmin, ImportExportModelAdmin):
@@ -0,0 +1,26 @@
# Generated by Django 5.1.2 on 2025-02-01 15:15
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('product', '0004_alter_subcategorymodel_parent'),
]
operations = [
migrations.AlterModelOptions(
name='dollormodel',
options={'verbose_name': 'مدل دلار', 'verbose_name_plural': 'مدل دلار'},
),
migrations.AlterModelOptions(
name='productmodel',
options={'verbose_name': 'محصول', 'verbose_name_plural': 'محصولات'},
),
migrations.AddField(
model_name='productmodel',
name='color',
field=models.CharField(blank=True, max_length=255, null=True, verbose_name='color'),
),
]
@@ -0,0 +1,27 @@
# Generated by Django 5.1.2 on 2025-02-01 15:32
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('product', '0005_alter_dollormodel_options_alter_productmodel_options_and_more'),
]
operations = [
migrations.CreateModel(
name='InStuckColors',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('color', models.CharField(blank=True, max_length=255, null=True, verbose_name='color')),
('in_stuck', models.PositiveIntegerField(default=0)),
('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='colors', to='product.productmodel')),
],
options={
'verbose_name': 'تعداد موجود رنگ',
'verbose_name_plural': 'تعداد موجود رنگ ها',
},
),
]
@@ -0,0 +1,22 @@
# Generated by Django 5.1.2 on 2025-02-01 15:42
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('product', '0006_instuckcolors'),
]
operations = [
migrations.RemoveField(
model_name='productmodel',
name='in_stock',
),
migrations.AlterField(
model_name='instuckcolors',
name='in_stuck',
field=models.PositiveIntegerField(default=0, verbose_name='تعداد موجود'),
),
]
@@ -0,0 +1,22 @@
# Generated by Django 5.1.2 on 2025-02-01 15:54
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('product', '0007_remove_productmodel_in_stock_and_more'),
]
operations = [
migrations.RemoveField(
model_name='productmodel',
name='color',
),
migrations.AlterField(
model_name='instuckcolors',
name='color',
field=models.CharField(blank=True, max_length=255, null=True, verbose_name='رنگ'),
),
]
@@ -0,0 +1,18 @@
# Generated by Django 5.1.2 on 2025-02-01 16:57
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('product', '0008_remove_productmodel_color_alter_instuckcolors_color'),
]
operations = [
migrations.AddField(
model_name='productmodel',
name='related_products',
field=models.ManyToManyField(to='product.productmodel'),
),
]
@@ -0,0 +1,18 @@
# Generated by Django 5.1.2 on 2025-02-01 18:32
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('product', '0009_productmodel_related_products'),
]
operations = [
migrations.AlterField(
model_name='productmodel',
name='related_products',
field=models.ManyToManyField(blank=True, null=True, to='product.productmodel'),
),
]
@@ -0,0 +1,24 @@
# Generated by Django 5.1.2 on 2025-02-01 23:47
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('product', '0010_alter_productmodel_related_products'),
]
operations = [
migrations.AlterField(
model_name='productmodel',
name='category',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='products', to='product.subcategorymodel', verbose_name='دسته بندی محصول'),
),
migrations.AlterField(
model_name='productmodel',
name='related_products',
field=models.ManyToManyField(blank=True, to='product.productmodel'),
),
]
+16 -4
View File
@@ -3,7 +3,7 @@ from django.utils.text import slugify
from account.models import User
from django.urls import reverse
import requests
from django.utils.translation import gettext_lazy as _
class MainCategoryModel(models.Model):
name = models.CharField(max_length=50, verbose_name='نام')
@@ -80,6 +80,9 @@ class DollorModel(models.Model):
return self.defualt_price
return price_in_usd
class Meta:
verbose_name = 'مدل دلار'
verbose_name_plural = 'مدل دلار'
class ProductModel(models.Model):
@@ -100,7 +103,6 @@ class ProductModel(models.Model):
show = models.BooleanField(default=False, verbose_name='نمایش در خانه')
view = models.IntegerField(default=0, verbose_name='بازدید')
sell = models.IntegerField(default=0, verbose_name='فروش')
in_stock = models.IntegerField(default=0, verbose_name="تعداد موجود")
discount = models.SmallIntegerField(default=0, verbose_name='تخفیف')
slug = models.SlugField(max_length=50, unique=True, blank=True, null=True, allow_unicode=True,
verbose_name='نام یکتا', help_text="این فیلد را خالی بگذارید")
@@ -108,8 +110,8 @@ class ProductModel(models.Model):
meta_keywords = models.CharField(max_length=300, blank=True, null=True, help_text='این فیلد را حتما پر کنید')
meta_rating = models.FloatField(default=5, help_text='امتیاز محصول')
created_at = models.DateTimeField(auto_now_add=True, verbose_name='زمان ثبت محصول')
category = models.ForeignKey(SubCategoryModel, blank=True, null=True, on_delete=models.SET_NULL, related_name='products', verbose_name='دسته بندی محصول')
category = models.ForeignKey(SubCategoryModel, null=True, on_delete=models.SET_NULL, related_name='products', verbose_name='دسته بندی محصول')
related_products = models.ManyToManyField('self', blank=True,)
def format_discount_price(self):
discount_price = int(self.price * (100 - self.discount) / 100)
formatted_num = "{:,.0f}".format(discount_price)
@@ -146,6 +148,16 @@ class ProductModel(models.Model):
verbose_name_plural = 'محصولات'
class InStuckColors(models.Model):
color = models.CharField(_("رنگ"), null=True, blank=True, max_length=255)
in_stuck = models.PositiveIntegerField(default=0, verbose_name="تعداد موجود")
product = models.ForeignKey(ProductModel, on_delete=models.CASCADE, related_name='colors')
class Meta:
verbose_name = 'تعداد موجود رنگ'
verbose_name_plural = 'تعداد موجود رنگ ها'
def __str__(self):
return f'{self.product} - {self.color}'
class CommentModel(models.Model):
product = models.ForeignKey(ProductModel, on_delete=models.CASCADE, related_name='comments', verbose_name='محصول')
content = models.TextField(verbose_name='محتوای نظر')
+45 -6
View File
@@ -3,12 +3,39 @@ from rest_framework import serializers
from django.utils import timezone
from datetime import timedelta
class ProductChatSerializer(serializers.ModelSerializer):
class InStuckColorsSerializer(serializers.ModelSerializer):
class Meta:
model = InStuckColors
fields = ['color', 'in_stuck']
class DynamicProductSerializer(serializers.ModelSerializer):
colors = InStuckColorsSerializer(many=True, read_only=True)
price = serializers.SerializerMethodField()
is_new = serializers.SerializerMethodField()
related_products = serializers.SerializerMethodField()
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
view_type = self.context.get('view_type', 'all')
if view_type != 'all':
allowed_fields = self.Meta.view_type[view_type]
allowed = set(allowed_fields)
existing = set(self.fields.keys())
for field_name in existing - allowed:
self.fields.pop(field_name)
class Meta:
model = ProductModel
fields = ['name', 'description', 'price', 'in_stock', 'discount', 'is_new']
fields = "__all__"
view_type = {
'list': ['name', 'price', 'image1', 'video', 'rating', 'discount', 'slug', 'category', 'colors'],
'instance': ['name', 'description', 'price', 'image1', 'image2', 'image3', 'video', 'rating', 'discount', 'slug', 'meta_description', 'meta_keywords', 'meta_rating', 'category', 'colors', 'related_products'],
'chat': ['name', 'description', 'price', 'in_stock', 'discount', 'colors']
}
def get_price(self, obj):
dollor_price = self.context.get('dollor_price')
dollar_to_dirham = 0.27
@@ -21,13 +48,25 @@ class ProductChatSerializer(serializers.ModelSerializer):
elif obj.currency == 'derham':
toman_price = obj.price * dollor_price * dollar_to_dirham
return "{:,.0f} تومان".format(toman_price)
def get_is_new(self, obj):
return timezone.now() < obj.created_at + timedelta(days=7)
class ProductSerializer(ProductChatSerializer):
class Meta:
model = ProductModel
fields = "__all__"
def get_related_products(self, obj):
if obj.related_products.all().count() >= 5:
related_products = obj.related_products.all()
else:
related_products = obj.category.products
serializer = DynamicProductSerializer(
related_products,
many=True,
context={
'view_type': 'list',
'dollor_price': self.context.get('dollor_price')
}
)
return serializer.data
class CommentSerializer(serializers.ModelSerializer):
class Meta:
+5 -5
View File
@@ -49,19 +49,19 @@ class AllCategories(APIView):
return Response(categories_ser.data, status=status.HTTP_200_OK)
class ProductView(APIView):
serializer_class = ProductSerializer
serializer_class = DynamicProductSerializer
permission_classes = [AllowAny]
authentication_classes = []
def get(self, request, pk):
product = get_object_or_404(ProductModel, id=pk)
dollor_object, _ = DollorModel.objects.get_or_create(unique_filed='unique')
dollor_price = dollor_object.price
product_ser = self.serializer_class(instance=product, many=False, context={'dollor_price': dollor_price, 'request': request})
product_ser = self.serializer_class(instance=product, many=False, context={'dollor_price': dollor_price, 'request': request, 'view_type': 'instance'})
return Response(product_ser.data, status=status.HTTP_200_OK)
class AllProductsView(APIView):
serializer_class = ProductSerializer
serializer_class = DynamicProductSerializer
pagination_class = StructurePagination
authentication_classes = []
@extend_schema(
@@ -137,7 +137,7 @@ class AllProductsView(APIView):
"Provide a list of category IDs to filter products by those categories and their subcategories."
),
responses={
200: ProductSerializer(many=True),
200: DynamicProductSerializer(many=True, context={'view_type': 'list'}),
404: OpenApiTypes.OBJECT,
},
)
@@ -191,7 +191,7 @@ class AllProductsView(APIView):
paginated_products = paginator.paginate_queryset(products, request)
dollor_object, _ = DollorModel.objects.get_or_create(unique_filed='unique')
dollor_price = dollor_object.price
serializer = self.serializer_class(paginated_products, many=True, context={'dollor_price': dollor_price, 'request': request})
serializer = self.serializer_class(paginated_products, many=True, context={'dollor_price': dollor_price, 'request': request, 'view_type': 'list'})
return paginator.get_paginated_response(serializer.data)
except MainCategoryModel.DoesNotExist:
+1 -1
View File
@@ -19,7 +19,7 @@ django-dbbackup==4.2.1
django-filter==24.3
django-import-export==4.1.1
django-iranian-cities==1.0.2
django-unfold==0.39.0
django-unfold==0.46.0
djangorestframework==3.15.2
djangorestframework-simplejwt==5.3.1
djoser==2.3.1
+13
View File
@@ -0,0 +1,13 @@
{% extends "admin/base.html" %}
{% block title %}{% if subtitle %}{{ subtitle }} | {% endif %}{{ title }} | {{ site_title|default:_('Django site admin') }}{% endblock %}
{% block branding %}
<h1 id="site-name"><a href="{% url 'admin:index' %}">{{ site_header|default:_('Django administration') }}</a></h1>
{% endblock %}
{% block extrahead %}
{% if plausible_domain %}
<script defer data-domain="{{ plausible_domain }}" src="https://plausible.io/js/script.js"></script>
{% endif %}
{% endblock %}
+93
View File
@@ -0,0 +1,93 @@
{% extends 'admin/base.html' %}
{% load i18n unfold %}
{% block breadcrumbs %}{% endblock %}
{% block title %}
{% trans 'Dashboard' %} | {{ site_title|default:_('Django site admin') }}
{% endblock %}
{% block branding %}
<h1 id="site-name"><a href="{% url 'admin:index' %}">{{ site_header|default:_('Django administration') }}</a></h1>
{% endblock %}
{% block content %}
{% include "unfold/helpers/messages.html" %}
{% component "unfold/components/container.html" %}
<div class="flex flex-col gap-8 mb-12">
<div class="flex gap-4">
{% component "unfold/components/navigation.html" with items=navigation %}{% endcomponent %}
</div>
{% include "formula/service.html" %}
<div class="flex flex-col gap-8 lg:flex-row">
{% for stats in kpi %}
{% component "unfold/components/card.html" with class="lg:w-1/3" label=_("Last 7 days") footer=stats.footer %}
{% component "unfold/components/text.html" %}
{{ stats.title }}
{% endcomponent %}
{% component "unfold/components/title.html" %}
{{ stats.metric }}
{% endcomponent %}
{% endcomponent %}
{% endfor %}
</div>
{% component "unfold/components/card.html" with title=_("Product performance in last 28 days") %}
{% component "unfold/components/chart/bar.html" with data=chart height=320 %}{% endcomponent %}
{% endcomponent %}
<div class="flex flex-col gap-8 lg:flex-row">
{% component "unfold/components/card.html" with class="lg:w-1/2" title=_("The most trending products in last 2 weeks") %}
{% component "unfold/components/title.html" with class="mb-2" %}
$1,234,567.89
{% endcomponent %}
{% component "unfold/components/text.html" %}
{% blocktrans %}
Total revenue between <strong class="font-semibold text-font-important-light dark:text-font-important-dark dark:text-white">1 - 31 October</strong>. Increase <span class="text-green-700 font-semibold dark:text-green-400">+3.14%</span> comparing to previous month <strong class="font-semibold text-font-important-light dark:text-font-important-dark dark:text-white">1 - 30 September</strong>.
{% endblocktrans %}
{% endcomponent %}
{% component "unfold/components/separator.html" %}{% endcomponent %}
<div class="flex flex-col gap-5">
{% for metric in progress %}
{% component "unfold/components/progress.html" with title=metric.title description=metric.description value=metric.value %}{% endcomponent %}
{% endfor %}
</div>
{% endcomponent %}
<div class="flex flex-col gap-8 lg:w-1/2">
{% for stats in performance %}
{% component "unfold/components/card.html" %}
{% component "unfold/components/text.html" %}
{{ stats.title }}
{% endcomponent %}
{% component "unfold/components/title.html" with class="mb-8" %}
{{ stats.metric }}
{% endcomponent %}
{% component "unfold/components/chart/line.html" with data=stats.chart %}{% endcomponent %}
{% endcomponent %}
{% endfor %}
</div>
</div>
</div>
{% endcomponent %}
{% endblock %}
+25
View File
@@ -0,0 +1,25 @@
{% load unfold i18n %}
<div class="bg-base-50 border border-base-200 border-dashed flex flex-col gap-4 p-6 rounded dark:bg-white/[.02] dark:border-base-700 lg:flex-row lg:items-center">
<div class="flex flex-col">
<h2 class="block font-semibold text-font-important-light text-base dark:text-font-important-dark">
{% trans "Are you looking for a custom dashboard?" %}
</h2>
<p class="leading-relaxed max-w-3xl mt-2">
{% trans "Did you decide to start using Unfold in your application but you need help with integration? Feel free to get in touch for consulting, priority support on specific tickets or development services." %}
</p>
</div>
<div class="lg:ml-auto">
{% component "unfold/components/flex.html" with class="flex-col gap-4 lg:flex-row" %}
{% component "unfold/components/button.html" with href="https://unfoldadmin.com/consulting/?utm_medium=referral&amp;utm_source=formula" %}
{% trans "Book a call with Lukas" %}
{% endcomponent %}
{% component "unfold/components/button.html" with href="https://unfoldadmin.com/docs" variant="default" %}
{% trans "I'm good with docs" %}
{% endcomponent %}
{% endcomponent %}
</div>
</div>
+1
View File
@@ -26,6 +26,7 @@ class TicketAdmin(ModelAdmin, ImportExportModelAdmin):
}
}
inlines = [MessageInline]
radio_fields = {'status': admin.VERTICAL}
@admin.register(Message)
class MessageAdmin(ModelAdmin, ImportExportModelAdmin):
@@ -0,0 +1,21 @@
# Generated by Django 5.1.2 on 2025-02-01 15:15
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('ticket', '0001_initial'),
]
operations = [
migrations.AlterModelOptions(
name='message',
options={'verbose_name': 'پیام تیکت', 'verbose_name_plural': 'پیام های تیکت'},
),
migrations.AlterModelOptions(
name='ticket',
options={'verbose_name': 'تیکت', 'verbose_name_plural': 'تیکت ها'},
),
]
@@ -0,0 +1,18 @@
# Generated by Django 5.1.2 on 2025-02-01 23:47
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('ticket', '0002_alter_message_options_alter_ticket_options'),
]
operations = [
migrations.AlterField(
model_name='ticket',
name='status',
field=models.CharField(choices=[('open', 'باز'), ('in_progress', 'در حال پردازش'), ('resolved', 'حل شده'), ('closed', 'بسته')], default='open', max_length=20),
),
]
+13 -3
View File
@@ -3,10 +3,10 @@ from account.models import User
class Ticket(models.Model):
STATUS_CHOICES = [
('open', 'یاز'),
('open', 'باز'),
('in_progress', 'در حال پردازش'),
('resolved', 'حل شده'),
('closed', 'باز'),
('closed', 'بسته'),
]
subject = models.CharField(max_length=255)
@@ -19,6 +19,12 @@ class Ticket(models.Model):
def __str__(self):
return self.subject
class Meta:
verbose_name = 'تیکت'
verbose_name_plural = 'تیکت ها'
class Message(models.Model):
ticket = models.ForeignKey(Ticket, on_delete=models.CASCADE, related_name="messages")
sender = models.ForeignKey(User, on_delete=models.CASCADE)
@@ -26,4 +32,8 @@ class Message(models.Model):
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return f"Message by {self.sender.username} on {self.ticket.subject}"
return f"Message by {self.sender.username} on {self.ticket.subject}"
class Meta:
verbose_name = 'پیام تیکت'
verbose_name_plural = 'پیام های تیکت'
+9
View File
@@ -0,0 +1,9 @@
from order.models import OrderModel
from product.models import DollorModel
def admin_pending_count(request):
pending_count = OrderModel.objects.filter(status='ADMIN_PENDING').count()
return str(pending_count)
def dollor_price(request):
dollor_object, _ = DollorModel.objects.get_or_create(unique_filed='unique')
return str(dollor_object.price)[:2]
+14
View File
@@ -0,0 +1,14 @@
# from unfold.widgets import UnfoldAdminCheckboxSelectMultiple
# from django import forms
# class DriverAdminForm(forms.ModelForm):
# flags = forms.MultipleChoiceField(
# label=("Flags"),
# choices=[
# ("POPULAR", ("Popular")),
# ("FASTEST", ("Fastest")),
# ("TALENTED", ("Talented")),
# ],
# required=False,
# widget=UnfoldAdminCheckboxSelectMultiple,
# )
# form = DriverAdminForm
+4 -4
View File
@@ -23,12 +23,12 @@ const emit = defineEmits(["select"]);
:class="
isSelected
? 'border-cyan-500 ring-2 ring-offset-2 ring-cyan-500'
: 'border-gray-300'
: 'border-slate-200'
"
class="flex flex-col items-center transition-all cursor-pointer w-full gap-4 p-4 border rounded-xl bg-gray-50"
class="flex flex-col items-center transition-all cursor-pointer w-full gap-4 p-4 border rounded-xl bg-slate-50"
>
<span
class="flex items-center justify-start w-full lg:text-[1.125rem] font-semibold text-gray-900"
class="flex items-center justify-start w-full lg:text-[1.125rem] font-semibold text-slate-900"
>
آدرس
</span>
@@ -37,7 +37,7 @@ const emit = defineEmits(["select"]);
class="flex flex-col items-center justify-between w-full gap-4 lg:flex-row"
>
<span
class="w-full text-start text-sm lg:text-[1rem] lg:w-9/12 text-gray-700"
class="w-full text-start text-sm lg:text-[1rem] lg:w-9/12 text-slate-700"
>
{{
!!address
+6 -16
View File
@@ -62,25 +62,15 @@ const addNew = () => {
"
>
<DialogTrigger>
<button
class="flex items-center gap-1 rtl:flex-row-reverse font-iran-yekan-x cursor-pointer"
<Button
:end-icon="!!address ? 'bi:pen' : 'bi:plus'"
size="md"
class="rounded-full"
>
<span class="font-bold text-cyan-500 text-sm lg:text-[1rem]">
<span class="font-bold">
{{ !!address ? "ویرایش آدرس" : "افزودن آدرس" }}
</span>
<Icon
v-if="!!address"
name="bi:pen"
class="**:fill-cyan-500"
size="16"
/>
<Icon
v-else
name="bi:plus"
class="**:stroke-cyan-500"
size="20"
/>
</button>
</Button>
</DialogTrigger>
<DialogPortal>
<DialogOverlay
+17 -13
View File
@@ -10,11 +10,11 @@ const handleDeleteFromCart = () => {};
<template>
<div
class="flex flex-col items-center w-full gap-4 p-4 border lg:flex-row border-gray-300 rounded-xl bg-gray-50"
class="flex flex-col items-center w-full gap-4 p-4 border lg:flex-row border-slate-200 rounded-xl bg-slate-50"
>
<div class="flex items-center justify-start w-full gap-2.5 lg:gap-4">
<div
class="size-[88px] aspect-square shrink-0 rounded-100 border border-gray-300 overflow-hidden"
class="size-[10rem] aspect-square shrink-0 rounded-xl border border-slate-200 overflow-hidden"
>
<img
src="/img/product-1.jpg"
@@ -23,8 +23,12 @@ const handleDeleteFromCart = () => {};
/>
</div>
<div class="flex flex-col w-full gap-4">
<span class="font-semibold lg:text-[1.125rem] text-gray-900">
<div class="flex flex-col w-full gap-5">
<span class="font-semibold typo-sub-h-md text-slate-600">
موبایل
</span>
<span class="font-semibold typo-sub-h-xl text-black">
فشارسنج بازویی امرن Omron M3
</span>
@@ -32,9 +36,9 @@ const handleDeleteFromCart = () => {};
<div class="flex items-center">
<button
@click="counter++"
class="border size-10 flex-center rounded-100 border-gray-400"
class="border size-10 flex-center rounded-100 border-slate-300"
>
<Icon name="bi:plus" class="**:stroke-gray-800" />
<Icon name="bi:plus" class="**:stroke-slate-800" />
</button>
<div class="size-10 flex-center">{{ counter }}</div>
@@ -43,7 +47,7 @@ const handleDeleteFromCart = () => {};
@click="
counter > 1 ? counter-- : handleDeleteFromCart
"
class="border size-10 flex-center rounded-100 border-gray-400"
class="border size-10 flex-center rounded-100 border-slate-300"
>
<Icon
v-if="counter == 1"
@@ -53,12 +57,12 @@ const handleDeleteFromCart = () => {};
<Icon
v-else
name="bi:dash"
class="**:stroke-gray-800"
class="**:stroke-slate-800"
/>
</button>
</div>
<span class="text-[1.25rem] text-gray-900 font-semibold">
<span class="typo-p-lg text-black font-semibold">
۲,۸۹۱,۰۰۰&nbsp;تومان
</span>
</div>
@@ -68,14 +72,14 @@ const handleDeleteFromCart = () => {};
<div class="flex items-center justify-between w-full lg:hidden">
<div class="flex items-center">
<button
class="border size-10 flex-center rounded-100 border-gray-400"
class="border size-10 flex-center rounded-100 border-slate-400"
>
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
class="stroke-gray-800"
class="stroke-slate-800"
xmlns="http://www.w3.org/2000/svg"
>
<path
@@ -96,7 +100,7 @@ const handleDeleteFromCart = () => {};
<div class="size-10 text-[1.125rem] flex-center">1</div>
<button
class="border size-10 flex-center rounded-100 border-gray-400"
class="border size-10 flex-center rounded-100 border-slate-400"
>
<svg
width="16"
@@ -128,7 +132,7 @@ const handleDeleteFromCart = () => {};
</button>
</div>
<span class="text-[1.125rem] text-gray-900 font-semibold">
<span class="text-[1.125rem] text-slate-900 font-semibold">
۲,۸۹۱,۰۰۰&nbsp;تومان
</span>
</div>
+2 -2
View File
@@ -73,13 +73,13 @@ const nav_links = ref<NavLink[]>([
class="**:stroke-black"
/>
</NuxtLink>
<button class="flex-center">
<NuxtLink to="/cart" class="flex-center">
<Icon
name="ci:cart"
size="24px"
class="**:stroke-black"
/>
</button>
</NuxtLink>
</div>
<nav
@@ -17,10 +17,16 @@ defineProps<Props>();
// state
const { data : homeData } = useHomeData();
const { data: homeData, suspense } = useHomeData();
const swiper_instance = ref<SwiperClass | null>(null);
// queries
await useAsyncData(async () => {
await suspense();
});
// methods
const onSwiper = (swiper: SwiperClass) => {
@@ -79,7 +85,10 @@ const onSwiper = (swiper: SwiperClass) => {
</div>
<div class="w-full">
<Swiper :slides-per-view="3" :space-between="24" @swiper="onSwiper">
<SwiperSlide v-for="product in homeData!.products" :key="product.id">
<SwiperSlide
v-for="product in homeData!.products"
:key="product.id"
>
<ProductCard
:id="product.id"
brand="برند محصول"
+73 -58
View File
@@ -28,7 +28,7 @@ const nextPage = computed(() => route.meta.nextPage);
>
<NuxtLink
v-if="prevPage"
:to="{ name: prevPage.name }"
:to="{ name: prevPage?.name }"
class="flex items-center gap-2 text-sm lg:text-[1rem]"
>
<Icon
@@ -36,7 +36,7 @@ const nextPage = computed(() => route.meta.nextPage);
class="**:stroke-cyan-400"
/>
<span class="font-bold text-cyan-400">
{{ prevPage.label }}
{{ prevPage?.label }}
</span>
</NuxtLink>
</div>
@@ -57,68 +57,83 @@ const nextPage = computed(() => route.meta.nextPage);
</div>
<div
class="space-y-[1.25rem] bg-gray-50 p-4 sticky top-44 w-full lg:w-3/12 transition-all border border-gray-300 rounded-xl"
class="flex flex-col bg-slate-50 sticky top-44 w-full lg:w-3/12 transition-all border border-slate-200 rounded-xl"
>
<div
class="flex items-center justify-between w-full text-gray-800"
class="w-full flex items-center justify-between p-5 border-b border-slate-200"
>
<span class="max-w-1/2 text-sm lg:text-[1rem]">
جمع سبد خرید:
</span>
<span class="max-w-1/2 text-sm lg:text-[1rem]">
۳,۲۹۱,۰۰۰ تومان
</span>
</div>
<div
class="flex items-center justify-between w-full text-status-error-primary"
>
<span class="max-w-1/2 text-sm lg:text-[1rem]">
تخفیف:
</span>
<span class="max-w-1/2 text-sm lg:text-[1rem]">
۹۰۰,۰۰۰ تومان
</span>
</div>
<div
class="flex items-center justify-between w-full text-gray-900"
>
<span class="max-w-1/2 text-sm lg:text-[1rem]">
جمع کل:
</span>
<span class="max-w-1/2 text-sm lg:text-[1rem]">
۲,۳۹۱,۰۰۰ تومان
</span>
</div>
<label
v-if="route.name == 'cart-checkout'"
class="flex items-center w-full group gap-2 p-3 my-5 text-sm transition-all border text-gray-600 focus-within:ring-2 focus-within:ring-offset-2 focus-within:ring-cyan-500 bg-gray-50 border-gray-200 rounded-100"
>
<input
type="text"
placeholder="کد تخفیف"
class="w-full border-none focus:border-none focus:outline-none placeholder:text-gray-600 h-[22px]"
<span class="typo-sub-h-xl text-black"
>فاکتور خرید</span
>
<Icon
name="ci:cart"
class="**:stroke-black"
size="24"
/>
<button
class="ring ring-offset-[-4px] active:ring-offset-2 transition-all duration-75 font-bold text-cyan-500 rounded-50"
>
ثبت
</button>
</label>
</div>
<NuxtLink :to="{ name: nextPage.name }">
<Button
start-icon="bi:arrow-right"
class="w-full rounded-full"
<div class="flex flex-col p-5 gap-[1rem]">
<div
class="flex items-center justify-between w-full text-slate-800"
>
{{ nextPage.label }}
</Button>
</NuxtLink>
<span class="max-w-1/2 text-sm lg:text-[1rem]">
جمع سبد خرید:
</span>
<span class="max-w-1/2 text-sm lg:text-[1rem]">
۳,۲۹۱,۰۰۰ تومان
</span>
</div>
<div
class="flex items-center justify-between w-full text-status-error-primary"
>
<span class="max-w-1/2 text-sm lg:text-[1rem]">
تخفیف:
</span>
<span class="max-w-1/2 text-sm lg:text-[1rem]">
۹۰۰,۰۰۰ تومان
</span>
</div>
<div
class="flex items-center justify-between w-full text-slate-900"
>
<span class="max-w-1/2 text-sm lg:text-[1rem]">
جمع کل:
</span>
<span class="max-w-1/2 text-sm lg:text-[1rem]">
۲,۳۹۱,۰۰۰ تومان
</span>
</div>
<label
v-if="route.name == 'cart-checkout'"
class="flex items-center w-full group gap-2 p-3 text-sm transition-all border text-slate-600 focus-within:ring-2 focus-within:ring-offset-2 focus-within:ring-cyan-500 bg-slate-50 border-slate-200 rounded-100"
>
<input
type="text"
placeholder="کد تخفیف"
class="w-full border-none focus:border-none focus:outline-none placeholder:text-slate-600 h-[22px]"
/>
<button
class="ring ring-offset-[-4px] active:ring-offset-2 transition-all duration-75 font-bold text-cyan-500 rounded-50"
>
ثبت
</button>
</label>
<NuxtLink :to="{ name: nextPage?.name }">
<Button
start-icon="bi:arrow-right"
class="w-full rounded-full mt-2"
>
{{ nextPage?.label }}
</Button>
</NuxtLink>
</div>
</div>
</div>
</div>
+3 -6
View File
@@ -69,7 +69,7 @@ const selectedGateway = ref<PaymentGateway>(paymentGateways.value[0]);
:key="index"
:class="
selectedGateway.id == gateway.id
? 'ring-2 ring-offset-2 ring-cyan-500 border-cyan-500'
? 'ring-2 ring-offset-2 ring-black border-black'
: 'border-slate-200'
"
class="w-full p-5 border rounded-xl flex flex-col gap-4 transition-all cursor-pointer"
@@ -99,13 +99,10 @@ const selectedGateway = ref<PaymentGateway>(paymentGateways.value[0]);
<div class="h-7 flex-center col-span-full lg:hidden">
<button class="gap-2 flex-center">
<span class="text-sm font-bold text-cyan-500">
<span class="text-sm font-bold text-black">
مشاهده بیشتر
</span>
<Icon
name="bi:chevron-down"
class="**:stroke-cyan-500"
/>
<Icon name="bi:chevron-down" class="**:stroke-black" />
</button>
</div>
</div>
+20 -23
View File
@@ -60,96 +60,96 @@ const handleSelectAddress = (address: Address) => {
</div>
<div
class="flex flex-col items-center w-full gap-4 p-4 border border-gray-300 rounded-xl bg-gray-50"
class="flex flex-col items-center w-full gap-4 p-4 border border-slate-200 rounded-xl bg-slate-50"
>
<span
class="flex items-center justify-start w-full lg:text-[1.125rem] font-semibold text-gray-900"
class="flex items-center justify-start w-full lg:text-[1.125rem] font-semibold text-slate-900"
>
زمان و شیوه ارسال
شیوه ارسال
</span>
<label
@click="deliveryData.deliveryMethod.pishtaz = true"
:class="
deliveryData.deliveryMethod.pishtaz
? 'ring-cyan-500 ring-offset-2 ring-2'
? 'ring-black ring-offset-2 ring-2'
: ''
"
class="flex flex-col select-none w-full gap-2 p-3 transition-all border cursor-pointer delivery-option focus-within:ring-2 ring-cyan-500 ring-offset-2 focus-within:border-cyan-500 rounded-100 border-gray-300 bg-gray-50"
class="flex flex-col select-none w-full gap-2 p-3 transition-all border cursor-pointer delivery-option focus-within:ring-2 ring-black ring-offset-2 focus-within:border-black rounded-100 border-slate-200 bg-slate-50"
>
<div class="flex items-center justify-between w-full">
<div class="flex items-center gap-2.5">
<SwitchRoot
v-model="deliveryData.deliveryMethod.pishtaz"
:defaultValue="false"
class="w-[3rem] h-[1.8rem] shrink-0 flex data-[state=unchecked]:bg-slate-200 data-[state=checked]:bg-cyan-500 border border-slate-300 data-[state=checked]:border-cyan-800/20 rounded-full relative transition-all focus-within:outline-none"
class="w-[3rem] h-[1.8rem] shrink-0 flex data-[state=unchecked]:bg-slate-200 data-[state=checked]:bg-black border border-slate-200 data-[state=checked]:border-black/20 rounded-full relative transition-all focus-within:outline-none"
>
<SwitchThumb
class="size-6 my-auto bg-white text-sm ms-1 flex items-center justify-center shadow-xl rounded-full transition-transform translate-x-0.5 will-change-transform data-[state=checked]:-translate-x-[68%]"
/>
</SwitchRoot>
<span
class="w-full text-gray-800 text-sm lg:text-[1rem]"
class="w-full text-slate-800 text-sm lg:text-[1rem]"
>پست پیشتاز</span
>
</div>
<span class="text-gray-800 text-sm lg:text-[1rem]">
<span class="text-slate-800 text-sm lg:text-[1rem]">
۱۵۰٬۰۰۰ تومان
</span>
</div>
</label>
<label
class="flex items-center opacity-50 select-none pointer-events-none justify-between w-full p-3 transition-all border cursor-pointer delivery-option focus-within:ring-2 ring-cyan-500 ring-offset-2 focus-within:border-cyan-500 rounded-100 border-gray-300 bg-gray-50"
class="flex items-center opacity-50 select-none pointer-events-none justify-between w-full p-3 transition-all border cursor-pointer delivery-option focus-within:ring-2 ring-black ring-offset-2 focus-within:border-black rounded-100 border-slate-200 bg-slate-50"
>
<div class="flex items-center gap-2.5">
<SwitchRoot
v-model="deliveryData.deliveryMethod.tipax"
class="w-[3rem] h-[1.8rem] shrink-0 flex data-[state=unchecked]:bg-slate-200 data-[state=checked]:bg-cyan-500 border border-slate-300 data-[state=checked]:border-cyan-800/20 rounded-full relative transition-all focus-within:outline-none"
class="w-[3rem] h-[1.8rem] shrink-0 flex data-[state=unchecked]:bg-slate-200 data-[state=checked]:bg-black border border-slate-200 data-[state=checked]:border-black/20 rounded-full relative transition-all focus-within:outline-none"
>
<SwitchThumb
class="size-6 my-auto bg-white text-sm ms-1 flex items-center justify-center shadow-xl rounded-full transition-transform translate-x-0.5 will-change-transform data-[state=checked]:-translate-x-[68%]"
/>
</SwitchRoot>
<span class="w-full text-gray-800 text-sm lg:text-[1rem]"
<span class="w-full text-slate-800 text-sm lg:text-[1rem]"
>تیپاکس</span
>
</div>
<span class="text-gray-800 text-sm lg:text-[1rem]">
<span class="text-slate-800 text-sm lg:text-[1rem]">
۱۵۰٬۰۰۰ تومان
</span>
</label>
<label
class="flex items-center opacity-50 select-none pointer-events-none justify-between w-full p-3 transition-all border cursor-pointer delivery-option focus-within:ring-2 ring-cyan-500 ring-offset-2 focus-within:border-cyan-500 rounded-100 border-gray-300 bg-gray-50"
class="flex items-center opacity-50 select-none pointer-events-none justify-between w-full p-3 transition-all border cursor-pointer delivery-option focus-within:ring-2 ring-black ring-offset-2 focus-within:border-black rounded-100 border-slate-200 bg-slate-50"
>
<div class="flex items-center gap-2.5">
<SwitchRoot
v-model="deliveryData.deliveryMethod.peyk"
class="w-[3rem] h-[1.8rem] shrink-0 flex data-[state=unchecked]:bg-slate-200 data-[state=checked]:bg-cyan-500 border border-slate-300 data-[state=checked]:border-cyan-800/20 rounded-full relative transition-all focus-within:outline-none"
class="w-[3rem] h-[1.8rem] shrink-0 flex data-[state=unchecked]:bg-slate-200 data-[state=checked]:bg-black border border-slate-200 data-[state=checked]:border-black/20 rounded-full relative transition-all focus-within:outline-none"
>
<SwitchThumb
class="size-6 my-auto bg-white text-sm ms-1 flex items-center justify-center shadow-xl rounded-full transition-transform translate-x-0.5 will-change-transform data-[state=checked]:-translate-x-[68%]"
/>
</SwitchRoot>
<span class="w-full text-gray-800 text-sm lg:text-[1rem]"
<span class="w-full text-slate-800 text-sm lg:text-[1rem]"
>ارسال با پیک (فقط ارسال درون شهری شیراز)</span
>
</div>
<span class="text-gray-800 text-sm lg:text-[1rem]">
<span class="text-slate-800 text-sm lg:text-[1rem]">
۱۵۰٬۰۰۰ تومان
</span>
</label>
</div>
<div
class="flex flex-col items-center w-full gap-4 p-4 border lg:gap-6 border-gray-300 rounded-xl bg-gray-50"
class="flex flex-col items-center w-full gap-4 p-4 border lg:gap-6 border-slate-200 rounded-xl bg-slate-50"
>
<span
class="flex items-center justify-start w-full lg:text-[1.125rem] font-semibold text-gray-900"
class="flex items-center justify-start w-full lg:text-[1.125rem] font-semibold text-slate-900"
>
خلاصه سفارش
</span>
@@ -161,13 +161,10 @@ const handleSelectAddress = (address: Address) => {
<div class="h-7 flex-center col-span-full lg:hidden">
<button class="gap-2 flex-center">
<span class="text-sm font-bold text-cyan-500">
<span class="text-sm font-bold text-black">
مشاهده بیشتر
</span>
<Icon
name="bi:chevron-down"
class="**:stroke-cyan-500"
/>
<Icon name="bi:chevron-down" class="**:stroke-black" />
</button>
</div>
</div>
+1
View File
@@ -2,6 +2,7 @@
definePageMeta({
layout: "cart",
pageTitle: "سبد خرید",
prevPage: { name: "index", label: "بازگشت به خانه" },
nextPage: { name: "cart-delivery", label: "انتخاب آدرس" },
});
</script>