feat: setup

This commit is contained in:
Maulana Sodiqin
2025-03-13 22:29:56 +07:00
parent 4ec41164f8
commit 28849e30b0
15 changed files with 606 additions and 7 deletions
+5
View File
@@ -3,6 +3,11 @@
"version": "0.0.1",
"main": "./index.js",
"types": "./index.d.ts",
"devDependencies": {
"tailwindcss": "^4.0.13",
"postcss": "^8.5.3",
"@tailwindcss/postcss": "^4.0.13"
},
"exports": {
".": {
"import": "./index.mjs",
+5
View File
@@ -0,0 +1,5 @@
export default {
plugins: {
'@tailwindcss/postcss': {},
},
};
+1
View File
@@ -1 +1,2 @@
import '../index.css';
export * from './button';
+120
View File
@@ -0,0 +1,120 @@
@import url('https://fonts.googleapis.com/css2?family=Bai+Jamjuree:ital,wght@0,200;0,300;0,400;0,500;0,600;0,700;1,200;1,300;1,400;1,500;1,600;1,700&display=swap');
@import 'tailwindcss';
@theme {
--color-primary-50: #f0f8ff;
--color-primary-100: #e1f0fd;
--color-primary-200: #b8e1f8;
--color-primary-300: #a1d4f0;
--color-primary-400: #4aa4da;
--color-primary-500: #1a8ce6;
--color-primary-600: #1673c7;
--color-primary-700: #0f5ca5;
--color-primary-800: #0a4780;
--color-primary-900: #04305d;
--color-neutral-50: #f7f7f7;
--color-neutral-100: #e7e7e7;
--color-neutral-200: #d0d0d0;
--color-neutral-300: #b7b7b7;
--color-neutral-400: #9f9f9f;
--color-neutral-500: #7e7e7e;
--color-neutral-600: #5d5d5d;
--color-neutral-700: #4a4a4a;
--color-neutral-800: #3a3a3a;
--color-neutral-900: #2f2f2f;
--color-success-50: #e7f2ee;
--color-success-100: #cde6dd;
--color-success-200: #9bccbc;
--color-success-300: #78bdab;
--color-success-400: #4ca88f;
--color-success-500: #349078;
--color-success-600: #2f7c66;
--color-success-700: #26624f;
--color-success-800: #1e4a3b;
--color-success-900: #1b513b;
--color-info-50: #e7f4f5;
--color-info-100: #cce9ea;
--color-info-200: #99d3d5;
--color-info-300: #78c4c7;
--color-info-400: #3aa9ad;
--color-info-500: #279599;
--color-info-600: #207d82;
--color-info-700: #1a666b;
--color-info-800: #124c52;
--color-info-900: #093d43;
--color-warning-50: #fffce6;
--color-warning-100: #fff8cc;
--color-warning-200: #fef39b;
--color-warning-300: #fde967;
--color-warning-400: #fbd93d;
--color-warning-500: #f3c215;
--color-warning-600: #d7a20f;
--color-warning-700: #aa7e0c;
--color-warning-800: #805c07;
--color-warning-900: #665c00;
--color-danger-50: #ffe7e7;
--color-danger-100: #ffcece;
--color-danger-200: #ff9f9f;
--color-danger-300: #ff6e6e;
--color-danger-400: #fa4646;
--color-danger-500: #ef1f1f;
--color-danger-600: #d11515;
--color-danger-700: #a51111;
--color-danger-800: #7f0c0c;
--color-danger-900: #5d2d2d;
--radius-none: 0px;
--radius-sm: 2px;
--radius-md: 4px;
--radius-lg: 8px;
--radius-xl: 12px;
--radius-2xl: 16px;
--radius-3xl: 24px;
--radius-full: 50%;
--font-bai-jamjuree: 'Bai Jamjuree', sans-serif;
--text-h1: 3rem; /* ~48px */
--text-h2: 2.4rem; /* ~38.4px */
--text-h3: 1.92rem; /* ~30.72px */
--text-p1: 1.54rem; /* ~24.58px */
--text-p2: 1.23rem; /* ~19.68px */
--text-label1: 0.98rem; /* ~15.74px */
--text-label2: 0.78rem; /* ~12.59px */
}
@layer base {
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-neutral-200, currentColor);
}
/* Custom scrollbar */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: var(--color-neutral-50);
}
::-webkit-scrollbar-thumb {
background: #cccccc;
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--color-neutral-300);
}
html {
font-family: 'Bai Jamjuree', sans-serif;
font-weight: 400;
}
}
+2
View File
@@ -0,0 +1,2 @@
import '../index.css';
export {};
+1
View File
@@ -1 +1,2 @@
import '../index.css';
export * from './navbar';
@@ -0,0 +1,206 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import {
render,
screen,
fireEvent,
RenderResult,
} from '@testing-library/react';
import { BrowserRouter } from 'react-router-dom';
import { Navbar } from './navbar';
// Interface untuk hasil matchMedia
interface MatchMediaResult {
matches: boolean;
media: string;
onchange: null;
addListener: ReturnType<typeof vi.fn>;
removeListener: ReturnType<typeof vi.fn>;
addEventListener: ReturnType<typeof vi.fn>;
removeEventListener: ReturnType<typeof vi.fn>;
dispatchEvent: ReturnType<typeof vi.fn>;
}
// Fungsi untuk mock matchMedia dengan tipe yang tepat
function mockMatchMedia(width: { matches: boolean }): void {
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: vi.fn().mockImplementation(
(query: string): MatchMediaResult => ({
matches: width.matches,
media: query,
onchange: null,
addListener: vi.fn(),
removeListener: vi.fn(),
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(),
})
),
});
}
describe('Navbar', () => {
// Setup dan cleanup untuk setiap test
beforeEach(() => {
// Reset DOM sebelum setiap test
document.body.innerHTML = '';
});
afterEach(() => {
// Cleanup setelah setiap test
vi.restoreAllMocks();
});
// Test yang sudah ada sebelumnya
it('renders the logo', () => {
render(
<BrowserRouter>
<Navbar />
</BrowserRouter>
);
const logo: HTMLElement = screen.getByAltText(/IMPHNEN Logo/i);
expect(logo).toBeInTheDocument();
});
it('renders the Home link', () => {
render(
<BrowserRouter>
<Navbar />
</BrowserRouter>
);
const homeLink: HTMLElement = screen.getByRole('link', { name: /home/i });
expect(homeLink).toBeInTheDocument();
});
it('renders the Merch Gacha link', () => {
render(
<BrowserRouter>
<Navbar />
</BrowserRouter>
);
const merchLink: HTMLElement = screen.getByRole('link', {
name: /merch gacha/i,
});
expect(merchLink).toBeInTheDocument();
});
// Test tambahan untuk responsifitas
// Untuk Desktop Screen
it('displays horizontal menu on desktop screens', () => {
// Set viewport untuk desktop (di atas 768px)
mockMatchMedia({ matches: true });
// Setup
global.innerWidth = 1024;
global.dispatchEvent(new Event('resize'));
render(
<BrowserRouter>
<Navbar />
</BrowserRouter>
);
// Cek apakah menu horizontal ditampilkan
const navItems: HTMLElement = screen.getByRole('list');
expect(navItems).toHaveClass('md:flex');
// Hamburger menu tidak terlihat di desktop
const hamburgerButton: HTMLElement = screen.getByRole('button');
expect(hamburgerButton).toHaveClass('md:hidden');
});
// Untuk Mobile Screen
it('displays hamburger menu on mobile screens and can toggle dropdown', () => {
// Set viewport untuk mobile (di bawah 768px)
mockMatchMedia({ matches: false });
// Setup
global.innerWidth = 375;
global.dispatchEvent(new Event('resize'));
render(
<BrowserRouter>
<Navbar />
</BrowserRouter>
);
// Hamburger menu harus terlihat
const hamburgerButton: HTMLElement = screen.getByRole('button');
expect(hamburgerButton).toBeVisible();
// Klik hamburger menu untuk membuka dropdown
fireEvent.click(hamburgerButton);
// Dropdown menu harus terlihat setelah diklik
const dropdownMenu: HTMLElement[] = screen.getAllByRole('list');
expect(dropdownMenu[1]).toBeVisible();
// Klik lagi untuk menutup dropdown
fireEvent.click(hamburgerButton);
// Dropdown tidak lagi terlihat (karena state berubah)
const updatedDropdownMenus: HTMLElement[] = screen.getAllByRole('list');
expect(updatedDropdownMenus.length).toBe(1); // Hanya menu horizontal yang tersisa
});
// Untuk Tablet Screen
it('has correct height on tablet screens', () => {
// Set viewport untuk tablet (antara mobile dan desktop)
mockMatchMedia({ matches: true });
// Setup
global.innerWidth = 768;
global.dispatchEvent(new Event('resize'));
const { container }: RenderResult = render(
<BrowserRouter>
<Navbar />
</BrowserRouter>
);
// Navbar harus memiliki kelas height untuk tablet
const header: HTMLElement | null = container.querySelector('header');
expect(header).not.toBeNull();
if (header) {
expect(header).toHaveClass('md:min-h-[60px]');
expect(header).toHaveClass('md:max-h-[60px]');
}
});
// Untuk Max Width pada 4K Screen
it('has max width constraint on large screens', () => {
// Set viewport untuk layar besar
mockMatchMedia({ matches: true });
// Setup
global.innerWidth = 3840; // 4K resolution
global.dispatchEvent(new Event('resize'));
const { container }: RenderResult = render(
<BrowserRouter>
<Navbar />
</BrowserRouter>
);
// Navbar harus memiliki max-width dan posisi tengah
const header: HTMLElement | null = container.querySelector('header');
expect(header).not.toBeNull();
if (header) {
expect(header).toHaveClass('max-w-[1280px]');
expect(header).toHaveClass('xl:mx-auto');
}
});
// Test tambahan untuk memeriksa peran dan aksesibilitas
it('has correct ARIA role for navigation', () => {
const { container }: RenderResult = render(
<BrowserRouter>
<Navbar />
</BrowserRouter>
);
const header: HTMLElement | null = container.querySelector('header');
expect(header).toHaveAttribute('role', 'navigation');
});
});
+2
View File
@@ -5,12 +5,14 @@ import dts from 'vite-plugin-dts';
import * as path from 'path';
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
import { nxCopyAssetsPlugin } from '@nx/vite/plugins/nx-copy-assets.plugin';
import tailwindcss from '@tailwindcss/vite';
export default defineConfig(() => ({
root: __dirname,
cacheDir: '../../node_modules/.vite/libs/ui',
plugins: [
react(),
tailwindcss(),
nxViteTsPaths(),
nxCopyAssetsPlugin(['*.md']),
dts({