Merge remote-tracking branch 'origin/main'
This commit is contained in:
+2
-2
@@ -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
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
]
|
||||
@@ -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}"
|
||||
@@ -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/'),
|
||||
),
|
||||
]
|
||||
@@ -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):
|
||||
|
||||
@@ -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,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
@@ -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):
|
||||
|
||||
@@ -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> 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> 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> 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> 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> 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': 'مدل عکس تفاوت خانه'},
|
||||
),
|
||||
]
|
||||
@@ -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 = 'مدل عکس تفاوت خانه'
|
||||
@@ -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})
|
||||
|
||||
@@ -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):
|
||||
|
||||
+26
@@ -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='تعداد موجود'),
|
||||
),
|
||||
]
|
||||
+22
@@ -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'),
|
||||
),
|
||||
]
|
||||
@@ -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='محتوای نظر')
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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&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>
|
||||
@@ -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),
|
||||
),
|
||||
]
|
||||
@@ -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 = 'پیام های تیکت'
|
||||
@@ -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]
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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">
|
||||
۲,۸۹۱,۰۰۰ تومان
|
||||
</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">
|
||||
۲,۸۹۱,۰۰۰ تومان
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
definePageMeta({
|
||||
layout: "cart",
|
||||
pageTitle: "سبد خرید",
|
||||
prevPage: { name: "index", label: "بازگشت به خانه" },
|
||||
nextPage: { name: "cart-delivery", label: "انتخاب آدرس" },
|
||||
});
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user