feat: add landing page components including FAQ, features, footer, header, hero, modalities, steps, and section title

This commit is contained in:
2026-09-21 04:32:59 +03:30
parent 1e599b917b
commit 5f19f82f86
41 changed files with 1518 additions and 84 deletions
+14
View File
@@ -0,0 +1,14 @@
.git
node_modules
vendor
public/build
public/hot
public/storage
storage/logs/*
storage/framework/cache/*
storage/framework/sessions/*
storage/framework/views/*
.env
.env.*
!.env.example
graphify-out
+17
View File
@@ -0,0 +1,17 @@
<?php
namespace App\Providers;
use App\Enums\UserEnum;
use Illuminate\Support\Facades\Gate;
use Laravel\Horizon\HorizonApplicationServiceProvider;
class HorizonServiceProvider extends HorizonApplicationServiceProvider
{
protected function gate(): void
{
Gate::define('viewHorizon', function ($user = null) {
return $user?->level === UserEnum::Admin->value;
});
}
}
+1
View File
@@ -2,4 +2,5 @@
return [
App\Providers\AppServiceProvider::class,
App\Providers\HorizonServiceProvider::class,
];
+1
View File
@@ -10,6 +10,7 @@
"hekmatinasser/verta": "^8.5",
"inertiajs/inertia-laravel": "^2.0",
"laravel/framework": "^12.0",
"laravel/horizon": "^5.49",
"laravel/tinker": "^2.10.1",
"maatwebsite/excel": "^3.1",
"spatie/laravel-permission": "^6.17",
Generated
+137 -1
View File
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "7aaff316edab008067e96caa26142a24",
"content-hash": "2f1cb367cfe5d850b1483797ac0810ee",
"packages": [
{
"name": "brick/math",
@@ -1741,6 +1741,86 @@
},
"time": "2025-04-08T19:58:59+00:00"
},
{
"name": "laravel/horizon",
"version": "v5.49.0",
"source": {
"type": "git",
"url": "https://github.com/laravel/horizon.git",
"reference": "6dce8a97b426be5305adad537ad4b6a99de61b31"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/horizon/zipball/6dce8a97b426be5305adad537ad4b6a99de61b31",
"reference": "6dce8a97b426be5305adad537ad4b6a99de61b31",
"shasum": ""
},
"require": {
"ext-json": "*",
"ext-pcntl": "*",
"ext-posix": "*",
"illuminate/contracts": "^9.21|^10.0|^11.0|^12.0|^13.0",
"illuminate/queue": "^9.21|^10.0|^11.0|^12.0|^13.0",
"illuminate/support": "^9.21|^10.0|^11.0|^12.0|^13.0",
"laravel/sentinel": "^1.0",
"nesbot/carbon": "^2.17|^3.0",
"php": "^8.0",
"ramsey/uuid": "^4.0",
"symfony/console": "^6.0|^7.0|^8.0",
"symfony/error-handler": "^6.0|^7.0|^8.0",
"symfony/polyfill-php83": "^1.28",
"symfony/process": "^6.0|^7.0|^8.0"
},
"require-dev": {
"mockery/mockery": "^1.0",
"orchestra/testbench": "^7.56|^8.37|^9.16|^10.9|^11.0",
"phpstan/phpstan": "^1.10|^2.0",
"predis/predis": "^1.1|^2.0|^3.0"
},
"suggest": {
"ext-redis": "Required to use the Redis PHP driver.",
"predis/predis": "Required when not using the Redis PHP driver (^1.1|^2.0|^3.0)."
},
"type": "library",
"extra": {
"laravel": {
"aliases": {
"Horizon": "Laravel\\Horizon\\Horizon"
},
"providers": [
"Laravel\\Horizon\\HorizonServiceProvider"
]
},
"branch-alias": {
"dev-master": "6.x-dev"
}
},
"autoload": {
"psr-4": {
"Laravel\\Horizon\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Taylor Otwell",
"email": "taylor@laravel.com"
}
],
"description": "Dashboard and code-driven configuration for Laravel queues.",
"keywords": [
"laravel",
"queue"
],
"support": {
"issues": "https://github.com/laravel/horizon/issues",
"source": "https://github.com/laravel/horizon/tree/v5.49.0"
},
"time": "2026-09-07T12:00:27+00:00"
},
{
"name": "laravel/prompts",
"version": "v0.3.5",
@@ -1800,6 +1880,62 @@
},
"time": "2025-02-11T13:34:40+00:00"
},
{
"name": "laravel/sentinel",
"version": "v1.1.0",
"source": {
"type": "git",
"url": "https://github.com/laravel/sentinel.git",
"reference": "972d9885d9d14312a118e9565c4e6ecc5e751ea1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/sentinel/zipball/972d9885d9d14312a118e9565c4e6ecc5e751ea1",
"reference": "972d9885d9d14312a118e9565c4e6ecc5e751ea1",
"shasum": ""
},
"require": {
"ext-json": "*",
"illuminate/container": "^8.37|^9.0|^10.0|^11.0|^12.0|^13.0",
"php": "^8.0"
},
"require-dev": {
"laravel/pint": "^1.27",
"orchestra/testbench": "^6.47.1|^7.56|^8.37|^9.16|^10.9|^11.0",
"phpstan/phpstan": "^2.1.33"
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"Laravel\\Sentinel\\SentinelServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"Laravel\\Sentinel\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Taylor Otwell",
"email": "taylor@laravel.com"
},
{
"name": "Mior Muhammad Zaki",
"email": "mior@laravel.com"
}
],
"support": {
"source": "https://github.com/laravel/sentinel/tree/v1.1.0"
},
"time": "2026-03-24T14:03:38+00:00"
},
{
"name": "laravel/serializable-closure",
"version": "v2.0.4",
+254
View File
@@ -0,0 +1,254 @@
<?php
use Illuminate\Support\Str;
return [
/*
|--------------------------------------------------------------------------
| Horizon Name
|--------------------------------------------------------------------------
|
| This name appears in notifications and in the Horizon UI. Unique names
| can be useful while running multiple instances of Horizon within an
| application, allowing you to identify the Horizon you're viewing.
|
*/
'name' => env('HORIZON_NAME'),
/*
|--------------------------------------------------------------------------
| Horizon Domain
|--------------------------------------------------------------------------
|
| This is the subdomain where Horizon will be accessible from. If this
| setting is null, Horizon will reside under the same domain as the
| application. Otherwise, this value will serve as the subdomain.
|
*/
'domain' => env('HORIZON_DOMAIN'),
/*
|--------------------------------------------------------------------------
| Horizon Path
|--------------------------------------------------------------------------
|
| This is the URI path where Horizon will be accessible from. Feel free
| to change this path to anything you like. Note that the URI will not
| affect the paths of its internal API that aren't exposed to users.
|
*/
'path' => env('HORIZON_PATH', 'admin/horizon'),
/*
|--------------------------------------------------------------------------
| Horizon Redis Connection
|--------------------------------------------------------------------------
|
| This is the name of the Redis connection where Horizon will store the
| meta information required for it to function. It includes the list
| of supervisors, failed jobs, job metrics, and other information.
|
*/
'use' => 'default',
/*
|--------------------------------------------------------------------------
| Horizon Redis Prefix
|--------------------------------------------------------------------------
|
| This prefix will be used when storing all Horizon data in Redis. You
| may modify the prefix when you are running multiple installations
| of Horizon on the same server so that they don't have problems.
|
*/
'prefix' => env(
'HORIZON_PREFIX',
Str::slug(env('APP_NAME', 'laravel'), '_').'_horizon:'
),
/*
|--------------------------------------------------------------------------
| Horizon Route Middleware
|--------------------------------------------------------------------------
|
| These middleware will get attached onto each Horizon route, giving you
| the chance to add your own middleware to this list or change any of
| the existing middleware. Or, you can simply stick with this list.
|
*/
'middleware' => ['web', 'admin.access'],
/*
|--------------------------------------------------------------------------
| Queue Wait Time Thresholds
|--------------------------------------------------------------------------
|
| This option allows you to configure when the LongWaitDetected event
| will be fired. Every connection / queue combination may have its
| own, unique threshold (in seconds) before this event is fired.
|
*/
'waits' => [
'redis:default' => 60,
],
/*
|--------------------------------------------------------------------------
| Job Trimming Times
|--------------------------------------------------------------------------
|
| Here you can configure for how long (in minutes) you desire Horizon to
| persist the recent and failed jobs. Typically, recent jobs are kept
| for one hour while all failed jobs are stored for an entire week.
|
*/
'trim' => [
'recent' => 60,
'pending' => 60,
'completed' => 60,
'recent_failed' => 10080,
'failed' => 10080,
'monitored' => 10080,
],
/*
|--------------------------------------------------------------------------
| Silenced Jobs
|--------------------------------------------------------------------------
|
| Silencing a job will instruct Horizon to not place the job in the list
| of completed jobs within the Horizon dashboard. This setting may be
| used to fully remove any noisy jobs from the completed jobs list.
|
*/
'silenced' => [
// App\Jobs\ExampleJob::class,
],
'silenced_tags' => [
// 'notifications',
],
/*
|--------------------------------------------------------------------------
| Metrics
|--------------------------------------------------------------------------
|
| Here you can configure how many snapshots should be kept to display in
| the metrics graph. This will get used in combination with Horizon's
| `horizon:snapshot` schedule to define how long to retain metrics.
|
*/
'metrics' => [
'trim_snapshots' => [
'job' => 24,
'queue' => 24,
],
],
/*
|--------------------------------------------------------------------------
| Fast Termination
|--------------------------------------------------------------------------
|
| When this option is enabled, Horizon's "terminate" command will not
| wait on all of the workers to terminate unless the --wait option
| is provided. Fast termination can shorten deployment delay by
| allowing a new instance of Horizon to start while the last
| instance will continue to terminate each of its workers.
|
*/
'fast_termination' => false,
/*
|--------------------------------------------------------------------------
| Memory Limit (MB)
|--------------------------------------------------------------------------
|
| This value describes the maximum amount of memory the Horizon master
| supervisor may consume before it is terminated and restarted. For
| configuring these limits on your workers, see the next section.
|
*/
'memory_limit' => 64,
/*
|--------------------------------------------------------------------------
| Queue Worker Configuration
|--------------------------------------------------------------------------
|
| Here you may define the queue worker settings used by your application
| in all environments. These supervisors and settings handle all your
| queued jobs and will be provisioned by Horizon during deployment.
|
*/
'defaults' => [
'supervisor-1' => [
'connection' => 'redis',
'queue' => ['default'],
'balance' => 'auto',
'autoScalingStrategy' => 'time',
'maxProcesses' => 1,
'maxTime' => 0,
'maxJobs' => 0,
'memory' => 128,
'tries' => 1,
'timeout' => 60,
'nice' => 0,
],
],
'environments' => [
'production' => [
'supervisor-1' => [
'maxProcesses' => 10,
'balanceMaxShift' => 1,
'balanceCooldown' => 3,
],
],
'local' => [
'supervisor-1' => [
'maxProcesses' => 3,
],
],
],
/*
|--------------------------------------------------------------------------
| File Watcher Configuration
|--------------------------------------------------------------------------
|
| The following list of directories and files will be watched when using
| the `horizon:listen` command. Whenever any directories or files are
| changed, Horizon will automatically restart to apply all changes.
|
*/
'watch' => [
'app',
'bootstrap',
'config/**/*.php',
'database/**/*.php',
'public/**/*.php',
'resources/**/*.php',
'routes',
'composer.lock',
'composer.json',
'.env',
],
];
+87
View File
@@ -0,0 +1,87 @@
x-app: &app
build:
context: .
dockerfile: docker/php/Dockerfile
image: radical-app
volumes:
- .:/var/www/html
environment: &app-env
DB_CONNECTION: pgsql
DB_HOST: postgres
DB_PORT: 5432
DB_DATABASE: ${DB_DATABASE:-radical}
DB_USERNAME: ${DB_USERNAME:-radical}
DB_PASSWORD: ${DB_PASSWORD:-secret}
REDIS_HOST: redis
REDIS_PORT: 6379
QUEUE_CONNECTION: redis
CACHE_STORE: redis
SESSION_DRIVER: redis
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
services:
app:
<<: *app
horizon:
<<: *app
command: php artisan horizon
restart: unless-stopped
depends_on:
app:
condition: service_started
nginx:
image: nginx:1.27-alpine
ports:
- "${APP_PORT:-8000}:80"
volumes:
- .:/var/www/html:ro
- ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
depends_on:
- app
node:
image: node:24-alpine
working_dir: /var/www/html
command: sh -c "npm install && npm run dev -- --host 0.0.0.0"
volumes:
- .:/var/www/html
- node-modules:/var/www/html/node_modules
ports:
- "5173:5173"
postgres:
image: postgres:16-alpine
environment:
POSTGRES_DB: ${DB_DATABASE:-radical}
POSTGRES_USER: ${DB_USERNAME:-radical}
POSTGRES_PASSWORD: ${DB_PASSWORD:-secret}
ports:
- "${DB_FORWARD_PORT:-5432}:5432"
volumes:
- postgres-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USERNAME:-radical} -d ${DB_DATABASE:-radical}"]
interval: 5s
timeout: 5s
retries: 10
redis:
image: redis:7-alpine
volumes:
- redis-data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 5s
retries: 10
volumes:
postgres-data:
redis-data:
node-modules:
+22
View File
@@ -0,0 +1,22 @@
server {
listen 80;
server_name _;
root /var/www/html/public;
index index.php;
client_max_body_size 64M;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_pass app:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
}
location ~ /\.(?!well-known).* {
deny all;
}
}
+19
View File
@@ -0,0 +1,19 @@
FROM php:8.4-fpm-alpine
RUN apk add --no-cache \
bash curl git postgresql-dev libzip-dev icu-dev libpng-dev libjpeg-turbo-dev freetype-dev oniguruma-dev linux-headers $PHPIZE_DEPS \
&& docker-php-ext-configure gd --with-freetype --with-jpeg \
&& docker-php-ext-install -j"$(nproc)" pdo_pgsql pgsql gd zip intl bcmath exif pcntl opcache \
&& pecl install redis \
&& docker-php-ext-enable redis \
&& apk del $PHPIZE_DEPS
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
COPY docker/php/php.ini /usr/local/etc/php/conf.d/app.ini
COPY docker/php/entrypoint.sh /usr/local/bin/entrypoint
RUN chmod +x /usr/local/bin/entrypoint
WORKDIR /var/www/html
ENTRYPOINT ["entrypoint"]
CMD ["php-fpm"]
+19
View File
@@ -0,0 +1,19 @@
#!/bin/bash
set -e
if [ "$1" = "php-fpm" ]; then
[ -d vendor ] || composer install --no-interaction --prefer-dist
[ -f .env ] || cp .env.example .env
grep -q '^APP_KEY=.\+' .env || php artisan key:generate --no-interaction
until php -r "new PDO('pgsql:host=${DB_HOST};port=${DB_PORT};dbname=${DB_DATABASE}', '${DB_USERNAME}', '${DB_PASSWORD}');" >/dev/null 2>&1; do
echo "waiting for postgres..."
sleep 2
done
php artisan migrate --force --no-interaction
php artisan storage:link --no-interaction || true
chown -R www-data:www-data storage bootstrap/cache
fi
exec "$@"
+6
View File
@@ -0,0 +1,6 @@
upload_max_filesize = 64M
post_max_size = 64M
memory_limit = 512M
max_execution_time = 120
opcache.enable = 1
opcache.validate_timestamps = 1
Binary file not shown.

After

Width:  |  Height:  |  Size: 419 KiB

+195
View File
@@ -0,0 +1,195 @@
@keyframes landing-float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-12px); }
}
@keyframes landing-scan {
0% { top: 0; opacity: 0; }
8% { opacity: 1; }
92% { opacity: 1; }
100% { top: 100%; opacity: 0; }
}
@keyframes landing-pulse {
0% { transform: scale(0.9); opacity: 0.8; }
100% { transform: scale(1.7); opacity: 0; }
}
@keyframes landing-blob {
0%, 100% { border-radius: 42% 58% 60% 40% / 45% 40% 60% 55%; }
50% { border-radius: 58% 42% 40% 60% / 55% 60% 40% 45%; }
}
@keyframes landing-kenburns {
from { transform: scale(1); }
to { transform: scale(1.08); }
}
@keyframes landing-spin {
to { transform: rotate(360deg); }
}
@keyframes landing-marquee {
from { transform: translateX(0); }
to { transform: translateX(50%); }
}
@keyframes landing-draw {
to { stroke-dashoffset: 0; }
}
@keyframes landing-timeline {
0%, 12% { opacity: 0.35; }
18%, 100% { opacity: 1; }
}
@keyframes landing-toggle {
0%, 40% { transform: translateX(0); }
50%, 90% { transform: translateX(-100%); }
100% { transform: translateX(0); }
}
@keyframes landing-bubble {
0% { opacity: 0; transform: translateY(12px) scale(0.96); }
100% { opacity: 1; transform: translateY(0) scale(1); }
}
.landing-page {
background-image: radial-gradient(rgba(128, 14, 19, 0.08) 1px, transparent 1px);
background-size: 28px 28px;
}
.landing-grid {
background-image: linear-gradient(#DEE3EB 1px, transparent 1px), linear-gradient(90deg, #DEE3EB 1px, transparent 1px);
background-size: 32px 32px;
}
.landing-float {
animation: landing-float 6s ease-in-out infinite;
}
.landing-float--delay {
animation-delay: 1.6s;
}
.landing-scan {
animation: landing-scan 3.6s ease-in-out infinite;
}
.landing-pulse::after {
content: '';
@apply absolute inset-0 rounded-full border-2 border-complement-700;
animation: landing-pulse 2.2s ease-out infinite;
}
.landing-blob {
animation: landing-blob 14s ease-in-out infinite;
}
.landing-kenburns {
animation: landing-kenburns 24s ease-out forwards;
}
.landing-spin svg {
animation: landing-spin 18s linear infinite;
}
.landing-marquee {
animation: landing-marquee 32s linear infinite;
}
.landing-marquee:hover {
animation-play-state: paused;
}
.landing-underline path {
stroke-dasharray: 320;
stroke-dashoffset: 320;
animation: landing-draw 1.1s ease-out 0.5s forwards;
}
.landing-timeline-step {
animation: landing-timeline 8s ease-in-out infinite;
}
.landing-timeline-step:nth-child(2) { animation-delay: 1.6s; }
.landing-timeline-step:nth-child(3) { animation-delay: 3.2s; }
.landing-timeline-step:nth-child(4) { animation-delay: 4.8s; }
.landing-toggle-thumb {
animation: landing-toggle 6s ease-in-out infinite;
}
.landing-bubble {
opacity: 0;
animation: landing-bubble 0.5s ease-out forwards;
}
.landing-bubble:nth-child(2) { animation-delay: 0.6s; }
.landing-bubble:nth-child(3) { animation-delay: 1.2s; }
.landing-reveal {
@apply opacity-0 translate-y-8 transition-all duration-700 ease-out;
}
.landing-reveal.is-visible {
@apply opacity-100 translate-y-0;
}
.landing-reveal--1 { transition-delay: 100ms; }
.landing-reveal--2 { transition-delay: 200ms; }
.landing-reveal--3 { transition-delay: 300ms; }
.landing-reveal--4 { transition-delay: 400ms; }
.landing-reveal--5 { transition-delay: 500ms; }
.landing-timeline-line {
@apply origin-top scale-y-0 transition-transform duration-1000 ease-out;
}
.landing-reveal.is-visible .landing-timeline-line {
@apply scale-y-100;
}
.landing-faq summary::-webkit-details-marker {
display: none;
}
.landing-faq[open] .landing-faq-icon {
@apply rotate-45;
}
html:has(.landing-page) {
scroll-behavior: smooth;
}
@media (prefers-reduced-motion: reduce) {
.landing-float,
.landing-scan,
.landing-kenburns,
.landing-spin svg,
.landing-blob,
.landing-marquee,
.landing-timeline-step,
.landing-toggle-thumb,
.landing-pulse::after {
animation: none;
}
.landing-underline path {
animation: none;
stroke-dashoffset: 0;
}
.landing-bubble {
animation: none;
opacity: 1;
}
.landing-reveal {
@apply opacity-100 translate-y-0 transition-none;
}
.landing-timeline-line {
@apply scale-y-100 transition-none;
}
}
+4 -4
View File
@@ -23,19 +23,19 @@
}
.ui-button--dominant.p-button.p-button-outlined {
@apply bg-transparent text-dominant-500 border-dominant-500;
@apply !bg-transparent !text-dominant-500 border-dominant-500;
}
.ui-button--dominant.p-button.p-button-outlined:hover {
@apply bg-dominant-500 text-white;
@apply !bg-dominant-500 !text-white;
}
.ui-button--dominant.p-button.p-button-text {
@apply bg-transparent text-dominant-500 border-transparent;
@apply !bg-transparent !text-dominant-500 !border-transparent;
}
.ui-button--dominant.p-button.p-button-text:hover {
background-color: rgba(128, 14, 19, 0.1);
background-color: rgba(128, 14, 19, 0.1) !important;
}
+7
View File
@@ -81,3 +81,10 @@
background: #576989 !important;
}
@media (max-width: 767px) {
.fast-request-dialog {
width: calc(100vw - 1.5rem) !important;
max-width: 688px;
}
}
+10
View File
@@ -48,3 +48,13 @@
.p-datatable-tbody > tr > td{
@apply !border-0
}
.table-generic {
@apply w-full min-w-0;
}
.table-generic-scroll {
@apply w-full overflow-x-auto;
}
.table-generic-scroll .p-datatable-table {
@apply min-w-full;
}
+1
View File
@@ -14,6 +14,7 @@
@import "tailwindcss";
@import "./Client/sidebar/sidebar.css";
@import "./Client/page-transitions.css";
@import "./Client/landing.css";
@config "../../Client-tailwind.config.js";
@@ -55,7 +55,7 @@ defineProps({
<ActivitiesWidget :stats="stats" />
<UiCard title="ثبت آنی درخواست جدید" variant="default" class="mt-4">
<p class="w-full flex items-center justify-start font-[400] text-[11px]">فقط چند قدم ساده برای ثبت درخواست کافیه.</p>
<div class="w-full h-[179px] grid grid-cols-5 gap-3 justify-start pt-[10px]">
<div class="w-full grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-3 justify-start pt-[10px]">
<RequestCard
v-for="modality in modalities"
:key="modality.code"
+30 -28
View File
@@ -1,39 +1,41 @@
<script setup>
import { Link } from '@inertiajs/inertia-vue3'
import { ref } from 'vue'
import { usePage } from '@inertiajs/inertia-vue3'
import { useScrollReveal } from '../composables/useScrollReveal'
import LandingHeader from '../components/landing/LandingHeader.vue'
import LandingHero from '../components/landing/LandingHero.vue'
import LandingMarquee from '../components/landing/LandingMarquee.vue'
import LandingSteps from '../components/landing/LandingSteps.vue'
import LandingFeatures from '../components/landing/LandingFeatures.vue'
import LandingModalities from '../components/landing/LandingModalities.vue'
import LandingComparison from '../components/landing/LandingComparison.vue'
import LandingFaq from '../components/landing/LandingFaq.vue'
import LandingCta from '../components/landing/LandingCta.vue'
import LandingFooter from '../components/landing/LandingFooter.vue'
const {url ,props} = usePage();
const user = props.value.auth.User || null;
const { props } = usePage()
const user = props.value.auth.User || null
const page = ref(null)
useScrollReveal(page)
</script>
<template>
<div class="min-h-screen flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
<div class="max-w-2xl w-full space-y-8">
<h1 class="mt-6 text-center text-3xl font-extrabold text-gray-900">
پروژه رادیکال
</h1>
<div ref="page" class="landing-page min-h-screen bg-white text-dark">
<LandingHeader :user="user" />
<div class="bg-white p-6 rounded-lg space-y-6">
<p>
لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ و با استفاده از طراحان گرافیک است. چاپگرها و متون بلکه روزنامه و مجله در ستون و سطرآنچنان که لازم است و برای شرایط فعلی تکنولوژی مورد نیاز و کاربردهای متنوع با هدف بهبود ابزارهای کاربردی می باشد. کتابهای زیادی در شصت و سه درصد گذشته، حال و آینده شناخت فراوان جامعه و متخصصان را می طلبد تا با نرم افزارها شناخت بیشتری را برای طراحان رایانه ای علی الخصوص طراحان خلاقی و فرهنگ پیشرو در زبان فارسی ایجاد کرد. در این صورت می توان امید داشت که تمام و دشواری موجود در ارائه راهکارها و شرایط سخت تایپ به پایان رسد و زمان مورد نیاز شامل حروفچینی دستاوردهای اصلی و جوابگوی سوالات پیوسته اهل دنیای موجود طراحی اساسا مورد استفاده قرار گیرد.
</p>
</div>
<main>
<LandingHero :user="user" />
<LandingMarquee />
<LandingSteps />
<LandingFeatures />
<LandingModalities />
<LandingComparison />
<LandingFaq />
<LandingCta :user="user" />
</main>
<div class="flex items-center justify-center gap-4" v-if="!user">
<Link :href="route('login')" class="text-center bg-primary text-white px-4 py-2 rounded-md hover:bg-primary-active transition-all duration-300">
ورود
</Link>
<Link :href="route('login')" class="text-center bg-primary text-white px-4 py-2 rounded-md hover:bg-primary-active transition-all duration-300">
ثبت نام
</Link>
</div>
<div class="flex items-center justify-center gap-4" v-if="user">
<a :href="route('dashboard.index')" class="text-center bg-primary text-white px-4 py-2 rounded-md hover:bg-primary-active transition-all duration-300">
داشبورد
</a>
</div>
</div>
<LandingFooter />
</div>
</template>
@@ -65,9 +65,9 @@
props.selectable && 'cursor-pointer',
fastRequestStore.requestType === props.value ? 'border-[#576989] border-2': 'border-[#DEE3EB]'
]">
<component :is="iconComponent" class="w-[87px] h-[80px]" />
<component :is="iconComponent" class="w-[72px] h-[66px] sm:w-[87px] sm:h-[80px] shrink-0" />
<div class="w-full flex flex-col gap-1">
<p class="w-full flex items-center justify-center font-[700] text-[14px]">{{ title }}</p>
<p class="w-full flex items-center justify-center text-center font-[700] text-[13px] sm:text-[14px] leading-6">{{ title }}</p>
<ui-button v-if="!selectable" @click="submitRequest" label="ثبت درخواست" severity="secondary" size="small" full-width="true"/>
</div>
</div>
@@ -32,7 +32,7 @@ const showChooseArchive = () => {
</p>
<button type="button" @click="fastRequestStore.clearArchivePet" class="text-[11px] font-[400] text-[#7A1419] cursor-pointer">حذف و ثبت کیس جدید</button>
</div>
<form v-else class="p-4 grid grid-cols-2 gap-4 w-full">
<form v-else class="p-4 grid grid-cols-1 sm:grid-cols-2 gap-4 w-full">
<InputText
v-model="fastRequestStore.requestCase.name"
type="text"
@@ -11,7 +11,7 @@ const fastRequestStore = useFastRequestStore();
<template>
<div class="w-full">
<p class="text-[11px] font-[400] text-[#3E4C63] py-3">جزئیات درخواست و اطلاعات مورد نیاز را با دقت و حوصله وارد نمایید.</p>
<div class="w-full grid grid-cols-2 gap-2 items-center justify-between">
<div class="w-full grid grid-cols-1 sm:grid-cols-2 gap-2 items-center justify-between">
<Select
v-model="fastRequestStore.requestDetails.priority"
:options="priorities"
@@ -29,7 +29,7 @@ const fastRequestStore = useFastRequestStore();
rows="3"
class="w-full h-full" />
</div>
<div class="w-full grid grid-cols-2 gap-2 items-start justify-between my-4">
<div class="w-full grid grid-cols-1 sm:grid-cols-2 gap-2 items-start justify-between my-4">
<FileUploadBox v-model="fastRequestStore.historyFiles" title="سوابق پزشکی قبلی (اختیاری)" />
<FileUploadBox v-model="fastRequestStore.scanFiles" :title="`فایل «${modalityTitle(fastRequestStore.requestType)}»`" />
</div>
@@ -9,7 +9,7 @@ const selectDoctor = (doctorId) => {
</script>
<template>
<div class="w-full h-full grid grid-cols-3 gap-2 items-center justify-between py-4">
<div class="w-full h-full grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-2 items-center justify-between py-4">
<button
type="button"
@click="selectDoctor(null)"
@@ -17,7 +17,7 @@ const selectDoctor = (doctorId) => {
:class="fastRequestStore.doctorId === null ? 'border-[2px] border-[#576989]' : 'border-[1px] border-[#DEE3EB]'"
>
<h3 class="text-[13px] font-[600] text-[#576989]">انتخاب خودکار</h3>
<p class="w-[190px] text-center text-[11px] font-[600] text-[#3E4C63]">انتخاب در دسترس‌ ترین پزشک جهت بررسی سریع درخواست شما</p>
<p class="max-w-[190px] px-2 text-center text-[11px] font-[600] text-[#3E4C63]">انتخاب در دسترس‌ ترین پزشک جهت بررسی سریع درخواست شما</p>
</button>
<button
v-for="doctor in fastRequestStore.doctors"
@@ -6,7 +6,7 @@ import { modalities } from '../../../data/fastRequestOptions';
<template>
<div class="w-full space-y-3 py-6">
<p class="h-[17px] flex items-center justify-start text-[11px] font-[600] text-[#3E4C63]">نوع درخواست خود را مشخص کنید</p>
<div class="w-full h-[133px] grid grid-cols-5 gap-3 justify-start">
<div class="w-full grid grid-cols-2 sm:grid-cols-3 md:grid-cols-5 gap-3 justify-start">
<RequestCard
v-for="modality in modalities"
:key="modality.code"
@@ -45,44 +45,38 @@ const menuItems = [
</script>
<template>
<header class="grid items-center
shadow-client-gray/10 grid-cols-2a pe-4 justify-between !ps-[16rem] max-sm:!ps-4 fixed top-0 left-0 right-0 z-20 w-full h-[4.5rem] bg-white">
<div class="grid grid-cols-2a justify-start items-center gap-4">
<div class="grid grid-cols-1 max-sm:!grid-cols-2a justify-start items-center">
<header class="flex items-center gap-3 sm:gap-4 shadow-client-gray/10 px-4 lg:!ps-[16rem] fixed top-0 left-0 right-0 z-20 w-full h-[4.5rem] bg-white">
<Button
icon="fi-rr-menu-burger"
class="!hidden !invisible !px-0 !w-0 !h-0 max-sm:!w-10 max-sm:!h-10 max-sm:!visible max-sm:!block items-center !border-dominant-50"
class="lg:!hidden !w-10 !h-10 shrink-0 !border-dominant-50"
pt:icon="flex !text-dominant-300 justify-center items-center"
variant="outlined"
size="large"
@click="toggleSidebar"
/>
</div>
<div class="grid grid-cols-1 h-[4rem] content-center justify-start items-center">
<div class="flex-1 min-w-0 max-w-[30rem]">
<UiInput
placeholder="جستجو سریع: نام سرپرست، نام حیوان و یا شماره پرونده"
variant="outlined"
prefixClasses="!border-dominant-50"
class="!w-[30rem] max-lg:!w-[22rem] max-md:!w-[26rem] max-sm:!w-[15rem] max-xs:!w-[14rem]"
class="!w-full"
prefixColor="dominant-700"
iconStart="svg:SearchSvg"
wrapperBorderClass="!border-dominant-50"
customClass="placeholder:!text-dominant-300 !border-dominant-50 !font-normal !text-[11px]"
customClass="placeholder:!text-dominant-300 !border-dominant-50 !font-normal !text-[11px] truncate"
/>
</div>
</div>
<div class="flex items-center gap-4">
<div class="flex items-center gap-2 sm:gap-4 shrink-0 me-auto">
<Link href="/dashboard/wallet" class="flex items-center gap-2.5 rounded-lg bg-dominant-light border border-dominant-50 ps-1.5 pe-3 py-1.5 transition-colors hover:border-dominant-100">
<span class="flex items-center justify-center w-8 h-8 rounded-lg bg-white border border-dominant-50 text-dominant-400">
<i class="fi fi-rr-wallet flex text-[15px]"></i>
</span>
<span class="flex flex-col max-sm:hidden">
<span class="hidden md:flex flex-col">
<span class="text-[10px] font-normal text-dominant-200">موجودی کیف پول</span>
<span class="text-[12px] font-bold text-dominant-500">{{ walletBalance }} <span class="text-[10px] font-normal text-dominant-300">تومان</span></span>
</span>
</Link>
<div class="flex items-center justify-center border-l-[1px] !border-dominant-50 pl-4">
<div class="hidden sm:flex items-center justify-center border-l-[1px] !border-dominant-50 pl-4">
<GlobalSvg />
</div>
<div ref="userMenuRef" class="relative">
@@ -91,7 +85,7 @@ const menuItems = [
<img v-if="globalStore.current_user.profile_url" :src="globalStore.current_user.profile_url" alt="تصویر پروفایل" class="w-full h-full object-cover" />
<i v-else class="fi fi-rr-user flex text-sm text-dominant-300"></i>
</span>
<span class="text-dominant-300 text-[12px] font-normal max-sm:hidden">
<span class="text-dominant-300 text-[12px] font-normal hidden md:inline">
{{ globalStore.current_user.fullname }}
</span>
<i class="fi fi-rr-angle-small-down flex text-sm text-dominant-300 transition-transform duration-200" :class="{ 'rotate-180': userMenuOpen }"></i>
@@ -0,0 +1,34 @@
<script setup>
import LandingSectionTitle from './LandingSectionTitle.vue'
const rows = [
{ label: 'جابه‌جایی پت', before: 'سفر پراسترس تا کلینیک', after: 'پت در خانه می‌ماند' },
{ label: 'دسترسی به رادیولوژیست', before: 'وابسته به شهر و کلینیک', after: 'رادیولوژیست در شیفت، از هر جای ایران' },
{ label: 'زمان دریافت گزارش', before: 'روزها و تماس‌های پیگیری', after: '۲۴ ساعت عادی · ۳ ساعت اورژانسی' },
{ label: 'خبردار شدن', before: 'باید خودتان زنگ بزنید', after: 'پیامک در هر مرحله' },
{ label: 'نگهداری سوابق', before: 'کاغذ و CD گم‌شدنی', after: 'پرونده‌ی دیجیتال دائمی' },
]
</script>
<template>
<section class="bg-light py-20 lg:py-28">
<div class="container max-w-5xl px-4 sm:px-6">
<LandingSectionTitle eyebrow="چرا رادیکال" title="همان گزارش تخصصی، بدون دردسرهایش" />
<div class="landing-reveal mt-14 overflow-hidden rounded-[28px] border-2 border-dominant-50 bg-white">
<div class="grid grid-cols-[1.1fr_1fr_1fr] bg-dominant-50/40 text-xs sm:text-sm font-bold">
<span class="px-4 sm:px-6 py-4 text-dark/50">مقایسه</span>
<span class="px-4 sm:px-6 py-4 text-dark/50">مراجعه‌ی حضوری</span>
<span class="px-4 sm:px-6 py-4 bg-dominant-500 text-white flex items-center gap-2"><i class="fi fi-rr-paw leading-none text-[11px]"></i>رادیکال</span>
</div>
<dl>
<div v-for="row in rows" :key="row.label" class="grid grid-cols-[1.1fr_1fr_1fr] border-t border-dominant-50 text-xs sm:text-sm">
<dt class="px-4 sm:px-6 py-4 font-bold text-dark">{{ row.label }}</dt>
<dd class="px-4 sm:px-6 py-4 text-dark/50 flex items-start gap-2"><i class="fi fi-rr-cross-small text-danger leading-none mt-1"></i>{{ row.before }}</dd>
<dd class="px-4 sm:px-6 py-4 bg-dominant-50/50 font-semibold text-dark flex items-start gap-2"><i class="fi fi-rr-check text-success leading-none mt-1 text-[11px]"></i>{{ row.after }}</dd>
</div>
</dl>
</div>
</div>
</section>
</template>
@@ -0,0 +1,47 @@
<script setup>
import { Inertia } from '@inertiajs/inertia'
import UiButton from '../ui/UiButton.vue'
defineProps({
user: {
type: Object,
default: null
}
})
</script>
<template>
<section class="bg-white pb-20 lg:pb-28">
<div class="container max-w-7xl px-4 sm:px-6">
<div class="landing-reveal relative overflow-hidden rounded-[36px] bg-dark px-6 py-16 sm:px-12 lg:px-20 lg:py-20 grid lg:grid-cols-[1.3fr_1fr] gap-10 items-center">
<svg class="absolute -bottom-16 -left-10 w-72 h-72 text-white/[0.04] pointer-events-none" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M8.35 3c1.18-.17 2.43 1.12 2.79 2.9.36 1.77-.29 3.35-1.47 3.53-1.17.18-2.43-1.11-2.8-2.89-.36-1.77.3-3.35 1.48-3.54zm7.3 0c1.18.19 1.84 1.77 1.48 3.54-.37 1.78-1.63 3.07-2.8 2.89-1.18-.18-1.83-1.76-1.47-3.53.36-1.78 1.61-3.07 2.79-2.9zM3 7.6c1.14-.49 2.72.38 3.53 1.95.81 1.56.55 3.22-.58 3.71-1.14.48-2.72-.39-3.53-1.95C1.61 9.75 1.87 8.09 3 7.6zm18 0c1.13.49 1.39 2.15.58 3.71-.81 1.56-2.39 2.43-3.53 1.95-1.13-.49-1.39-2.15-.58-3.71.81-1.57 2.39-2.44 3.53-1.95zM12 12c2.8 0 5.6 3.2 6.2 6.2.4 2-.8 3.8-3 3.8-1.2 0-2.2-.7-3.2-.7s-2 .7-3.2.7c-2.2 0-3.4-1.8-3-3.8C6.4 15.2 9.2 12 12 12z" />
</svg>
<div class="relative">
<h2 class="text-2xl sm:text-3xl lg:text-[2.6rem] font-black leading-[1.45] text-white">
اولین پرونده‌ی پت‌تان را
<span class="text-dominant-300">همین امروز</span>
باز کنید
</h2>
<p class="mt-5 max-w-lg text-base leading-8 text-white/60">
ثبت‌نام فقط با شماره موبایل و در کمتر از یک دقیقه. تا وقتی درخواستی ثبت نکنید، هزینه‌ای در کار نیست.
</p>
<div class="mt-8 flex flex-wrap items-center gap-4">
<UiButton v-if="user" label="رفتن به پنل کاربری" severity="secondary" size="large" @click="Inertia.visit(route('dashboard.index'))" />
<UiButton v-else label="ثبت‌نام به‌عنوان سرپرست" severity="secondary" size="large" @click="Inertia.visit(route('register.chooseType'))" />
<span class="text-xs text-white/40">بدون نیاز به کارت بانکی</span>
</div>
</div>
<ul class="relative space-y-3" aria-hidden="true">
<li v-for="(item, i) in ['ساخت پروفایل پت', 'آپلود تصاویر', 'انتخاب رادیولوژیست', 'دریافت گزارش']" :key="item" class="flex items-center gap-3 rounded-2xl bg-white/5 border border-white/10 px-4 py-3 text-sm text-white/80">
<span :class="i < 3 ? 'bg-success text-white' : 'bg-white/10 text-white/50'" class="w-6 h-6 rounded-full flex items-center justify-center text-[10px]"><i class="fi fi-rr-check leading-none"></i></span>
{{ item }}
<span v-if="i === 3" class="mr-auto text-[10px] text-dominant-300">پیامک ↓</span>
</li>
</ul>
</div>
</div>
</section>
</template>
@@ -0,0 +1,54 @@
<script setup>
import LandingSectionTitle from './LandingSectionTitle.vue'
const faqs = [
{
question: 'برای ثبت‌نام به چه چیزی نیاز دارم؟',
answer: 'فقط شماره موبایل. با کد تأیید پیامکی وارد می‌شوید و به‌عنوان «سرپرست» ثبت‌نام می‌کنید. نیازی به مدارک پزشکی یا پروانه نیست.'
},
{
question: 'تصاویر را با چه فرمتی می‌توانم بفرستم؟',
answer: 'تصاویر و فایل‌هایی که مرکز تصویربرداری به شما داده (عکس، PDF یا فایل فشرده) را می‌توانید مستقیم در فرم درخواست بارگذاری کنید. سوابق قبلی پت را هم می‌توانید جداگانه ضمیمه کنید.'
},
{
question: 'گزارش چقدر طول می‌کشد؟',
answer: 'با اولویت عادی، مهلت بررسی پرونده ۲۴ ساعت پس از پرداخت است و با اولویت اورژانسی ۳ ساعت. اولویت را هنگام ثبت درخواست انتخاب می‌کنید.'
},
{
question: 'گزارش را چطور دریافت می‌کنم؟',
answer: 'به‌محض آماده شدن گزارش، پیامک دریافت می‌کنید و گزارش کامل همراه با پیوست‌های پزشک در بخش «پرونده‌ها»ی پنل شما قابل مشاهده و دانلود است. برای حفظ حریم خصوصی، متن گزارش هرگز در پیامک ارسال نمی‌شود.'
},
{
question: 'اگر پزشک پرونده را رد کند چه می‌شود؟',
answer: 'اگر تصاویر قابل گزارش‌دهی نباشند، پزشک با ذکر دلیل پرونده را رد می‌کند و مبلغ فاکتور به‌صورت کامل به کیف پول شما بازگردانده می‌شود.'
},
]
</script>
<template>
<section id="faq" class="bg-white py-20 lg:py-28 scroll-mt-20">
<div class="container max-w-7xl px-4 sm:px-6 grid lg:grid-cols-[1fr_1.6fr] gap-12 lg:gap-20 items-start">
<div class="lg:sticky lg:top-28">
<LandingSectionTitle class="!text-right !mx-0" eyebrow="سوالات متداول" title="چیزهایی که سرپرستان قبل از اولین درخواست می‌پرسند" />
<p class="landing-reveal landing-reveal--2 mt-6 text-sm leading-7 text-dark/55">جوابتان را پیدا نکردید؟ بعد از ورود، از داخل پنل با ما در تماس باشید.</p>
</div>
<div class="space-y-3">
<details
v-for="(faq, index) in faqs"
:key="faq.question"
:class="`landing-reveal--${Math.min(index + 1, 5)}`"
class="landing-faq landing-reveal group rounded-2xl bg-light border-2 border-transparent open:border-dominant-100 open:bg-white transition-colors duration-300"
>
<summary class="flex items-center justify-between gap-4 cursor-pointer list-none px-6 py-5 text-sm sm:text-base font-bold text-dark">
{{ faq.question }}
<span class="landing-faq-icon shrink-0 w-8 h-8 rounded-full bg-white border border-dominant-50 flex items-center justify-center text-dominant-500 transition-transform duration-300">
<i class="fi fi-rr-plus text-xs leading-none"></i>
</span>
</summary>
<p class="px-6 pb-6 text-sm leading-7 text-dark/60">{{ faq.answer }}</p>
</details>
</div>
</div>
</section>
</template>
@@ -0,0 +1,72 @@
<script setup>
import LandingSectionTitle from './LandingSectionTitle.vue'
</script>
<template>
<section id="features" class="bg-light py-20 lg:py-28 scroll-mt-20">
<div class="container max-w-7xl px-4 sm:px-6">
<LandingSectionTitle
eyebrow="امکانات"
title="ساخته‌شده برای کسی که نگران پتش است، نه برای کلینیک"
description="بدون اصطلاحات تخصصی، بدون فرم‌های طولانی. فقط چیزهایی که یک سرپرست واقعاً لازم دارد."
/>
<div class="mt-14 grid gap-5 md:grid-cols-2 lg:grid-cols-4 lg:grid-rows-2">
<article class="landing-reveal lg:col-span-2 lg:row-span-2 relative overflow-hidden rounded-[28px] bg-dark p-7 sm:p-9 text-white">
<div class="landing-blob absolute -bottom-24 -left-24 w-72 h-72 bg-dominant-500/40 pointer-events-none" aria-hidden="true"></div>
<div class="relative">
<span class="inline-flex w-12 h-12 items-center justify-center rounded-2xl bg-white/10 text-xl"><i class="fi fi-rr-comment-alt-dots leading-none"></i></span>
<h3 class="mt-6 text-2xl font-extrabold">در هر مرحله پیامک می‌گیرید</h3>
<p class="mt-3 max-w-md text-sm leading-7 text-white/65">ثبت درخواست، پرداخت موفق و آماده شدن گزارش؛ سه پیامک، صفر پیگیری. متن گزارش هیچ‌وقت در پیامک نیست — فقط خبرش.</p>
</div>
<div class="relative mt-8 mr-auto w-full max-w-[260px] rounded-[26px] bg-white/10 p-3" aria-hidden="true">
<div class="rounded-[20px] bg-white p-3 space-y-2.5">
<div class="landing-bubble rounded-xl rounded-tr-sm bg-light px-3 py-2 text-[11px] leading-5 text-dark">درخواست شما ثبت شد. شماره پرونده: ۲۶۴۹۵۸</div>
<div class="landing-bubble rounded-xl rounded-tr-sm bg-light px-3 py-2 text-[11px] leading-5 text-dark">پرداخت فاکتور ۰۰۰۶۹ به مبلغ ۱٬۲۰۰٬۰۰۰ تومان انجام شد.</div>
<div class="landing-bubble rounded-xl rounded-tr-sm bg-dominant-500 px-3 py-2 text-[11px] leading-5 text-white">گزارش پرونده ۲۶۴۹۵۸ آماده شد.</div>
</div>
</div>
</article>
<article class="landing-reveal landing-reveal--1 lg:col-span-2 rounded-[28px] bg-white border-2 border-dominant-50 p-7 flex flex-col sm:flex-row sm:items-center gap-6">
<div class="flex-1">
<span class="inline-flex w-12 h-12 items-center justify-center rounded-2xl bg-dominant-50 text-dominant-500 text-xl"><i class="fi fi-rr-time-fast leading-none"></i></span>
<h3 class="mt-5 text-lg font-extrabold text-dark">عادی یا اورژانسی؟ شما تعیین می‌کنید</h3>
<p class="mt-2 text-sm leading-7 text-dark/60">مهلت بررسی با اولویت عادی ۲۴ ساعت است؛ با اورژانسی، ۳ ساعت.</p>
</div>
<div class="relative w-full sm:w-[200px] shrink-0 rounded-full bg-light p-1.5 grid grid-cols-2 text-center text-xs font-bold" aria-hidden="true">
<span class="landing-toggle-thumb absolute top-1.5 bottom-1.5 right-1.5 w-[calc(50%-6px)] rounded-full bg-dominant-500"></span>
<span class="relative z-10 py-2.5 text-white mix-blend-difference">عادی · ۲۴h</span>
<span class="relative z-10 py-2.5 text-white mix-blend-difference">اورژانسی · ۳h</span>
</div>
</article>
<article class="landing-reveal landing-reveal--2 rounded-[28px] bg-white border-2 border-dominant-50 p-7">
<span class="inline-flex w-12 h-12 items-center justify-center rounded-2xl bg-dominant-50 text-dominant-500 text-xl"><i class="fi fi-rr-wallet leading-none"></i></span>
<h3 class="mt-5 text-lg font-extrabold text-dark">پرداخت شفاف، برگشت کامل</h3>
<p class="mt-2 text-sm leading-7 text-dark/60">قیمت هر خدمت قبل از پرداخت مشخص است. اگر پزشک پرونده را رد کند، کل مبلغ به کیف پول برمی‌گردد.</p>
</article>
<article class="landing-reveal landing-reveal--3 rounded-[28px] bg-dominant-500 p-7 text-white">
<span class="inline-flex w-12 h-12 items-center justify-center rounded-2xl bg-white/15 text-xl"><i class="fi fi-rr-lock-alt leading-none"></i></span>
<h3 class="mt-5 text-lg font-extrabold">گزارش فقط برای شما</h3>
<p class="mt-2 text-sm leading-7 text-white/70">گزارش و تصاویر فقط با ورود به حساب شما و توسط پزشک پرونده قابل مشاهده‌اند؛ هیچ لینک عمومی وجود ندارد.</p>
</article>
</div>
<ul class="mt-5 grid gap-5 md:grid-cols-3">
<li v-for="item in [
{ icon: 'fi fi-rr-document', title: 'آرشیو دائمی پرونده', text: 'همه‌ی تصاویر و گزارش‌های هر پت، مرتب و قابل دانلود.' },
{ icon: 'fi fi-rr-stethoscope', title: 'انتخاب پزشک یا شیفت خودکار', text: 'رادیولوژیست دلخواه، یا اختصاص فوری به پزشکِ در شیفت.' },
{ icon: 'fi fi-rr-badge-check', title: 'رد پرونده با دلیل', text: 'اگر تصویر قابل گزارش نباشد، دلیلش را می‌بینید و هزینه برمی‌گردد.' },
]" :key="item.title" class="landing-reveal flex items-start gap-4 rounded-2xl bg-white/60 border border-dominant-50 px-5 py-4">
<i :class="item.icon" class="mt-1 text-dominant-500 leading-none"></i>
<div>
<h3 class="text-sm font-bold text-dark">{{ item.title }}</h3>
<p class="mt-1 text-xs leading-6 text-dark/55">{{ item.text }}</p>
</div>
</li>
</ul>
</div>
</section>
</template>
@@ -0,0 +1,48 @@
<script setup>
import { Link } from '@inertiajs/inertia-vue3'
const links = [
{ label: 'چطور کار می‌کند', href: '#how-it-works' },
{ label: 'امکانات', href: '#features' },
{ label: 'خدمات تصویربرداری', href: '#modalities' },
{ label: 'سوالات متداول', href: '#faq' },
]
</script>
<template>
<footer class="bg-dominant-50 border-t border-dominant-100/60">
<div class="container max-w-7xl px-4 sm:px-6 py-12 grid gap-10 md:grid-cols-[1.5fr_1fr_1fr]">
<div>
<Link href="/">
<img src="@/images/radical_logo.png" alt="تله‌رادیولوژی رادیکال" class="h-10 w-auto object-contain">
</Link>
<p class="mt-4 max-w-sm text-sm leading-7 text-dark/60">
پلتفرم تله‌رادیولوژی دامپزشکی رادیکال؛ پل ارتباطی سرپرستان حیوانات خانگی و رادیولوژیست‌های دامپزشکی برای گزارش‌دهی تخصصی تصاویر پزشکی.
</p>
</div>
<nav aria-label="دسترسی سریع">
<h3 class="text-sm font-bold text-dominant-700">دسترسی سریع</h3>
<ul class="mt-4 space-y-2">
<li v-for="link in links" :key="link.href">
<a :href="link.href" class="text-sm text-dark/60 hover:text-dominant-500 transition-colors duration-200">{{ link.label }}</a>
</li>
</ul>
</nav>
<nav aria-label="حساب کاربری">
<h3 class="text-sm font-bold text-dominant-700">حساب کاربری</h3>
<ul class="mt-4 space-y-2">
<li><Link :href="route('login')" class="text-sm text-dark/60 hover:text-dominant-500 transition-colors duration-200">ورود</Link></li>
<li><Link :href="route('register.chooseType')" class="text-sm text-dark/60 hover:text-dominant-500 transition-colors duration-200">ثبت‌نام سرپرست</Link></li>
</ul>
</nav>
</div>
<div class="border-t border-dominant-100/60">
<p class="container max-w-7xl px-4 sm:px-6 py-5 text-center text-[13px] text-dominant-700">
تمامی حقوق این وب‌سایت برای مجموعه <span class="font-semibold">تله‌رادیولوژی رادیکال</span> محفوظ می‌باشد.
</p>
</div>
</footer>
</template>
@@ -0,0 +1,73 @@
<script setup>
import { ref } from 'vue'
import { Link } from '@inertiajs/inertia-vue3'
import { Inertia } from '@inertiajs/inertia'
import UiButton from '../ui/UiButton.vue'
defineProps({
user: {
type: Object,
default: null
}
})
const isMenuOpen = ref(false)
const links = [
{ label: 'چطور کار می‌کند', href: '#how-it-works' },
{ label: 'امکانات', href: '#features' },
{ label: 'خدمات تصویربرداری', href: '#modalities' },
{ label: 'سوالات متداول', href: '#faq' },
]
</script>
<template>
<header class="sticky top-0 z-40 bg-white/90 backdrop-blur border-b border-dominant-50">
<div class="container max-w-7xl px-4 sm:px-6 h-[72px] flex items-center justify-between gap-6">
<Link href="/" class="shrink-0">
<img src="@/images/radical_logo.png" alt="تله‌رادیولوژی رادیکال" class="h-10 w-auto object-contain">
</Link>
<nav class="hidden lg:block">
<ul class="flex items-center gap-8">
<li v-for="link in links" :key="link.href">
<a :href="link.href" class="text-sm font-medium text-dark hover:text-dominant-500 transition-colors duration-200">
{{ link.label }}
</a>
</li>
</ul>
</nav>
<div class="hidden lg:flex items-center gap-3">
<template v-if="user">
<UiButton label="ورود به پنل" severity="dominant" icon="fi fi-rr-user" @click="Inertia.visit(route('dashboard.index'))" />
</template>
<template v-else>
<UiButton label="ورود" severity="dominant" variant="text" text @click="Inertia.visit(route('login'))" />
<UiButton label="ثبت‌نام رایگان" severity="dominant" @click="Inertia.visit(route('register.chooseType'))" />
</template>
</div>
<button type="button" class="lg:hidden w-10 h-10 flex items-center justify-center rounded-lg text-dominant-700 hover:bg-dominant-50" :aria-expanded="isMenuOpen" aria-controls="landing-mobile-menu" aria-label="منو" @click="isMenuOpen = !isMenuOpen">
<i :class="isMenuOpen ? 'fi fi-rr-cross' : 'fi fi-rr-menu-burger'" class="text-lg leading-none"></i>
</button>
</div>
<nav v-show="isMenuOpen" id="landing-mobile-menu" class="lg:hidden border-t border-dominant-50 bg-white">
<ul class="container max-w-7xl px-4 py-4 flex flex-col gap-1">
<li v-for="link in links" :key="link.href">
<a :href="link.href" class="block px-3 py-2 rounded-lg text-sm font-medium text-dark hover:bg-dominant-50" @click="isMenuOpen = false">
{{ link.label }}
</a>
</li>
<li class="pt-3 flex flex-col gap-2">
<UiButton v-if="user" label="ورود به پنل" severity="dominant" full-width @click="Inertia.visit(route('dashboard.index'))" />
<template v-else>
<UiButton label="ثبت‌نام رایگان" severity="dominant" full-width @click="Inertia.visit(route('register.chooseType'))" />
<UiButton label="ورود" severity="dominant" outlined full-width @click="Inertia.visit(route('login'))" />
</template>
</li>
</ul>
</nav>
</header>
</template>
@@ -0,0 +1,75 @@
<script setup>
import { Link } from '@inertiajs/inertia-vue3'
import { Inertia } from '@inertiajs/inertia'
import UiButton from '../ui/UiButton.vue'
defineProps({
user: {
type: Object,
default: null
}
})
const facts = [
{ value: '۵', label: 'نوع تصویربرداری، از MRI تا رادیولوژی' },
{ value: '۳ ساعت', label: 'مهلت گزارش برای پرونده‌های اورژانسی' },
{ value: '۳ پیامک', label: 'ثبت، پرداخت، آماده شدن گزارش' },
]
</script>
<template>
<section class="relative min-h-[92svh] flex flex-col overflow-hidden bg-dark text-white">
<img
src="@/images/landing-hero.jpg"
alt="یک گربه و یک سگ کنار هم روی چمن"
class="landing-kenburns absolute inset-0 w-full h-full object-cover object-[30%_center] lg:object-[15%_center]"
>
<div class="absolute inset-0 bg-gradient-to-l from-dark via-dark/85 via-45% to-dark/25" aria-hidden="true"></div>
<div class="absolute inset-x-0 bottom-0 h-2/3 bg-gradient-to-t from-dark via-dark/70 to-transparent" aria-hidden="true"></div>
<div class="container max-w-7xl px-4 sm:px-6 relative flex-1 flex flex-col justify-end pt-16 pb-10 lg:pb-14">
<div class="max-w-3xl">
<p class="landing-reveal text-xs sm:text-sm font-semibold tracking-wide text-dominant-300">
تله‌رادیولوژی دامپزشکی رادیکال
</p>
<h1 class="mt-5 text-[2.6rem] leading-[1.25] sm:text-6xl lg:text-[5.2rem] lg:leading-[1.2] font-black">
<span class="landing-reveal landing-reveal--1 block">اون که نگرانشی،</span>
<span class="landing-reveal landing-reveal--2 block">لازم نیست از خونه</span>
<span class="landing-reveal landing-reveal--3 block">بیرون بره<span class="text-dominant-300">.</span></span>
</h1>
<p class="landing-reveal landing-reveal--4 mt-7 max-w-xl text-base sm:text-lg leading-8 text-white/70">
تصاویر MRI، سی‌تی‌اسکن یا سونوگرافی پت‌تان را همین‌جا آپلود کنید؛ رادیولوژیست دامپزشکی گزارش می‌نویسد و شما با پیامک خبردار می‌شوید.
</p>
<div class="landing-reveal landing-reveal--5 mt-9 flex flex-wrap items-center gap-6">
<UiButton v-if="user" label="رفتن به پنل کاربری" severity="dominant" size="large" icon="fi fi-rr-arrow-small-left" icon-position="end" @click="Inertia.visit(route('dashboard.index'))" />
<UiButton v-else label="ثبت اولین درخواست" severity="dominant" size="large" icon="fi fi-rr-arrow-small-left" icon-position="end" @click="Inertia.visit(route('register.chooseType'))" />
<Link v-if="!user" :href="route('login')" class="text-sm font-semibold text-white/80 underline underline-offset-8 decoration-white/30 hover:text-white hover:decoration-dominant-300 transition-colors duration-200">
قبلاً حساب دارم
</Link>
</div>
</div>
<dl class="landing-reveal landing-reveal--5 mt-14 lg:mt-20 grid sm:grid-cols-3 border-t border-white/15 pt-6 gap-6 sm:gap-0 sm:divide-x sm:divide-x-reverse sm:divide-white/15">
<div v-for="fact in facts" :key="fact.label" class="sm:px-6 first:pr-0 last:pl-0">
<dt class="text-2xl sm:text-3xl font-black text-white">{{ fact.value }}</dt>
<dd class="mt-1 text-xs sm:text-sm leading-6 text-white/55">{{ fact.label }}</dd>
</div>
</dl>
</div>
<a href="#how-it-works" class="landing-spin absolute top-28 left-6 sm:left-10 hidden md:flex w-28 h-28 items-center justify-center rounded-full bg-dark/50 backdrop-blur-sm border border-white/15 text-white/85 hover:text-white hover:border-white/40 transition-colors duration-300" aria-label="ببینید چطور کار می‌کند">
<svg viewBox="0 0 100 100" class="absolute inset-0 w-full h-full" aria-hidden="true">
<defs>
<path id="landing-hero-circle" d="M50,50 m-38,0 a38,38 0 1,1 76,0 a38,38 0 1,1 -76,0" />
</defs>
<text class="fill-current text-[9px] font-semibold tracking-[0.25em]">
<textPath href="#landing-hero-circle">گزارش تخصصی · پیامک · بدون مراجعه ·</textPath>
</text>
</svg>
<i class="fi fi-rr-arrow-small-down text-lg leading-none"></i>
</a>
</section>
</template>
@@ -0,0 +1,16 @@
<script setup>
const items = ['MRI', 'سی‌تی‌اسکن', 'سونوگرافی', 'اکوکاردیوگرافی', 'رادیولوژی', 'سگ', 'گربه', 'خرگوش', 'پرنده', 'همستر', 'لاک‌پشت']
</script>
<template>
<div class="overflow-hidden border-y border-dominant-100/60 bg-dominant-500 py-4" aria-hidden="true">
<div class="landing-marquee flex w-max">
<ul v-for="copy in 2" :key="copy" class="flex shrink-0 items-center">
<li v-for="item in items" :key="item" class="flex items-center gap-6 px-6 text-sm font-semibold text-white/90 whitespace-nowrap">
{{ item }}
<i class="fi fi-rr-paw text-[10px] text-white/40 leading-none"></i>
</li>
</ul>
</div>
</div>
</template>
@@ -0,0 +1,55 @@
<script setup>
import LandingSectionTitle from './LandingSectionTitle.vue'
const modalities = [
{ index: '۰۱', title: 'MRI', description: 'مغز، نخاع و بافت‌های نرم؛ برای تشنج، لنگش عصبی و توده‌ها' },
{ index: '۰۲', title: 'سی‌تی‌اسکن', description: 'استخوان، جمجمه، قفسه‌ی سینه و شکم با جزئیات مقطعی' },
{ index: '۰۳', title: 'سونوگرافی', description: 'اندام‌های شکمی، بارداری، کلیه و کبد' },
{ index: '۰۴', title: 'اکوکاردیوگرافی', description: 'ساختار و عملکرد قلب، دریچه‌ها و جریان خون' },
{ index: '۰۵', title: 'رادیولوژی', description: 'شکستگی، مفاصل، قفسه‌ی سینه و اجسام خارجی' },
]
const species = [
{ icon: 'fi fi-rr-dog', label: 'سگ' },
{ icon: 'fi fi-rr-cat', label: 'گربه' },
{ icon: 'fi fi-rr-rabbit', label: 'خرگوش' },
{ icon: 'fi fi-rr-bird', label: 'پرنده' },
{ icon: 'fi fi-rr-turtle', label: 'لاک‌پشت' },
{ icon: 'fi fi-rr-fish', label: 'ماهی' },
{ icon: 'fi fi-rr-paw', label: 'همستر و خوکچه' },
]
</script>
<template>
<section id="modalities" class="bg-white py-20 lg:py-28 scroll-mt-20">
<div class="container max-w-7xl px-4 sm:px-6 grid lg:grid-cols-[1fr_1.4fr] gap-12 lg:gap-20 items-start">
<div class="lg:sticky lg:top-28">
<LandingSectionTitle
class="!text-right !mx-0"
eyebrow="خدمات تصویربرداری"
title="هر تصویری که دارید، جای خودش را دارد"
description="تصویر را از هر مرکز تصویربرداری گرفته باشید فرقی نمی‌کند؛ فقط نوعش را انتخاب کنید."
/>
<ul class="landing-reveal landing-reveal--2 mt-8 flex flex-wrap gap-2">
<li v-for="item in species" :key="item.label" class="flex items-center gap-2 rounded-full bg-light border border-dominant-50 px-3.5 py-2 text-xs font-semibold text-dark/70">
<i :class="item.icon" class="text-dominant-500 leading-none"></i>
{{ item.label }}
</li>
</ul>
</div>
<ol class="divide-y-2 divide-dominant-50 border-y-2 border-dominant-50">
<li
v-for="(modality, index) in modalities"
:key="modality.title"
:class="`landing-reveal--${index + 1}`"
class="landing-reveal group grid grid-cols-[3.5rem_1fr] sm:grid-cols-[5rem_11rem_1fr] items-center gap-4 py-6 transition-colors duration-300 hover:bg-white"
>
<span class="text-3xl sm:text-4xl font-black text-dominant-100 transition-colors duration-300 group-hover:text-dominant-500">{{ modality.index }}</span>
<h3 class="text-xl sm:text-2xl font-extrabold text-dark">{{ modality.title }}</h3>
<p class="col-span-2 sm:col-span-1 text-sm leading-7 text-dark/55 sm:pl-4">{{ modality.description }}</p>
</li>
</ol>
</div>
</section>
</template>
@@ -0,0 +1,24 @@
<script setup>
defineProps({
eyebrow: {
type: String,
required: true
},
title: {
type: String,
required: true
},
description: {
type: String,
default: ''
}
})
</script>
<template>
<div class="landing-reveal max-w-2xl mx-auto text-center">
<span class="inline-block rounded-full bg-dominant-50 px-4 py-1.5 text-xs font-semibold text-dominant-500">{{ eyebrow }}</span>
<h2 class="mt-4 text-2xl sm:text-3xl lg:text-4xl font-extrabold leading-[1.5] text-dark">{{ title }}</h2>
<p v-if="description" class="mt-4 text-base leading-8 text-dark/60">{{ description }}</p>
</div>
</template>
@@ -0,0 +1,120 @@
<script setup>
import LandingSectionTitle from './LandingSectionTitle.vue'
const steps = [
{
number: '۰۱',
title: 'پروفایل پت را بسازید',
description: 'نام، گونه، نژاد، سن و وزن؛ یک‌بار ثبت می‌کنید و برای همه‌ی درخواست‌های بعدی آماده است. هر پت پرونده‌ی مستقل خودش را دارد.',
mock: 'pet'
},
{
number: '۰۲',
title: 'تصاویر را آپلود کنید',
description: 'نوع تصویربرداری را انتخاب کنید، فایل اسکن و سوابق قبلی را بکشید و رها کنید. اگر عجله دارید، اولویت اورژانسی بگذارید.',
mock: 'upload'
},
{
number: '۰۳',
title: 'رادیولوژیست را انتخاب کنید',
description: 'پزشک مورد اعتمادتان را از لیست انتخاب کنید، یا بگذارید سیستم رادیولوژیستِ در شیفت را همان لحظه به پرونده اختصاص دهد.',
mock: 'doctor'
},
{
number: '۰۴',
title: 'پیامک بگیرید، گزارش را بخوانید',
description: 'به‌محض ثبت گزارش، پیامک می‌گیرید. گزارش کامل با تصاویر پیوست در پنل شماست؛ بدون تماس، بدون پیگیری.',
mock: 'sms'
},
]
</script>
<template>
<section id="how-it-works" class="bg-white py-20 lg:py-28 scroll-mt-20">
<div class="container max-w-7xl px-4 sm:px-6">
<LandingSectionTitle
eyebrow="چطور کار می‌کند"
title="از عکس تا گزارش، چهار قدم"
description="همان مسیری که در پنل طی می‌کنید؛ نه بیشتر."
/>
<ol class="landing-reveal relative mt-16 space-y-10 lg:space-y-0">
<span class="landing-timeline-line hidden lg:block absolute right-1/2 translate-x-1/2 top-0 bottom-0 w-0.5 bg-dominant-100" aria-hidden="true"></span>
<li
v-for="(step, index) in steps"
:key="step.number"
class="relative grid lg:grid-cols-2 gap-8 lg:gap-24 items-center lg:py-10"
>
<span class="hidden lg:flex absolute right-1/2 translate-x-1/2 w-12 h-12 items-center justify-center rounded-full bg-dominant-500 text-white text-sm font-bold ring-8 ring-white" aria-hidden="true">{{ step.number }}</span>
<div :class="index % 2 === 0 ? 'lg:order-1 lg:pl-6' : 'lg:order-2 lg:pr-6'">
<span class="lg:hidden inline-flex w-10 h-10 items-center justify-center rounded-full bg-dominant-500 text-white text-xs font-bold">{{ step.number }}</span>
<h3 class="mt-4 lg:mt-0 text-xl sm:text-2xl font-extrabold text-dark">{{ step.title }}</h3>
<p class="mt-3 text-sm sm:text-base leading-8 text-dark/60">{{ step.description }}</p>
</div>
<div :class="index % 2 === 0 ? 'lg:order-2' : 'lg:order-1'" class="rounded-[28px] bg-light border-2 border-dominant-50 p-5 sm:p-7" aria-hidden="true">
<div v-if="step.mock === 'pet'" class="rounded-2xl bg-white p-5 space-y-3">
<div class="flex items-center gap-3">
<span class="w-12 h-12 rounded-2xl bg-dominant-50 text-dominant-500 flex items-center justify-center text-xl"><i class="fi fi-rr-cat leading-none"></i></span>
<div class="flex-1 space-y-1.5">
<span class="block h-2 w-1/3 rounded-full bg-dark/10"></span>
<span class="block h-2 w-1/5 rounded-full bg-dark/5"></span>
</div>
</div>
<div class="grid grid-cols-2 gap-3 pt-2">
<div v-for="field in ['نام: پیشی', 'گونه: گربه', 'سن: ۳ سال', 'وزن: ۴.۲ کیلو']" :key="field" class="rounded-xl border border-dominant-50 px-3 py-2.5 text-xs text-dark/70">{{ field }}</div>
</div>
<div class="flex justify-end pt-1">
<span class="rounded-lg bg-dominant-500 px-4 py-2 text-[11px] font-semibold text-white">ذخیره‌ی پروفایل</span>
</div>
</div>
<div v-else-if="step.mock === 'upload'" class="rounded-2xl bg-white p-5 space-y-3">
<div class="flex gap-2">
<span v-for="(modality, i) in ['MRI', 'سی‌تی', 'سونو', 'اکو']" :key="modality" :class="i === 0 ? 'bg-dominant-500 text-white' : 'bg-dominant-50 text-dark/60'" class="rounded-full px-3 py-1 text-[11px] font-semibold">{{ modality }}</span>
</div>
<div class="rounded-2xl border-2 border-dashed border-dominant-200 bg-light/60 p-5 text-center">
<i class="fi fi-rr-cloud-upload text-2xl text-dominant-500 leading-none"></i>
<p class="mt-2 text-xs font-semibold text-dark">فایل‌ها را اینجا رها کنید</p>
</div>
<ul class="space-y-2">
<li v-for="file in ['Pishi_MRI_T2.zip · ۴۸ MB', 'سابقه‌ی آزمایش خون.pdf']" :key="file" class="flex items-center justify-between rounded-xl bg-light px-3 py-2 text-[11px] text-dark/70">
{{ file }}
<i class="fi fi-rr-check-circle text-success leading-none"></i>
</li>
</ul>
<div class="flex items-center justify-between rounded-xl bg-dominant-50 px-3 py-2 text-[11px] font-semibold text-dominant-700">
اولویت
<span class="flex gap-1"><span class="rounded-md bg-white px-2 py-0.5">عادی</span><span class="rounded-md bg-dominant-500 px-2 py-0.5 text-white">اورژانسی</span></span>
</div>
</div>
<div v-else-if="step.mock === 'doctor'" class="space-y-3">
<div v-for="(doctor, i) in [{ name: 'دکتر آرمان صادقی', spec: 'رادیولوژیست · MRI و CT', shift: true }, { name: 'دکتر نگار کریمی', spec: 'رادیولوژیست · سونوگرافی', shift: false }]" :key="doctor.name" :class="i === 0 ? 'border-dominant-500' : 'border-transparent'" class="flex items-center gap-3 rounded-2xl bg-white border-2 p-4">
<span class="w-11 h-11 rounded-full bg-dark text-white flex items-center justify-center"><i class="fi fi-rr-user-md leading-none"></i></span>
<div class="flex-1">
<p class="text-xs font-bold text-dark">{{ doctor.name }}</p>
<p class="text-[10px] text-dark/50">{{ doctor.spec }}</p>
</div>
<span v-if="doctor.shift" class="flex items-center gap-1.5 rounded-full bg-complement-50 px-2.5 py-1 text-[10px] font-semibold text-complement-700"><span class="w-1.5 h-1.5 rounded-full bg-success"></span>در شیفت</span>
<span v-else class="text-[10px] text-dark/40">شیفت بعدی: ۱۸:۰۰</span>
</div>
<p class="text-center text-[11px] text-dark/50">یا <span class="font-semibold text-dominant-500">انتخاب خودکار</span> پزشکِ در شیفت</p>
</div>
<div v-else class="mx-auto w-full max-w-[280px] rounded-[30px] bg-dark p-3">
<div class="rounded-[22px] bg-white p-4 space-y-3 min-h-[230px]">
<p class="text-center text-[10px] text-dark/40">رادیکال</p>
<div class="landing-bubble rounded-2xl rounded-tr-md bg-light px-3 py-2.5 text-[11px] leading-5 text-dark">درخواست شما ثبت شد. شماره پرونده: ۲۶۴۹۵۸</div>
<div class="landing-bubble rounded-2xl rounded-tr-md bg-light px-3 py-2.5 text-[11px] leading-5 text-dark">پرداخت فاکتور ۰۰۰۶۹ با موفقیت انجام شد. پرونده به پزشک ارجاع شد.</div>
<div class="landing-bubble rounded-2xl rounded-tr-md bg-dominant-500 px-3 py-2.5 text-[11px] leading-5 text-white">گزارش پرونده ۲۶۴۹۵۸ آماده شد. برای مشاهده به پنل مراجعه کنید.</div>
</div>
</div>
</div>
</li>
</ol>
</div>
</section>
</template>
@@ -176,12 +176,12 @@ const updateExpandedKeys = () => {
<template>
<div v-if="globalStore.sidebarOpen"
class="fixed inset-0 bg-black/10 backdrop-blur-sm bg-opacity-50 z-10 sm:hidden"
class="fixed inset-0 bg-black/10 backdrop-blur-sm bg-opacity-50 z-10 lg:hidden"
@click="globalStore.sidebarOpen = false">
</div>
<aside class="shadow-client-gray/5 w-[16rem] bg-white
py-4 pt-0 pb-8 fixed h-full top-0 z-30 inset-y-0 right-0 sm:inset-auto transition-transform duration-300"
:class="[globalStore.sidebarOpen ? 'translate-x-0' : 'translate-x-full sm:translate-x-0']">
py-4 pt-0 pb-8 fixed h-full top-0 z-30 inset-y-0 right-0 lg:inset-auto transition-transform duration-300"
:class="[globalStore.sidebarOpen ? 'translate-x-0' : 'translate-x-full lg:translate-x-0']">
<figure class="w-[16rem] h-[5rem] flex justify-center">
<img class="!w-full h-full object-contain" src="@/images/SidebarPanelLogo.png" alt="">
</figure>
@@ -123,6 +123,7 @@ const getColumnStyle = (column) => {
</template>
</Toolbar>
</div>
<div class="table-generic-scroll">
<DataTable
:value="data"
dataKey="id"
@@ -159,6 +160,7 @@ const getColumnStyle = (column) => {
</template>
</Column>
</DataTable>
</div>
<Pagination
v-if="pagination && pagination.total"
@@ -172,8 +174,3 @@ const getColumnStyle = (column) => {
</div>
</template>
<style scoped>
.table-generic {
width: 100%;
}
</style>
@@ -0,0 +1,27 @@
import { onBeforeUnmount, onMounted } from 'vue'
export function useScrollReveal(rootRef, selector = '.landing-reveal') {
let observer = null
onMounted(() => {
const elements = rootRef.value?.querySelectorAll(selector) ?? []
if (!('IntersectionObserver' in window)) {
elements.forEach((element) => element.classList.add('is-visible'))
return
}
observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add('is-visible')
observer.unobserve(entry.target)
}
})
}, { threshold: 0.05 })
elements.forEach((element) => observer.observe(element))
})
onBeforeUnmount(() => observer?.disconnect())
}
+2 -2
View File
@@ -19,8 +19,8 @@ onMounted(() => {
<header-component />
<div class="!bg-white">
<section class="">
<main class="overflow-hidden !ps-[16rem] max-sm:!ps-4 pt-20 min-h-[calc(100svh)] p-4 grid grid-cols-1">
<section class="!rounded-2xl border-[1px] border-dominant-50 !bg-dominant-50/50 content-start p-4 grid grid-cols-1">
<main class="overflow-hidden lg:!ps-[16rem] pt-20 min-h-[calc(100svh)] p-4 grid grid-cols-1">
<section class="!rounded-2xl border-[1px] border-dominant-50 !bg-dominant-50/50 content-start p-3 sm:p-4 grid grid-cols-1 min-w-0">
<div v-if="$page.props.flash.success" class="w-full mb-4">
<Message size="small" closable class="!text-xs py-1.5" severity="success">{{$page.props.flash.success}}</Message>
</div>
+7
View File
@@ -32,6 +32,13 @@ export default defineConfig({
'@': '/public',
},
},
server: {
host: '0.0.0.0',
port: 5173,
hmr: {
host: 'localhost',
},
},
optimizeDeps: {
include: [
'@fullcalendar/core',