@crm-market/template-shared 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/css/custom.css +70 -0
- package/assets/css/remixicon.css +2782 -0
- package/assets/css/satoshi-font.css +31 -0
- package/assets/fonts/flaticon.css +463 -0
- package/assets/fonts/flaticon.eot +0 -0
- package/assets/fonts/flaticon.html +2153 -0
- package/assets/fonts/flaticon.svg +441 -0
- package/assets/fonts/flaticon.ttf +0 -0
- package/assets/fonts/flaticon.woff +0 -0
- package/assets/fonts/flaticon.woff2 +0 -0
- package/assets/fonts/remixicon.eot +0 -0
- package/assets/fonts/remixicon.svg +8230 -0
- package/assets/fonts/remixicon.ttf +0 -0
- package/assets/fonts/remixicon.woff +0 -0
- package/assets/fonts/remixicon.woff2 +0 -0
- package/assets/scss/_variables.scss +31 -0
- package/assets/scss/components/_about.scss +58 -0
- package/assets/scss/components/_authentication.scss +124 -0
- package/assets/scss/components/_backtoptop.scss +27 -0
- package/assets/scss/components/_banner.scss +396 -0
- package/assets/scss/components/_best-deals.scss +74 -0
- package/assets/scss/components/_blank.scss +40 -0
- package/assets/scss/components/_blog.scss +193 -0
- package/assets/scss/components/_cart.scss +108 -0
- package/assets/scss/components/_categories.scss +82 -0
- package/assets/scss/components/_checkout.scss +110 -0
- package/assets/scss/components/_dashboard.scss +388 -0
- package/assets/scss/components/_faq.scss +22 -0
- package/assets/scss/components/_filter-rang.scss +109 -0
- package/assets/scss/components/_footer.scss +270 -0
- package/assets/scss/components/_global.scss +550 -0
- package/assets/scss/components/_header.scss +587 -0
- package/assets/scss/components/_hurry.scss +52 -0
- package/assets/scss/components/_navbar.scss +1008 -0
- package/assets/scss/components/_offers.scss +689 -0
- package/assets/scss/components/_pagination.scss +71 -0
- package/assets/scss/components/_popup.scss +172 -0
- package/assets/scss/components/_preloader.scss +108 -0
- package/assets/scss/components/_products.scss +1147 -0
- package/assets/scss/components/_rtl.scss +806 -0
- package/assets/scss/components/_services.scss +16 -0
- package/assets/scss/components/_sidebar.scss +259 -0
- package/assets/scss/style.css +6676 -0
- package/assets/scss/style.css.map +1 -0
- package/assets/scss/style.scss +40 -0
- package/assets/webfonts/Satoshi-Bold.eot +0 -0
- package/assets/webfonts/Satoshi-Bold.woff +0 -0
- package/assets/webfonts/Satoshi-Bold.woff2 +0 -0
- package/assets/webfonts/Satoshi-Medium.eot +0 -0
- package/assets/webfonts/Satoshi-Medium.woff +0 -0
- package/assets/webfonts/Satoshi-Medium.woff2 +0 -0
- package/assets/webfonts/Satoshi-Regular.eot +0 -0
- package/assets/webfonts/Satoshi-Regular.woff +0 -0
- package/assets/webfonts/Satoshi-Regular.woff2 +0 -0
- package/components/AboutUs/AboutUsTuan.vue +25 -0
- package/components/AboutUs/Statistics.vue +42 -0
- package/components/AboutUs/SubscribeToTheNewsletter.vue +57 -0
- package/components/AddAddress/index.vue +70 -0
- package/components/BestSellers/Products.vue +1562 -0
- package/components/BestSellers/RecentlyViewed.vue +304 -0
- package/components/Cart/ProductQuantity.vue +29 -0
- package/components/Cart/index.vue +167 -0
- package/components/Categories/index.vue +305 -0
- package/components/ChangePassword/index.vue +71 -0
- package/components/Checkout/index.vue +192 -0
- package/components/Common/DashboardNavigation.vue +37 -0
- package/components/Common/PageBanner.vue +28 -0
- package/components/Common/ProductCard.vue +152 -0
- package/components/Common/Services.vue +58 -0
- package/components/Contact/ContactForm.vue +91 -0
- package/components/Contact/ContactInfo.vue +74 -0
- package/components/Dashboard/RecentOrder.vue +173 -0
- package/components/Dashboard/index.vue +79 -0
- package/components/EditAddress/index.vue +119 -0
- package/components/EditProfile/index.vue +97 -0
- package/components/FAQ/index.vue +121 -0
- package/components/FeaturedProduct/FeaturedProducts.vue +304 -0
- package/components/FeaturedProduct/Products.vue +1562 -0
- package/components/ForgotPassword/index.vue +51 -0
- package/components/Layout/BackToUp.vue +38 -0
- package/components/Layout/Copyright.vue +25 -0
- package/components/Layout/Footer.vue +183 -0
- package/components/Layout/FooterTwo.vue +165 -0
- package/components/Layout/LocationOption.vue +178 -0
- package/components/Layout/MiddleHeader.vue +229 -0
- package/components/Layout/MiddleHeaderThree.vue +204 -0
- package/components/Layout/MiddleHeaderTwo.vue +240 -0
- package/components/Layout/Navbar.vue +185 -0
- package/components/Layout/NavbarStyleFour.vue +334 -0
- package/components/Layout/NavbarStyleThree.vue +188 -0
- package/components/Layout/NavbarStyleTwo.vue +108 -0
- package/components/Layout/Preloader.vue +18 -0
- package/components/Layout/RTLSwitchBtn.vue +40 -0
- package/components/Layout/ResponsiveNavbar.vue +431 -0
- package/components/Layout/TopHeader.vue +130 -0
- package/components/Login/index.vue +94 -0
- package/components/MyAccount/index.vue +154 -0
- package/components/NewArrivals/Products.vue +1969 -0
- package/components/NewArrivals/RecentlyViewed.vue +304 -0
- package/components/OrderDetails/index.vue +77 -0
- package/components/OrderHistory/index.vue +197 -0
- package/components/PrivacyPolicy/index.vue +112 -0
- package/components/ProductDetails/ProductDetailsTab.vue +343 -0
- package/components/ProductDetails/ProductQuantity.vue +29 -0
- package/components/ProductDetails/RecentlyViewed.vue +304 -0
- package/components/ProductDetails/index.vue +268 -0
- package/components/Products/RecentlyViewed.vue +304 -0
- package/components/Products/index.vue +292 -0
- package/components/Register/index.vue +88 -0
- package/components/TermsConditions/index.vue +112 -0
- package/components/TrendingProducts/FeaturedProducts.vue +304 -0
- package/components/TrendingProducts/Products.vue +1564 -0
- package/components/Wishlist/ProductQuantity.vue +29 -0
- package/components/Wishlist/index.vue +128 -0
- package/composables/useCart.ts +149 -0
- package/composables/useCategories.ts +87 -0
- package/composables/useCheckout.ts +131 -0
- package/composables/useProducts.ts +162 -0
- package/composables/useSiteConfig.ts +236 -0
- package/composables/useTemplateSections.ts +74 -0
- package/e2e/cart.spec.ts +71 -0
- package/e2e/fixtures/mock-api.ts +166 -0
- package/e2e/homepage.spec.ts +65 -0
- package/e2e/layout.spec.ts +73 -0
- package/e2e/navigation.spec.ts +61 -0
- package/e2e/pages/cart.page.ts +44 -0
- package/e2e/pages/homepage.page.ts +46 -0
- package/e2e/playwright.config.ts +32 -0
- package/e2e/products.spec.ts +33 -0
- package/layouts/default.vue +94 -0
- package/layouts/inner.vue +70 -0
- package/nuxt.config.ts +86 -0
- package/package.json +38 -0
- package/pages/about-us.vue +12 -0
- package/pages/address.vue +10 -0
- package/pages/cart.vue +10 -0
- package/pages/categories.vue +10 -0
- package/pages/change-password.vue +10 -0
- package/pages/checkout.vue +10 -0
- package/pages/contact.vue +11 -0
- package/pages/dashboard.vue +10 -0
- package/pages/edit-address.vue +10 -0
- package/pages/edit-profile.vue +10 -0
- package/pages/faq.vue +10 -0
- package/pages/forgot-password.vue +10 -0
- package/pages/login.vue +10 -0
- package/pages/my-account.vue +10 -0
- package/pages/order-details.vue +10 -0
- package/pages/order-history.vue +10 -0
- package/pages/privacy-policy.vue +10 -0
- package/pages/product-details.vue +10 -0
- package/pages/products.vue +10 -0
- package/pages/register.vue +10 -0
- package/pages/terms-conditions.vue +10 -0
- package/pages/wishlist.vue +10 -0
- package/plugins/site-init.client.ts +24 -0
- package/plugins/vuetify.ts +18 -0
- package/types/index.ts +121 -0
- package/utils/image.ts +13 -0
- package/utils/store.ts +21 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { test, expect } from '@playwright/test';
|
|
2
|
+
import { setupMockApi } from './fixtures/mock-api';
|
|
3
|
+
|
|
4
|
+
test.describe('Навігація між сторінками', () => {
|
|
5
|
+
test.beforeEach(async ({ page }) => {
|
|
6
|
+
await setupMockApi(page);
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
const pages = [
|
|
10
|
+
{ path: '/about-us', title: 'Про нас' },
|
|
11
|
+
{ path: '/contact', title: 'Контакти' },
|
|
12
|
+
{ path: '/products', title: 'Товари' },
|
|
13
|
+
{ path: '/categories', title: 'Категорії' },
|
|
14
|
+
{ path: '/cart', title: 'Кошик' },
|
|
15
|
+
{ path: '/login', title: 'Увійти' },
|
|
16
|
+
{ path: '/register', title: 'Реєстрація' },
|
|
17
|
+
{ path: '/faq', title: 'Часті запитання' },
|
|
18
|
+
{ path: '/terms-conditions', title: 'Умови' },
|
|
19
|
+
{ path: '/privacy-policy', title: 'Політика' },
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
for (const p of pages) {
|
|
23
|
+
test(`відкривається сторінка ${p.path}`, async ({ page }) => {
|
|
24
|
+
const response = await page.goto(p.path);
|
|
25
|
+
expect(response?.status()).toBeLessThan(500);
|
|
26
|
+
|
|
27
|
+
// Сторінка має рендеритись без помилок
|
|
28
|
+
const errorOverlay = page.locator('#nuxt-error, .nuxt-error-page');
|
|
29
|
+
const hasError = await errorOverlay.isVisible().catch(() => false);
|
|
30
|
+
|
|
31
|
+
if (!hasError) {
|
|
32
|
+
// Перевіряємо наявність page banner
|
|
33
|
+
const banner = page.locator('.page-banner-area, .breadcrumb').first();
|
|
34
|
+
await expect(banner).toBeVisible({ timeout: 5000 }).catch(() => {
|
|
35
|
+
// Деякі сторінки можуть не мати banner — це ок
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test.describe('Responsive навігація', () => {
|
|
43
|
+
test.beforeEach(async ({ page }) => {
|
|
44
|
+
await setupMockApi(page);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('мобільне меню відкривається', async ({ page }) => {
|
|
48
|
+
// Встановлюємо мобільний viewport
|
|
49
|
+
await page.setViewportSize({ width: 375, height: 812 });
|
|
50
|
+
await page.goto('/');
|
|
51
|
+
|
|
52
|
+
// Шукаємо кнопку мобільного меню
|
|
53
|
+
const menuButton = page.locator('.navbar-toggler, .hamburger-menu, .ri-menu-line, button[aria-label*="menu"]').first();
|
|
54
|
+
if (await menuButton.isVisible()) {
|
|
55
|
+
await menuButton.click();
|
|
56
|
+
// Перевіряємо що мобільне меню з'явилось
|
|
57
|
+
const mobileMenu = page.locator('.responsive-navbar, .mobile-menu, .offcanvas, .sidebar-menu').first();
|
|
58
|
+
await expect(mobileMenu).toBeVisible({ timeout: 3000 });
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { Page, Locator } from '@playwright/test';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Page Object для сторінки кошика.
|
|
5
|
+
*/
|
|
6
|
+
export class CartPage {
|
|
7
|
+
readonly page: Page;
|
|
8
|
+
readonly cartItems: Locator;
|
|
9
|
+
readonly emptyCartMessage: Locator;
|
|
10
|
+
readonly subtotal: Locator;
|
|
11
|
+
readonly checkoutButton: Locator;
|
|
12
|
+
readonly removeButtons: Locator;
|
|
13
|
+
readonly quantityInputs: Locator;
|
|
14
|
+
|
|
15
|
+
constructor(page: Page) {
|
|
16
|
+
this.page = page;
|
|
17
|
+
this.cartItems = page.locator('.cart-item, .cart-table tbody tr');
|
|
18
|
+
this.emptyCartMessage = page.locator('.empty-cart, :text("порожній"), :text("empty")');
|
|
19
|
+
this.subtotal = page.locator('.subtotal, .cart-total');
|
|
20
|
+
this.checkoutButton = page.locator('a[href*="checkout"], button:has-text("Checkout"), button:has-text("Оформити")');
|
|
21
|
+
this.removeButtons = page.locator('.remove-btn, .ri-delete-bin-line, .ri-close-line');
|
|
22
|
+
this.quantityInputs = page.locator('.quantity-input, input[type="number"]');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async goto() {
|
|
26
|
+
await this.page.goto('/cart');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async getItemCount() {
|
|
30
|
+
return await this.cartItems.count();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async removeItem(index: number) {
|
|
34
|
+
await this.removeButtons.nth(index).click();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async proceedToCheckout() {
|
|
38
|
+
await this.checkoutButton.click();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async isEmpty() {
|
|
42
|
+
return await this.emptyCartMessage.isVisible();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { Page, Locator } from '@playwright/test';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Page Object для головної сторінки шаблону.
|
|
5
|
+
*/
|
|
6
|
+
export class HomepagePage {
|
|
7
|
+
readonly page: Page;
|
|
8
|
+
readonly topHeader: Locator;
|
|
9
|
+
readonly middleHeader: Locator;
|
|
10
|
+
readonly navbar: Locator;
|
|
11
|
+
readonly banner: Locator;
|
|
12
|
+
readonly footer: Locator;
|
|
13
|
+
readonly searchInput: Locator;
|
|
14
|
+
readonly cartIcon: Locator;
|
|
15
|
+
readonly logo: Locator;
|
|
16
|
+
|
|
17
|
+
constructor(page: Page) {
|
|
18
|
+
this.page = page;
|
|
19
|
+
this.topHeader = page.locator('.top-header-area');
|
|
20
|
+
this.middleHeader = page.locator('.middle-header-area');
|
|
21
|
+
this.navbar = page.locator('.navbar-area');
|
|
22
|
+
this.banner = page.locator('.banner-area, .swiper');
|
|
23
|
+
this.footer = page.locator('.footer-area');
|
|
24
|
+
this.searchInput = page.locator('.search-input, input[type="search"], .search-box input');
|
|
25
|
+
this.cartIcon = page.locator('.cart-btn, .ri-shopping-cart-line').first();
|
|
26
|
+
this.logo = page.locator('.logo img, .navbar-brand img').first();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async goto() {
|
|
30
|
+
await this.page.goto('/');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async searchProduct(query: string) {
|
|
34
|
+
await this.searchInput.fill(query);
|
|
35
|
+
await this.searchInput.press('Enter');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async openCart() {
|
|
39
|
+
await this.cartIcon.click();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async getSectionCount() {
|
|
43
|
+
// Підраховує кількість видимих секцій на головній сторінці
|
|
44
|
+
return await this.page.locator('section, .section-area, [class*="area"]').count();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { defineConfig, devices } from '@playwright/test';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Базова конфігурація Playwright для шаблонів магазинів.
|
|
5
|
+
* Кожен шаблон може розширити цю конфігурацію, змінивши baseURL та порт.
|
|
6
|
+
*/
|
|
7
|
+
export default defineConfig({
|
|
8
|
+
testDir: '.',
|
|
9
|
+
fullyParallel: true,
|
|
10
|
+
forbidOnly: !!process.env.CI,
|
|
11
|
+
retries: process.env.CI ? 2 : 0,
|
|
12
|
+
workers: process.env.CI ? 1 : undefined,
|
|
13
|
+
reporter: 'html',
|
|
14
|
+
timeout: 30_000,
|
|
15
|
+
|
|
16
|
+
use: {
|
|
17
|
+
baseURL: process.env.BASE_URL || 'http://localhost:3000',
|
|
18
|
+
trace: 'on-first-retry',
|
|
19
|
+
screenshot: 'only-on-failure',
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
projects: [
|
|
23
|
+
{
|
|
24
|
+
name: 'chromium',
|
|
25
|
+
use: { ...devices['Desktop Chrome'] },
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
name: 'mobile',
|
|
29
|
+
use: { ...devices['iPhone 14'] },
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { test, expect } from '@playwright/test';
|
|
2
|
+
import { setupMockApi } from './fixtures/mock-api';
|
|
3
|
+
import { ProductsPage } from './pages/products.page';
|
|
4
|
+
|
|
5
|
+
test.describe('Сторінка товарів', () => {
|
|
6
|
+
let productsPage: ProductsPage;
|
|
7
|
+
|
|
8
|
+
test.beforeEach(async ({ page }) => {
|
|
9
|
+
await setupMockApi(page);
|
|
10
|
+
productsPage = new ProductsPage(page);
|
|
11
|
+
await productsPage.goto();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test('відображає банер сторінки', async ({ page }) => {
|
|
15
|
+
await expect(page.locator('.page-banner-area, .breadcrumb').first()).toBeVisible();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test('відображає товари з API', async () => {
|
|
19
|
+
// Чекаємо завантаження товарів
|
|
20
|
+
await productsPage.page.waitForTimeout(2000);
|
|
21
|
+
const count = await productsPage.getProductCount();
|
|
22
|
+
expect(count).toBeGreaterThan(0);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('товар має назву та ціну', async ({ page }) => {
|
|
26
|
+
await page.waitForTimeout(2000);
|
|
27
|
+
// Перевіряємо що є текст з цінами (грн або числа)
|
|
28
|
+
const priceElements = page.locator('.product-card .price, .product-item .price, .product-price');
|
|
29
|
+
if (await priceElements.count() > 0) {
|
|
30
|
+
await expect(priceElements.first()).toBeVisible();
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* Головний layout шаблону магазину.
|
|
4
|
+
* Динамічно обирає варіант header/navbar/footer
|
|
5
|
+
* на основі templateType або componentSettings з API.
|
|
6
|
+
*/
|
|
7
|
+
const config = useRuntimeConfig();
|
|
8
|
+
const { siteConfig, componentSettings } = useSiteConfig();
|
|
9
|
+
|
|
10
|
+
const templateType = computed(() => {
|
|
11
|
+
return siteConfig.value?.template?.type || config.public.templateType || 'electronics';
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
// Визначаємо варіант header на основі шаблону або componentSettings
|
|
15
|
+
const headerStyle = computed(() => {
|
|
16
|
+
return componentSettings.value?.header?.style || getDefaultHeaderStyle(templateType.value);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const navbarStyle = computed(() => {
|
|
20
|
+
return componentSettings.value?.navbar?.style || getDefaultNavbarStyle(templateType.value);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const footerStyle = computed(() => {
|
|
24
|
+
return componentSettings.value?.footer?.style || getDefaultFooterStyle(templateType.value);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
function getDefaultHeaderStyle(type: string): string {
|
|
28
|
+
const map: Record<string, string> = {
|
|
29
|
+
electronics: 'default',
|
|
30
|
+
grocery: 'two',
|
|
31
|
+
healthy_diet: 'three',
|
|
32
|
+
};
|
|
33
|
+
return map[type] || 'default';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function getDefaultNavbarStyle(type: string): string {
|
|
37
|
+
const map: Record<string, string> = {
|
|
38
|
+
electronics: 'default',
|
|
39
|
+
grocery: 'two',
|
|
40
|
+
healthy_diet: 'three',
|
|
41
|
+
};
|
|
42
|
+
return map[type] || 'default';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function getDefaultFooterStyle(type: string): string {
|
|
46
|
+
const map: Record<string, string> = {
|
|
47
|
+
electronics: 'default',
|
|
48
|
+
grocery: 'default',
|
|
49
|
+
healthy_diet: 'two',
|
|
50
|
+
};
|
|
51
|
+
return map[type] || 'default';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Маппінг стилів до компонентів
|
|
55
|
+
const headerComponents: Record<string, any> = {
|
|
56
|
+
default: resolveComponent('LayoutMiddleHeader'),
|
|
57
|
+
two: resolveComponent('LayoutMiddleHeaderTwo'),
|
|
58
|
+
three: resolveComponent('LayoutMiddleHeaderThree'),
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const navbarComponents: Record<string, any> = {
|
|
62
|
+
default: resolveComponent('LayoutNavbar'),
|
|
63
|
+
two: resolveComponent('LayoutNavbarStyleTwo'),
|
|
64
|
+
three: resolveComponent('LayoutNavbarStyleThree'),
|
|
65
|
+
four: resolveComponent('LayoutNavbarStyleFour'),
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const footerComponents: Record<string, any> = {
|
|
69
|
+
default: resolveComponent('LayoutFooter'),
|
|
70
|
+
two: resolveComponent('LayoutFooterTwo'),
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const currentHeader = computed(() => headerComponents[headerStyle.value] || headerComponents.default);
|
|
74
|
+
const currentNavbar = computed(() => navbarComponents[navbarStyle.value] || navbarComponents.default);
|
|
75
|
+
const currentFooter = computed(() => footerComponents[footerStyle.value] || footerComponents.default);
|
|
76
|
+
</script>
|
|
77
|
+
|
|
78
|
+
<template>
|
|
79
|
+
<div>
|
|
80
|
+
<LayoutTopHeader />
|
|
81
|
+
<component :is="currentHeader" />
|
|
82
|
+
<component :is="currentNavbar" />
|
|
83
|
+
|
|
84
|
+
<slot />
|
|
85
|
+
|
|
86
|
+
<CommonServices class="bg-gray1" />
|
|
87
|
+
<component :is="currentFooter" />
|
|
88
|
+
<LayoutBackToUp />
|
|
89
|
+
<LayoutResponsiveNavbar />
|
|
90
|
+
<LayoutLocationOption />
|
|
91
|
+
<LayoutCopyright />
|
|
92
|
+
<LayoutRTLSwitchBtn />
|
|
93
|
+
</div>
|
|
94
|
+
</template>
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* Layout для внутрішніх сторінок (cart, checkout, login, тощо).
|
|
4
|
+
* Використовує NavbarStyleFour замість основного навбару.
|
|
5
|
+
*/
|
|
6
|
+
const config = useRuntimeConfig();
|
|
7
|
+
const { siteConfig, componentSettings } = useSiteConfig();
|
|
8
|
+
|
|
9
|
+
const templateType = computed(() => {
|
|
10
|
+
return siteConfig.value?.template?.type || config.public.templateType || 'electronics';
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
const headerStyle = computed(() => {
|
|
14
|
+
return componentSettings.value?.header?.style || getDefaultHeaderStyle(templateType.value);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const footerStyle = computed(() => {
|
|
18
|
+
return componentSettings.value?.footer?.style || getDefaultFooterStyle(templateType.value);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
function getDefaultHeaderStyle(type: string): string {
|
|
22
|
+
const map: Record<string, string> = {
|
|
23
|
+
electronics: 'default',
|
|
24
|
+
grocery: 'two',
|
|
25
|
+
healthy_diet: 'three',
|
|
26
|
+
};
|
|
27
|
+
return map[type] || 'default';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function getDefaultFooterStyle(type: string): string {
|
|
31
|
+
const map: Record<string, string> = {
|
|
32
|
+
electronics: 'default',
|
|
33
|
+
grocery: 'default',
|
|
34
|
+
healthy_diet: 'two',
|
|
35
|
+
};
|
|
36
|
+
return map[type] || 'default';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const headerComponents: Record<string, any> = {
|
|
40
|
+
default: resolveComponent('LayoutMiddleHeader'),
|
|
41
|
+
two: resolveComponent('LayoutMiddleHeaderTwo'),
|
|
42
|
+
three: resolveComponent('LayoutMiddleHeaderThree'),
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const footerComponents: Record<string, any> = {
|
|
46
|
+
default: resolveComponent('LayoutFooter'),
|
|
47
|
+
two: resolveComponent('LayoutFooterTwo'),
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const currentHeader = computed(() => headerComponents[headerStyle.value] || headerComponents.default);
|
|
51
|
+
const currentFooter = computed(() => footerComponents[footerStyle.value] || footerComponents.default);
|
|
52
|
+
</script>
|
|
53
|
+
|
|
54
|
+
<template>
|
|
55
|
+
<div>
|
|
56
|
+
<LayoutTopHeader />
|
|
57
|
+
<component :is="currentHeader" />
|
|
58
|
+
<LayoutNavbarStyleFour />
|
|
59
|
+
|
|
60
|
+
<slot />
|
|
61
|
+
|
|
62
|
+
<CommonServices class="bg-gray1" />
|
|
63
|
+
<component :is="currentFooter" />
|
|
64
|
+
<LayoutBackToUp />
|
|
65
|
+
<LayoutResponsiveNavbar />
|
|
66
|
+
<LayoutLocationOption />
|
|
67
|
+
<LayoutCopyright />
|
|
68
|
+
<LayoutRTLSwitchBtn />
|
|
69
|
+
</div>
|
|
70
|
+
</template>
|
package/nuxt.config.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { defineNuxtConfig } from "nuxt/config";
|
|
2
|
+
import type { Plugin } from "vite";
|
|
3
|
+
import vuetify, { transformAssetUrls } from "vite-plugin-vuetify";
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import { dirname, resolve } from 'path';
|
|
6
|
+
|
|
7
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
|
|
9
|
+
export default defineNuxtConfig({
|
|
10
|
+
ssr: true,
|
|
11
|
+
|
|
12
|
+
app: {
|
|
13
|
+
pageTransition: {
|
|
14
|
+
name: "fade",
|
|
15
|
+
mode: "out-in",
|
|
16
|
+
},
|
|
17
|
+
layoutTransition: {
|
|
18
|
+
name: "slide",
|
|
19
|
+
mode: "out-in",
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
runtimeConfig: {
|
|
24
|
+
public: {
|
|
25
|
+
apiBase: process.env.API_BASE_URL || 'http://localhost:3001/api',
|
|
26
|
+
templateType: process.env.TEMPLATE_TYPE || 'electronics',
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
// CSS з абсолютними шляхами відносно цього layer
|
|
31
|
+
css: [
|
|
32
|
+
"bootstrap/dist/css/bootstrap.min.css",
|
|
33
|
+
resolve(__dirname, 'assets/css/remixicon.css'),
|
|
34
|
+
resolve(__dirname, 'assets/fonts/flaticon.css'),
|
|
35
|
+
resolve(__dirname, 'assets/css/satoshi-font.css'),
|
|
36
|
+
],
|
|
37
|
+
|
|
38
|
+
// Plugins авто-скануються з plugins/ — не потрібно оголошувати явно
|
|
39
|
+
|
|
40
|
+
build: {
|
|
41
|
+
transpile: ["vuetify"],
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
modules: [
|
|
45
|
+
(
|
|
46
|
+
_options: any,
|
|
47
|
+
nuxt: {
|
|
48
|
+
hooks: {
|
|
49
|
+
hook: (
|
|
50
|
+
arg0: string,
|
|
51
|
+
arg1: (config: { plugins: Plugin<any>[][] }) => void
|
|
52
|
+
) => void;
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
) => {
|
|
56
|
+
nuxt.hooks.hook(
|
|
57
|
+
"vite:extendConfig",
|
|
58
|
+
(config: { plugins: Plugin<any>[][] }) => {
|
|
59
|
+
config.plugins.push(vuetify({ autoImport: true }));
|
|
60
|
+
}
|
|
61
|
+
);
|
|
62
|
+
},
|
|
63
|
+
"nuxt-swiper",
|
|
64
|
+
],
|
|
65
|
+
|
|
66
|
+
vite: {
|
|
67
|
+
vue: {
|
|
68
|
+
template: {
|
|
69
|
+
transformAssetUrls,
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
css: {
|
|
73
|
+
preprocessorOptions: {
|
|
74
|
+
scss: {
|
|
75
|
+
sourceMap: true,
|
|
76
|
+
additionalData: `
|
|
77
|
+
@import "${resolve(__dirname, 'assets/scss/_variables.scss')}";
|
|
78
|
+
`,
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
components: true,
|
|
85
|
+
compatibilityDate: "2026-01-02",
|
|
86
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@crm-market/template-shared",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Shared Nuxt 3 layer for CRM Market store templates (layouts, components, composables, pages)",
|
|
6
|
+
"publishConfig": {
|
|
7
|
+
"access": "public"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"dev": "nuxt dev",
|
|
11
|
+
"build": "nuxt build",
|
|
12
|
+
"postinstall": "nuxt prepare"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"nuxt.config.ts",
|
|
16
|
+
"assets",
|
|
17
|
+
"components",
|
|
18
|
+
"composables",
|
|
19
|
+
"layouts",
|
|
20
|
+
"pages",
|
|
21
|
+
"plugins",
|
|
22
|
+
"types",
|
|
23
|
+
"utils",
|
|
24
|
+
"e2e"
|
|
25
|
+
],
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"bootstrap": "^5.3.3",
|
|
28
|
+
"nuxt": "^3.12.4",
|
|
29
|
+
"nuxt-swiper": "^1.2.2",
|
|
30
|
+
"vue": "^3.4.34",
|
|
31
|
+
"vue-router": "^4.4.0"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"sass": "^1.77.8",
|
|
35
|
+
"vite-plugin-vuetify": "^2.0.3",
|
|
36
|
+
"vuetify": "^3.6.13"
|
|
37
|
+
}
|
|
38
|
+
}
|
package/pages/cart.vue
ADDED
package/pages/faq.vue
ADDED
package/pages/login.vue
ADDED