This commit is contained in:
Mamalizz
2025-02-02 23:07:07 +03:30
59 changed files with 3028 additions and 398 deletions
+2 -2
View File
@@ -20,8 +20,8 @@ TELEGRAM_BOT_TOKEN = ''
DOMAIN = 'heymlz.com'
# domain for api (the domain that django will use)
API_DOMAIN = 'api.heymlz.com'
SITE_TITLE = 'Heymlz Shop'
SITE_HEADER = 'Heymlz Shop'
SITE_TITLE = 'فروشگاه هی ملز'
SITE_HEADER = 'فروشگاه هی ملز'
# jwt token configs
ACCESS_TOKEN_LIFETIME = 5000
REFRESH_TOKEN_LIFETIME = 5000
@@ -0,0 +1,25 @@
# Generated by Django 5.1.2 on 2025-02-02 14:55
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('account', '0002_alter_user_options'),
]
operations = [
migrations.AddField(
model_name='useraddressmodel',
name='city',
field=models.CharField(default='', max_length=30),
preserve_default=False,
),
migrations.AddField(
model_name='useraddressmodel',
name='province',
field=models.CharField(default='', max_length=30),
preserve_default=False,
),
]
@@ -0,0 +1,18 @@
# Generated by Django 5.1.2 on 2025-02-02 15:52
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('account', '0003_useraddressmodel_city_useraddressmodel_province'),
]
operations = [
migrations.AddField(
model_name='useraddressmodel',
name='for_me',
field=models.BooleanField(default=False),
),
]
+3 -1
View File
@@ -110,6 +110,8 @@ class UserAddressModel(models.Model):
address = models.TextField()
postal_code = models.CharField(max_length=10)
phone = models.CharField(max_length=11)
city = models.CharField(max_length=30)
province = models.CharField(max_length=30)
for_me = models.BooleanField(default=False)
def __str__(self):
return f"{self.user.phone}, {self.name}"
+2 -2
View File
@@ -17,8 +17,8 @@ class ProfileSerializer(serializers.ModelSerializer):
class UserAddressSerializer(serializers.ModelSerializer):
class Meta:
model = UserAddressModel
fields = ['id', 'name', 'address', 'postal_code', 'phone']
fields = ['id', 'name', 'address', 'postal_code', 'phone', 'city', 'province', 'for_me']
read_only_fields = ('id',)
def validate(self, data):
user = self.context['request'].user
if not user.is_authenticated:
+12 -10
View File
@@ -131,7 +131,7 @@ ROOT_URLCONF = 'core.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'DIRS': [BASE_DIR / "templates"],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
@@ -178,7 +178,7 @@ STATIC_ROOT = '/app/static'
STATICFILES_DIRS = [
os.path.join(BASE_DIR, 'custom_static'),
# BASE_DIR / "core" / "static"
BASE_DIR / "core" / "static"
]
STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
@@ -225,7 +225,9 @@ UNFOLD = {
"SITE_TITLE": os.getenv("SITE_TITLE"),
"SITE_HEADER": os.getenv("SITE_HEADER"),
"SITE_URL": DOMAIN,
"THEME": 'dark',
"SITE_SYMBOL": "shield_person",
"DASHBOARD_CALLBACK": "core.views.dashboard_callback",
"SITE_FAVICONS": [
{
"rel": "icon",
@@ -239,7 +241,7 @@ UNFOLD = {
# },
"BORDER_RADIUS": "15px",
"BORDER_RADIUS": "8px",
"SHOW_HISTORY": True,
"SHOW_VIEW_ON_SITE": True,
"ENVIRONMENT": "core.settings.environment_callback",
@@ -289,8 +291,8 @@ UNFOLD = {
},
"SIDEBAR": {
"show_search": False,
"show_all_applications": False,
"show_search": True,
"show_all_applications": True,
"navigation": [
{
@@ -314,7 +316,7 @@ UNFOLD = {
{
"title": _("Shop Products"),
"title": _("محصولات فروشگاه"),
"separator": True,
"collapsible": False,
"items": [
@@ -341,7 +343,7 @@ UNFOLD = {
{
"title": _("Categories section"),
"title": _("سکشن دسته بندی"),
"separator": True,
"collapsible": False,
"items": [
@@ -360,7 +362,7 @@ UNFOLD = {
],
},
{
"title": _("Visual Sections "),
"title": _("سکشن های نمایشی"),
"separator": True,
"collapsible": True,
"items": [
@@ -386,7 +388,7 @@ UNFOLD = {
},
{
"title": _("Users and Customers"),
"title": _("کاربران و مشتریان"),
"separator": True,
"collapsible": True,
"items": [
@@ -410,7 +412,7 @@ UNFOLD = {
},
{
"title": _("Ticket and Support"),
"title": _("پشتیبانی و تیکت"),
"separator": True,
"collapsible": True,
"items": [
+88
View File
@@ -0,0 +1,88 @@
@layer base {
@font-face {
font-family: "IRANYekanXVF";
src: url("./fonts/IranYekanX/IRANYekanX-Thin.woff2");
font-weight: 100;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "IRANYekanXVF";
src: url("./fonts/IranYekanX/IRANYekanX-UltraLight.woff2");
font-weight: 200;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "IRANYekanXVF";
src: url("./fonts/IranYekanX/IRANYekanX-Light.woff2");
font-weight: 300;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "IRANYekanXVF";
src: url("./fonts/IranYekanX/IRANYekanX-Regular.woff2");
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "IRANYekanXVF";
src: url("./fonts/IranYekanX/IRANYekanX-Medium.woff2");
font-weight: 500;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "IRANYekanXVF";
src: url("./fonts/IranYekanX/IRANYekanX-DemiBold.woff2");
font-weight: 600;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "IRANYekanXVF";
src: url("./fonts/IranYekanX/IRANYekanX-Bold.woff2");
font-weight: 700;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "IRANYekanXVF";
src: url("./fonts/IranYekanX/IRANYekanX-ExtraBold.woff2");
font-weight: 800;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "IRANYekanXVF";
src: url("./fonts/IranYekanX/IRANYekanX-Black.woff2");
font-weight: 900;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "IRANYekanXVF";
src: url("./fonts/IranYekanX/IRANYekanX-ExtraBlack.woff2");
font-weight: 950;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "IRANYekanXVF";
src: url("./fonts/IranYekanX/IRANYekanX-Heavy.woff2");
font-weight: 1000;
font-style: normal;
font-display: swap;
}
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+3
View File
@@ -0,0 +1,3 @@
*:not(span[class^="material"]){
font-family: 'IRANYekanXVF' !important;
}
+190
View File
@@ -0,0 +1,190 @@
import json
import random
from functools import lru_cache
from django.contrib.humanize.templatetags.humanize import intcomma
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _
from django.views.generic import RedirectView, TemplateView
from unfold.views import UnfoldModelAdminViewMixin
class HomeView(RedirectView):
pattern_name = "admin:index"
def dashboard_callback(request, context):
context.update(random_data())
return context
@lru_cache
def random_data():
WEEKDAYS = [
"Mon",
"Tue",
"Wed",
"Thu",
"Fri",
"Sat",
"Sun",
]
positive = [[1, random.randrange(8, 28)] for i in range(1, 28)]
negative = [[-1, -random.randrange(8, 28)] for i in range(1, 28)]
average = [r[1] - random.randint(3, 5) for r in positive]
performance_positive = [[1, random.randrange(8, 28)] for i in range(1, 28)]
performance_negative = [[-1, -random.randrange(8, 28)] for i in range(1, 28)]
response = {
"navigation": [
{"title": _("Dashboard"), "link": "/", "active": True},
{"title": _("Products"), "link": "/admin/product/productmodel/"},
{"title": _("Orders"), "link": "/admin/order/ordermodel/"},
],
"kpi": [
{
"title": "Product A Performance",
"metric": f"${intcomma(f"{random.uniform(1000, 9999):.02f}")}",
"footer": mark_safe(
f'<strong class="text-green-700 font-semibold dark:text-green-400">+{intcomma(f"{random.uniform(1, 9):.02f}")}%</strong>&nbsp;progress from last week'
),
"chart": json.dumps(
{
"labels": [WEEKDAYS[day % 7] for day in range(1, 28)],
"datasets": [{"data": average, "borderColor": "#9333ea"}],
}
),
},
{
"title": "Product B Performance",
"metric": f"${intcomma(f"{random.uniform(1000, 9999):.02f}")}",
"footer": mark_safe(
f'<strong class="text-green-700 font-semibold dark:text-green-400">+{intcomma(f"{random.uniform(1, 9):.02f}")}%</strong>&nbsp;progress from last week'
),
},
{
"title": "Product C Performance",
"metric": f"${intcomma(f"{random.uniform(1000, 9999):.02f}")}",
"footer": mark_safe(
f'<strong class="text-green-700 font-semibold dark:text-green-400">+{intcomma(f"{random.uniform(1, 9):.02f}")}%</strong>&nbsp;progress from last week'
),
},
],
"progress": [
{
"title": "🦆 Social marketing e-book",
"description": f"${intcomma(f"{random.uniform(1000, 9999):.02f}")}",
"value": random.randint(10, 90),
},
{
"title": "🦍 Freelancing tasks",
"description": f"${intcomma(f"{random.uniform(1000, 9999):.02f}")}",
"value": random.randint(10, 90),
},
{
"title": "🐋 Development coaching",
"description": f"${intcomma(f"{random.uniform(1000, 9999):.02f}")}",
"value": random.randint(10, 90),
},
{
"title": "🦑 Product consulting",
"description": f"${intcomma(f"{random.uniform(1000, 9999):.02f}")}",
"value": random.randint(10, 90),
},
{
"title": "🐨 Other income",
"description": f"${intcomma(f"{random.uniform(1000, 9999):.02f}")}",
"value": random.randint(10, 90),
},
{
"title": "🐶 Course sales",
"description": f"${intcomma(f"{random.uniform(1000, 9999):.02f}")}",
"value": random.randint(10, 90),
},
{
"title": "🐻‍❄️ Ads revenue",
"description": f"${intcomma(f"{random.uniform(1000, 9999):.02f}")}",
"value": random.randint(10, 90),
},
{
"title": "🦩 Customer Retention Rate",
"description": f"${intcomma(f"{random.uniform(1000, 9999):.02f}")}",
"value": random.randint(10, 90),
},
{
"title": "🦊 Marketing ROI",
"description": f"${intcomma(f"{random.uniform(1000, 9999):.02f}")}",
"value": random.randint(10, 90),
},
{
"title": "🦁 Affiliate partnerships",
"description": f"${intcomma(f"{random.uniform(1000, 9999):.02f}")}",
"value": random.randint(10, 90),
},
],
"chart": json.dumps(
{
"labels": [WEEKDAYS[day % 7] for day in range(1, 28)],
"datasets": [
{
"label": "Example 1",
"type": "line",
"data": average,
"borderColor": "var(--color-primary-500)",
},
{
"label": "Example 2",
"data": positive,
"backgroundColor": "var(--color-primary-700)",
},
{
"label": "Example 3",
"data": negative,
"backgroundColor": "var(--color-primary-300)",
},
],
}
),
"performance": [
{
"title": _("Last week revenue"),
"metric": "$1,234.56",
"footer": mark_safe(
'<strong class="text-green-600 font-medium">+3.14%</strong>&nbsp;progress from last week'
),
"chart": json.dumps(
{
"labels": [WEEKDAYS[day % 7] for day in range(1, 28)],
"datasets": [
{
"data": performance_positive,
"borderColor": "var(--color-primary-700)",
}
],
}
),
},
{
"title": _("Last week expenses"),
"metric": "$1,234.56",
"footer": mark_safe(
'<strong class="text-green-600 font-medium">+3.14%</strong>&nbsp;progress from last week'
),
"chart": json.dumps(
{
"labels": [WEEKDAYS[day % 7] for day in range(1, 28)],
"datasets": [
{
"data": performance_negative,
"borderColor": "var(--color-primary-300)",
},
],
}
),
},
],
}
return response
+17
View File
@@ -0,0 +1,17 @@
{% extends "admin/base.html" %}
{% load static %}
{% block extrastyle %}{{ block.super }}<link rel="stylesheet" type="text/css" href="{% static 'override.css' %}" />
<link rel="stylesheet" type="text/css" href="{% static 'fonts.css' %}" />
{% endblock %}
{% 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 %}
+96
View File
@@ -0,0 +1,96 @@
{% extends 'admin/base.html' %}
{% load static %}
{% block extrastyle %}{{ block.super }}<link rel="stylesheet" type="text/css" href="{% static 'override.css' %}" />
<link rel="stylesheet" type="text/css" href="{% static 'fonts.css' %}" />
{% endblock %}
{% 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">
{% include "formula/service.html" %}
<!-- <div class="flex gap-4">
{% component "unfold/components/navigation.html" with items=navigation %}{% endcomponent %}
</div> -->
<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 %}
+18
View File
@@ -0,0 +1,18 @@
{% load unfold i18n %}
<div dir='rtl' class="bg-base-50 border border-base-200 border-dashed flex flex-col gap-4 p-4 rounded dark:bg-white/[.02] dark:border-base-700 lg:flex-row lg:justify-between w-full shrink-0 lg:items-center" style="justify-content: space-between;">
<div class="flex flex-col lg:flex-row lg:items-center">
<h2 class="font-semibold text-font-important-light text-base dark:text-font-important-dark flex items-center">
<span class="material-symbols-outlined md-18 mr-3 w-4.5 align-middle">notifications</span>
<span class="align-middle">سفارش جدید داری</span>
</h2>
</div>
<div class="flex lg:flex-row lg:items-center">
{% component "unfold/components/flex.html" with class="flex-col gap-4 lg:flex-row" %}
{% component "unfold/components/button.html" with href="/admin/order/ordermodel/" %}
نمایش سفارشات
{% endcomponent %}
{% endcomponent %}
</div>
</div>
+1
View File
@@ -22,3 +22,4 @@ node_modules
.env
.env.*
!.env.example
/test-results/.last-run.json
+6 -6
View File
@@ -55,27 +55,27 @@
/* TYPE PARAGRAPH */
@utility typo-p-2xl {
@apply text-[24px] leading-[40px] font-light ;
@apply text-[24px] leading-[42px] font-light ;
}
@utility typo-p-xl {
@apply text-[20px] leading-[32px] font-light ;
@apply text-[20px] leading-[34px] font-light ;
}
@utility typo-p-lg {
@apply text-[18px] leading-[32px] font-light ;
@apply text-[18px] leading-[34px] font-light ;
}
@utility typo-p-md {
@apply text-[16px] leading-[28px] font-light ;
@apply text-[16px] leading-[30px] font-light ;
}
@utility typo-p-sm {
@apply text-[14px] leading-[24px] font-light ;
@apply text-[14px] leading-[26px] font-light ;
}
@utility typo-p-xs {
@apply text-[12px] leading-[16px] font-light ;
@apply text-[12px] leading-[18px] font-light ;
}
/* TYPO LABEL */
@@ -0,0 +1,63 @@
<script lang="ts" setup>
// import
import Masonry from "masonry-layout";
// type
type Props = {
articles: Article[],
}
// props
const props = defineProps<Props>();
const { articles } = toRefs(props);
// state
onMounted(() => {
new Masonry(".masonry-articles-container", {
itemSelector: ".grid-item",
columnWidth: ".grid-sizer",
percentPosition: true,
gutter: ".gutter-sizer"
});
});
</script>
<template>
<div class="masonry-articles-container w-full">
<div class="grid-sizer"></div>
<div class="gutter-sizer"></div>
<BlogPost
v-for="article in articles"
:key="article.id"
class="grid-item"
:image="article.cover_image"
:description="article.summery"
:title="article.title"
:comments="2"
:id="article.id"
:date="article.created_at"
tag="تگ ندارد"
/>
</div>
</template>
<style>
.grid-sizer,
.grid-item {
margin-bottom: 24px;
width: 48%;
}
.gutter-sizer {
width: 4%;
}
</style>
+11 -13
View File
@@ -3,12 +3,11 @@
// types
type Props = {
id: number;
tag: string;
date: string;
comments: number;
title: string;
description: string;
link: string;
variant?: "sm" | "lg";
image: string,
}
@@ -23,9 +22,10 @@ const {} = toRefs(props);
</script>
<template>
<NuxtLink :to="`/article/${id}`">
<div
:class="variant === 'lg' ? 'rounded-150 overflow-hidden' : ''"
class="group max-h-[700px] h-[700px] relative"
:class="variant === 'lg' ? 'h-[600px] rounded-150 overflow-hidden' : 'h-fit'"
class="group w-full relative"
>
<Tag
@@ -82,24 +82,21 @@ const {} = toRefs(props);
<div class="flex gap-4 flex-col">
<span
:class="variant === 'lg' ? 'typo-h-4' : 'typo-h-6'"
:class="variant === 'lg' ? 'typo-h-5' : 'typo-h-6'"
class="text-white"
>
برسی آیفون ۱۶ پرومکس
{{ title }}
</span>
<p
:class="variant === 'lg' ? 'typo-h-4' : 'typo-h-6 text-slate-500'"
class="typo-p-md text-white text-justify"
>
نیاز و کاربردهای متنوع با هدف بهبود ابزارهای کاربردی می باشد.
نیاز و کاربردهای متنوع با هدف بهبود ابزارهای کاربردی می باشد.
کتابهای زیادی در شصت و سه درصد گذشته.
</p>
v-html="description"
/>
</div>
<span class="underline text-white typo-p-md">
<NuxtLink :to="`/article/${id}`" class="underline text-white typo-p-md">
بیشتر بخوانید...
</span>
</NuxtLink>
</div>
@@ -115,4 +112,5 @@ const {} = toRefs(props);
class="w-full h-full bg-linear-to-t from-black to-transparent absolute inset-0 z-15"
/>
</div>
</NuxtLink>
</template>
@@ -44,9 +44,7 @@ const filters = computed(() => {
const { data: categories, suspense } = useGetCategories();
await useAsyncData(async () => {
await suspense();
});
const { isPending: productsIsPending } = useGetProducts(filters);
+15 -3
View File
@@ -10,6 +10,7 @@ import useHomeData from "~/composables/api/home/useHomeData";
const { data: homeData } = useHomeData();
const swiper_instance = ref<SwiperClass | null>(null);
const isMuted = ref(true);
// methods
@@ -37,18 +38,29 @@ const onChange = (swiper: SwiperClass) => {
@slide-change="onChange"
>
<SwiperSlide
v-for="slide in homeData!.sliders"
v-for="(slide, index) in homeData!.sliders"
:key="slide.id"
>
<div class="relative w-full rounded-200 h-[80svh] overflow-hidden">
<template v-if="!!slide.video">
<button
@click="isMuted = !isMuted"
class="transition-all hover:invert cursor-pointer flex-center hover:scale-110 size-[50px] border border-white hover:border-transparent rounded-full absolute z-20 top-10 right-20 bg-black"
>
<Icon
:name="isMuted ? 'bi:volume-mute-fill' : 'bi:volume-up-fill'"
class="text-white"
size="24px"
/>
</button>
<video
v-if="!!slide.video"
muted
:muted="swiper_instance?.realIndex !== index ? true : isMuted"
autoplay
loop
class="absolute inset-0 size-full object-cover"
:src="slide.video"
/>
</template>
<img
v-else
class="absolute inset-0 size-full object-cover"
+10 -49
View File
@@ -1,13 +1,15 @@
<script setup lang="ts">
// types
// state
type Props = {}
import useGetArticles from "~/composables/api/blog/useGetArticles";
// props
const page = ref(1);
const { data: articles, suspense } = useGetArticles(page);
const props = defineProps<Props>();
const {} = toRefs(props);
// ssr
await suspense();
</script>
@@ -23,49 +25,8 @@ const {} = toRefs(props);
</Button>
</NuxtLink>
</div>
<div class="flex gap-12">
<div class="flex-1 flex flex-col gap-12">
<BlogPost
image="/img/blog-1.jpeg"
description="aaasd"
title="asd"
:comments="2"
link="#"
date="2020-06-10"
tag="asdsa"
/>
<BlogPost
image="/img/blog-2.jpeg"
description="aaasd"
title="asd"
:comments="2"
link="#"
date="2020-06-10"
tag="asdsa"
/>
</div>
<div class="flex-[0.8] flex flex-col">
<BlogPost
image="/img/blog-3.jpeg"
description="aaasd"
title="asd"
:comments="2"
link="#"
date="2020-06-10"
tag="asdsa"
variant="sm"
/>
<BlogPost
image="/img/blog-4.jpeg"
description="aaasd"
title="asd"
:comments="2"
link="#"
date="2020-06-10"
tag="asdsa"
variant="sm"
/>
</div>
</div>
<ClientOnly>
<ArticlesList :articles="articles!.results" />
</ClientOnly>
</section>
</template>
+14 -2
View File
@@ -3,6 +3,7 @@
// import
import useGetProduct from "~/composables/api/product/useGetProduct";
import { sanitize } from "isomorphic-dompurify";
// state
@@ -14,6 +15,9 @@ const { data: product } = useGetProduct(id);
const quantity = ref(1);
const selectedSlide = ref(0);
// computed
const slides = computed(() => {
return [
{
@@ -30,6 +34,11 @@ const slides = computed(() => {
}
];
});
const sanitizedProductDescription = computed(() => {
return sanitize(product.value!.description);
});
</script>
<template>
@@ -47,16 +56,18 @@ const slides = computed(() => {
<Rating />
</div>
<p
<div
class="py-8 typo-p-md text-slate-500 text-justify [&_a]:text-blue-400 [&_strong]:font-bold [&_u]:text-red-400"
v-html="product!.description"
v-html="sanitizedProductDescription"
/>
<div class="w-full flex flex-col gap-6 mt-4">
<RemainQuantity
:maxQuantity="product!.in_stock"
:quantity="quantity"
/>
<div class="w-full flex gap-3 flex-col">
<div class="w-full flex gap-3">
<Button class="w-full rounded-full" end-icon="ci:plus">
@@ -71,6 +82,7 @@ const slides = computed(() => {
همین الان بخر
</Button>
</div>
<InfoCard />
<Share />
</div>
@@ -0,0 +1,29 @@
// imports
import { useQuery } from "@tanstack/vue-query";
import { API_ENDPOINTS, QUERY_KEYS } from "~/constants";
// types
export type GetArticleResponse = Article;
const useGetArticle = (id: number | string | undefined) => {
// state
const { $axios: axios } = useNuxtApp();
// methods
const handleGetArticle = async () => {
const { data } = await axios.get<GetArticleResponse>(`${API_ENDPOINTS.blog.article}/${id}`);
return data;
};
return useQuery({
queryKey: [QUERY_KEYS.article, id],
queryFn: () => handleGetArticle()
});
};
export default useGetArticle;
@@ -5,11 +5,11 @@ import { API_ENDPOINTS, QUERY_KEYS } from "~/constants";
// types
export type GetArticlesResponse = ApiPaginated<UserComment>;
export type GetArticlesResponse = ApiPaginated<Article>;
const useGetArticles = (
page: Ref<number>,
search: Ref<string>
search?: Ref<string>
) => {
// state
@@ -23,7 +23,7 @@ const useGetArticles = (
params: {
offset: (page.value * 10) - 10,
limit: 10,
search: search.value.length > 0 ? search.value : undefined,
search: search ? (search.value.length > 0 ? search.value : undefined) : undefined,
}
});
return data;
+2 -1
View File
@@ -58,7 +58,8 @@ export default defineNuxtConfig({
"@nuxt/icon",
"reka-ui/nuxt",
"@vueuse/nuxt",
"@formkit/auto-animate/nuxt"
"@formkit/auto-animate/nuxt",
'@nuxt/test-utils/module'
],
runtimeConfig: {
+2131 -143
View File
File diff suppressed because it is too large Load Diff
+11 -1
View File
@@ -7,6 +7,7 @@
"build": "nuxt build",
"dev": "nuxt dev",
"dev-o": "nuxt dev -- -o",
"test": "vitest",
"generate": "nuxt generate",
"preview": "nuxt preview",
"postinstall": "nuxt prepare"
@@ -25,6 +26,8 @@
"axios": "^1.7.9",
"fast-average-color": "^9.4.0",
"gsap": "^3.12.5",
"isomorphic-dompurify": "^2.21.0",
"masonry-layout": "^4.2.2",
"nuxt": "^3.14.1592",
"reka-ui": "^1.0.0-alpha.6",
"swiper": "^11.1.15",
@@ -35,9 +38,16 @@
"vue-skeletor": "^1.0.6"
},
"devDependencies": {
"@nuxt/test-utils": "^3.15.4",
"@tailwindcss/postcss": "^4.0.0-beta.5",
"@types/masonry-layout": "^4.2.8",
"@vue/test-utils": "^2.4.6",
"autoprefixer": "^10.4.20",
"happy-dom": "^16.8.1",
"msw": "^2.7.0",
"playwright-core": "^1.50.1",
"postcss": "^8.4.49",
"tailwindcss": "^4.0.0-beta.5"
"tailwindcss": "^4.0.0-beta.5",
"vitest": "^3.0.4"
}
}
+115
View File
@@ -0,0 +1,115 @@
<script lang="ts" setup>
// import
import { sanitize } from "isomorphic-dompurify";
import useGetArticle from "~/composables/api/blog/useGetArticle";
// state
const route = useRoute();
const id = route.params.id as string | undefined;
const { data: article, suspense } = useGetArticle(id);
// ssr
const response = await suspense();
if (response.isError) {
throw createError({
statusCode: 500,
statusMessage: `Error in categories page prefetch`
});
}
// computed
const sanitizedArticleContent = computed(() => {
return sanitize(article.value!.content);
});
const sanitizedArticleSummery = computed(() => {
return sanitize(article.value!.summery);
});
</script>
<template>
<div class="container">
<div class="w-full h-[80svh] rounded-3xl relative overflow-hidden">
<img class="absolute object-cover size-full" :alt="article!.title" :src="article!.cover_image" />
<div class="absolute bg-linear-to-t from-black/75 to-transparent size-full" />
<div class="absolute pl-10 right-10 bottom-10 flex flex-col gap-6">
<h1 class="typo-h-4 text-white pl-8">
{{ article!.title }}
</h1>
<div
class="typo-p-lg text-slate-200 mb-6 text-justify w-[70%]"
v-html="sanitizedArticleSummery"
/>
<div class="flex items-center justify-between">
<div class="flex items-center gap-4">
<div
class="w-fit pr-2 pl-5 h-[50px] rounded-full flex items-center justify-center gap-3 bg-white">
<div
class="relative flex items-center justify-center rounded-full overflow-hidden size-[35px]">
<img
class="size-full object-cover absolute"
:src="article!.author.profile_photo"
alt="article-author"
/>
</div>
<span class="typo-label-sm">
{{ article!.author.full_name }}
</span>
</div>
<div
class="w-fit pr-4 pl-5 h-[50px] rounded-full flex items-center justify-center gap-2 border-[1.5px] border-white text-white">
<span class="typo-label-sm mt-0.5">
دسته بندی موبایل
</span>
</div>
</div>
<div class="flex items-center gap-4">
<div
class="w-fit pr-4 pl-5 h-[50px] rounded-full flex items-center justify-center gap-2 border-[1.5px] border-white text-white">
<Icon name="ci:calendar" size="24px" class="**:stroke-white" />
<span class="typo-label-sm mt-0.5">
۲۴ مهر 1403
</span>
</div>
<div
class="w-fit pr-4 pl-5 h-[50px] rounded-full flex items-center justify-center gap-2 border-[1.5px] border-white text-white">
<Icon name="ci:eye-open" size="24px" class="**:stroke-white" />
<span class="typo-label-sm mt-0.5">
{{ article!.views }}
</span>
</div>
</div>
</div>
</div>
</div>
<div class="flex gap-4 mt-8">
<div
class="p-8 flex-1 text-zinc-800 flex flex-col gap-6 [&_p,ul]:text-zinc-500 [&_h1]:typo-h-4 [&_h2]:typo-h-5 [&_h3]:typo-h-6 [&_p]:typo-p-md [&_ul]:list-disc [&_ul]:typo-p-md [&_ul]:space-y-2"
v-html="sanitizedArticleContent"
/>
<aside class="mt-8 p-8 h-fit bg-slate-100 w-[400px] sticky top-4 rounded-3xl">
asdsa
</aside>
</div>
</div>
</template>
+9 -47
View File
@@ -3,6 +3,7 @@
// import
import useGetArticles from "~/composables/api/blog/useGetArticles";
import ArticlesList from "~/components/articles/ArticlesList.vue";
// state
@@ -14,8 +15,6 @@ const { data: articles, suspense } = useGetArticles(page, debouncedSearch);
// ssr
await useAsyncData(async () => {
const response = await suspense();
if (response.isError) {
@@ -24,7 +23,7 @@ await useAsyncData(async () => {
statusMessage: `Error in categories page prefetch`
});
}
});
</script>
<template>
@@ -50,50 +49,13 @@ await useAsyncData(async () => {
</template>
</Input>
</div>
<div class="flex gap-12">
<div class="flex-1 flex flex-col gap-12">
<BlogPost
image="/img/blog-1.jpeg"
description="aaasd"
title="asd"
:comments="2"
link="#"
date="2020-06-10"
tag="asdsa"
/>
<BlogPost
image="/img/blog-2.jpeg"
description="aaasd"
title="asd"
:comments="2"
link="#"
date="2020-06-10"
tag="asdsa"
/>
</div>
<div class="flex-[0.8] flex flex-col">
<BlogPost
image="/img/blog-3.jpeg"
description="aaasd"
title="asd"
:comments="2"
link="#"
date="2020-06-10"
tag="asdsa"
variant="sm"
/>
<BlogPost
image="/img/blog-4.jpeg"
description="aaasd"
title="asd"
:comments="2"
link="#"
date="2020-06-10"
tag="asdsa"
variant="sm"
/>
</div>
</div>
<!-- This is for masonry js package -->
<ClientOnly>
<ArticlesList :articles="articles!.results" />
</ClientOnly>
<div class="w-full flex-center pt-24 pb-10">
<Pagination :items="[]" :total="100" />
</div>
-3
View File
@@ -31,8 +31,6 @@ const filteredCategories = computed(() => {
// ssr
await useAsyncData(async () => {
const response = await suspense();
if (response.isError) {
@@ -41,7 +39,6 @@ await useAsyncData(async () => {
statusMessage: `Error in categories page prefetch`
});
}
});
</script>
+2 -4
View File
@@ -10,16 +10,14 @@ const { suspense } = useHomeData();
// ssr
await useAsyncData(async () => {
const response = await suspense();
if (response.isError) {
throw createError({
statusCode: 500,
statusMessage: `Landing error : ${response.error.message}`,
})
}
statusMessage: `Landing error : ${response.error.message}`
});
}
</script>
-2
View File
@@ -14,7 +14,6 @@ const { suspense : suspenseComments} = useGetComments(id, page);
// ssr
await useAsyncData(async () => {
const productResponse = await suspenseProduct();
const commentsResponse = await suspenseComments();
@@ -24,7 +23,6 @@ await useAsyncData(async () => {
statusMessage: `error : product ${id} prefetch error`
});
}
});
</script>
+2
View File
@@ -158,6 +158,7 @@ const resetForm = () => {
class="flex items-center gap-2 w-full"
>
<Input
data-testid="phone-input"
class="w-full"
v-model="loginInfo.phone"
placeholder="9380123456"
@@ -185,6 +186,7 @@ const resetForm = () => {
/>
<Button
data-testid="send-otp-code-button"
v-if="!showOtp"
class="rounded-full w-full mt-4"
type="submit"
+19
View File
@@ -43,6 +43,25 @@ declare global {
meta_rating: number | null;
};
type Article = {
"id": number,
"title": string,
"slug": string,
"content": string,
"summery": string,
"created_at": string,
"updated_at": string,
"cover_image": string,
"views": number,
"meta_description": string,
"meta_keywords": string,
"author": {
"full_name": string,
"profile_photo": string
},
"category": number
}
type UserComment = {
id: number;
content: string;
+7
View File
@@ -0,0 +1,7 @@
import { defineVitestConfig } from "@nuxt/test-utils/config";
export default defineVitestConfig({
test: {
environment: "nuxt",
},
});