Compare commits

..
29 Commits
Author SHA1 Message Date
boboiazumi 4ac1278a33 fix: conflict 2025-03-17 18:13:24 +07:00
Maulana Sodiqin d20f0d89e5 feat: init backoffice 2025-03-17 12:48:11 +07:00
Maulana Sodiqin 515b78c57f feat: add backoffice apps 2025-03-17 09:39:04 +07:00
Maulana Sodiqin 8a53575cd1 Merge pull request #4 from IMPHNEN/feat/simple-landing-page
Roulette page
2025-03-17 09:29:43 +07:00
Hafid Nur c8b12db1b3 feat: linear gradient diffuser 2025-03-16 22:53:13 +07:00
Hafid Nur e3c4b7a23a feat: infinite scroll animation for gacha items 2025-03-16 22:25:29 +07:00
boboiazumi 41a0842026 feat: modals-gacha 2025-03-16 22:18:48 +07:00
Hafid Nur a9164e5e5f feat: gacha item horizontal scroll snap 2025-03-16 22:16:52 +07:00
Hafid Nur d7e39095bb feat: separate GachaItem component and add more Gacha items (images and labels)
TODO:
- implement an infinite scroll snap slider for Gacha items
2025-03-16 21:42:45 +07:00
Hafid Nur 84ddf2b771 feat: draft roulette page 2025-03-16 20:40:18 +07:00
Hafid Nur cb632adbb4 feat: change the layout of the landing page from using CSS Flexbox to CSS Grid
- minor adjustment for navbar max width and padding
2025-03-16 19:54:42 +07:00
Hafid Nur 6a1fa42956 feat: update navbar to cover top of the page
- Update the navbar to be sticky when scrolling the page and cover the top of the page.
- Fix the margins for every viewport.
2025-03-16 19:19:44 +07:00
Hafid Nur 9002e6a261 feat: merge landing page component into single page file
- Component landing page sebelumnya terpisah, tetapi perlu digabungkan lagi karena tidak reusable pada aplikasi
2025-03-16 19:13:49 +07:00
Maulana Sodiqin 254d4fe04a Merge pull request #2 from IMPHNEN/feat/simple-landing-page
Slicing simple landing page
2025-03-16 12:39:12 +07:00
Hafid Nur 02e06caca1 feat: landing page for desktop viewport 2025-03-16 07:41:16 +07:00
Hafid Nur 00033b9c68 feat: landing page for tablet viewport
- change "scroll for gacha" to button to roulette page
2025-03-16 07:11:06 +07:00
Hafid Nur 02863ebc8b feat: add landing page for mobile viewport 2025-03-16 06:38:14 +07:00
Hafid Nur a31166e3ff feat: update navbar responsive 2025-03-16 06:07:07 +07:00
Hafid Nur c7b74bc9b6 feat: update theme to match the Design System
adjustment
- primary color
- base + typography size
2025-03-16 05:42:24 +07:00
Maulana Sodiqin 01621a8786 Merge pull request #1 from IMPHNEN/feat/component-migrate
Migrate component from 'imphnen-gacha' repository
2025-03-16 01:45:33 +07:00
hafidnrzs a07e3c21d6 feat: add Textarea component 2025-03-16 01:40:02 +07:00
hafidnrzs fb99b41b3a feat: add PasswordInput component 2025-03-16 01:34:24 +07:00
Hafid Nur 183ca100dc feat: add Input component 2025-03-16 00:56:02 +07:00
Maulana Sodiqin 203ee5de97 chore: remove uneeded folder/file 2025-03-16 00:35:48 +07:00
Maulana Sodiqin 58843041db faet 2025-03-16 00:33:48 +07:00
Maulana Sodiqin 3964c22c66 feat: navbar unit test 2025-03-16 00:16:58 +07:00
Maulana Sodiqin ec02a4ef83 fix: unit test 2025-03-16 00:12:15 +07:00
Maulana Sodiqin 587cb9facf fix: ui error build 2025-03-16 00:07:02 +07:00
Maulana Sodiqin 5c5faaf8a1 fix: libs cannot apply style 2025-03-16 00:05:17 +07:00
70 changed files with 8387 additions and 410 deletions

No files matched your search

+2
View File
@@ -43,3 +43,5 @@ Thumbs.db
vite.config.*.timestamp* vite.config.*.timestamp*
vitest.config.*.timestamp* vitest.config.*.timestamp*
storybook-static
+12
View File
@@ -0,0 +1,12 @@
import nx from '@nx/eslint-plugin';
import baseConfig from '../../eslint.config.mjs';
export default [
...baseConfig,
...nx.configs['flat/react'],
{
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
// Override or add rules here
rules: {},
},
];
+16
View File
@@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Backoffice</title>
<base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<link rel="stylesheet" href="/src/styles.css" />
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
+9
View File
@@ -0,0 +1,9 @@
import { join } from 'path';
export default {
plugins: {
'@tailwindcss/postcss': {
base: join(import.meta.dirname, '../../'),
},
},
};
+9
View File
@@ -0,0 +1,9 @@
{
"name": "backoffice",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "apps/backoffice/src",
"projectType": "application",
"tags": [],
"// targets": "to see all targets run: nx show project backoffice --web",
"targets": {}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

+14
View File
@@ -0,0 +1,14 @@
import { FC, ReactElement } from 'react';
import { Outlet } from 'react-router-dom';
import { Navbar } from '@imphnen-frontend-service/ui/organisms';
export const AppLayout: FC = (): ReactElement => {
return (
<main className="bg-primary-50 min-h-screen">
<Navbar />
<Outlet />
</main>
);
};
export default AppLayout;
+7
View File
@@ -0,0 +1,7 @@
import { FC, ReactElement } from 'react';
export const Components: FC = (): ReactElement => {
return <>Hallo</>;
};
export default Components;
+140
View File
@@ -0,0 +1,140 @@
@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';
@source "../../../libs/ui/**/*.{ts,tsx}";
@theme {
--color-primary-50: #f0f8ff;
--color-primary-100: #e1f0fd;
--color-primary-200: #bce1fb;
--color-primary-300: #81cbf8;
--color-primary-400: #3eb0f2;
--color-primary-500: #23a1eb;
--color-primary-600: #0877c1;
--color-primary-700: #085f9c;
--color-primary-800: #0b5181;
--color-primary-900: #0f446b;
--color-primary-950: #0a2b47;
--color-neutral-50: #f6f6f6;
--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-neutral-950: #2b2b2b;
--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: 3.833rem;
/* ~46px */
--text-h2: 3.083rem;
/* ~37px */
--text-h3: 2.417rem;
/* ~29px */
--text-p1: 1.917rem;
/* ~23px */
--text-p2: 1.583rem;
/* ~19px */
--text-p3: 1.25rem;
/* 15px */
--text-label1: 1rem;
/* 12px */
--text-label2: 0.833rem;
/* ~10px */
--text-label3: 0.677rem;
/* ~8px */
}
@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;
font-size: 12px;
line-height: 1.2;
}
}
+40
View File
@@ -0,0 +1,40 @@
import { createRoot } from 'react-dom/client';
import { middleware } from './middleware';
import { StrictMode } from 'react';
import { createBrowserRouter, RouteObject, RouterProvider } from 'react-router';
import {
add404PageToRoutesChildren,
addErrorElementToRoutes,
convertPagesToRoute,
QueryProvider,
} from '@imphnen-frontend-service/utils';
import './index.css';
const files = import.meta.glob('./app/**/*(page|layout).tsx');
const errorFiles = import.meta.glob('./app/**/*error.tsx');
const notFoundFiles = import.meta.glob('./app/**/*404.tsx');
const loadingFiles = import.meta.glob('./app/**/*loading.tsx');
const routes = convertPagesToRoute(files, loadingFiles) as RouteObject;
addErrorElementToRoutes(errorFiles, routes);
add404PageToRoutesChildren(notFoundFiles, routes);
const router = createBrowserRouter([
{
...routes,
loader: middleware,
shouldRevalidate: () => true,
},
]);
const rootElement = document.getElementById('root');
if (!rootElement) throw new Error('Failed to find the root element');
createRoot(rootElement).render(
<StrictMode>
<QueryProvider>
<RouterProvider router={router} />
</QueryProvider>
</StrictMode>
);
+11
View File
@@ -0,0 +1,11 @@
import { LoaderFunctionArgs } from 'react-router-dom';
export const middleware = async ({ request }: LoaderFunctionArgs) => {
const url = new URL(request.url);
const pathname = url.pathname;
if (pathname) return null;
return null;
};
+27
View File
@@ -0,0 +1,27 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"types": [
"node",
"@nx/react/typings/cssmodule.d.ts",
"@nx/react/typings/image.d.ts",
"vite/client"
]
},
"exclude": [
"src/**/*.spec.ts",
"src/**/*.test.ts",
"src/**/*.spec.tsx",
"src/**/*.test.tsx",
"src/**/*.spec.js",
"src/**/*.test.js",
"src/**/*.spec.jsx",
"src/**/*.test.jsx",
"vite.config.ts",
"vite.config.mts",
"vitest.config.ts",
"vitest.config.mts"
],
"include": ["src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.tsx"]
}
+21
View File
@@ -0,0 +1,21 @@
{
"compilerOptions": {
"jsx": "react-jsx",
"allowJs": false,
"esModuleInterop": false,
"allowSyntheticDefaultImports": true,
"strict": true,
"types": ["vite/client", "vitest"]
},
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.app.json"
},
{
"path": "./tsconfig.spec.json"
}
],
"extends": "../../tsconfig.base.json"
}
+30
View File
@@ -0,0 +1,30 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"types": [
"vitest/globals",
"vitest/importMeta",
"vite/client",
"node",
"vitest",
"@nx/react/typings/cssmodule.d.ts",
"@nx/react/typings/image.d.ts"
]
},
"include": [
"vite.config.ts",
"vite.config.mts",
"vitest.config.ts",
"vitest.config.mts",
"src/**/*.test.ts",
"src/**/*.spec.ts",
"src/**/*.test.tsx",
"src/**/*.spec.tsx",
"src/**/*.test.js",
"src/**/*.spec.js",
"src/**/*.test.jsx",
"src/**/*.spec.jsx",
"src/**/*.d.ts"
]
}
+42
View File
@@ -0,0 +1,42 @@
/// <reference types='vitest' />
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
import { nxCopyAssetsPlugin } from '@nx/vite/plugins/nx-copy-assets.plugin';
export default defineConfig(() => ({
root: __dirname,
cacheDir: '../../node_modules/.vite/apps/backoffice',
server: {
port: 4200,
host: 'localhost',
},
preview: {
port: 4300,
host: 'localhost',
},
plugins: [react(), nxViteTsPaths(), nxCopyAssetsPlugin(['*.md'])],
// Uncomment this if you are using workers.
// worker: {
// plugins: [ nxViteTsPaths() ],
// },
build: {
outDir: '../../dist/apps/backoffice',
emptyOutDir: true,
reportCompressedSize: true,
commonjsOptions: {
transformMixedEsModules: true,
},
},
test: {
watch: false,
globals: true,
environment: 'jsdom',
include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
reporters: ['default'],
coverage: {
reportsDirectory: '../../coverage/apps/backoffice',
provider: 'v8' as const,
},
},
}));
+2 -2
View File
@@ -6,8 +6,8 @@
<base href="/" /> <base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/x-icon" href="/favicon.ico" /> <link rel="icon" type="image/x-icon" href="/logos/simple.svg" />
<link rel="stylesheet" href="/src/styles.css" /> <link rel="stylesheet" href="/src/index.css" />
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
+5 -1
View File
@@ -1,5 +1,9 @@
import { join } from 'path';
export default { export default {
plugins: { plugins: {
'@tailwindcss/postcss': {}, '@tailwindcss/postcss': {
base: join(import.meta.dirname, '../../'),
},
}, },
}; };
+1
View File
@@ -1,5 +1,6 @@
@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 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'; @import 'tailwindcss';
@source "../../../libs/ui/**/*.{ts,tsx}";
@theme { @theme {
--color-primary-50: #f0f8ff; --color-primary-50: #f0f8ff;
+1 -1
View File
@@ -6,7 +6,7 @@
<base href="/" /> <base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/x-icon" href="/favicon.ico" /> <link rel="icon" type="image/x-icon" href="/logos/simple.svg" />
<link rel="stylesheet" href="/src/index.css" /> <link rel="stylesheet" href="/src/index.css" />
</head> </head>
<body> <body>
+5 -1
View File
@@ -1,5 +1,9 @@
import { join } from 'path';
export default { export default {
plugins: { plugins: {
'@tailwindcss/postcss': {}, '@tailwindcss/postcss': {
base: join(import.meta.dirname, '../../'),
},
}, },
}; };
Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 KiB

+3
View File
@@ -0,0 +1,3 @@
<svg width="47" height="62" viewBox="0 0 47 62" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.5 38.4999L9.19156 38.4166L8.5 38.4999ZM23.5 42.4999L24.0479 42.9301L23.5 42.4999ZM1.45141 0.804946C1.06764 0.831723 0.778242 1.16453 0.805019 1.5483L1.24135 7.80212C1.26813 8.18588 1.60094 8.47528 1.9847 8.4485C2.36847 8.42173 2.65787 8.08892 2.63109 7.70515L2.24324 2.1462L7.80219 1.75835C8.18595 1.73157 8.47535 1.39876 8.44858 1.015C8.4218 0.631233 8.08899 0.341836 7.70523 0.368611L1.45141 0.804946ZM44.8219 58.1179C44.6435 57.777 44.2226 57.6453 43.8818 57.8237C43.5409 58.002 43.40Line truncated
</svg>

After

Width:  |  Height:  |  Size: 4.8 KiB

+3
View File
@@ -0,0 +1,3 @@
<svg width="79" height="88" viewBox="0 0 79 88" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1.00004 87.8788C0.5147 87.8788 0.121257 87.4853 0.121261 87C0.121265 86.5147 0.514715 86.1212 1.00006 86.1212L1.00004 87.8788ZM43.4999 69.5L44.1213 70.1214L43.4999 69.5ZM74.1915 0.677276C74.6459 0.506859 75.1525 0.737104 75.3229 1.19154L78.1 8.59705C78.2704 9.05149 78.0401 9.55803 77.5857 9.72845C77.1313 9.89887 76.6247 9.66862 76.4543 9.21418L73.9858 2.6315L67.4031 5.10001C66.9487 5.27042 66.4421 5.04018 66.2717 4.58574C66.1013 4.1313 66.3316 3.62475 66.786 3.45434L74.1915 0.677276ZMLine truncated
</svg>

After

Width:  |  Height:  |  Size: 3.6 KiB

+9
View File
@@ -0,0 +1,9 @@
<svg width="317" height="170" viewBox="0 0 317 170" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<rect width="317" height="169.613" fill="url(#pattern0_164_317)"/>
<defs>
<pattern id="pattern0_164_317" patternContentUnits="objectBoundingBox" width="1" height="1">
<use xlink:href="#image0_164_317" transform="scale(0.00184502 0.00344828)"/>
</pattern>
<image id="image0_164_317" width="542" height="290" preserveAspectRatio="none" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAh4AAAEiCAYAAAC2k9sBAAAABGdBTUEAALGPC/xhBQAACklpQ0NQc1JHQiBJRUM2MTk2Ni0yLjEAAEiJnVN3WJP3Fj7f92UPVkLY8LGXbIEAIiOsCMgQWaIQkgBhhBASQMWFiApWFBURnEhVxILVCkidiOKgKLhnQYqIWotVXDjuH9yntX167+3t+9f7vOec5/zOec8PgBESJpHmomoAOVKFPDrYH49PSMTJvYACFUjgBCAQ5svCZwXFAADwA3l4fnSwP/wBr28AAgBw1S4kEsfh/4O6UCZXACCRAOAiEucLAZBSAMguVMgUAMgYALBTs2QKAJQAAGx5fEIiAKoNAOz0ST4FANipk9wXANiiHKLine truncated
</defs>
</svg>

After

Width:  |  Height:  |  Size: 351 KiB

+4 -4
View File
@@ -1,9 +1,9 @@
<svg width="138" height="52" viewBox="0 0 138 52" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> <svg width="138" height="52" viewBox="0 0 138 52" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<rect width="137.505" height="51.7538" fill="url(#pattern0_410_578)"/> <rect width="137.505" height="51.7538" fill="url(#pattern0_164_316)"/>
<defs> <defs>
<pattern id="pattern0_410_578" patternContentUnits="objectBoundingBox" width="1" height="1"> <pattern id="pattern0_164_316" patternContentUnits="objectBoundingBox" width="1" height="1">
<use xlink:href="#image0_410_578" transform="matrix(0.000440723 0 0 0.00117096 -0.173204 -1.3185)"/> <use xlink:href="#image0_164_316" transform="matrix(0.000440723 0 0 0.00117096 -0.173204 -1.3185)"/>
</pattern> </pattern>
<image id="image0_410_578" width="3000" height="3000" preserveAspectRatio="none" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAC7gAAAu4CAYAAABxFfV5AAAACXBIWXMAAC4jAAAuIwF4pT92AAX6AElEQVR4nOzaMQEAIAzAsIF/z0MGPRIFFdCzuwMAAAAAAAAAAAAAAL/d3wEAAAAAAAAAAAAAADBjcAcAAAAAAAAAAAAAIMLgDgAAAAAAAAAAAABAgsEdAAAAAAAAAAAAAIAEgzsAAAAAAAAAAAAAAAkGdwAAAAAAAAAAAAAAEgzuAAAAAAAAAAAAAAAkGNwBAAAAAAAAAAAAAEgwuAMAAAAAAAAAAAAAkGBwBwAAAAAAAAAAAAAgweAOAAAAAAAAAAAAAECCwR0AAAAAAAAAAAAAgASDOwAAAAAAAAAAAAAACQZ3AAAALine truncated <image id="image0_164_316" width="3000" height="3000" preserveAspectRatio="none" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAC7gAAAu4CAYAAABxFfV5AAAACXBIWXMAAC4jAAAuIwF4pT92AAX6AElEQVR4nOzaMQEAIAzAsIF/z0MGPRIFFdCzuwMAAAAAAAAAAAAAAL/d3wEAAAAAAAAAAAAAADBjcAcAAAAAAAAAAAAAIMLgDgAAAAAAAAAAAABAgsEdAAAAAAAAAAAAAIAEgzsAAAAAAAAAAAAAAAkGdwAAAAAAAAAAAAAAEgzuAAAAAAAAAAAAAAAkGNwBAAAAAAAAAAAAAEgwuAMAAAAAAAAAAAAAkGBwBwAAAAAAAAAAAAAgweAOAAAAAAAAAAAAAECCwR0AAAAAAAAAAAAAgASDOwAAAAAAAAAAAAAACQZ3AAAALine truncated
</defs> </defs>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 511 KiB

After

Width:  |  Height:  |  Size: 511 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

+3
View File
@@ -0,0 +1,3 @@
<svg width="283" height="222" viewBox="0 0 283 222" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M41.2124 71.9365L30.3686 94.0759L1 115.312L8.2292 133.385L15.0066 149.65L26.3022 157.783L30.3686 179.471L35.7905 221.491L73.7438 218.328L104.92 214.713L124.8 221.491L181.278 218.328L192.574 210.195L205.225 214.713C213.207 214.864 229.894 215.075 232.786 214.713C235.678 214.352 241.22 219.081 243.63 221.491H263.962L276.613 196.64L281.583 166.368L272.999 154.169L263.962 143.325L254.926 133.385L235.949 127.963V121.185C236.702 116.065 238.298 105.1 238.66 102.209C239.021 99.3171 245.437 90Line truncated
</svg>

After

Width:  |  Height:  |  Size: 1017 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="304" height="200" viewBox="0 0 304 200" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M21.5132 38.0564L0.729248 148.302V175.411C13.531 178.725 40.038 185.351 43.6526 185.351C47.2672 185.351 66.8463 187.761 76.184 188.966L92.9015 167.278H112.33H134.469C137.632 167.278 151.639 173.152 160.675 175.411C169.712 177.67 181.911 183.544 188.688 188.966C195.466 194.388 199.984 193.032 205.858 197.551C210.557 201.165 224.985 199.057 231.612 197.551H258.721L284.023 188.966C288.542 183.243 297.578 170.893 297.578 167.278V139.717V105.378L303 64.7141L291.704 38.0564L278.602 32.6345L2Line truncated
</svg>

After

Width:  |  Height:  |  Size: 941 B

+1 -1
View File
@@ -4,7 +4,7 @@ import { Navbar } from '@imphnen-frontend-service/ui/organisms';
export const AppLayout: FC = (): ReactElement => { export const AppLayout: FC = (): ReactElement => {
return ( return (
<main className="bg-primary-50 min-h-screen px-20 py-[60px]"> <main className="bg-primary-50 min-h-screen">
<Navbar /> <Navbar />
<Outlet /> <Outlet />
</main> </main>
+235 -2
View File
@@ -1,7 +1,240 @@
import { FC, ReactElement } from 'react'; import { ArrowDownOutlined } from '@ant-design/icons';
import { Button } from '@imphnen-frontend-service/ui/atoms';
import { ModalsGacha } from '@imphnen-frontend-service/ui/organisms';
import { cn } from '@imphnen-frontend-service/utils';
import { FC, Fragment, ReactElement, useEffect, useState } from 'react';
import { createPortal } from 'react-dom';
interface GachaItemProps {
src: string;
label: string;
className?: string;
}
export const Components: FC = (): ReactElement => { export const Components: FC = (): ReactElement => {
return <>Hallo</>; // Pinjem
const [showModal, setShowModal] = useState(false);
//
const scrollToRoulette = () => {
const rouletteSection = document.getElementById('roulette');
if (rouletteSection) {
rouletteSection.scrollIntoView({ behavior: 'smooth' });
}
};
useEffect(() => {
const gachaPlaySection = document.getElementById('gacha-play');
if (gachaPlaySection) {
let currentIndex = 0;
const items = gachaPlaySection.children;
const totalItems = items.length;
const scrollItems = () => {
if (currentIndex >= totalItems) {
currentIndex = 0;
}
items[currentIndex].scrollIntoView({
behavior: 'smooth',
inline: 'center',
});
currentIndex++;
};
const intervalId = setInterval(scrollItems, 2800);
return () => clearInterval(intervalId);
}
}, []);
const GachaItem: FC<GachaItemProps> = ({
src,
label,
className,
}): ReactElement => {
return (
<div className="snap-center flex-auto justify-center items-center flex flex-col min-w-full">
<div className="max-h-[180px] md:max-h-[270px] md:max-w-[500px] mb-2 md:mb-5 flex flex-1 justify-center items-center">
<img
src={src}
alt=""
className={cn('h-[150px] md:h-[255px] object-contain', className)}
/>
</div>
<p className="font-semibold text-center md:text-p2">{label}</p>
</div>
);
};
return (
<Fragment>
{/* Landing Page */}
<section
id="landing-page"
className="my-12 md:my-24 grid grid-cols-4 md:grid-cols-8 lg:grid-cols-12 justify-items-center max-w-[1280px] items-center justify-center mx-[32px] md:mx-[60px] lg:mx-[80px] xl:mx-auto"
>
<div className="order-none lg:order-1 flex flex-col col-span-4 md:col-span-8 lg:col-span-4 md:w-full lg:w-fit items-center gap-7 md:gap-13 text-primary-500 mb-10 md:mb-20">
<img
src="/logos/logo.svg"
alt="IMPHNEN Logo"
className="w-[170px] md:w-[317px]"
/>
<div className="font-semibold text-center">
<div className="text-p1 md:text-h2 lg:font-bold text-center">
<p>New Merchandise</p>
<p>Coming UP</p>
</div>
<div className="text-base md:text-p2 mt-3">
Periode Gacha: 1 - 31 Maret 2025
</div>
</div>
<div className="text-center font-semibold text-p3 md:text-p1">
<p>Let's Go Checkout Our Merch &</p>
<p>Gacha Your Prize Here</p>
</div>
<Button
size="sm"
variant="bordered"
className="flex gap-3 px-3 md:px-5 py-2 md:py-4 bg-primary-100 border border-primary-500 border-dashed rounded md:rounded-md animate-bounce text-label2 md:text-p2"
onClick={scrollToRoulette}
>
<ArrowDownOutlined />
<span>Scroll For Gacha</span>
<ArrowDownOutlined />
</Button>
</div>
<div className="order-1 lg:order-none flex flex-col col-span-4 justify-center items-center mb-8 relative">
<div className="relative h-[150px] md:h-[230px]">
<img
src="/merch/1.png"
alt="Merch 1"
className="relative top-[3px] md:top-[6px] left-[4px] md:left-[7px] z-10 w-[165px] md:w-[266px]"
/>
<img
className="absolute top-0 left-0 min-w-[174px] md:min-w-[280px] z-0"
src="/merch/Vector-1.svg"
alt=""
/>
<img
className="absolute hidden lg:block -bottom-[50px] -right-[50px]"
src="/landing-arrow-1.svg"
alt=""
/>
</div>
<div className="px-3 py-1 text-primary-500 font-semibold text-base md:text-p2 bg-white shadow-md rounded">
Official Merch IMPHNEN
</div>
<div className="mt-2 text-primary-500 font-bold text-p2 md:text-h3">
~ 175k ~
</div>
</div>
<div className="order-2 flex flex-col col-span-4 justify-center items-center mb-8 relative">
<div className="relative h-[130px] md:h-[230px]">
<img
src="/merch/2.png"
alt="Merch 2"
className="relative top-[5px] md:top-[8px] left-0 md:left-[5px] z-10 w-[165px] md:w-[277px]"
/>
<img
className="absolute top-0 -left-[5px] md:-left-[8px] z-0 min-w-[174px] md:min-w-[302px]"
src="/merch/Vector-2.svg"
alt=""
/>
<img
className="absolute hidden lg:block -bottom-[100px] -left-[55px]"
src="/landing-arrow-2.svg"
alt=""
/>
</div>
<div className="px-3 py-1 text-primary-500 font-semibold text-base md:text-p2 bg-white shadow-md rounded">
IMPHNEN Mini Merch
</div>
<div className="mt-2 text-primary-500 font-bold text-p2 md:text-h3">
~ 90k ~
</div>
</div>
</section>
{/* Roulette Page */}
<section
id="roulette"
className="mt-20 pb-50 lg:py-90 mx-[32px] md:mx-[60px] lg:mx-[80px] xl:mx-auto lg:max-w-[1280px] grid grid-cols-4 md:grid-cols-8 lg:grid-cols-12 justify-items-center gap-y-16 md:gap-y-28"
>
<div className="col-span-4 md:col-span-8 lg:col-span-6 text-primary-500 bg-white rounded-lg md:rounded-xl p-5 md:py-9 md:px-13 shadow md:max-w-[430px]">
<header className="mb-4">
<h3 className="font-semibold text-p3 md:text-h2">Gacha Roulette</h3>
</header>
<h4 className="font-semibold my-2 text-base md:text-p1">
How to participate
</h4>
<p className="text-base md:text-p2">
Kamu harus melakukan pembelian merch batch 2 di shopee IMPHNEN
</p>
<h4 className="font-semibold my-2 text-base md:text-p1">
How to gacha
</h4>
<ol className="text-base md:text-p2 list-decimal list-outside ml-4 md:ml-6">
<li>First, press the "Spin Now" button</li>
<li>
If the merch does not match you can reroll by paying IDR5,000
</li>
<li>If it is appropriate you can submit</li>
<li>Then fill in the data for your merch delivery process</li>
</ol>
</div>
<div
id="roulette-spin"
className="col-span-4 md:col-span-8 lg:col-span-6 flex flex-col items-center gap-4 md:gap-8 overflow-x-hidden"
>
<div className="bg-white text-primary-500 font-medium text-p3 md:text-h3 shadow py-2 px-4 md:py-4 md:px-8 max-w-fit rounded-md md:rounded-lg">
Here Take Your Prize
</div>
<section
id="gacha-play"
className="flex flex-nowrap overflow-auto w-full gap-x-8 snap-x snap-mandatory"
>
<GachaItem
src="/gacha/certificate.png"
label="Sertifikat + Laminating"
/>
<GachaItem
src="/gacha/lanyard-id-card.png"
label="Lanyard + ID Card"
/>
<GachaItem src="/gacha/pin.png" label="Pin" />
<GachaItem src="/gacha/sticker.png" label="Sticker Isi 3" />
<GachaItem src="/gacha/sticker.png" label="Sticker Isi 5" />
<GachaItem
src="/gacha/gelang-karet.png"
label="Gelang Karet"
className="h-[86px] md:h-[160px]"
/>
</section>
<Button variant="secondary" size="md">
Spin Now
</Button>
</div>
</section>
{/* Diffuser & Cloud */}
<div className="sticky bottom-0 h-[86px] md:h-[200px] bg-gradient-to-b from-primary-500/0 to-primary-500/50 to-80%"></div>
{/**Izin pake untuk debug modals gacha */}
<div>
<p>Debug : Test Modals Gacha</p>
<Button onClick={
() => setShowModal(true)
}>Tekan</Button>
</div>
{showModal && createPortal(
<ModalsGacha></ModalsGacha>,
document.body
)}
{/**Izin pake untuk debug modals gacha */}
</Fragment>
);
}; };
export default Components; export default Components;
+36 -16
View File
@@ -1,19 +1,21 @@
@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 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'; @import 'tailwindcss';
@source "../../../libs/ui/**/*.{ts,tsx}";
@theme { @theme {
--color-primary-50: #f0f8ff; --color-primary-50: #f0f8ff;
--color-primary-100: #e1f0fd; --color-primary-100: #e1f0fd;
--color-primary-200: #b8e1f8; --color-primary-200: #bce1fb;
--color-primary-300: #a1d4f0; --color-primary-300: #81cbf8;
--color-primary-400: #4aa4da; --color-primary-400: #3eb0f2;
--color-primary-500: #1a8ce6; --color-primary-500: #23a1eb;
--color-primary-600: #1673c7; --color-primary-600: #0877c1;
--color-primary-700: #0f5ca5; --color-primary-700: #085f9c;
--color-primary-800: #0a4780; --color-primary-800: #0b5181;
--color-primary-900: #04305d; --color-primary-900: #0f446b;
--color-primary-950: #0a2b47;
--color-neutral-50: #f7f7f7; --color-neutral-50: #f6f6f6;
--color-neutral-100: #e7e7e7; --color-neutral-100: #e7e7e7;
--color-neutral-200: #d0d0d0; --color-neutral-200: #d0d0d0;
--color-neutral-300: #b7b7b7; --color-neutral-300: #b7b7b7;
@@ -23,6 +25,7 @@
--color-neutral-700: #4a4a4a; --color-neutral-700: #4a4a4a;
--color-neutral-800: #3a3a3a; --color-neutral-800: #3a3a3a;
--color-neutral-900: #2f2f2f; --color-neutral-900: #2f2f2f;
--color-neutral-950: #2b2b2b;
--color-success-50: #e7f2ee; --color-success-50: #e7f2ee;
--color-success-100: #cde6dd; --color-success-100: #cde6dd;
@@ -79,16 +82,28 @@
--font-bai-jamjuree: 'Bai Jamjuree', sans-serif; --font-bai-jamjuree: 'Bai Jamjuree', sans-serif;
--text-h1: 3rem; /* ~48px */ --text-h1: 3.833rem;
--text-h2: 2.4rem; /* ~38.4px */ /* ~46px */
--text-h3: 1.92rem; /* ~30.72px */ --text-h2: 3.083rem;
--text-p1: 1.54rem; /* ~24.58px */ /* ~37px */
--text-p2: 1.23rem; /* ~19.68px */ --text-h3: 2.417rem;
--text-label1: 0.98rem; /* ~15.74px */ /* ~29px */
--text-label2: 0.78rem; /* ~12.59px */ --text-p1: 1.917rem;
/* ~23px */
--text-p2: 1.583rem;
/* ~19px */
--text-p3: 1.25rem;
/* 15px */
--text-label1: 1rem;
/* 12px */
--text-label2: 0.833rem;
/* ~10px */
--text-label3: 0.677rem;
/* ~8px */
} }
@layer base { @layer base {
*, *,
::after, ::after,
::before, ::before,
@@ -102,13 +117,16 @@
width: 6px; width: 6px;
height: 6px; height: 6px;
} }
::-webkit-scrollbar-track { ::-webkit-scrollbar-track {
background: var(--color-neutral-50); background: var(--color-neutral-50);
} }
::-webkit-scrollbar-thumb { ::-webkit-scrollbar-thumb {
background: #cccccc; background: #cccccc;
border-radius: 3px; border-radius: 3px;
} }
::-webkit-scrollbar-thumb:hover { ::-webkit-scrollbar-thumb:hover {
background: var(--color-neutral-300); background: var(--color-neutral-300);
} }
@@ -116,5 +134,7 @@
html { html {
font-family: 'Bai Jamjuree', sans-serif; font-family: 'Bai Jamjuree', sans-serif;
font-weight: 400; font-weight: 400;
font-size: 12px;
line-height: 1.2;
} }
} }
+20
View File
@@ -0,0 +1,20 @@
import type { StorybookConfig } from '@storybook/react-vite';
const config: StorybookConfig = {
stories: ['../src/**/*.@(mdx|stories.@(js|jsx|ts|tsx))'],
addons: ['@storybook/addon-essentials', '@storybook/addon-interactions'],
framework: {
name: '@storybook/react-vite',
options: {
builder: {
viteConfigPath: 'vite.config.ts',
},
},
},
};
export default config;
// To customize your Vite configuration you can use the viteFinal field.
// Check https://storybook.js.org/docs/react/builders/vite#configuration
// and https://nx.dev/recipes/storybook/custom-builder-configs
+15
View File
@@ -0,0 +1,15 @@
import type { Preview } from '@storybook/react';
import '../src/index.css';
const preview: Preview = {
parameters: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
},
};
export default preview;
-5
View File
@@ -3,11 +3,6 @@
"version": "0.0.1", "version": "0.0.1",
"main": "./index.js", "main": "./index.js",
"types": "./index.d.ts", "types": "./index.d.ts",
"devDependencies": {
"tailwindcss": "^4.0.13",
"postcss": "^8.5.3",
"@tailwindcss/postcss": "^4.0.13"
},
"exports": { "exports": {
".": { ".": {
"import": "./index.mjs", "import": "./index.mjs",
+2
View File
@@ -1,3 +1,5 @@
import { join } from 'path';
export default { export default {
plugins: { plugins: {
'@tailwindcss/postcss': {}, '@tailwindcss/postcss': {},
+104
View File
@@ -0,0 +1,104 @@
import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './button';
const meta: Meta<typeof Button> = {
title: 'Components/Button',
component: Button,
argTypes: {
variant: {
control: 'select',
options: ['primary', 'secondary', 'success', 'danger'],
},
size: {
control: 'select',
options: ['sm', 'md', 'lg'],
},
disabled: {
control: 'boolean',
},
},
};
export default meta;
type Story = StoryObj<typeof Button>;
export const Primary: Story = {
args: {
variant: 'primary',
size: 'md',
children: 'Primary Button',
},
};
export const Secondary: Story = {
args: {
variant: 'secondary',
size: 'md',
children: 'Secondary Button',
},
};
export const Text: Story = {
args: {
variant: 'text',
size: 'md',
children: 'Text Button',
},
};
export const Bordered: Story = {
args: {
variant: 'bordered',
size: 'md',
children: 'Bordered Button',
},
};
export const Success: Story = {
args: {
variant: 'success',
size: 'md',
children: 'Success Button',
},
};
export const Danger: Story = {
args: {
variant: 'danger',
size: 'md',
children: 'Danger Button',
},
};
export const Disabled: Story = {
args: {
variant: 'primary',
size: 'md',
disabled: true,
children: 'Disabled Button',
},
};
export const Large: Story = {
args: {
variant: 'primary',
size: 'lg',
children: 'Large Button',
},
};
export const Medium: Story = {
args: {
variant: 'primary',
size: 'md',
children: 'Medium Button',
},
};
export const Small: Story = {
args: {
variant: 'primary',
size: 'sm',
children: 'Small Button',
},
};
+3 -1
View File
@@ -1,2 +1,4 @@
import '../index.css';
export * from './button'; export * from './button';
export * from './input';
export * from './password-input';
export * from './textarea';
+1
View File
@@ -0,0 +1 @@
export * from "./input";
+36
View File
@@ -0,0 +1,36 @@
import { describe, it, expect } from "vitest";
import { render, screen } from "@testing-library/react";
import { Input } from "./input";
describe("Test Input Component", () => {
it("renders the input with placeholder text", () => {
render(<Input placeholder="Placeholder" />);
expect(screen.getByPlaceholderText("Placeholder")).toBeInTheDocument();
});
// it("renders the input with different sizes", () => {
// const sizeClasses = {
// sm: "text-[10px] max-h-[28px]",
// md: "text-[12px] max-h-[30px]",
// lg: "text-[15px] max-h-[34px]",
// };
// Object.entries(sizeClasses).forEach(([size, className]) => {
// render(<Input size={size} />);
// expect(screen.getByRole("textbox")).toHaveClass(className);
// });
// });
// it("renders the input with different types", () => {
// const types = ["text", "password", "email", "number"];
// types.forEach((type) => {
// render(<Input type={type} />);
// const input = screen.getByRole("textbox");
// expect(input).toHaveAttribute("type", type);
// });
// });
it("disables the input field when 'disabled' prop is set", () => {
render(<Input disabled />);
expect(screen.getByRole("textbox")).toBeDisabled();
});
});
+71
View File
@@ -0,0 +1,71 @@
import { Meta, StoryObj } from '@storybook/react';
import { Input } from './input';
const meta: Meta<typeof Input> = {
title: 'Components/Input',
component: Input,
argTypes: {
type: {
control: 'select',
options: ['text', 'email'],
},
size: {
control: 'select',
options: ['sm', 'md', 'lg'],
},
},
};
export default meta;
type Story = StoryObj<typeof Input>;
export const Large: Story = {
args: {
size: 'lg',
placeholder: 'Placeholder',
},
};
export const Medium: Story = {
args: {
size: 'md',
placeholder: 'Placeholder',
},
};
export const Small: Story = {
args: {
size: 'sm',
placeholder: 'Placeholder',
},
};
export const TextInput: Story = {
args: {
size: 'md',
type: 'text',
},
};
export const EmailInput: Story = {
args: {
size: 'md',
type: 'email',
},
};
export const Disabled: Story = {
args: {
size: 'md',
type: 'text',
disabled: true,
},
};
export const WithError: Story = {
args: {
size: 'md',
type: 'text',
error: 'This field is required',
},
};
+60
View File
@@ -0,0 +1,60 @@
import {
DetailedHTMLProps,
FC,
InputHTMLAttributes,
ReactElement,
} from 'react';
import { cn } from '@imphnen-frontend-service/utils';
type TInputType = 'text' | 'email';
type TInputSize = 'sm' | 'md' | 'lg';
type TInputProps = Omit<
DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>,
'size' | 'type'
> & {
type?: TInputType;
size?: TInputSize;
error?: string;
};
const sizeClasses: Record<TInputSize, string> = {
sm: 'text-[10px] max-h-[28px]',
md: 'text-[12px] max-h-[30px]',
lg: 'text-[15px] max-h-[34px]',
};
const disabledClass = 'opacity-50 hover:border-neutral-200 cursor-not-allowed';
const errorClass =
'border-danger-500 hover:border-danger-500 focus:outline-danger-500';
export const Input: FC<TInputProps> = ({
size = 'md',
type,
placeholder = 'Placeholder',
disabled,
error,
className,
...rest
}): ReactElement => {
const mergedClassName = cn(
'px-[12px] py-[8px] text-neutral-800 placeholder:text-neutral-300 border border-neutral-200 hover:border-blue-300 focus:outline-1 focus:outline-blue-500 rounded-md font-bai-jamjuree',
sizeClasses[size],
disabled && disabledClass,
error && errorClass,
className
);
return (
<>
<input
className={mergedClassName}
type={type}
disabled={disabled}
placeholder={placeholder}
{...rest}
/>
{error && <p className="text-danger-500 text-xs mt-1">{error}</p>}
</>
);
};
@@ -0,0 +1 @@
export * from "./password-input";
@@ -0,0 +1,58 @@
import { Meta, StoryObj } from '@storybook/react';
import { PasswordInput } from './password-input';
const meta: Meta<typeof PasswordInput> = {
title: 'Components/Password Input',
component: PasswordInput,
argTypes: {
size: {
control: 'select',
options: ['sm', 'md', 'lg'],
},
},
};
export default meta;
type Story = StoryObj<typeof PasswordInput>;
export const Large: Story = {
args: {
size: 'lg',
placeholder: 'Placeholder',
},
};
export const Medium: Story = {
args: {
size: 'md',
placeholder: 'Placeholder',
},
};
export const Small: Story = {
args: {
size: 'sm',
placeholder: 'Placeholder',
},
};
export const Input: Story = {
args: {
size: 'md',
placeholder: 'Placeholder',
},
};
export const Disabled: Story = {
args: {
size: 'md',
disabled: true,
},
};
export const WithError: Story = {
args: {
size: 'md',
error: 'This field is required',
},
};
@@ -0,0 +1,111 @@
import {
DetailedHTMLProps,
FC,
InputHTMLAttributes,
ReactElement,
useState,
} from 'react';
import { cn } from '@imphnen-frontend-service/utils';
import { Button } from '@imphnen-frontend-service/ui/atoms';
import { EyeInvisibleOutlined, EyeOutlined } from '@ant-design/icons';
type TPasswordInputType = 'password';
type TPasswordInputSize = 'sm' | 'md' | 'lg';
type TPasswordInputProps = Omit<
DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>,
'size' | 'type'
> & {
type?: TPasswordInputType;
size?: TPasswordInputSize;
error?: string;
};
const sizeClasses: Record<TPasswordInputSize, string> = {
sm: 'text-[10px] max-h-[28px]',
md: 'text-[12px] max-h-[30px]',
lg: 'text-[15px] max-h-[34px]',
};
const iconSizeClasses: Record<TPasswordInputSize, string> = {
sm: 'text-[10px]',
md: 'text-[12px]',
lg: 'text-[16px]',
};
const disabledClass = 'opacity-50 hover:border-neutral-200 cursor-not-allowed';
const errorClass =
'border-danger-500 hover:border-danger-500 focus:outline-danger-500';
export const PasswordInput: FC<TPasswordInputProps> = ({
size = 'md',
placeholder = 'Placeholder',
disabled,
error,
className,
...rest
}): ReactElement => {
const [showPassword, setShowPassword] = useState(false);
const togglePasswordVisibility = () => {
if (!disabled) setShowPassword((prev) => !prev);
};
const mergedClassName = cn(
'px-[12px] py-[8px] text-neutral-800 placeholder:text-neutral-300 border border-neutral-200 hover:border-blue-300 focus:outline-1 focus:outline-blue-500 rounded-md font-bai-jamjuree',
sizeClasses[size],
disabled && disabledClass,
error && errorClass,
className
);
return (
<>
<div
className={cn(
'relative inline-flex items-center border border-neutral-200 rounded-md',
mergedClassName
)}
>
<input
className="focus:outline-0"
type={showPassword ? 'text' : 'password'}
disabled={disabled}
placeholder={placeholder}
{...rest}
/>
<div className="absolute end-0 px-[12px] h-full flex items-center">
<Button
variant="text"
size={size}
onClick={togglePasswordVisibility}
className={cn(
'relative aspect-square -me-[10px] p-[6px]',
iconSizeClasses[size],
disabled && 'cursor-not-allowed'
)}
>
{showPassword ? (
<EyeInvisibleOutlined
style={{
color: error
? 'var(--color-danger-500)'
: 'var(--color-neutral-500)',
}}
/>
) : (
<EyeOutlined
style={{
color: error
? 'var(--color-danger-500)'
: 'var(--color-neutral-500)',
}}
/>
)}
</Button>
</div>
</div>
{error && <p className="text-danger-500 text-xs mt-1">{error}</p>}
</>
);
};
+1
View File
@@ -0,0 +1 @@
export * from "./textarea";
Loaded 50 of 70 files, more files were not shown because too many files have changed in this diff. Show more