Initial commit: Nuxt site

This commit is contained in:
Noro Studio
2026-06-30 00:46:55 +03:30
commit 7af8fbca2d
125 changed files with 20167 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
NUXT_PUBLIC_CMS_API_URL=http://localhost:4000
+24
View File
@@ -0,0 +1,24 @@
# Nuxt dev/build outputs
.output
.data
.nuxt
.nitro
.cache
dist
# Node dependencies
node_modules
# Logs
logs
*.log
# Misc
.DS_Store
.fleet
.idea
# Local env files
.env
.env.*
!.env.example
+7
View File
@@ -0,0 +1,7 @@
import withNuxt from './.nuxt/eslint.config.mjs'
export default withNuxt({
rules: {
'vue/multi-word-component-names': 'off'
}
})
+44
View File
@@ -0,0 +1,44 @@
import { defineNuxtConfig } from 'nuxt/config'
export default defineNuxtConfig({
compatibilityDate: '2026-05-27',
srcDir: 'src/',
modules: ['@nuxtjs/tailwindcss', '@pinia/nuxt'],
css: ['~/assets/css/fonts.css'],
tailwindcss: {
cssPath: '~/assets/css/tailwind.css',
configPath: 'tailwind.config.ts',
viewer: false
},
app: {
head: {
htmlAttrs: {
lang: 'fa',
dir: 'rtl'
},
title: 'استودیو نورو | Noro Studio',
meta: [
{ charset: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
{
name: 'description',
content:
'استودیو نورو — طراحی هویت بصری، UI/UX، سوشال مدیا و محتوای دیجیتال'
},
{ name: 'color-scheme', content: 'dark light' }
],
link: [
{ rel: 'icon', type: 'image/png', href: '/favicon.png' }
]
}
},
typescript: {
strict: true
},
runtimeConfig: {
public: {
cmsApiUrl: process.env.NUXT_PUBLIC_CMS_API_URL || 'http://localhost:4000'
}
},
devtools: { enabled: true }
})
+16476
View File
File diff suppressed because it is too large Load Diff
+28
View File
@@ -0,0 +1,28 @@
{
"name": "nuxt-tailwind-app",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"dev": "nuxt dev",
"build": "nuxt build",
"generate": "nuxt generate",
"preview": "nuxt preview",
"lint": "eslint .",
"lint:fix": "eslint . --fix"
},
"dependencies": {
"nuxt": "3.12.4",
"@nuxtjs/tailwindcss": "^6.12.0",
"@pinia/nuxt": "^0.5.5",
"pinia": "^2.1.7",
"vue": "^3.5.0"
},
"devDependencies": {
"@nuxt/eslint-config": "^0.5.4",
"@types/node": "^22.0.0",
"eslint": "^9.0.0",
"eslint-plugin-vue": "^9.0.0",
"typescript": "^5.5.0"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

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.
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.
+9
View File
@@ -0,0 +1,9 @@
const themeInitScript = `(function(){try{var k='noro-theme';var t=localStorage.getItem(k);var d=t==='dark'||(t!=='light'&&window.matchMedia('(prefers-color-scheme: dark)').matches);document.documentElement.classList.toggle('dark',d);document.documentElement.style.colorScheme=d?'dark':'light'}catch(e){}})();`
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('render:html', (htmlContext) => {
htmlContext.head.unshift(
`<script id="noro-theme-init">${themeInitScript}</script>`
)
})
})
+5
View File
@@ -0,0 +1,5 @@
<template>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</template>
+145
View File
@@ -0,0 +1,145 @@
/* Peyda — متن و بدنه */
@font-face {
font-family: Peyda;
src: url('../fonts/peyda/PeydaWeb-Thin.woff2') format('woff2');
font-weight: 100;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: Peyda;
src: url('../fonts/peyda/peydaWeb-extralight.woff2') format('woff2');
font-weight: 200;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: Peyda;
src: url('../fonts/peyda/peydaWeb-light.woff2') format('woff2');
font-weight: 300;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: Peyda;
src: url('../fonts/peyda/PeydaWeb-Regular.woff2') format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: Peyda;
src: url('../fonts/peyda/PeydaWeb-Medium.woff2') format('woff2');
font-weight: 500;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: Peyda;
src: url('../fonts/peyda/PeydaWeb-SemiBold.woff2') format('woff2');
font-weight: 600;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: Peyda;
src: url('../fonts/peyda/PeydaWeb-Bold.woff2') format('woff2');
font-weight: 700;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: Peyda;
src: url('../fonts/peyda/PeydaWeb-ExtraBold.woff2') format('woff2');
font-weight: 800;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: Peyda;
src: url('../fonts/peyda/PeydaWeb-Black.woff2') format('woff2');
font-weight: 900;
font-style: normal;
font-display: swap;
}
/* Rokh Pro — عناوین اصلی */
@font-face {
font-family: Rokh;
src: url('../fonts/rokh/Rokh-Thin.woff2') format('woff2');
font-weight: 100;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: Rokh;
src: url('../fonts/rokh/Rokh-ExtraLight.woff2') format('woff2');
font-weight: 200;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: Rokh;
src: url('../fonts/rokh/Rokh-Light.woff2') format('woff2');
font-weight: 300;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: Rokh;
src: url('../fonts/rokh/Rokh-Regular.woff2') format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: Rokh;
src: url('../fonts/rokh/Rokh-Medium.woff2') format('woff2');
font-weight: 500;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: Rokh;
src: url('../fonts/rokh/Rokh-SemiBold.woff2') format('woff2');
font-weight: 600;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: Rokh;
src: url('../fonts/rokh/Rokh-Bold.woff2') format('woff2');
font-weight: 700;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: Rokh;
src: url('../fonts/rokh/Rokh-ExtraBold.woff2') format('woff2');
font-weight: 800;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: Rokh;
src: url('../fonts/rokh/Rokh-Black.woff2') format('woff2');
font-weight: 900;
font-style: normal;
font-display: swap;
}
+177
View File
@@ -0,0 +1,177 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
html {
@apply scroll-smooth antialiased;
}
/* Light — مینیمال و تمیز */
html:not(.dark) {
--noro-bg: 250 252 252;
--noro-surface: 255 255 255;
--noro-border: 226 232 232;
--noro-text: 15 20 20;
--noro-muted: 100 116 116;
--noro-brand: 57 192 196;
--noro-brand-hover: 46 179 183;
--noro-brand-light: 147 224 226;
--noro-glow-primary: 57 192 196;
--noro-glow-secondary: 93 206 209;
--noro-hero-gradient-from: 57 192 196;
--noro-hero-gradient-via: 93 206 209;
--noro-hero-gradient-to: 46 179 183;
--noro-btn-primary-bg: 57 192 196;
--noro-btn-primary-text: 255 255 255;
--noro-btn-secondary-bg: 255 255 255;
--noro-btn-secondary-text: 15 20 20;
--noro-btn-secondary-border: 226 232 232;
--noro-accent-btn-bg: 57 192 196;
--noro-accent-btn-hover: 46 179 183;
--noro-header-bg: 250 252 252;
--noro-glass-bg: 255 255 255;
--noro-glass-opacity: 0.58;
--noro-glass-border: 255 255 255;
--noro-glass-border-opacity: 0.72;
--noro-glass-blur: 24px;
}
/* Dark — futuristic & premium */
html.dark {
--noro-bg: 5 8 8;
--noro-surface: 10 14 14;
--noro-border: 30 41 41;
--noro-text: 250 252 252;
--noro-muted: 148 163 163;
--noro-brand: 57 192 196;
--noro-brand-hover: 93 206 209;
--noro-brand-light: 147 224 226;
--noro-glow-primary: 57 192 196;
--noro-glow-secondary: 46 179 183;
--noro-hero-gradient-from: 57 192 196;
--noro-hero-gradient-via: 147 224 226;
--noro-hero-gradient-to: 93 206 209;
--noro-btn-primary-bg: 57 192 196;
--noro-btn-primary-text: 5 8 8;
--noro-btn-secondary-bg: 255 255 255;
--noro-btn-secondary-text: 5 8 8;
--noro-btn-secondary-border: 255 255 255;
--noro-accent-btn-bg: 57 192 196;
--noro-accent-btn-hover: 93 206 209;
--noro-header-bg: 5 8 8;
--noro-glass-bg: 255 255 255;
--noro-glass-opacity: 0.05;
--noro-glass-border: 255 255 255;
--noro-glass-border-opacity: 0.09;
--noro-glass-blur: 24px;
}
body {
@apply bg-noro-bg font-sans text-noro-text transition-[background-color,color] duration-500;
}
h1,
h2,
h3,
h4,
h5,
h6 {
font-family: Rokh, Peyda, Tahoma, system-ui, sans-serif;
}
}
@layer components {
.font-heading {
font-family: Rokh, Peyda, Tahoma, system-ui, sans-serif;
}
.noro-container {
@apply mx-auto max-w-6xl px-4 sm:px-6;
}
.noro-glass {
border-color: rgb(var(--noro-glass-border) / var(--noro-glass-border-opacity));
background-color: rgb(var(--noro-glass-bg) / var(--noro-glass-opacity));
-webkit-backdrop-filter: blur(var(--noro-glass-blur)) saturate(180%);
backdrop-filter: blur(var(--noro-glass-blur)) saturate(180%);
}
html:not(.dark) .noro-glass {
box-shadow:
inset 0 1px 0 0 rgb(255 255 255 / 0.85),
0 1px 2px rgb(15 20 20 / 0.04);
}
html.dark .noro-glass {
box-shadow: inset 0 1px 0 0 rgb(255 255 255 / 0.07);
}
.noro-glass-header {
@apply noro-glass sticky top-0 z-50 border-b;
}
.noro-section {
@apply py-20 sm:py-28;
}
.noro-glow {
@apply pointer-events-none absolute rounded-full blur-[120px];
}
.noro-service-card {
@apply relative overflow-hidden rounded-2xl border border-noro-border bg-transparent transition duration-300 hover:-translate-y-1 hover:shadow-lg;
}
.noro-service-card:hover {
border-color: rgb(var(--noro-brand) / 0.3);
}
html.dark .noro-service-card:hover {
box-shadow:
0 10px 15px -3px rgb(0 0 0 / 0.1),
0 4px 6px -4px rgb(var(--noro-brand) / 0.1);
}
.noro-card {
@apply rounded-2xl border border-noro-border bg-noro-surface transition duration-300;
}
.noro-card-hover {
@apply noro-card hover:-translate-y-1 hover:shadow-lg;
}
.noro-card-hover:hover {
border-color: rgb(var(--noro-brand) / 0.3);
}
html.dark .noro-card-hover:hover {
box-shadow:
0 10px 15px -3px rgb(0 0 0 / 0.1),
0 4px 6px -4px rgb(var(--noro-brand) / 0.1);
}
.noro-gradient-text {
@apply bg-gradient-to-l from-[rgb(var(--noro-hero-gradient-from))] via-[rgb(var(--noro-hero-gradient-via))] to-[rgb(var(--noro-hero-gradient-to))] bg-clip-text text-transparent;
}
.text-noro-brand {
color: rgb(var(--noro-brand));
}
.bg-noro-brand {
background-color: rgb(var(--noro-brand));
}
.border-noro-brand {
border-color: rgb(var(--noro-brand));
}
.noro-btn-primary {
@apply inline-flex items-center justify-center rounded-xl bg-[rgb(var(--noro-btn-primary-bg))] px-6 py-3 text-sm font-semibold text-[rgb(var(--noro-btn-primary-text))] transition hover:bg-[rgb(var(--noro-brand-hover))];
}
.noro-btn-secondary {
@apply inline-flex items-center justify-center rounded-xl border border-[rgb(var(--noro-btn-secondary-border))] bg-[rgb(var(--noro-btn-secondary-bg))] px-6 py-3 text-sm font-semibold text-[rgb(var(--noro-btn-secondary-text))] transition hover:opacity-90;
}
}
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.
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.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

@@ -0,0 +1,12 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
<rect x="2" y="2" width="20" height="20" rx="5" stroke="url(#ig-gradient)" stroke-width="2"/>
<circle cx="12" cy="12" r="4.5" stroke="url(#ig-gradient)" stroke-width="2"/>
<circle cx="17.5" cy="6.5" r="1.25" fill="url(#ig-gradient)"/>
<defs>
<linearGradient id="ig-gradient" x1="2" y1="22" x2="22" y2="2" gradientUnits="userSpaceOnUse">
<stop stop-color="#F58529"/>
<stop offset="0.5" stop-color="#DD2A7B"/>
<stop offset="1" stop-color="#8134AF"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 590 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

+80
View File
@@ -0,0 +1,80 @@
<script setup lang="ts">
const { navLinks, site, messengersLabel } = useNoroData()
</script>
<template>
<footer class="border-t border-noro-border bg-noro-surface">
<div class="noro-container py-12">
<div class="grid gap-10 md:grid-cols-3">
<div>
<NoroLogo variant="horizontal-text" class="h-[101px] max-h-[60px] w-auto" />
<p class="mt-4 text-sm leading-7 text-noro-muted">
{{ site.description }}
</p>
<a
:href="`https://${site.website}`"
target="_blank"
rel="noopener noreferrer"
class="mt-3 inline-block text-sm text-noro-brand transition hover:text-[rgb(var(--noro-brand-hover))]"
>
{{ site.website }}
</a>
</div>
<div>
<p class="mb-4 text-sm font-semibold text-noro-text">صفحات</p>
<nav class="flex flex-col gap-2">
<NuxtLink
v-for="link in navLinks"
:key="link.to"
:to="link.to"
class="text-sm text-noro-muted transition hover:text-noro-brand"
>
{{ link.label }}
</NuxtLink>
</nav>
</div>
<div>
<p class="mb-4 text-sm font-semibold text-noro-text">ارتباط</p>
<ul class="space-y-2 text-sm text-noro-muted">
<li>
<a
:href="`mailto:${site.email}`"
class="transition hover:text-noro-brand"
>
{{ site.email }}
</a>
</li>
<li>
<a
:href="`tel:${site.phoneTel}`"
class="transition hover:text-noro-brand"
dir="ltr"
>
{{ site.phone }}
</a>
</li>
<li>
<span class="mb-2 block text-noro-muted">{{ messengersLabel }}</span>
<MessengerLinks
:messengers="site.messengers"
:handle="site.messengerHandle"
/>
</li>
<li>{{ site.location }}</li>
</ul>
</div>
</div>
<div
class="mt-10 flex flex-col items-center justify-between gap-4 border-t border-noro-border pt-8 sm:flex-row"
>
<p class="text-sm text-noro-muted">
© {{ new Date().getFullYear() }} {{ site.name }}. تمامی حقوق محفوظ است.
</p>
<NoroLogo variant="icon" class="h-6 w-auto opacity-80" />
</div>
</div>
</footer>
</template>
+87
View File
@@ -0,0 +1,87 @@
<script setup lang="ts">
const { navLinks } = useNoroData()
const mobileOpen = ref(false)
const route = useRoute()
watch(
() => route.path,
() => {
mobileOpen.value = false
}
)
</script>
<template>
<header class="noro-glass-header">
<div class="noro-container flex h-16 items-center justify-between">
<NuxtLink to="/" class="group flex shrink-0 transition opacity-90 hover:opacity-100">
<NoroLogo variant="horizontal" class="h-[29px] w-auto sm:h-8" />
</NuxtLink>
<nav class="hidden items-center gap-8 md:flex">
<NuxtLink
v-for="link in navLinks"
:key="link.to"
:to="link.to"
class="text-sm text-noro-muted transition hover:text-noro-text"
active-class="!text-noro-brand font-medium"
>
{{ link.label }}
</NuxtLink>
</nav>
<div class="flex items-center gap-2 sm:gap-3">
<ThemeToggle />
<UiButton to="/contact" size="sm" variant="accent" class="hidden sm:inline-flex">
شروع همکاری
</UiButton>
<button
type="button"
class="rounded-lg border border-white/15 p-2 text-noro-muted transition hover:bg-white/10 hover:text-noro-text dark:border-white/10 md:hidden"
aria-label="منو"
@click="mobileOpen = !mobileOpen"
>
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
v-if="!mobileOpen"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 6h16M4 12h16M4 18h16"
/>
<path
v-else
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
</div>
</div>
<div
v-if="mobileOpen"
class="noro-glass border-t px-4 py-4 md:hidden"
>
<nav class="flex flex-col gap-3">
<NuxtLink
v-for="link in navLinks"
:key="link.to"
:to="link.to"
class="rounded-lg px-3 py-2 text-sm text-noro-muted transition hover:bg-white/10 hover:text-noro-text dark:hover:bg-white/5"
active-class="!bg-noro-brand/10 !text-noro-brand"
>
{{ link.label }}
</NuxtLink>
<UiButton to="/contact" size="sm" variant="accent" class="mt-2 w-full justify-center">
شروع همکاری
</UiButton>
</nav>
</div>
</header>
</template>
+136
View File
@@ -0,0 +1,136 @@
<script setup lang="ts">
import logoChehelsotoun from '~/assets/images/clients/chehelsotoun-souleh.png'
import logoMontzar from '~/assets/images/clients/montzar.png'
import logoMadar from '~/assets/images/clients/madar.png'
import logoAzadKhorasgan from '~/assets/images/clients/azad-khorasgan.png'
import logoIsfahan from '~/assets/images/clients/isfahan.png'
import logoMontech from '~/assets/images/clients/montech.png'
import logoFarshchin from '~/assets/images/clients/farshchin.png'
import logoRouyesh from '~/assets/images/clients/rouyesh.png'
const { clients } = useNoroData()
const logos: Record<string, string> = {
'chehelsotoun-souleh': logoChehelsotoun,
montzar: logoMontzar,
madar: logoMadar,
'azad-khorasgan': logoAzadKhorasgan,
isfahan: logoIsfahan,
montech: logoMontech,
farshchin: logoFarshchin,
rouyesh: logoRouyesh
}
function clientLogo(client: { id: string; logoUrl?: string }) {
return client.logoUrl || logos[client.id]
}
const sequence = computed(() => Array.from({ length: 4 }, () => clients.value).flat())
const trackRef = ref<HTMLElement | null>(null)
const innerRef = ref<HTMLElement | null>(null)
let offset = 0
let rafId = 0
let halfWidth = 0
let prefersReducedMotion = false
function measure() {
if (!innerRef.value) return
halfWidth = innerRef.value.scrollWidth / 2
}
function tick() {
if (!innerRef.value || halfWidth <= 0) {
rafId = requestAnimationFrame(tick)
return
}
if (!prefersReducedMotion) {
offset -= 0.6
if (Math.abs(offset) >= halfWidth) {
offset += halfWidth
}
innerRef.value.style.transform = `translate3d(${offset}px, 0, 0)`
}
rafId = requestAnimationFrame(tick)
}
onMounted(async () => {
prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches
await nextTick()
measure()
setTimeout(measure, 150)
window.addEventListener('resize', measure)
rafId = requestAnimationFrame(tick)
})
onUnmounted(() => {
cancelAnimationFrame(rafId)
window.removeEventListener('resize', measure)
})
</script>
<template>
<section class="noro-section border-t border-noro-border">
<div class="noro-container">
<SectionHeading
eyebrow="مشتریان ما"
title="برندهایی که با ما همکاری کرده‌اند"
description="افتخار همکاری با مجموعه‌های معتبر در حوزه‌های مختلف را داریم."
align="center"
/>
</div>
<div
ref="trackRef"
class="marquee-root mt-12 min-h-11 overflow-hidden sm:min-h-12"
aria-label="لوگوی مشتریان"
>
<div
ref="innerRef"
dir="ltr"
class="marquee-inner flex w-max items-center will-change-transform"
>
<template v-for="copy in 2" :key="copy">
<div
v-for="(client, index) in sequence"
:key="`${copy}-${client.id}-${index}`"
class="flex h-10 w-24 shrink-0 items-center justify-center sm:h-11 sm:w-28"
:aria-hidden="copy === 2 ? true : undefined"
>
<img
:src="clientLogo(client)"
:alt="copy === 1 && index < clients.length ? client.name : undefined"
:title="client.name"
width="96"
height="36"
class="max-h-8 max-w-[88px] object-contain opacity-60 transition duration-300 hover:opacity-100 sm:max-h-9 sm:max-w-[96px]"
loading="eager"
decoding="async"
@load="measure"
>
</div>
</template>
</div>
</div>
</section>
</template>
<style scoped>
.marquee-root {
-webkit-mask-image: linear-gradient(to right, transparent, black 5%, black 95%, transparent);
mask-image: linear-gradient(to right, transparent, black 5%, black 95%, transparent);
}
.marquee-inner {
gap: 2.5rem;
}
@media (min-width: 640px) {
.marquee-inner {
gap: 3.5rem;
}
}
</style>
+16
View File
@@ -0,0 +1,16 @@
<template>
<div class="pointer-events-none absolute inset-0 overflow-hidden" aria-hidden="true">
<div
class="noro-glow left-1/2 top-1/3 h-[520px] w-[720px] -translate-x-1/2 -translate-y-1/2 bg-gradient-to-r from-[#39c0c4]/25 via-[#39c0c4]/15 to-[#2eb3b7]/20 opacity-60 dark:opacity-100"
/>
<div
class="noro-glow -left-32 top-0 h-96 w-96 bg-[rgb(var(--noro-glow-primary)/0.12)] dark:bg-[rgb(var(--noro-glow-primary)/0.3)]"
/>
<div
class="noro-glow -right-32 top-1/3 h-80 w-80 bg-[rgb(var(--noro-glow-secondary)/0.08)] dark:bg-[rgb(var(--noro-glow-secondary)/0.2)]"
/>
<div
class="absolute inset-0 bg-[radial-gradient(ellipse_80%_50%_at_50%_0%,rgb(var(--noro-glow-primary)/0.1),transparent_70%)] dark:bg-[radial-gradient(ellipse_80%_50%_at_50%_0%,rgb(var(--noro-glow-primary)/0.2),transparent_70%)]"
/>
</div>
</template>
+102
View File
@@ -0,0 +1,102 @@
<script setup lang="ts">
import heroBackgroundDark from '~/assets/images/hero/studio-background.png'
import heroBackgroundLight from '~/assets/images/hero/studio-background-light.png'
const { site, stats, heroDarkImage, heroLightImage } = useNoroData()
const heroLight = computed(() => heroLightImage.value || heroBackgroundLight)
const heroDark = computed(() => heroDarkImage.value || heroBackgroundDark)
</script>
<template>
<section class="relative -mt-16 min-h-[92vh] overflow-hidden pt-16">
<div class="absolute inset-0" aria-hidden="true">
<img
:src="heroLight"
alt=""
class="block h-full w-full object-cover object-center dark:hidden"
decoding="async"
>
<img
:src="heroDark"
alt=""
class="hidden h-full w-full object-cover object-center dark:block"
decoding="async"
>
<div class="absolute inset-0 bg-white/65 dark:bg-black/55" />
<div
class="absolute inset-0 bg-[radial-gradient(ellipse_60%_50%_at_50%_38%,rgb(255_255_255/0.55),transparent_80%)] dark:bg-gradient-to-b dark:from-black/30 dark:via-black/45 dark:to-black/80"
/>
<div
class="absolute inset-0 bg-gradient-to-b from-transparent via-transparent to-[rgb(var(--noro-bg)/0.85)] dark:to-black/80"
/>
<div
class="absolute inset-0 hidden dark:block dark:bg-[radial-gradient(ellipse_70%_55%_at_50%_35%,rgb(var(--noro-brand)/0.12),transparent_70%)]"
/>
</div>
<div class="noro-container relative pb-24 pt-20 sm:pb-32 sm:pt-28">
<div class="mx-auto flex max-w-3xl flex-col items-center text-center">
<div
class="animate-fade-up mb-12 sm:mb-14"
style="animation-delay: 0.1s"
>
<NoroLogo variant="vertical" class="h-32 w-auto sm:h-40" />
</div>
<p
class="animate-fade-up mb-5 inline-flex rounded-full border border-noro-brand/25 bg-noro-brand/10 px-4 py-1.5 text-sm font-medium text-noro-brand dark:border-noro-brand/40 dark:bg-noro-brand/15 sm:mb-6"
style="animation-delay: 0.15s"
>
{{ site.tagline }}
</p>
<h1
class="animate-fade-up text-4xl font-bold leading-tight tracking-tight text-noro-text drop-shadow-[0_1px_12px_rgb(255_255_255/0.9)] dark:text-white dark:drop-shadow-none sm:text-5xl md:text-6xl"
style="animation-delay: 0.2s"
>
طراحی دیجیتال
<span class="mt-2 block noro-gradient-text">
با هویت متمایز
</span>
</h1>
<p
class="animate-fade-up mx-auto mt-7 max-w-xl text-base leading-relaxed text-noro-muted drop-shadow-[0_1px_8px_rgb(255_255_255/0.85)] dark:text-white/75 dark:drop-shadow-none sm:mt-8 sm:text-lg"
style="animation-delay: 0.3s"
>
{{ site.description }}
</p>
<div
class="animate-fade-up mt-10 flex flex-col items-center justify-center gap-3 sm:flex-row sm:gap-4"
style="animation-delay: 0.4s"
>
<NuxtLink to="/contact" class="noro-btn-primary">
شروع همکاری
</NuxtLink>
<NuxtLink
to="/portfolio"
class="inline-flex items-center justify-center rounded-xl border border-noro-border bg-noro-surface px-6 py-3 text-sm font-semibold text-noro-text transition hover:bg-noro-bg dark:border-white/25 dark:bg-white/10 dark:text-white dark:backdrop-blur-sm dark:hover:bg-white/20"
>
مشاهده نمونه‌کارها
</NuxtLink>
</div>
<div
class="animate-fade-up mt-16 grid w-full grid-cols-3 gap-4 border-t border-noro-border pt-10 dark:border-white/15 sm:gap-8"
style="animation-delay: 0.5s"
>
<div v-for="item in stats" :key="item.label">
<p class="text-2xl font-bold text-noro-text dark:text-white sm:text-3xl">
{{ item.value }}
</p>
<p class="mt-1 text-xs text-noro-muted dark:text-white/60 sm:text-sm">
{{ item.label }}
</p>
</div>
</div>
</div>
</div>
</section>
</template>
+50
View File
@@ -0,0 +1,50 @@
<script setup lang="ts">
import type { Messenger } from '~/data/noro'
import iconTelegram from '~/assets/images/messengers/telegram.png'
import iconBale from '~/assets/images/messengers/bale.png'
import iconRubika from '~/assets/images/messengers/rubika.png'
import iconInstagram from '~/assets/images/messengers/instagram.svg'
defineProps<{
messengers: Array<Messenger & { iconUrl?: string }>
handle: string
showHandle?: boolean
}>()
const icons: Record<Messenger['id'], string> = {
telegram: iconTelegram,
bale: iconBale,
rubika: iconRubika,
instagram: iconInstagram
}
</script>
<template>
<div class="flex flex-wrap items-center gap-3">
<div class="flex items-center gap-2">
<a
v-for="messenger in messengers"
:key="messenger.id"
:href="messenger.url"
target="_blank"
rel="noopener noreferrer"
:aria-label="`${messenger.name} — ${handle}`"
class="flex h-9 w-9 items-center justify-center overflow-hidden rounded-lg border border-noro-border bg-white/80 transition hover:border-noro-brand/40 hover:shadow-sm dark:bg-noro-surface/80"
>
<img
:src="messenger.iconUrl || icons[messenger.id]"
:alt="messenger.name"
class="h-6 w-6 object-contain"
decoding="async"
>
</a>
</div>
<span
v-if="showHandle !== false"
class="text-sm font-medium text-noro-brand"
dir="ltr"
>
{{ handle }}
</span>
</div>
</template>
+38
View File
@@ -0,0 +1,38 @@
<script setup lang="ts">
withDefaults(
defineProps<{
size?: 'sm' | 'md' | 'lg'
}>(),
{
size: 'md'
}
)
const sizeClasses = {
sm: 'h-6 w-10',
md: 'h-8 w-14',
lg: 'h-12 w-20'
}
</script>
<template>
<svg
:class="sizeClasses[size]"
viewBox="0 0 56 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
>
<rect
x="1"
y="1"
width="54"
height="30"
rx="15"
class="stroke-noro-text"
stroke-width="2"
fill="none"
/>
<circle cx="42" cy="16" r="7" fill="#39c0c4" />
</svg>
</template>
+107
View File
@@ -0,0 +1,107 @@
<script setup lang="ts">
import logoHorizontalFullDark from '~/assets/images/logo/logo-horizontal-full.png'
import logoHorizontalTextDark from '~/assets/images/logo/logo-horizontal-text.png'
import logoVerticalFullDark from '~/assets/images/logo/logo-vertical-full.png'
import logoStackedDark from '~/assets/images/logo/logo-stacked.png'
import logoIconDark from '~/assets/images/logo/logo-icon.png'
import logoHorizontalFullLight from '~/assets/images/logo/logo-horizontal-full-light.png'
import logoHorizontalTextLight from '~/assets/images/logo/logo-horizontal-text-light.png'
import logoVerticalFullLight from '~/assets/images/logo/logo-vertical-full-light.png'
import logoStackedLight from '~/assets/images/logo/logo-stacked-light.png'
import logoIconLight from '~/assets/images/logo/logo-icon-light.png'
type LogoVariant = 'horizontal' | 'horizontal-text' | 'vertical' | 'stacked' | 'icon'
const props = withDefaults(
defineProps<{
variant?: LogoVariant
class?: string
}>(),
{
variant: 'horizontal'
}
)
const darkSources: Record<LogoVariant, string> = {
horizontal: logoHorizontalFullDark,
'horizontal-text': logoHorizontalTextDark,
vertical: logoVerticalFullDark,
stacked: logoStackedDark,
icon: logoIconDark
}
const lightSources: Record<LogoVariant, string> = {
horizontal: logoHorizontalFullLight,
'horizontal-text': logoHorizontalTextLight,
vertical: logoVerticalFullLight,
stacked: logoStackedLight,
icon: logoIconLight
}
const altText = 'استودیو نورو — Noro Studio'
const imgClass = computed(() => ['select-none', props.class])
</script>
<template>
<span class="inline-flex items-center justify-center">
<img
:src="darkSources[variant]"
:alt="altText"
:class="[...imgClass, 'hidden dark:flex dark:justify-center dark:items-center dark:gap-[44px] dark:pl-1 dark:grid-cols-11 dark:grid-rows-5 dark:flex-wrap dark:overflow-visible']"
decoding="async"
>
<img
:src="lightSources[variant]"
:alt="altText"
:class="[...imgClass, 'block dark:hidden']"
decoding="async"
>
</span>
</template>
+48
View File
@@ -0,0 +1,48 @@
<script setup lang="ts">
import type { PortfolioProject } from '~/data/noro'
defineProps<{
project: PortfolioProject
}>()
const { portfolioCategories } = useNoroData()
</script>
<template>
<NuxtLink
:to="`/portfolio/${project.id}`"
class="group noro-card-hover block cursor-pointer overflow-hidden"
>
<div class="relative aspect-[4/3] overflow-hidden bg-noro-surface">
<img
:src="project.image"
:alt="project.title"
class="h-full w-full object-cover transition duration-500 group-hover:scale-105"
loading="lazy"
decoding="async"
>
<div
class="absolute inset-0 bg-gradient-to-t from-black/50 via-black/10 to-transparent transition duration-300 group-hover:from-black/60"
/>
<span
class="absolute bottom-4 right-4 rounded-full bg-white/20 px-3 py-1 text-xs font-medium text-white backdrop-blur-sm"
>
{{ portfolioCategories[project.category].label }}
</span>
</div>
<div class="p-5">
<div class="flex items-start justify-between gap-3">
<h3 class="font-semibold text-noro-text transition group-hover:text-noro-brand">
{{ project.title }}
</h3>
<span class="shrink-0 text-xs text-noro-muted">{{ project.year }}</span>
</div>
<p class="mt-2 line-clamp-2 text-sm leading-6 text-noro-muted">
{{ project.description }}
</p>
<p class="mt-3 text-xs font-medium text-noro-brand opacity-0 transition group-hover:opacity-100">
مشاهده جزئیات ←
</p>
</div>
</NuxtLink>
</template>
+91
View File
@@ -0,0 +1,91 @@
<script setup lang="ts">
const props = defineProps<{
projectTitle: string
}>()
const rating = ref(0)
const hoveredRating = ref(0)
const comment = ref('')
const submitted = ref(false)
const displayRating = computed(() => hoveredRating.value || rating.value)
function setRating(value: number) {
rating.value = value
}
function handleSubmit() {
if (rating.value === 0) return
submitted.value = true
}
</script>
<template>
<div class="noro-card p-6 sm:p-8">
<template v-if="!submitted">
<h3 class="text-xl font-bold text-noro-text">
نظر شما درباره این پروژه
</h3>
<p class="mt-2 text-sm text-noro-muted">
امتیاز و نظر خود را درباره «{{ projectTitle }}» با ما به اشتراک بگذارید.
</p>
<div class="mt-6">
<p class="mb-3 text-sm text-noro-muted">امتیاز شما</p>
<div class="flex gap-1" role="radiogroup" aria-label="امتیاز پروژه">
<button
v-for="star in 5"
:key="star"
type="button"
class="rounded-lg p-1 transition hover:scale-110 focus-visible:outline focus-visible:outline-2 focus-visible:outline-noro-brand"
:aria-label="`${star} از ۵`"
@click="setRating(star)"
@mouseenter="hoveredRating = star"
@mouseleave="hoveredRating = 0"
>
<svg
class="h-8 w-8 transition"
:class="star <= displayRating ? 'text-amber-400' : 'text-noro-border'"
fill="currentColor"
viewBox="0 0 24 24"
>
<path
d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"
/>
</svg>
</button>
</div>
</div>
<label class="mt-6 block">
<span class="mb-2 block text-sm text-noro-muted">نظر شما (اختیاری)</span>
<textarea
v-model="comment"
rows="4"
placeholder="نظر یا پیشنهاد خود را بنویسید..."
class="w-full resize-none rounded-xl border border-noro-border bg-noro-bg px-4 py-3 text-sm text-noro-text outline-none transition focus:border-noro-brand"
/>
</label>
<UiButton
type="button"
size="lg"
class="mt-6"
:class="{ 'opacity-50 pointer-events-none': rating === 0 }"
@click="handleSubmit"
>
ثبت نظر
</UiButton>
</template>
<div v-else class="text-center">
<p class="text-4xl">★</p>
<h3 class="mt-4 text-xl font-bold text-noro-text">
ممنون از نظر شما!
</h3>
<p class="mt-2 text-noro-muted">
بازخورد شما برای بهبود کیفیت کارهای ما ارزشمند است.
</p>
</div>
</div>
</template>

Some files were not shown because too many files have changed in this diff Show More