This commit is contained in:
Mamalizz
2025-01-28 23:32:51 +03:30
51 changed files with 717 additions and 307 deletions
+22 -2
View File
@@ -2,8 +2,28 @@ from django.contrib import admin
from .models import *
from unfold.admin import ModelAdmin
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
@admin.register(User)
class UserAdmin(ModelAdmin):
class UserAdmin(ModelAdmin, ImportExportModelAdmin):
list_display = ['phone', 'email', 'is_superuser']
readonly_fields = ['password', 'last_login', 'otp_expiry', 'otp_hash']
readonly_fields = ['phone']
exclude = ('otp_hash', 'otp_expiry', 'is_active', 'is_staff', 'password', 'last_login')
import_form_class = ImportForm
export_form_class = ExportForm
compressed_fields = True
warn_unsaved_form = True
formfield_overrides = {
ArrayField: {
"widget": ArrayWidget,
}
}
+1 -1
View File
@@ -1,4 +1,4 @@
# Generated by Django 5.1.2 on 2025-01-27 18:07
# Generated by Django 5.1.2 on 2025-01-28 17:00
import django.db.models.deletion
from django.conf import settings
+19 -2
View File
@@ -2,7 +2,24 @@ from django.contrib import admin
from .models import *
from unfold.admin import ModelAdmin
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
@admin.register(ProductChatModel)
class ProductChatAdmin(ModelAdmin):
pass
class ProductChatAdmin(ModelAdmin, ImportExportModelAdmin):
import_form_class = ImportForm
export_form_class = ExportForm
readonly_fields = ('user', 'product', 'thread')
compressed_fields = True
warn_unsaved_form = True
formfield_overrides = {
ArrayField: {
"widget": ArrayWidget,
}
}
+1 -1
View File
@@ -1,4 +1,4 @@
# Generated by Django 5.1.2 on 2025-01-27 18:07
# Generated by Django 5.1.2 on 2025-01-28 17:00
import django.db.models.deletion
from django.conf import settings
+3 -2
View File
@@ -27,13 +27,13 @@ EMAIL_HOST_PASSWORD = os.getenv("EMAIL_HOST_PASSWORD")
DEFAULT_FROM_EMAIL = os.getenv("SECRET_KEY")
SECRET_KEY = os.getenv("SECRET_KEY")
DEBUG = False
DEBUG = True
# in production lists of allowed hosts and allowed orgins will genrate
# in development every host and orgin will be true
# in prodcution it will use the postgres info you enterd in .env.local
# in development it will use the sqlite
BASE_DIR = Path(__file__).resolve().parent.parent
if not DEBUG:
if DEBUG:
ALLOWED_HOSTS = ['127.0.0.1', 'localhost', DOMAIN, API_DOMAIN]
CSRF_TRUSTED_ORIGINS = [
f"https://{DOMAIN}",
@@ -103,6 +103,7 @@ INSTALLED_APPS = [
'rest_framework_simplejwt',
'rest_framework_simplejwt.token_blacklist',
'rest_framework.authtoken',
'import_export',
# custom apps
'product',
'account',
+31 -4
View File
@@ -2,11 +2,38 @@ from django.contrib import admin
from .models import *
from unfold.admin import ModelAdmin
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
@admin.register(SliderModel)
class SliderAdmin(ModelAdmin):
pass
class SliderAdmin(ModelAdmin, ImportExportModelAdmin):
import_form_class = ImportForm
export_form_class = ExportForm
compressed_fields = False
warn_unsaved_form = True
formfield_overrides = {
ArrayField: {
"widget": ArrayWidget,
}
}
@admin.register(HomeImageModel)
class HomeImageAdmin(ModelAdmin):
pass
class HomeImageAdmin(ModelAdmin, ImportExportModelAdmin):
import_form_class = ImportForm
export_form_class = ExportForm
compressed_fields = True
warn_unsaved_form = True
formfield_overrides = {
ArrayField: {
"widget": ArrayWidget,
}
}
+18 -4
View File
@@ -1,6 +1,5 @@
# Generated by Django 5.1.2 on 2025-01-27 18:07
# Generated by Django 5.1.2 on 2025-01-28 17:00
import django.db.models.deletion
from django.db import migrations, models
@@ -9,18 +8,33 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
('product', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='HomeImageModel',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('image1', models.ImageField(upload_to='diff_image/')),
('image2', models.ImageField(upload_to='diff_image/')),
('title1', models.CharField(max_length=50)),
('title2', models.CharField(max_length=50)),
('description1', models.TextField()),
('description2', models.TextField()),
('link1', models.URLField()),
('link2', models.URLField()),
('unique_filed', models.CharField(choices=[('unique', 'unique')], default='unique', max_length=20, unique=True)),
],
),
migrations.CreateModel(
name='SliderModel',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('link', models.URLField(verbose_name='لینک')),
('title', models.CharField(max_length=50, verbose_name='عنوان')),
('description', models.TextField(verbose_name='توضیحات')),
('image', models.ImageField(blank=True, null=True, upload_to='slider_image/', verbose_name='عکس اسلایدر')),
('video', models.FileField(blank=True, null=True, upload_to='slider_video/', verbose_name='ویدیواسلایدر')),
('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='product.productmodel', verbose_name='محصول')),
],
options={
'verbose_name': 'اسلایدر',
@@ -1,29 +0,0 @@
# Generated by Django 5.1.2 on 2025-01-27 18:15
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('home', '0001_initial'),
]
operations = [
migrations.RemoveField(
model_name='slidermodel',
name='product',
),
migrations.AddField(
model_name='slidermodel',
name='description',
field=models.TextField(default='', verbose_name='توضیحات'),
preserve_default=False,
),
migrations.AddField(
model_name='slidermodel',
name='link',
field=models.URLField(default='', verbose_name='لینک'),
preserve_default=False,
),
]
@@ -1,26 +0,0 @@
# Generated by Django 5.1.2 on 2025-01-27 18:31
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('home', '0002_remove_slidermodel_product_slidermodel_description_and_more'),
]
operations = [
migrations.CreateModel(
name='HomeImageModel',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('image1', models.ImageField(upload_to='diff_image/')),
('image2', models.ImageField(upload_to='diff_image/')),
('title1', models.CharField(max_length=50)),
('title2', models.CharField(max_length=50)),
('description1', models.TextField()),
('description2', models.TextField()),
('unique_filed', models.CharField(choices=[('unique', 'unique')], default='unique', max_length=20, unique=True)),
],
),
]
@@ -1,25 +0,0 @@
# Generated by Django 5.1.2 on 2025-01-27 18:33
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('home', '0003_homeimagemodel'),
]
operations = [
migrations.AddField(
model_name='homeimagemodel',
name='link1',
field=models.URLField(default=''),
preserve_default=False,
),
migrations.AddField(
model_name='homeimagemodel',
name='link2',
field=models.URLField(default=''),
preserve_default=False,
),
]
+35 -1
View File
@@ -1,3 +1,37 @@
from django.contrib import admin
from .models import *
from unfold.admin import ModelAdmin, TabularInline
# Register your models here.
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
class OrderItemModelInline(TabularInline):
model = OrderItemModel
extra = 0
max_num = 0
def has_delete_permission(self, request, obj=None):
return False
def get_readonly_fields(self, request, obj=None):
return [field.name for field in self.model._meta.fields]
@admin.register(OrderModel)
class OrderAdmin(ModelAdmin, ImportExportModelAdmin):
import_form_class = ImportForm
export_form_class = ExportForm
compressed_fields = True
warn_unsaved_form = True
formfield_overrides = {
ArrayField: {
"widget": ArrayWidget,
}
}
inlines = [OrderItemModelInline]
+1 -1
View File
@@ -1,4 +1,4 @@
# Generated by Django 5.1.2 on 2025-01-27 18:07
# Generated by Django 5.1.2 on 2025-01-28 17:00
import django.db.models.deletion
from django.conf import settings
+77 -9
View File
@@ -2,23 +2,91 @@ from django.contrib import admin
from .models import *
from unfold.admin import ModelAdmin
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
@admin.register(ProductModel)
class ProductModelAdmin(ModelAdmin):
pass
class ProductModelAdmin(ModelAdmin, ImportExportModelAdmin):
import_form_class = ImportForm
export_form_class = ExportForm
readonly_fields = ('slug', )
compressed_fields = True
warn_unsaved_form = True
formfield_overrides = {
models.TextField: {
"widget": WysiwygWidget,
},
ArrayField: {
"widget": ArrayWidget,
}
}
@admin.register(MainCategoryModel)
class MainCategoryModelAdmin(ModelAdmin):
pass
class MainCategoryModelAdmin(ModelAdmin, ImportExportModelAdmin):
import_form_class = ImportForm
export_form_class = ExportForm
readonly_fields = ('slug', )
compressed_fields = True
warn_unsaved_form = True
formfield_overrides = {
ArrayField: {
"widget": ArrayWidget,
}
}
@admin.register(SubCategoryModel)
class SubCategoryModelAdmin(ModelAdmin):
pass
class SubCategoryModelAdmin(ModelAdmin, ImportExportModelAdmin):
import_form_class = ImportForm
export_form_class = ExportForm
readonly_fields = ('slug', )
compressed_fields = True
warn_unsaved_form = True
formfield_overrides = {
ArrayField: {
"widget": ArrayWidget,
}
}
@admin.register(CommentModel)
class CommentAdmin(ModelAdmin):
pass
class CommentAdmin(ModelAdmin, ImportExportModelAdmin):
import_form_class = ImportForm
export_form_class = ExportForm
compressed_fields = True
warn_unsaved_form = True
formfield_overrides = {
ArrayField: {
"widget": ArrayWidget,
}
}
@admin.register(DollorModel)
class DollorAdmin(ModelAdmin):
class DollorAdmin(ModelAdmin, ImportExportModelAdmin):
import_form_class = ImportForm
export_form_class = ExportForm
exclude = ('unique_filed', )
compressed_fields = True
warn_unsaved_form = True
formfield_overrides = {
ArrayField: {
"widget": ArrayWidget,
}
}
readonly_fields = ('price',)
+7 -3
View File
@@ -1,4 +1,4 @@
# Generated by Django 5.1.2 on 2025-01-27 18:07
# Generated by Django 5.1.2 on 2025-01-28 17:00
import django.db.models.deletion
from django.conf import settings
@@ -81,7 +81,12 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='SubCategoryModel',
fields=[
('maincategorymodel_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='product.maincategorymodel')),
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=50, verbose_name='نام')),
('slug', models.SlugField(help_text='اسم دسته را برای مسیر به انگلیسی و بدون فاصله وارد کنید', unique=True)),
('icon', models.ImageField(blank=True, null=True, upload_to='category_model/', verbose_name='آیکون')),
('meta_title', models.CharField(blank=True, help_text='عنوان متا برای SEO', max_length=60, null=True, verbose_name='عنوان متا')),
('meta_description', models.TextField(blank=True, help_text='توضیحات متا برای SEO', max_length=160, null=True, verbose_name='توضیحات متا')),
('show', models.BooleanField(default=False, verbose_name='نمایش در خانه')),
('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='subcategorys', to='product.maincategorymodel', verbose_name='دسته\u200cبندی والد')),
],
@@ -89,7 +94,6 @@ class Migration(migrations.Migration):
'verbose_name': 'زیر دسته\u200cبندی',
'verbose_name_plural': 'زیر دسته\u200cبندی\u200cها',
},
bases=('product.maincategorymodel',),
),
migrations.AddField(
model_name='productmodel',
@@ -0,0 +1,18 @@
# Generated by Django 5.1.2 on 2025-01-28 17:48
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('product', '0001_initial'),
]
operations = [
migrations.RenameField(
model_name='subcategorymodel',
old_name='icon',
new_name='image',
),
]
@@ -0,0 +1,23 @@
# Generated by Django 5.1.2 on 2025-01-28 17:49
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('product', '0002_rename_icon_subcategorymodel_image'),
]
operations = [
migrations.AddField(
model_name='subcategorymodel',
name='icon',
field=models.ImageField(blank=True, null=True, upload_to='category_model/', verbose_name='آیکون'),
),
migrations.AlterField(
model_name='subcategorymodel',
name='image',
field=models.ImageField(blank=True, null=True, upload_to='category_model/', verbose_name='عکس'),
),
]
@@ -0,0 +1,20 @@
# Generated by Django 5.1.2 on 2025-01-28 18:53
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('product', '0003_subcategorymodel_icon_alter_subcategorymodel_image'),
]
operations = [
migrations.AlterField(
model_name='subcategorymodel',
name='parent',
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, related_name='subcategorys', to='product.maincategorymodel', verbose_name='دسته\u200cبندی والد'),
preserve_default=False,
),
]
+20 -4
View File
@@ -19,16 +19,32 @@ class MainCategoryModel(models.Model):
def __str__(self):
return self.name
# def get_absolute_url(self):
# return reverse('category_detail', kwargs={'slug': self.slug})
def save(self, *args, **kwargs):
if not self.slug:
self.slug = slugify(self.name, allow_unicode=True)
super().save(*args, **kwargs)
class SubCategoryModel(MainCategoryModel):
parent = models.ForeignKey(MainCategoryModel, on_delete=models.CASCADE, related_name='subcategorys', null=True, blank=True, verbose_name='دسته‌بندی والد')
class SubCategoryModel(models.Model):
name = models.CharField(max_length=50, verbose_name='نام')
slug = models.SlugField(max_length=50, unique=True, help_text="اسم دسته را برای مسیر به انگلیسی و بدون فاصله وارد کنید")
image = models.ImageField(upload_to='category_model/',verbose_name='عکس', blank=True, null=True)
icon = models.ImageField(upload_to='category_model/',verbose_name='آیکون', blank=True, null=True)
meta_title = models.CharField(max_length=60, verbose_name="عنوان متا", help_text="عنوان متا برای SEO", blank=True, null=True)
meta_description = models.TextField(max_length=160, verbose_name="توضیحات متا", help_text="توضیحات متا برای SEO", blank=True, null=True)
parent = models.ForeignKey(MainCategoryModel, on_delete=models.CASCADE, related_name='subcategorys', verbose_name='دسته‌بندی والد')
show = models.BooleanField(default=False, verbose_name='نمایش در خانه')
class Meta:
verbose_name = "زیر دسته‌بندی"
verbose_name_plural = "زیر دسته‌بندی‌ها"
def __str__(self):
return self.name
def save(self, *args, **kwargs):
if not self.slug:
self.slug = slugify(self.name, allow_unicode=True)
super().save(*args, **kwargs)
class DollorModel(models.Model):
price = models.FloatField(null=True, blank=True, verbose_name='قیمت دلار')
+4 -1
View File
@@ -37,11 +37,14 @@ class CommentSerializer(serializers.ModelSerializer):
class SubCategorySerializer(serializers.ModelSerializer):
product_count = serializers.SerializerMethodField()
parent = serializers.SerializerMethodField()
class Meta:
model = SubCategoryModel
fields = ['id', 'name', 'slug','icon', 'meta_title', 'meta_description', 'product_count', 'show']
fields = ['id', 'name', 'slug','icon', 'meta_title', 'meta_description', 'product_count', 'show', 'parent', 'image']
def get_product_count(self, obj):
return obj.products.count()
def get_parent(self, obj):
return obj.parent.name
class MainCategorySerializer(serializers.ModelSerializer):
+1 -1
View File
@@ -56,7 +56,7 @@ class ProductView(APIView):
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})
product_ser = self.serializer_class(instance=product, many=False, context={'dollor_price': dollor_price, 'request': request})
return Response(product_ser.data, status=status.HTTP_200_OK)
+44 -1
View File
@@ -1,3 +1,46 @@
from django.contrib import admin
from .models import *
from unfold.admin import ModelAdmin, TabularInline
# Register your models here.
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
class MessageInline(TabularInline):
model = Message
extra = 1
@admin.register(Ticket)
class TicketAdmin(ModelAdmin, ImportExportModelAdmin):
import_form_class = ImportForm
export_form_class = ExportForm
compressed_fields = True
warn_unsaved_form = True
formfield_overrides = {
ArrayField: {
"widget": ArrayWidget,
}
}
inlines = [MessageInline]
@admin.register(Message)
class MessageAdmin(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,
}
}
+2 -2
View File
@@ -1,4 +1,4 @@
# Generated by Django 5.1.2 on 2025-01-27 18:07
# Generated by Django 5.1.2 on 2025-01-28 17:00
import django.db.models.deletion
from django.conf import settings
@@ -19,7 +19,7 @@ class Migration(migrations.Migration):
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('subject', models.CharField(max_length=255)),
('status', models.CharField(choices=[('open', 'Open'), ('in_progress', 'In Progress'), ('resolved', 'Resolved'), ('closed', 'Closed')], default='open', max_length=20)),
('status', models.CharField(choices=[('open', 'یاز'), ('in_progress', 'در حال پردازش'), ('resolved', 'حل شده'), ('closed', 'باز')], default='open', max_length=20)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('admin', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='assigned_tickets', to=settings.AUTH_USER_MODEL)),
+4 -4
View File
@@ -3,10 +3,10 @@ from account.models import User
class Ticket(models.Model):
STATUS_CHOICES = [
('open', 'Open'),
('in_progress', 'In Progress'),
('resolved', 'Resolved'),
('closed', 'Closed'),
('open', 'یاز'),
('in_progress', 'در حال پردازش'),
('resolved', 'حل شده'),
('closed', 'باز'),
]
subject = models.CharField(max_length=255)
+1 -1
View File
@@ -23,7 +23,7 @@ const { colorObject } = useImageColor(`#category-image-${id.value}`);
</script>
<template>
<div class="relative rounded-150 overflow-hidden w-full h-[500px] bg-white">
<div class="relative rounded-150 overflow-hidden w-full h-[500px] bg-white brightness-[97%]">
<img
:id="`category-image-${id}`"
class="absolute object-contain size-full"
+1 -1
View File
@@ -14,7 +14,7 @@ defineProps<Props>();
<template>
<div
class="size-[30px] ring ring-offset-1 rounded-full shadow-black/30 shadow-inner"
class="size-[25px] rounded-full shadow-black/30 shadow-inner"
:class="selected ? 'ring-black' : 'ring-transparent'"
/>
</template>
+15 -9
View File
@@ -57,13 +57,13 @@ watch(
class="!bg-transparent outline-none text-black h-full selection:bg-slate-100 placeholder-slate-400"
:placeholder="placeholder"
/>
<ComboboxTrigger>
<ComboboxTrigger class="cursor-pointer">
<Icon name="ci:chevron-down" class="size-5" />
</ComboboxTrigger>
</ComboboxAnchor>
<ComboboxContent
class="absolute z-10 w-full mt-1.5 bg-slate-50 overflow-hidden rounded-xl shadow-sm border border-slate-200 will-change-[opacity,transform] data-[side=top]:animate-slideDownAndFade data-[side=bottom]:animate-slideUpAndFade"
class="absolute z-10 w-full mt-4 bg-slate-50 overflow-hidden rounded-xl shadow-sm border border-slate-200 will-change-[opacity,transform] data-[side=top]:animate-slideDownAndFade data-[side=bottom]:animate-slideUpAndFade"
>
<ComboboxViewport class="p-[1rem]">
<ComboboxEmpty
@@ -74,29 +74,35 @@ watch(
<ComboboxGroup>
<ComboboxSeparator
v-if="index !== 0"
class="h-[1px] bg-slate-200 m-[1rem]"
class="h-6"
/>
<ComboboxLabel
class="px-[1.2rem] w-full text-sm bg-black text-white rounded-full leading-[25px] py-1.5"
class="flex items-center justify-between px-[1.2rem] w-full text-md text-black font-bold bg-slate-200/50 leading-[25px] py-3 rounded-lg"
>
{{ group.name }}
<span>
{{ group.name }}
</span>
<Icon name="ci:delivery-boxes" size="18px"/>
</ComboboxLabel>
<ComboboxItem
v-for="option in group.children"
:key="option.name"
:value="option.name"
class="text-sm leading-none text-black/90 my-1.5 rounded-full hover:!bg-slate-200 flex items-center py-2.5 px-[1.2rem] relative select-none data-[disabled]:text-slate-50 data-[disabled]:pointer-events-none"
class="text-sm cursor-pointer leading-none text-slate-700 my-1.5 rounded-md hover:bg-slate-200/25 flex items-center py-2.5 px-[1.2rem] relative select-none data-[disabled]:text-slate-50 data-[disabled]:pointer-events-none"
>
<ComboboxItemIndicator
class="absolute left-3 w-[25px] inline-flex items-center justify-center"
>
<Icon name="ci:checkmark" size="18" />
</ComboboxItemIndicator>
<span>
{{ option.name }}
</span>
<div class="flex items-center gap-2">
<Icon name="ci:minus" class="opacity-50"/>
<span>
{{ option.name }}
</span>
</div>
</ComboboxItem>
</ComboboxGroup>
</template>
+39 -21
View File
@@ -3,7 +3,6 @@
import useGetAccount from "~/composables/api/account/useGetAccount";
import { useAuth } from "~/composables/api/auth/useAuth";
import useBaseUrl from "~/composables/global/useBaseUrl";
// types
@@ -16,20 +15,19 @@ type NavLink = {
const { data: account } = useGetAccount();
const { logout } = useAuth();
const baseUrl = useBaseUrl();
const nav_links = ref<NavLink[]>([
{
title: "فروشگاه",
path: "#",
title: "خانه",
path: "/",
},
{
title: "محصولات",
path: "/products",
},
{
title: "دسته بندی ها",
path: "#",
},
{
title: "جستجو",
path: "#",
path: "/category",
},
{
title: "ارتباط با ما",
@@ -53,28 +51,32 @@ const nav_links = ref<NavLink[]>([
v-if="!!account"
:title="account.first_name + ' ' + account.last_name"
@click="() => logout(true)"
class="size-[1.5rem] relative overflow-hidden rounded-full bg-slate-300"
class="size-[1.6rem] flex items-center justify-center relative overflow-hidden rounded-full bg-slate-300"
>
<img :src="baseUrl + account.profile_photo" alt="" />
<img
class="absolute object-cover size-full"
:src="account.profile_photo"
:alt="account.first_name + ' ' + account.last_name"
/>
</button>
<NuxtLink to="/signin" v-else class="flex-center">
<Icon
name="ci:profile"
size="20px"
size="24px"
class="**:stroke-black"
/>
</NuxtLink>
<NuxtLink to="/products" class="flex-center">
<Icon
name="ci:search"
size="21px"
class="**:stroke-black"
/>
</NuxtLink>
<button class="flex-center">
<Icon
name="ci:search"
size="18px"
class="**:stroke-black"
/>
</button>
<button class="flex-center">
<Icon
name="ci:cart"
size="20px"
size="24px"
class="**:stroke-black"
/>
</button>
@@ -93,7 +95,23 @@ const nav_links = ref<NavLink[]>([
</nav>
</div>
<div>LOGO</div>
<div>
<svg
xmlns="http://www.w3.org/2000/svg"
height="23"
fill="none"
viewBox="0 0 220 40"
>
<path
fill="#0E1534"
d="M20 40c11.046 0 20-8.954 20-20V6a6 6 0 0 0-6-6H21v8.774c0 2.002.122 4.076 1.172 5.78a9.999 9.999 0 0 0 6.904 4.627l.383.062a.8.8 0 0 1 0 1.514l-.383.062a10 10 0 0 0-8.257 8.257l-.062.383a.8.8 0 0 1-1.514 0l-.062-.383a10 10 0 0 0-4.627-6.904C12.85 21.122 10.776 21 8.774 21H.024C.547 31.581 9.29 40 20 40Z"
></path>
<path
fill="#0E1534"
d="M0 19h8.774c2.002 0 4.076-.122 5.78-1.172a10.018 10.018 0 0 0 3.274-3.274C18.878 12.85 19 10.776 19 8.774V0H6a6 6 0 0 0-6 6v13ZM46.455 2a2 2 0 1 1-4 0 2 2 0 0 1 4 0ZM211.711 12.104c5.591 0 8.289 3.905 8.289 8.428v8.495h-5.851V21.54c0-2.05-.748-3.742-2.893-3.742-2.145 0-2.86 1.692-2.86 3.742v7.486h-5.851V21.54c0-2.05-.715-3.742-2.861-3.742-2.145 0-2.893 1.692-2.893 3.742v7.486h-5.85v-8.495c0-4.523 2.697-8.428 8.288-8.428 3.056 0 5.266 1.204 6.274 3.189 1.072-1.985 3.413-3.19 6.208-3.19ZM180.427 23.82c1.885 0 2.698-1.725 2.698-3.776v-7.29h5.85v8.006c0 4.784-2.795 8.755-8.548 8.755-5.754 0-8.549-3.97-8.549-8.755v-8.006h5.851v7.29c0 2.05.812 3.776 2.698 3.776ZM163.275 29.547c-3.673 0-6.046-1.269-7.444-3.742l4.226-2.376c.585 1.041 1.462 1.562 2.925 1.562 1.203 0 1.755-.423 1.755-.944 0-1.985-8.581.033-8.581-6.28 0-3.06 2.6-5.533 7.021-5.533 3.868 0 5.981 1.887 6.924 3.71l-4.226 2.408c-.357-.976-1.463-1.562-2.568-1.562-.845 0-1.3.358-1.3.846 0 2.018 8.581.163 8.581 6.281 0 3.417-3.348 5.63-7.313 5.63ZM142.833 36.512h-5.851V20.858c0-4.98 3.738-8.592 8.939-8.592 5.071 0 8.939 3.873 8.939 8.592 0 5.207-3.446 8.657-8.614 8.657-1.203 0-2.405-.358-3.413-.912v7.909Zm3.088-12.497c1.853 0 3.088-1.432 3.088-3.125 0-1.724-1.235-3.124-3.088-3.124s-3.088 1.4-3.088 3.125c0 1.692 1.235 3.124 3.088 3.124ZM131.121 11.03c-1.918 0-3.51-1.595-3.51-3.515 0-1.92 1.592-3.515 3.51-3.515 1.918 0 3.511 1.595 3.511 3.515 0 1.92-1.593 3.515-3.511 3.515Zm-2.925 1.724h5.851v16.273h-5.851V12.754ZM116.97 29.515c-5.071 0-8.939-3.905-8.939-8.657 0-4.719 3.868-8.624 8.939-8.624s8.939 3.905 8.939 8.624c0 4.752-3.868 8.657-8.939 8.657Zm0-5.5c1.853 0 3.088-1.432 3.088-3.125 0-1.724-1.235-3.156-3.088-3.156s-3.088 1.432-3.088 3.156c0 1.693 1.235 3.125 3.088 3.125ZM96.983 37c-4.03 0-6.956-1.79-8.451-4.98l4.843-2.603c.52 1.107 1.495 2.246 3.51 2.246 2.114 0 3.511-1.335 3.674-3.678-.78.684-2.016 1.204-3.868 1.204-4.519 0-8.16-3.482-8.16-8.364 0-4.718 3.869-8.559 8.94-8.559 5.201 0 8.939 3.613 8.939 8.592v6.444c0 5.858-4.064 9.698-9.427 9.698Zm.39-13.31c1.755 0 3.088-1.205 3.088-2.995 0-1.757-1.332-2.929-3.088-2.929-1.723 0-3.088 1.172-3.088 2.93 0 1.79 1.365 2.993 3.088 2.993ZM78.607 29.515c-5.071 0-8.94-3.905-8.94-8.657 0-4.719 3.869-8.624 8.94-8.624 5.07 0 8.939 3.905 8.939 8.624 0 4.752-3.869 8.657-8.94 8.657Zm0-5.5c1.853 0 3.088-1.432 3.088-3.125 0-1.724-1.235-3.156-3.088-3.156s-3.088 1.432-3.088 3.156c0 1.693 1.235 3.125 3.088 3.125ZM59.013 7.06v16.434H68.7v5.533H58.2c-3.705 0-5.2-1.953-5.2-5.045V7.06h6.013Z"
></path>
</svg>
</div>
</div>
</header>
</template>
@@ -0,0 +1,49 @@
<script lang="ts" setup>
// state
const nuxtApp = useNuxtApp();
const isLoading = ref(false);
// hook
nuxtApp.hook("page:start", () => {
isLoading.value = true;
});
nuxtApp.hook("page:finish", () => {
isLoading.value = false;
});
</script>
<template>
<Transition name="fade">
<div
v-if="isLoading"
class="h-[20px] flex items-center justify-center bg-black w-full left-0 top-0 fixed z-100"
>
<div class="absolute progress-indicator w-1/3 bg-white h-1 rounded-full"></div>
</div>
</Transition>
</template>
<style>
.progress-indicator {
animation-name: progress;
animation-duration: 0.65s;
animation-iteration-count: infinite;
animation-timing-function: linear;
}
@keyframes progress {
0% {
left: -50%;
}
100% {
left: 110%;
}
}
</style>
+19 -3
View File
@@ -1,6 +1,22 @@
<script setup lang="ts">
// type
type Props = {
rate : number
}
// prop
defineProps<Props>();
</script>
<template>
<div class="bg-white flex justify-center items-center gap-2 rounded-full border-[0.5px] border-slate-200 px-4 py-2 typo-p-sm">
<slot />
<Icon name="ci:star-solid" class="**:fill-warning-500 size-4.5" />
<div class="bg-white flex justify-center items-center gap-2 rounded-full border-[0.5px] border-slate-300 px-4 py-2 typo-p-sm">
<Icon name="ci:star-solid" class="**:fill-warning-400 size-4.5" />
<span class="mt-0.5">
{{ rate }}
</span>
</div>
</template>
@@ -13,23 +13,23 @@ const highlights = ref<Highlight[]>([
{
icon: "ci:headset",
title: "خدمات مشتری",
description: "پشتیبانی استثنایی، راه‌حل‌های پایدار",
description: "پشتیبانی استثنایی، راه‌حل‌های پایدار برای شما عزیزان"
},
{
icon: "ci:delivery",
title: "ارسال سریع و رایگان",
description: "ارسال رایگان برای سفارش‌های بالای ۱۵۰ دلار",
description: "ارسال رایگان برای سفارش‌های بالای ۱۵۰ دلار و خورده"
},
{
icon: "ci:users",
title: "معرفی به دوستان",
description: "دوستان خود را معرفی کنید و هر دو ۱۵٪ تخفیف بگیرید",
description: "دوستان خود را معرفی کنید و هر دو ۱۵٪ تخفیف بگیرید"
},
{
icon: "ci:shield-done",
title: "پرداخت امن",
description: "اطلاعات پرداخت شما به‌صورت امن پردازش می‌شود",
},
description: "اطلاعات پرداخت شما به‌صورت امن پردازش می‌شود"
}
]);
</script>
@@ -38,13 +38,13 @@ const highlights = ref<Highlight[]>([
<div class="w-full flex-center py-[5rem] gap-[1.25rem] container">
<template v-for="(highlight, index) in highlights" :key="index">
<div class="flex flex-col-center gap-[.75rem] w-1/4 px-5">
<div class="size-[1.5rem] flex-center">
<Icon :name="highlight.icon" />
</div>
<Icon :name="highlight.icon" size="32px" />
<div class="w-full flex-col-center gap-[.25rem]">
<span class="typo-sub-h-md text-black text-center">
{{ highlight.title }}
</span>
<span class="typo-sub-h-md text-black text-center">
{{ highlight.title }}
</span>
<p class="text-slate-500 typo-p-sm mt-1 text-center">
{{ highlight.description }}
</p>
+1 -1
View File
@@ -54,7 +54,7 @@ watch(
<button
@click="isSideShow = !isSideShow"
class="size-[3.5rem] -me-5 flex-center rounded-full cursor-pointer"
class="cursor-pointer size-[3.5rem] -me-5 flex-center rounded-full"
>
<Icon
name="ci:close"
@@ -3,6 +3,7 @@
import { Swiper, SwiperSlide } from "swiper/vue";
import type { SwiperClass } from "swiper/react";
import useHomeData from "~/composables/api/home/useHomeData";
// types
@@ -16,6 +17,8 @@ defineProps<Props>();
// state
const { data : homeData } = useHomeData();
const swiper_instance = ref<SwiperClass | null>(null);
// methods
@@ -76,15 +79,16 @@ const onSwiper = (swiper: SwiperClass) => {
</div>
<div class="w-full">
<Swiper :slides-per-view="3" :space-between="24" @swiper="onSwiper">
<SwiperSlide v-for="i in 4" :key="i">
<SwiperSlide v-for="product in homeData!.products" :key="product.id">
<ProductCard
brand="Samsung"
title="Galaxy S20 Ultra"
picture="/assets/img/product-1.jpg"
:colors="['#0000ff', '#00ff00', 'red']"
:price="599"
:rate="2.4"
tag="New"
:id="product.id"
brand="برند محصول"
:title="product.name"
:picture="product.image1"
:colors="['white', 'black']"
:price="product.price"
:rate="product.rating"
:dark-layer="true"
/>
</SwiperSlide>
</Swiper>
@@ -35,20 +35,20 @@ const changeSlide = (id: number) => {
</script>
<template>
<div class="flex flex-col relative gap-4">
<div class="bg-red-300 w-full relative aspect-square overflow-hidden rounded-200">
<div class="flex flex-col relative gap-6">
<div class="bg-white brightness-[97%] w-full relative aspect-square overflow-hidden rounded-200">
<img
class="size-full absolute object-cover"
class="size-full absolute object-contain"
:src="selectedSlideDetail.picture"
:alt="String(selectedSlideDetail.id)"
/>
</div>
<div class="flex items-center justify-between gap-4">
<div class="flex items-center justify-between gap-6">
<div
@click="changeSlide(slide.id)"
v-for="slide in slides"
:class="selectedSlide === slide.id ? 'ring-black' : 'ring-transparent'"
class="cursor-pointer aspect-square w-[108px] ring-2 ring-offset-4 rounded-200 w-full overflow-hidden relative"
class="cursor-pointer brightness-[97%] bg-white aspect-square ring-2 ring-offset-4 rounded-200 w-full overflow-hidden relative"
:key="slide.id"
>
<img class="absolute object-cover size-full" :src="slide.picture" :alt="String(slide.id)" />
@@ -4,62 +4,88 @@
import Tag from "~/components/global/Tag.vue";
import Rate from "~/components/global/Rate.vue";
import ColorCircle from "~/components/global/ColorCircle.vue";
import { useImageColor } from "~/composables/global/useImageColor";
// types
type Props = {
id: number,
brand: string;
title: string;
colors: string[];
price: number;
price: string;
picture: string;
tag?: string;
rate?: number;
darkLayer?: boolean;
};
// props
defineProps<Props>();
const props = defineProps<Props>();
const { id } = toRefs(props);
// state
const { colorObject } = useImageColor(`#product-image-${id.value}`);
</script>
<template>
<div
class="relative size-full min-h-[31.25rem] rounded-2xl bg-black/10 overflow-hidden p-6"
>
<img
src="~/assets/img/product-2.jpg"
class="size-full object-cover absolute inset-0"
alt="product-background"
/>
<NuxtLink :to="'/product/' + id">
<div
class="flex justify-between items-center absolute px-6 pt-6 top-0 w-full inset-x-0"
class="relative size-full min-h-[31.25rem] rounded-2xl bg-white brightness-[98%] overflow-hidden p-6"
>
<Rate v-if="rate">
{{ rate }}
</Rate>
<Tag v-if="tag">
{{ tag }}
</Tag>
</div>
<div
class="absolute inset-x-0 bottom-0 pb-6 px-6 flex flex-row-reverse justify-between items-end"
>
<span class="typo-p-md"> {{ price }} </span>
<div class="flex flex-col gap-2 items-start">
<span class="typo-p-md">
<img
:id="`product-image-${id}`"
:src="picture"
class="size-full object-contain absolute inset-0"
alt="product-background"
/>
<div
v-if="darkLayer"
class="bg-linear-to-t inset-0 from-black/50 to-transparent to-40% absolute z-10 size-full"
/>
<div
class="flex justify-between items-center absolute px-6 pt-6 top-0 w-full inset-x-0"
>
<Rate v-if="rate" :rate="rate"/>
<Tag v-if="tag">
{{ tag }}
</Tag>
</div>
<div
:class="
colorObject?.isLight && !darkLayer
? 'text-black'
: 'text-white'
"
class="absolute inset-x-0 bottom-0 pb-6 px-6 flex flex-row-reverse justify-between items-end z-10"
>
<div class="flex flex-col gap-2 items-start w-full">
<span class="typo-p-md font-medium">
{{ brand }}
</span>
<span class="typo-sub-h-md">
<span class="typo-sub-h-lg">
{{ title }}
</span>
<!-- <div class="flex items-center gap-2 mt-1">
<ColorCircle
v-for="color in colors"
:key="color"
:style="{ backgroundColor: color }"
/>
</div> -->
<div class="flex items-center justify-between w-full mt-1">
<div class="flex items-center gap-2 mt-1">
<ColorCircle
v-for="color in colors"
:key="color"
:style="{ backgroundColor: color }"
/>
</div>
<span class="typo-p-md font-medium whitespace-nowrap">
{{ price }}
</span>
</div>
</div>
</div>
</div>
</div>
</NuxtLink>
</template>
@@ -131,16 +131,6 @@ watch(
دسته بندی
</div>
<ComboBox :options="options" v-model="params.category" />
<div
v-if="params.category"
class="w-full flex flex-wrap gap-2 px-[1rem]"
>
<span
class="py-1 px-3 cursor-pointer text-nowrap bg-slate-100 rounded-full text-sm"
>
{{ params.category }}
</span>
</div>
</div>
<div class="flex flex-col w-full gap-5">
@@ -177,7 +167,7 @@ watch(
<span class="text-sm text-black">
{{
"price_gte" in params
? params.price_gte.toLocaleString()
? sliderValue[0].toLocaleString()
: PRODUCT_RANGE.min
}}
</span>
@@ -187,7 +177,7 @@ watch(
<span class="text-sm text-black">
{{
"price_lte" in params
? params.price_lte.toLocaleString()
? sliderValue[1].toLocaleString()
: PRODUCT_RANGE.max
}}
</span>
@@ -240,5 +230,3 @@ watch(
</Button>
</div>
</template>
<style scoped></style>
+11 -1
View File
@@ -29,12 +29,13 @@ const onSwiper = (swiper: SwiperClass) => {
</script>
<template>
<section class="flex flex-col gap-4 bg-black h-[110svh] mt-40 py-32">
<section class="flex flex-col justify-center gap-4 bg-black h-[150svh] mt-40">
<div class="w-full flex justify-center items-center">
<span class="text-white typo-h-4">
دسته بندی ها
</span>
</div>
<div class="w-full my-20 relative">
<Swiper
:loop="true"
@@ -82,5 +83,14 @@ const onSwiper = (swiper: SwiperClass) => {
/>
</div>
</div>
<div class="w-full flex justify-center items-center">
<NuxtLink to="/category">
<Button variant="solid" class="invert rounded-full px-8" end-icon="ci:arrow-left">
مشاهده همه دسته ها
</Button>
</NuxtLink>
</div>
</section>
</template>
@@ -33,4 +33,4 @@ provide("isOpen", {
<ChatBoxContainer :isOpen="isOpen" />
</template>
</template>
@@ -3,7 +3,7 @@
</script>
<template>
<section class="bg-slate-50 p-20">
<section class="bg-slate-50">
<div class="flex gap-12 my-42 container">
<div class="flex flex-col gap-6 min-w-fit">
<h3 class="typo-h-3">
+17 -15
View File
@@ -14,20 +14,22 @@ const { data: product } = useGetProduct(id);
const quantity = ref(1);
const selectedSlide = ref(0);
const slides = [
{
id: 0,
picture: "/img/product-1.jpg",
},
{
id: 1,
picture: "/img/product-2.jpg",
},
{
id: 2,
picture: "/img/product-3.jpg",
},
];
const slides = computed(() => {
return [
{
id: 0,
picture: product.value!.image1
},
{
id: 1,
picture: product.value!.image2
},
{
id: 2,
picture: product.value!.image3
}
];
});
</script>
<template>
@@ -45,7 +47,7 @@ const slides = [
<Rating />
</div>
<p class="typo-p-md text-slate-500 text-justify">
{{product!.description}}
{{ product!.description }}
</p>
<div class="w-full flex flex-col gap-6 mt-4">
<RemainQuantity
+2 -2
View File
@@ -15,9 +15,9 @@ const { data: product } = useGetProduct(id);
</script>
<template>
<section class="h-[110svh] w-full relative bg-black mt-[5rem]">
<section v-if="product?.video" class="h-[110svh] w-full relative bg-black mt-[5rem]">
<video
src="/video/product-video.mp4"
:src="product.video"
class="object-cover absolute size-full"
muted
autoplay
@@ -5,8 +5,8 @@
<template #button>
<Button
end-icon="ci:filter"
variant="secondary"
class="rounded-full py-4 !bg-slate-100 !cursor-pointer"
variant="outlined"
class="rounded-full"
>
فیلتر محصولات
</Button>
@@ -5,7 +5,7 @@ import { API_ENDPOINTS, QUERY_KEYS } from "~/constants";
// types
export type GetCategoriesResponse = { categories: Category[] };
export type GetCategoriesResponse = Category[];
const useGetCategories = () => {
@@ -17,7 +17,8 @@ const useGetCategories = () => {
const handleGetCategories = async () => {
const { data } = await axios.get<GetCategoriesResponse>(`${API_ENDPOINTS.products.categories}`);
return data.categories;
await new Promise(resolve => setTimeout(resolve, 5000));
return data;
};
return useQuery({
+3
View File
@@ -70,6 +70,9 @@ onServerPrefetch(async () => {
class="w-full flex flex-col-center persian-number font-iran-yekan-x"
dir="rtl"
>
<LoadingIndicator />
<Header />
<main class="w-full overflow-x-hidden">
<slot />
+25 -15
View File
@@ -3,24 +3,34 @@ export default defineNuxtConfig({
compatibilityDate: "2024-11-01",
ssr: true,
devtools: { enabled: true },
css: ["~/assets/css/tailwind.css", "swiper/css"],
css: ["~/assets/css/tailwind.css", "swiper/css", "animate.css/animate.min.css"],
routeRules: {
"/products": { prerender: false, ssr: false },
"/products": { prerender: false, ssr: false }
},
app: {
pageTransition: {
enterActiveClass:
"animate__animated animate__fadeIn animate__faster",
leaveActiveClass:
"animate__animated animate__fadeOut animate__faster",
mode: "out-in"
}
},
postcss: {
plugins: {
"@tailwindcss/postcss": {},
autoprefixer: {},
},
autoprefixer: {}
}
},
components: [
{
path: "~/components",
pathPrefix: false,
},
pathPrefix: false
}
],
icon: {
@@ -28,9 +38,9 @@ export default defineNuxtConfig({
customCollections: [
{
prefix: "ci",
dir: "./public/icons",
},
],
dir: "./public/icons"
}
]
},
modules: [
@@ -41,9 +51,9 @@ export default defineNuxtConfig({
"DM Sans": "100..900",
Inter: "100..900",
download: true,
inject: false,
},
},
inject: false
}
}
],
"@nuxt/icon",
"reka-ui/nuxt",
@@ -53,7 +63,7 @@ export default defineNuxtConfig({
runtimeConfig: {
public: {
API_BASE_URL: "https://api.heymlz.com",
},
},
API_BASE_URL: "https://api.heymlz.com"
}
}
});
+7
View File
@@ -16,6 +16,7 @@
"@vuelidate/validators": "^2.0.4",
"@vueuse/integrations": "^12.4.0",
"@vueuse/nuxt": "^12.3.0",
"animate.css": "^4.1.1",
"axios": "^1.7.9",
"fast-average-color": "^9.4.0",
"gsap": "^3.12.5",
@@ -3685,6 +3686,12 @@
"node": ">= 6.0.0"
}
},
"node_modules/animate.css": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/animate.css/-/animate.css-4.1.1.tgz",
"integrity": "sha512-+mRmCTv6SbCmtYJCN4faJMNFVNN5EuCTTprDTAo7YzIGji2KADmakjVA3+8mVDkZ2Bf09vayB35lSQIex2+QaQ==",
"license": "MIT"
},
"node_modules/ansi-colors": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz",
+1
View File
@@ -21,6 +21,7 @@
"@vuelidate/validators": "^2.0.4",
"@vueuse/integrations": "^12.4.0",
"@vueuse/nuxt": "^12.3.0",
"animate.css": "^4.1.1",
"axios": "^1.7.9",
"fast-average-color": "^9.4.0",
"gsap": "^3.12.5",
+40 -22
View File
@@ -14,27 +14,34 @@ const debouncedSearch = refDebounced(search, 300);
// computed
const filteredCategories = computed(() => {
if (debouncedSearch.value.length > 0) {
return categories.value!.filter((cat) =>
cat.name.includes(debouncedSearch.value)
);
return categories.value!.map((cat) => {
cat.subcategorys = cat.subcategorys.filter((subcat) => {
return subcat.name.includes(debouncedSearch.value);
});
return cat;
});
}
return categories.value!;
});
// lifecycle
// ssr
await useAsyncData(async () => {
onServerPrefetch(async () => {
const response = await suspense();
if (response.isError) {
throw createError({
statusCode: 500,
statusMessage: `Error in categories page prefetch`,
statusMessage: `Error in categories page prefetch`
});
}
});
</script>
<template>
@@ -42,7 +49,7 @@ onServerPrefetch(async () => {
<div class="py-[5rem] flex gap-6 justify-between items-center">
<span class="typo-h-3 text-black">دسته بندی ها</span>
<Input
class="max-w-[400px] w-full"
class="max-w-[400px] w-full rounded-full"
variant="outlined"
placeholder="جستجو..."
v-model="search"
@@ -59,21 +66,32 @@ onServerPrefetch(async () => {
</Input>
</div>
<Transition name="fade" mode="out-in">
<div
v-if="filteredCategories.length !== 0"
v-auto-animate
class="grid grid-cols-3 gap-4 w-full mt-12"
>
<CategoryCard
v-for="category in filteredCategories"
:key="category.id"
:id="category.id"
:category="category.name"
picture="/img/product-1.jpg"
:count="20"
description="یک دسته بندی تستasdasd"
dark-layer
/>
<div v-if="filteredCategories">
<div
class="flex flex-col gap-6"
v-for="mainCategory in filteredCategories"
>
<div class="w-full flex items-center justify-between">
<span>
{{ mainCategory.name }}
</span>
</div>
<div
v-auto-animate
class="grid grid-cols-3 gap-4 w-full mt-12"
>
<CategoryCard
v-for="category in mainCategory.subcategorys"
:key="category.id"
:id="category.id"
:category="category.name"
:picture="category.icon"
:count="20"
description="یک دسته بندی تستasdasd"
dark-layer
/>
</div>
</div>
</div>
<div v-else class="flex w-full mt-12">
+2 -2
View File
@@ -8,9 +8,9 @@ import useHomeData from "~/composables/api/home/useHomeData";
const { suspense } = useHomeData();
// lifecycle
// ssr
onServerPrefetch(async () => {
await useAsyncData(async () => {
const response = await suspense();
if (response.isError) {
+4 -2
View File
@@ -9,7 +9,9 @@ const id = route.params.id as string | undefined;
const { suspense } = useGetProduct(id);
onServerPrefetch(async () => {
// ssr
await useAsyncData(async () => {
const response = await suspense();
if (response.isError) {
@@ -23,7 +25,7 @@ onServerPrefetch(async () => {
</script>
<template>
<div class="w-full flex flex-col">
<div class="w-full flex flex-col gap-20">
<ProductHero />
<ProductVideo />
<ProductComments />
+21 -8
View File
@@ -1,4 +1,5 @@
<script setup lang="ts">
// import
import useGetProducts, {
@@ -65,15 +66,26 @@ watch(
<span>/</span>
<span>همه</span>
</div> -->
<h1 class="typo-h-3">همه محصولات</h1>
<h1 class="typo-h-3">لیست محصولات</h1>
</div>
<div class="w-full flex items-center justify-end gap-4">
<Input
placeholder="جست و جو محصول ..."
v-model="search"
class="bg-slate-50 !border-slate-200 hover:border-slate-300 focus:!border-slate-800 !rounded-full w-8/12"
/>
variant="outlined"
class="rounded-full w-8/12"
>
<template #endItem>
<div class="flex items-center gap-1">
<Icon
class="translate-y-[-1px]"
name="ci:search"
size="24"
/>
</div>
</template>
</Input>
<FilterButton />
</div>
</div>
@@ -101,17 +113,18 @@ watch(
<ul v-else class="w-full grid grid-cols-3 gap-[1.5rem]">
<li v-for="(product, index) in products" :key="index">
<ProductCard
brand="Samsung"
:id="product.id"
brand="برند محصول"
:title="product.name"
picture="/assets/img/product-1.jpg"
:colors="['#0000ff', '#00ff00', 'red']"
:picture="product.image1"
:colors="['white', 'black']"
:price="product.price"
:rate="product.rating"
tag="New"
:dark-layer="true"
/>
</li>
</ul>
<div class="w-full flex-center py-10">
<div v-if="products!.length > 10" class="w-full flex-center py-10">
<Pagination :items="paginationData" :total="data?.count" />
</div>
</div>
+12 -4
View File
@@ -48,12 +48,20 @@ declare global {
name: string;
slug: string;
icon: string;
meta_title: string;
meta_description: string;
parent: number;
children: "string";
"product_count": string,
"subcategorys": SubCategory[]
};
type SubCategory = {
"id": number,
"name": string,
"slug": string,
"icon": string,
"product_count": string,
"parent": string,
"show": boolean
}
type Address = {
id: number;
province: string;