Compare commits
80 Commits
82dfd7fa56
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| fc5ad996c0 | |||
| 59de951edd | |||
| 99c3efe678 | |||
| e0f89df21c | |||
| ffd821dbd6 | |||
| b821af9747 | |||
| 4f9f2cd854 | |||
| 495162ff43 | |||
| c16f4f773a | |||
| 3fff4b7b20 | |||
| 4265e84764 | |||
| 9080e1193d | |||
| 3b90549202 | |||
| 05af7f19f5 | |||
| 0462ed1ef5 | |||
| 32dabcff9c | |||
| 82eca6ee1a | |||
| 7a335f9415 | |||
| 5595bc7371 | |||
| a4c4084f9b | |||
| 44a2a437df | |||
| 74b5b615ac | |||
| 282df2bc91 | |||
| 0ce198aa5e | |||
| b051b2778f | |||
| 22d30a2469 | |||
| 43a3d75f43 | |||
| 24a5abb1f1 | |||
| f41b14349c | |||
| 501d562025 | |||
| 6d7d63a891 | |||
| 820e0b5216 | |||
| 61949e7149 | |||
| e9657a4003 | |||
| 38b6e9fca6 | |||
| 35cd174c8b | |||
| 5aee0eccb2 | |||
| 9a5dfdb2ed | |||
| 794ad2913f | |||
| 6c509a6b54 | |||
| fb816fca11 | |||
| 82064af0db | |||
| 5b13ec8d8d | |||
| 288cde5dcf | |||
| d18c29f391 | |||
| 1a77d471ad | |||
| d270d9435b | |||
| 104d62bc13 | |||
| 86ca32bc01 | |||
| 793e980c93 | |||
| a987a83280 | |||
| da4dea7f5d | |||
| c9082a9f58 | |||
| e81dd69f19 | |||
| b94f5f44cb | |||
| 0dbe26fd4e | |||
| b3d2f56797 | |||
| 4c60e2d773 | |||
| ef3766bc2a | |||
| 3d21d078cf | |||
| 184abb25b4 | |||
| 55e8f13e32 | |||
| 2a5b894a1b | |||
| 092bbec6ac | |||
| 5a0e77ebb8 | |||
| cdf3323d77 | |||
| 2a3193a3fa | |||
| 8c4a9cc85f | |||
| faf5c56219 | |||
| e403413965 | |||
| 9d04076921 | |||
| 8306543828 | |||
| de91f8129e | |||
| f38ae2c288 | |||
| 9aa61dce27 | |||
| 395eb3de47 | |||
| a98e1ac5b0 | |||
| 0ebf8e1c65 | |||
| 23b2c6f534 | |||
| c8d2415e50 |
@@ -1,6 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2023 TailAdmin
|
Copyright (c) 2026 Pregnant guide
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|||||||
@@ -0,0 +1,254 @@
|
|||||||
|
# FrontEndUser Technical README
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
Tài liệu này mô tả phần **FrontEndUser** như một runtime web độc lập: biên dịch bằng Next.js, triển khai bằng Docker, phụ thuộc vào API backend để xử lý dữ liệu lịch sử, xác thực, media và proxy bản đồ. Nội dung ở đây không mô tả giá trị sản phẩm; nó mô tả ràng buộc hệ thống, quyết định kỹ thuật, điểm kiểm soát vận hành và số đo hiện có trong repository.
|
||||||
|
|
||||||
|
## Baseline Thực Nghiệm
|
||||||
|
|
||||||
|
Các số liệu dưới đây được đo trực tiếp tại working tree hiện tại.
|
||||||
|
|
||||||
|
```text
|
||||||
|
package name fe_admin_history_web
|
||||||
|
package version 2.2.3
|
||||||
|
runtime image node:24-alpine
|
||||||
|
production server port 3000
|
||||||
|
docker-compose host port 3014
|
||||||
|
dependencies 34
|
||||||
|
devDependencies 12
|
||||||
|
TypeScript/TSX files in src 232
|
||||||
|
Next app route files 20
|
||||||
|
TypeScript/TSX files in uhm 140
|
||||||
|
src size 3.4M
|
||||||
|
public size 43M
|
||||||
|
node_modules size 759M
|
||||||
|
.next size 941M
|
||||||
|
.next/standalone size 58M
|
||||||
|
.next/static size 5.3M
|
||||||
|
```
|
||||||
|
|
||||||
|
Diễn giải vận hành:
|
||||||
|
|
||||||
|
* Kích thước `node_modules` và `.next` không được dùng làm runtime artifact. Runtime Docker chỉ cần standalone server, static assets và `public`.
|
||||||
|
* `.next/standalone` đang ở mức 58M, thấp hơn rất nhiều so với full build directory 941M. Đây là lý do bắt buộc giữ `output: "standalone"` trong `next.config.ts`.
|
||||||
|
* 140/232 file TS/TSX nằm trong `src/uhm`; rủi ro thay đổi tập trung ở module bản đồ, editor, wiki, replay và geospatial client logic.
|
||||||
|
* `npm ls --depth=0` hiện báo một package extraneous: `@emnapi/runtime@1.9.1`. Đây không phải lỗi build mặc định, nhưng là tín hiệu cần dọn dependency tree trước khi khóa môi trường CI nghiêm ngặt.
|
||||||
|
|
||||||
|
## Runtime Boundary
|
||||||
|
|
||||||
|
FrontEndUser không sở hữu dữ liệu lõi. Nó là boundary hiển thị và tương tác.
|
||||||
|
|
||||||
|
Các trách nhiệm nằm trong frontend:
|
||||||
|
|
||||||
|
* render route bằng Next.js App Router;
|
||||||
|
* giữ state tương tác của bản đồ/editor/wiki;
|
||||||
|
* gọi API backend qua Axios/fetch;
|
||||||
|
* xử lý token phía client khi backend trả token trong payload;
|
||||||
|
* gửi cookie theo `withCredentials: true`;
|
||||||
|
* rewrite request Goong qua backend proxy;
|
||||||
|
* render MapLibre layer/source sau khi nhận manifest/style đã sanitize từ backend.
|
||||||
|
|
||||||
|
Các trách nhiệm không được đặt vào frontend:
|
||||||
|
|
||||||
|
* giữ Goong API key thật;
|
||||||
|
* quyết định quyền truy cập dữ liệu;
|
||||||
|
* xử lý refresh token như một nguồn tin cậy;
|
||||||
|
* proxy trực tiếp tới upstream map provider từ browser;
|
||||||
|
* query geospatial nặng;
|
||||||
|
* normalize dữ liệu lịch sử ở quy mô database.
|
||||||
|
|
||||||
|
## Stack
|
||||||
|
|
||||||
|
Stack chính theo `package.json` và lockfile hiện tại:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Next.js 16.x
|
||||||
|
React 19.x
|
||||||
|
TypeScript 5.9.x
|
||||||
|
Tailwind CSS 4.x
|
||||||
|
MapLibre GL 5.x
|
||||||
|
Axios 1.x
|
||||||
|
Redux Toolkit 2.x
|
||||||
|
Zustand 5.x
|
||||||
|
```
|
||||||
|
|
||||||
|
Các dependency nặng đã được tách chunk trong `next.config.ts`:
|
||||||
|
|
||||||
|
* `maplibre-gl` -> chunk `maplibre`
|
||||||
|
* `react-quill-new`, `quill`, `quill-blot-formatter` -> chunk `quill`
|
||||||
|
* `apexcharts`, `react-apexcharts` -> chunk `charts`
|
||||||
|
* `@fullcalendar/*` -> chunk `calendar`
|
||||||
|
|
||||||
|
Ràng buộc ở đây là rõ: bản đồ, rich text editor, chart và calendar không được kéo vào cùng một client bundle mặc định nếu route không cần chúng.
|
||||||
|
|
||||||
|
## Configuration Contract
|
||||||
|
|
||||||
|
Frontend đọc cấu hình từ environment tại thời điểm build.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
NEXT_PUBLIC_API_URL_ROOT="https://api.uhm.io.vn"
|
||||||
|
NEXT_PUBLIC_URL_MEDIA="https://cdn.uhm.io.vn/history-app/"
|
||||||
|
NEXT_PUBLIC_HOME_URL="http://localhost:3000"
|
||||||
|
|
||||||
|
BACKGROUND_MAP_API_KEY=
|
||||||
|
SEARCH_MAP_API_KEY=
|
||||||
|
```
|
||||||
|
|
||||||
|
Biến đang được code client đọc trực tiếp:
|
||||||
|
|
||||||
|
* `NEXT_PUBLIC_API_URL_ROOT`
|
||||||
|
* `NEXT_PUBLIC_URL_MEDIA`
|
||||||
|
* `NEXT_PUBLIC_HOME_URL`
|
||||||
|
|
||||||
|
Quy tắc triển khai: mọi biến `NEXT_PUBLIC_*` phải đúng trước `npm run build` hoặc trước `docker build`. Đổi biến ở runtime container không đảm bảo đổi behavior phía browser vì Next.js đã inline các giá trị public vào client bundle.
|
||||||
|
|
||||||
|
## Backend Dependency Contract
|
||||||
|
|
||||||
|
`NEXT_PUBLIC_API_URL_ROOT` là contract trung tâm. Từ biến này, frontend tạo các endpoint:
|
||||||
|
|
||||||
|
```text
|
||||||
|
<API_ROOT>/users/current
|
||||||
|
<API_ROOT>/auth/signin
|
||||||
|
<API_ROOT>/auth/refresh
|
||||||
|
<API_ROOT>/projects
|
||||||
|
<API_ROOT>/submissions
|
||||||
|
<API_ROOT>/geometries
|
||||||
|
<API_ROOT>/entities
|
||||||
|
<API_ROOT>/wikis
|
||||||
|
<API_ROOT>/battle-replays
|
||||||
|
```
|
||||||
|
|
||||||
|
Map proxy contract:
|
||||||
|
|
||||||
|
```text
|
||||||
|
<API_ROOT>/map/proxy/tiles.goong.io/...
|
||||||
|
<API_ROOT>/api/proxy/rsapi.goong.io/...
|
||||||
|
```
|
||||||
|
|
||||||
|
Backend phải thỏa các điều kiện sau:
|
||||||
|
|
||||||
|
* CORS cho phép frontend origin production.
|
||||||
|
* Cookie policy tương thích cross-origin nếu deploy khác domain.
|
||||||
|
* `/auth/refresh` hoạt động với cookie httpOnly hoặc trả access token mới theo payload mà frontend hiểu.
|
||||||
|
* Proxy Goong không trả JSON chứa `api_key`.
|
||||||
|
* Proxy Goong giữ shape đủ tương thích để frontend tiếp tục rewrite nested resource URLs.
|
||||||
|
|
||||||
|
## Map Constraint
|
||||||
|
|
||||||
|
MapLibre không được gọi trực tiếp Goong bằng URL chứa key. Flow hiện tại:
|
||||||
|
|
||||||
|
1. Frontend gọi style/source/font/tile qua backend proxy.
|
||||||
|
2. Backend gọi upstream Goong bằng server-side key.
|
||||||
|
3. Backend sanitize URL lồng bên trong response.
|
||||||
|
4. Frontend rewrite URL sạch qua `buildGoongProxyUrl`.
|
||||||
|
5. MapLibre chỉ thấy proxy URL.
|
||||||
|
|
||||||
|
Constraint quan trọng: backend không nên rewrite sẵn mọi nested URL thành `/proxy/...` nếu frontend vẫn gọi `buildGoongProxyUrl`; làm vậy có rủi ro double-proxy. Contract hiện tại yêu cầu backend trả upstream URL sạch hoặc relative URL, không trả URL đã chứa key.
|
||||||
|
|
||||||
|
## Auth Constraint
|
||||||
|
|
||||||
|
Axios instance dùng `withCredentials: true` và request interceptor gắn Bearer token nếu có token trong client storage. Response interceptor xử lý hai loại hết hạn token:
|
||||||
|
|
||||||
|
* HTTP `401`;
|
||||||
|
* response `200` nhưng body có `status: false` và message cho thấy token hết hạn/không hợp lệ.
|
||||||
|
|
||||||
|
Khi refresh thất bại với `401` hoặc `404`, frontend xóa token local và redirect về `/signin`.
|
||||||
|
|
||||||
|
Ràng buộc vận hành:
|
||||||
|
|
||||||
|
* backend không được trả message hết hạn token mơ hồ nếu muốn frontend tự refresh;
|
||||||
|
* refresh endpoint không được tạo vòng lặp interceptor;
|
||||||
|
* cookie refresh phải có domain, path, SameSite và Secure tương thích domain deploy.
|
||||||
|
|
||||||
|
## Build And Deployment
|
||||||
|
|
||||||
|
Development:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm ci
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Dev server khác port:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev -- --port 3005
|
||||||
|
```
|
||||||
|
|
||||||
|
Quality gates cục bộ:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run lint
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
Production bằng Docker Compose:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
Port mapping:
|
||||||
|
|
||||||
|
```text
|
||||||
|
host:3014 -> container:3000
|
||||||
|
```
|
||||||
|
|
||||||
|
Dockerfile hiện có ba lớp logic:
|
||||||
|
|
||||||
|
1. `deps`: cài dependency bằng `npm ci`.
|
||||||
|
2. `builder`: build Next.js standalone bằng `npm run build`.
|
||||||
|
3. `runner`: chạy `node server.js` từ `.next/standalone`.
|
||||||
|
|
||||||
|
Do `Dockerfile` copy `.env` vào build stage, file `.env` production phải được kiểm soát trước khi build image. Không dùng `.env.local` như nguồn cấu hình production trừ khi Dockerfile được đổi có chủ đích.
|
||||||
|
|
||||||
|
## Operational Checks
|
||||||
|
|
||||||
|
Sau khi deploy, kiểm tra theo thứ tự phụ thuộc:
|
||||||
|
|
||||||
|
1. `GET /` trả HTML và load client bundle không lỗi.
|
||||||
|
2. Browser không request trực tiếp `tiles.goong.io` hoặc `rsapi.goong.io`.
|
||||||
|
3. Map request đi qua `<API_ROOT>/map/proxy/...`.
|
||||||
|
4. Place search/reverse geocode request đi qua `<API_ROOT>/api/proxy/...`.
|
||||||
|
5. `/auth/signin` ghi được cookie hoặc trả token mà client lưu được.
|
||||||
|
6. `/auth/refresh` trả token mới khi access token hết hạn.
|
||||||
|
7. `/users/current` hoạt động sau refresh.
|
||||||
|
8. `/wiki/[slug]`, `/editor`, `/user/projects` không crash khi reload trực tiếp.
|
||||||
|
9. Media URL dưới `NEXT_PUBLIC_URL_MEDIA` trả được ảnh/video cần render.
|
||||||
|
|
||||||
|
## Failure Modes
|
||||||
|
|
||||||
|
Các lỗi có xác suất cao nhất khi triển khai:
|
||||||
|
|
||||||
|
* Build image bằng sai `NEXT_PUBLIC_API_URL_ROOT`: frontend deploy thành công nhưng mọi API call trỏ sai host.
|
||||||
|
* Backend CORS đúng cho API thường nhưng sai cho credentialed request: sign in được một phần, refresh/session hỏng.
|
||||||
|
* Goong proxy trả URL còn `api_key`: lộ key qua DevTools và cache layer.
|
||||||
|
* Goong proxy rewrite nested URL quá sớm: MapLibre nhận URL double-proxy và tile/font hỏng.
|
||||||
|
* Thêm dependency bản đồ/editor/chart vào shared component: tăng client bundle của route không liên quan.
|
||||||
|
* Deploy sau khi `npm install` tạo dependency tree khác lockfile: `npm ci` trong Docker/CI có thể fail hoặc build khác local.
|
||||||
|
|
||||||
|
## Change Control
|
||||||
|
|
||||||
|
Các thay đổi cần được xem như thay đổi kiến trúc, không phải chỉnh UI đơn thuần:
|
||||||
|
|
||||||
|
* đổi `NEXT_PUBLIC_API_URL_ROOT` semantics;
|
||||||
|
* đổi đường dẫn `/map/proxy` hoặc `/api/proxy`;
|
||||||
|
* đổi cơ chế refresh token;
|
||||||
|
* đưa Goong key xuống browser;
|
||||||
|
* bỏ `output: "standalone"`;
|
||||||
|
* thay MapLibre style loading từ incremental source/layer sang `map.setStyle(goongStyleJson)` trực tiếp;
|
||||||
|
* đưa `maplibre-gl`, Quill, chart hoặc calendar vào layout dùng chung.
|
||||||
|
|
||||||
|
## Measurement Commands
|
||||||
|
|
||||||
|
Dùng các lệnh này để cập nhật baseline khi cần:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
du -sh .next node_modules public src
|
||||||
|
du -sh .next/standalone .next/static
|
||||||
|
find src -type f \( -name '*.ts' -o -name '*.tsx' \) | wc -l
|
||||||
|
find src/app -type f \( -name 'page.tsx' -o -name 'layout.tsx' \) | wc -l
|
||||||
|
find src/uhm -type f \( -name '*.ts' -o -name '*.tsx' \) | wc -l
|
||||||
|
node -e "const p=require('./package.json'); console.log(Object.keys(p.dependencies||{}).length, Object.keys(p.devDependencies||{}).length)"
|
||||||
|
npm ls --depth=0
|
||||||
|
```
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# Third-party notices
|
||||||
|
|
||||||
|
This project includes portions of TailAdmin.
|
||||||
|
|
||||||
|
TailAdmin
|
||||||
|
Copyright (c) 2023 TailAdmin
|
||||||
|
Licensed under the MIT License.
|
||||||
|
Website: https://tailadmin.com
|
||||||
|
|
||||||
|
## TailAdmin MIT License
|
||||||
|
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2023 TailAdmin
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -66,5 +66,6 @@ export const API = {
|
|||||||
},
|
},
|
||||||
Chatbot:{
|
Chatbot:{
|
||||||
CHAT: `${API_URL_ROOT}/chatbot/chat`,
|
CHAT: `${API_URL_ROOT}/chatbot/chat`,
|
||||||
|
HISTORY: `${API_URL_ROOT}/chatbot/history`,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,11 +27,43 @@ const nextConfig: NextConfig = {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
output: 'standalone',
|
output: 'standalone',
|
||||||
webpack(config) {
|
webpack(config, { isServer }) {
|
||||||
config.module.rules.push({
|
config.module.rules.push({
|
||||||
test: /\.svg$/,
|
test: /\.svg$/,
|
||||||
use: ["@svgr/webpack"],
|
use: ["@svgr/webpack"],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!isServer) {
|
||||||
|
// Split heavy third-party vendor libraries into their own dedicated chunks
|
||||||
|
config.optimization.splitChunks.cacheGroups = {
|
||||||
|
...config.optimization.splitChunks.cacheGroups,
|
||||||
|
maplibre: {
|
||||||
|
test: /[\\/]node_modules[\\/]maplibre-gl[\\/]/,
|
||||||
|
name: "maplibre",
|
||||||
|
chunks: "all",
|
||||||
|
priority: 40,
|
||||||
|
},
|
||||||
|
quill: {
|
||||||
|
test: /[\\/]node_modules[\\/](react-quill-new|quill|quill-blot-formatter)[\\/]/,
|
||||||
|
name: "quill",
|
||||||
|
chunks: "all",
|
||||||
|
priority: 35,
|
||||||
|
},
|
||||||
|
charts: {
|
||||||
|
test: /[\\/]node_modules[\\/](apexcharts|react-apexcharts)[\\/]/,
|
||||||
|
name: "charts",
|
||||||
|
chunks: "all",
|
||||||
|
priority: 30,
|
||||||
|
},
|
||||||
|
calendar: {
|
||||||
|
test: /[\\/]node_modules[\\/]@fullcalendar[\\/]/,
|
||||||
|
name: "calendar",
|
||||||
|
chunks: "all",
|
||||||
|
priority: 25,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return config;
|
return config;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "fe_admin_history_web",
|
"name": "ultra_history_map",
|
||||||
"version": "2.2.3",
|
"version": "2.2.3",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "fe_admin_history_web",
|
"name": "ultra_history_map",
|
||||||
"version": "2.2.3",
|
"version": "2.2.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fullcalendar/core": "^6.1.19",
|
"@fullcalendar/core": "^6.1.19",
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M26.5002 14.9998C27.8808 14.9998 29 13.8806 29 12.5C29 11.1194 27.8807 10.0002 26.5001 10.0002C25.1194 10.0002 24 11.1195 24 12.5002V14.9998H26.5002ZM19.5 14.9998C20.8807 14.9998 22 13.8805 22 12.4998V5.50018C22 4.11947 20.8807 3.00018 19.5 3.00018C18.1193 3.00018 17 4.11947 17 5.50018V12.4998C17 13.8805 18.1193 14.9998 19.5 14.9998Z" fill="#2EB67D"/>
|
|
||||||
<path d="M5.49979 17.0002C4.11919 17.0002 3 18.1194 3 19.5C3 20.8806 4.1193 21.9998 5.49989 21.9998C6.8806 21.9998 8 20.8805 8 19.4998V17.0002H5.49979ZM12.5 17.0002C11.1193 17.0002 10 18.1195 10 19.5002V26.4998C10 27.8805 11.1193 28.9998 12.5 28.9998C13.8807 28.9998 15 27.8805 15 26.4998V19.5002C15 18.1195 13.8807 17.0002 12.5 17.0002Z" fill="#E01E5A"/>
|
|
||||||
<path d="M17.0002 26.5002C17.0002 27.8808 18.1194 29 19.5 29C20.8806 29 21.9998 27.8807 21.9998 26.5001C21.9998 25.1194 20.8805 24 19.4998 24L17.0002 24L17.0002 26.5002ZM17.0002 19.5C17.0002 20.8807 18.1195 22 19.5002 22L26.4998 22C27.8805 22 28.9998 20.8807 28.9998 19.5C28.9998 18.1193 27.8805 17 26.4998 17L19.5002 17C18.1195 17 17.0002 18.1193 17.0002 19.5Z" fill="#ECB22E"/>
|
|
||||||
<path d="M14.9998 5.49979C14.9998 4.11919 13.8806 3 12.5 3C11.1194 3 10.0002 4.1193 10.0002 5.49989C10.0002 6.88061 11.1195 8 12.5002 8L14.9998 8L14.9998 5.49979ZM14.9998 12.5C14.9998 11.1193 13.8805 10 12.4998 10L5.50024 10C4.11953 10 3.00024 11.1193 3.00024 12.5C3.00024 13.8807 4.11953 15 5.50024 15L12.4998 15C13.8805 15 14.9998 13.8807 14.9998 12.5Z" fill="#36C5F0"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.5 KiB |
@@ -1,10 +0,0 @@
|
|||||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<circle cx="16" cy="16" r="14" fill="url(#paint0_linear_1589_37170)"/>
|
|
||||||
<path d="M21.2137 20.2816L21.8356 16.3301H17.9452V13.767C17.9452 12.6857 18.4877 11.6311 20.2302 11.6311H22V8.26699C22 8.26699 20.3945 8 18.8603 8C15.6548 8 13.5617 9.89294 13.5617 13.3184V16.3301H10V20.2816H13.5617V29.8345C14.2767 29.944 15.0082 30 15.7534 30C16.4986 30 17.2302 29.944 17.9452 29.8345V20.2816H21.2137Z" fill="white"/>
|
|
||||||
<defs>
|
|
||||||
<linearGradient id="paint0_linear_1589_37170" x1="16" y1="2" x2="16" y2="29.917" gradientUnits="userSpaceOnUse">
|
|
||||||
<stop stop-color="#18ACFE"/>
|
|
||||||
<stop offset="1" stop-color="#0163E0"/>
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 725 B |
@@ -1,5 +0,0 @@
|
|||||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M19.3851 10.9171C20.4654 9.0306 19.8243 6.61829 17.9532 5.5291C16.0821 4.4399 13.6896 5.08628 12.6093 6.97282L4.53087 21.0806C3.45059 22.9671 4.09168 25.3794 5.96277 26.4686C7.83387 27.5578 10.2264 26.9114 11.3067 25.0249L19.3851 10.9171Z" fill="#F8BB2D"/>
|
|
||||||
<path d="M11.8263 23.0546C11.8263 25.2336 10.0743 27 7.91313 27C5.75197 27 4 25.2336 4 23.0546C4 20.8756 5.75197 19.1091 7.91313 19.1091C10.0743 19.1091 11.8263 20.8756 11.8263 23.0546Z" fill="#3BA757"/>
|
|
||||||
<path d="M12.621 10.9171C11.5407 9.0306 12.1818 6.61829 14.0529 5.5291C15.924 4.4399 18.3165 5.08628 19.3968 6.97282L27.4752 21.0806C28.5555 22.9671 27.9144 25.3794 26.0433 26.4686C24.1722 27.5578 21.7797 26.9114 20.6994 25.0249L12.621 10.9171Z" fill="#4689F2"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 836 B |
@@ -1,26 +0,0 @@
|
|||||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<rect x="2" y="2" width="28" height="28" rx="6" fill="url(#paint0_radial_1589_37182)"/>
|
|
||||||
<rect x="2" y="2" width="28" height="28" rx="6" fill="url(#paint1_radial_1589_37182)"/>
|
|
||||||
<rect x="2" y="2" width="28" height="28" rx="6" fill="url(#paint2_radial_1589_37182)"/>
|
|
||||||
<path d="M23 10.5C23 11.3284 22.3284 12 21.5 12C20.6716 12 20 11.3284 20 10.5C20 9.67157 20.6716 9 21.5 9C22.3284 9 23 9.67157 23 10.5Z" fill="white"/>
|
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M16 21C18.7614 21 21 18.7614 21 16C21 13.2386 18.7614 11 16 11C13.2386 11 11 13.2386 11 16C11 18.7614 13.2386 21 16 21ZM16 19C17.6569 19 19 17.6569 19 16C19 14.3431 17.6569 13 16 13C14.3431 13 13 14.3431 13 16C13 17.6569 14.3431 19 16 19Z" fill="white"/>
|
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M6 15.6C6 12.2397 6 10.5595 6.65396 9.27606C7.2292 8.14708 8.14708 7.2292 9.27606 6.65396C10.5595 6 12.2397 6 15.6 6H16.4C19.7603 6 21.4405 6 22.7239 6.65396C23.8529 7.2292 24.7708 8.14708 25.346 9.27606C26 10.5595 26 12.2397 26 15.6V16.4C26 19.7603 26 21.4405 25.346 22.7239C24.7708 23.8529 23.8529 24.7708 22.7239 25.346C21.4405 26 19.7603 26 16.4 26H15.6C12.2397 26 10.5595 26 9.27606 25.346C8.14708 24.7708 7.2292 23.8529 6.65396 22.7239C6 21.4405 6 19.7603 6 16.4V15.6ZM15.6 8H16.4C18.1132 8 19.2777 8.00156 20.1779 8.0751C21.0548 8.14674 21.5032 8.27659 21.816 8.43597C22.5686 8.81947 23.1805 9.43139 23.564 10.184C23.7234 10.4968 23.8533 10.9452 23.9249 11.8221C23.9984 12.7223 24 13.8868 24 15.6V16.4C24 18.1132 23.9984 19.2777 23.9249 20.1779C23.8533 21.0548 23.7234 21.5032 23.564 21.816C23.1805 22.5686 22.5686 23.1805 21.816 23.564C21.5032 23.7234 21.0548 23.8533 20.1779 23.9249C19.2777 23.9984 18.1132 24 16.4 24H15.6C13.8868 24 12.7223 23.9984 11.8221 23.9249C10.9452 23.8533 10.4968 23.7234 10.184 23.564C9.43139 23.1805 8.81947 22.5686 8.43597 21.816C8.27659 21.5032 8.14674 21.0548 8.0751 20.1779C8.00156 19.2777 8 18.1132 8 16.4V15.6C8 13.8868 8.00156 12.7223 8.0751 11.8221C8.14674 10.9452 8.27659 10.4968 8.43597 10.184C8.81947 9.43139 9.43139 8.81947 10.184 8.43597C10.4968 8.27659 10.9452 8.14674 11.8221 8.0751C12.7223 8.00156 13.8868 8 15.6 8Z" fill="white"/>
|
|
||||||
<defs>
|
|
||||||
<radialGradient id="paint0_radial_1589_37182" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(12 23) rotate(-55.3758) scale(25.5196)">
|
|
||||||
<stop stop-color="#B13589"/>
|
|
||||||
<stop offset="0.79309" stop-color="#C62F94"/>
|
|
||||||
<stop offset="1" stop-color="#8A3AC8"/>
|
|
||||||
</radialGradient>
|
|
||||||
<radialGradient id="paint1_radial_1589_37182" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(11 31) rotate(-65.1363) scale(22.5942)">
|
|
||||||
<stop stop-color="#E0E8B7"/>
|
|
||||||
<stop offset="0.444662" stop-color="#FB8A2E"/>
|
|
||||||
<stop offset="0.71474" stop-color="#E2425C"/>
|
|
||||||
<stop offset="1" stop-color="#E2425C" stop-opacity="0"/>
|
|
||||||
</radialGradient>
|
|
||||||
<radialGradient id="paint2_radial_1589_37182" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(0.500002 3) rotate(-8.1301) scale(38.8909 8.31836)">
|
|
||||||
<stop offset="0.156701" stop-color="#406ADC"/>
|
|
||||||
<stop offset="0.467799" stop-color="#6A45BE"/>
|
|
||||||
<stop offset="1" stop-color="#6A45BE" stop-opacity="0"/>
|
|
||||||
</radialGradient>
|
|
||||||
</defs>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 3.2 KiB |
@@ -1,6 +0,0 @@
|
|||||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M30.0014 16.3109C30.0014 15.1598 29.9061 14.3198 29.6998 13.4487H16.2871V18.6442H24.1601C24.0014 19.9354 23.1442 21.8798 21.2394 23.1864L21.2127 23.3604L25.4536 26.58L25.7474 26.6087C28.4458 24.1665 30.0014 20.5731 30.0014 16.3109Z" fill="#4285F4"/>
|
|
||||||
<path d="M16.2863 30C20.1434 30 23.3814 28.7555 25.7466 26.6089L21.2386 23.1865C20.0323 24.011 18.4132 24.5866 16.2863 24.5866C12.5086 24.5866 9.30225 22.1444 8.15929 18.7688L7.99176 18.7827L3.58208 22.1272L3.52441 22.2843C5.87359 26.8577 10.699 30 16.2863 30Z" fill="#34A853"/>
|
|
||||||
<path d="M8.16013 18.7688C7.85855 17.8977 7.68401 16.9643 7.68401 15.9999C7.68401 15.0354 7.85855 14.1021 8.14426 13.231L8.13627 13.0455L3.67132 9.64734L3.52524 9.71544C2.55703 11.6132 2.00146 13.7444 2.00146 15.9999C2.00146 18.2555 2.55703 20.3865 3.52524 22.2843L8.16013 18.7688Z" fill="#FBBC05"/>
|
|
||||||
<path d="M16.2864 7.4133C18.9689 7.4133 20.7784 8.54885 21.8102 9.4978L25.8419 5.64C23.3658 3.38445 20.1435 2 16.2864 2C10.699 2 5.8736 5.1422 3.52441 9.71549L8.14345 13.2311C9.30229 9.85555 12.5086 7.4133 16.2864 7.4133Z" fill="#EB4335"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.2 KiB |
@@ -1,4 +0,0 @@
|
|||||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M2.24451 9.94111C2.37304 7.96233 3.96395 6.41157 5.94447 6.31345C8.81239 6.17136 12.9115 6 16 6C19.0885 6 23.1876 6.17136 26.0555 6.31345C28.0361 6.41157 29.627 7.96233 29.7555 9.94111C29.8786 11.8369 30 14.1697 30 16C30 17.8303 29.8786 20.1631 29.7555 22.0589C29.627 24.0377 28.0361 25.5884 26.0555 25.6866C23.1876 25.8286 19.0885 26 16 26C12.9115 26 8.81239 25.8286 5.94447 25.6866C3.96395 25.5884 2.37304 24.0377 2.24451 22.0589C2.12136 20.1631 2 17.8303 2 16C2 14.1697 2.12136 11.8369 2.24451 9.94111Z" fill="#FC0D1B"/>
|
|
||||||
<path d="M13 12V20L21 16L13 12Z" fill="white"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 684 B |
@@ -1,4 +0,0 @@
|
|||||||
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M37.5 20C37.5 29.66 29.6687 37.5 20 37.5C10.3312 37.5 2.5 29.66 2.5 20C2.5 10.3312 10.3312 2.49999 20 2.49999C29.6687 2.49999 37.5 10.3312 37.5 20Z" fill="#283544"/>
|
|
||||||
<path d="M28.2026 15.5717C28.1071 15.6274 25.8338 16.8031 25.8338 19.4098C25.941 22.3826 28.7026 23.4252 28.75 23.4252C28.7026 23.4809 28.3331 24.8454 27.2383 26.2757C26.3696 27.5078 25.4053 28.75 23.941 28.75C22.5481 28.75 22.0481 27.9288 20.441 27.9288C18.715 27.9288 18.2267 28.75 16.9052 28.75C15.441 28.75 14.4052 27.4412 13.4891 26.2207C12.2989 24.6232 11.2872 22.1164 11.2515 19.7093C11.2274 18.4338 11.4899 17.18 12.156 16.1151C13.0962 14.6283 14.7748 13.619 16.6079 13.5858C18.0124 13.5416 19.2624 14.4843 20.1195 14.4843C20.941 14.4843 22.4767 13.5858 24.2143 13.5858C24.9643 13.5865 26.9643 13.797 28.2026 15.5717ZM20.0008 13.3311C19.7508 12.1663 20.441 11.0015 21.0838 10.2585C21.9053 9.3599 23.2026 8.75 24.3214 8.75C24.3928 9.91481 23.9402 11.0572 23.1312 11.8892C22.4053 12.7878 21.1553 13.4642 20.0008 13.3311Z" fill="white"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.1 KiB |
@@ -1,7 +0,0 @@
|
|||||||
<svg width="41" height="40" viewBox="0 0 41 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<rect x="0.5" width="40" height="40" rx="20" fill="#1B4BF1"/>
|
|
||||||
<path opacity="0.5" d="M28.9266 15.7265C29.1963 13.9679 28.9266 12.7956 27.9826 11.7134C26.9487 10.496 25.0607 10 22.6333 10H15.6657C15.1713 10 14.7667 10.3607 14.6768 10.8567L11.7549 29.3437C11.7099 29.7044 11.9797 30.02 12.3393 30.02H16.6547L16.34 31.9138C16.2951 32.2295 16.5198 32.5 16.8794 32.5H20.5205C20.9701 32.5 21.3297 32.1844 21.3746 31.7786L22.1388 26.999C22.1838 26.5932 22.5883 26.2776 22.9929 26.2776H23.5323C27.0386 26.2776 29.8256 24.8347 30.6348 20.6864C30.9494 18.9729 30.8146 17.485 29.9155 16.493C29.6458 16.1774 29.3311 15.9519 28.9266 15.7265" fill="white"/>
|
|
||||||
<path d="M28.9266 15.7265C29.1963 13.9679 28.9266 12.7956 27.9826 11.7134C26.9487 10.496 25.0607 10 22.6333 10H15.6657C15.1713 10 14.7667 10.3607 14.6768 10.8567L11.7549 29.3437C11.7099 29.7044 11.9797 30.02 12.3393 30.02H16.6547L17.6886 23.3467C17.7785 22.8507 18.183 22.49 18.6775 22.49H20.7453C24.791 22.49 27.9376 20.8667 28.8367 16.0872C28.8816 15.997 28.8816 15.8617 28.9266 15.7265Z" fill="white"/>
|
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M30.1209 16.096C30.1192 16.105 30.1161 16.121 30.1115 16.1467C30.1057 16.1785 30.0932 16.2474 30.0743 16.3214C30.0669 16.3504 30.0572 16.3854 30.0445 16.425C29.5373 19.0088 28.3926 20.8874 26.6974 22.0981C25.0064 23.3058 22.9195 23.74 20.7452 23.74H18.8924L17.7258 31.27H12.3392C11.1964 31.27 10.3804 30.2642 10.5144 29.1891L10.5169 29.1688L13.4442 10.6476L13.4467 10.6338C13.6357 9.59143 14.519 8.75 15.6656 8.75H22.6332C25.1361 8.75 27.5194 9.24302 28.9298 10.8979C29.516 11.5715 29.9235 12.3133 30.1267 13.1814C30.3266 14.0353 30.3118 14.9394 30.162 15.916L30.146 16.0204L30.1209 16.096ZM27.9825 11.7134C26.9486 10.496 25.0606 10 22.6332 10H15.6656C15.1712 10 14.7666 10.3607 14.6767 10.8567L11.7548 29.3437C11.7099 29.7044 11.9796 30.02 12.3392 30.02H16.6546L17.6885 23.3467C17.7784 22.8507 18.1829 22.49 18.6774 22.49H20.7452C24.7909 22.49 27.9375 20.8667 28.8366 16.0872C28.8609 16.0384 28.8721 15.9763 28.8843 15.9082C28.8947 15.8505 28.9059 15.7885 28.9265 15.7265C29.1962 13.9679 28.9265 12.7956 27.9825 11.7134Z" fill="#1B4BF1"/>
|
|
||||||
<path d="M18.9023 15.7715C18.9472 15.4559 19.3518 15.0501 19.7564 15.0501H25.2405C25.8698 15.0501 26.4992 15.0952 27.0386 15.1854C27.5331 15.2755 28.4321 15.501 28.8816 15.7715C29.1513 14.013 28.8816 12.8407 27.9376 11.7585C26.9487 10.496 25.0607 10 22.6333 10H15.6657C15.1713 10 14.7667 10.3607 14.6768 10.8567L11.7549 29.3437C11.7099 29.7044 11.9797 30.02 12.3393 30.02H16.6547L18.9023 15.7715V15.7715Z" fill="white"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 2.6 KiB |
@@ -1,5 +0,0 @@
|
|||||||
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<circle cx="20" cy="20" r="20" fill="#E31937"/>
|
|
||||||
<path d="M20.0005 31.4285L22.9444 14.8726C25.7502 14.8726 26.6353 15.1803 26.7632 16.4362C26.7632 16.4362 28.6456 15.7343 29.5948 14.3089C25.8901 12.5923 22.1678 12.5148 22.1678 12.5148L19.9957 15.1604L20.0006 15.16L17.8285 12.5144C17.8285 12.5144 14.106 12.5919 10.4019 14.3086C11.3504 15.734 13.2334 16.4358 13.2334 16.4358C13.362 15.1799 14.2459 14.8722 17.033 14.8702L20.0005 31.4285Z" fill="white"/>
|
|
||||||
<path d="M19.9996 11.7508C22.9943 11.7279 26.422 12.2141 29.9311 13.7434C30.4001 12.8993 30.5207 12.5262 30.5207 12.5262C26.6847 11.0086 23.0925 10.4893 19.9992 10.4762C16.906 10.4893 13.3139 11.0087 9.47852 12.5262C9.47852 12.5262 9.64962 12.9858 10.0677 13.7434C13.576 12.2141 17.0045 11.7279 19.9993 11.7508H19.9996Z" fill="white"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 892 B |
@@ -1,6 +0,0 @@
|
|||||||
<svg width="41" height="40" viewBox="0 0 41 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M20.5 2.5C10.8477 2.5 3 10.3477 3 20C3 29.6523 10.8477 37.5 20.5 37.5C30.1523 37.5 38 29.6523 38 20C38 10.3477 30.1523 2.5 20.5 2.5Z" fill="#F9981B"/>
|
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M26.5124 22.5475C26.461 22.4738 26.4093 22.4016 26.3578 22.3298C25.9392 21.7463 25.5402 21.1902 25.5402 20.0754V15.9195C25.5402 15.775 25.541 15.6315 25.5418 15.4891C25.551 13.8957 25.5593 12.4417 24.4101 11.3294C23.4246 10.3467 21.7876 10 20.5357 10C18.0872 10 15.3558 10.947 14.783 14.0851C14.7223 14.4184 14.9558 14.5929 15.1675 14.642L17.6607 14.9218C17.8941 14.9095 18.0627 14.6715 18.1079 14.4318C18.3217 13.3505 19.195 12.831 20.1762 12.831C20.7054 12.831 21.3063 13.0316 21.6196 13.5238C21.9425 14.0146 21.9374 14.6699 21.9328 15.2558C21.9323 15.3241 21.9317 15.3914 21.9317 15.4574V15.8058C21.7004 15.8326 21.458 15.858 21.2083 15.8842C19.8486 16.0267 18.2732 16.1919 17.0932 16.7283C15.4823 17.4517 14.3511 18.9238 14.3511 21.0876C14.3511 23.8606 16.0344 25.2469 18.2016 25.2469C20.0305 25.2469 21.0314 24.7993 22.4423 23.3077C22.5128 23.4136 22.5762 23.5113 22.6358 23.6033C22.9724 24.1222 23.1921 24.4609 23.9171 25.0864C24.1085 25.1923 24.3618 25.1883 24.5314 25.0289C25.0451 24.5552 25.9779 23.7146 26.5039 23.2576C26.714 23.0786 26.6767 22.7894 26.5124 22.5475ZM21.4505 21.3491C21.0421 22.1015 20.3916 22.5619 19.6689 22.5619C18.6834 22.5619 18.1047 21.7816 18.1047 20.6272C18.1047 18.3547 20.0714 17.9423 21.9312 17.9423C21.9312 18.0782 21.9325 18.2149 21.9337 18.352C21.9428 19.3807 21.9521 20.4299 21.4505 21.3491Z" fill="white"/>
|
|
||||||
<path d="M29.5873 27.0823C27.1246 28.9867 23.5531 30 20.4773 30C16.1681 30 12.2866 28.3307 9.34948 25.5515C9.11909 25.333 9.32473 25.0346 9.60151 25.2042C12.7708 27.1371 16.6894 28.3006 20.7367 28.3006C23.4672 28.3006 26.4682 27.7073 29.2295 26.479C29.646 26.2944 29.9951 26.7674 29.5873 27.0823Z" fill="white"/>
|
|
||||||
<path d="M27.7363 25.7559C28.5293 25.6555 30.2974 25.4334 30.6122 25.8556C30.9273 26.2775 30.2654 28.0124 29.967 28.7943L29.9649 28.8C29.8751 29.0358 30.0681 29.1299 30.2715 28.9512C31.5931 27.7933 31.9346 25.366 31.6638 25.0143C31.3951 24.6674 29.0847 24.367 27.6747 25.4049C27.458 25.5659 27.4951 25.7852 27.7363 25.7559Z" fill="white"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 2.3 KiB |
@@ -1,4 +0,0 @@
|
|||||||
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<circle cx="20" cy="20" r="17.5" fill="#1ED760"/>
|
|
||||||
<path d="M27.9554 27.0287C27.6564 27.5031 27.0186 27.6359 26.5203 27.3513C22.5937 25.0742 17.6705 24.5618 11.8503 25.8142C11.2922 25.9281 10.7341 25.6055 10.6145 25.0742C10.4949 24.5428 10.8338 24.0115 11.3919 23.8976C17.7502 22.5124 23.2116 23.1007 27.5966 25.6624C28.095 25.9471 28.2544 26.5543 27.9554 27.0287ZM29.9885 22.7022C29.6098 23.2904 28.8125 23.4612 28.1946 23.1196C23.7099 20.482 16.8732 19.7229 11.5712 21.26C10.8736 21.4497 10.1561 21.0892 9.95674 20.444C9.75742 19.7798 10.1361 19.0967 10.8338 18.9069C16.8931 17.1611 24.4274 17.9961 29.5899 21.0133C30.1679 21.3549 30.3672 22.1139 29.9885 22.7022ZM30.1679 18.1859C24.7862 15.1497 15.9164 14.865 10.774 16.3452C9.95674 16.5919 9.07973 16.1554 8.82061 15.3584C8.5615 14.5804 9.03987 13.7455 9.85708 13.4988C15.757 11.7909 25.5636 12.1325 31.7425 15.6241C32.48 16.0416 32.7192 16.9524 32.2807 17.6545C31.8621 18.3756 30.9054 18.6223 30.1679 18.1859Z" fill="white"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.1 KiB |
@@ -1,4 +0,0 @@
|
|||||||
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M20 2.5C10.3477 2.5 2.5 10.3477 2.5 20C2.5 29.6523 10.3477 37.5 20 37.5C29.6523 37.5 37.5 29.6523 37.5 20C37.5 10.3477 29.6523 2.5 20 2.5Z" fill="#FF5A5F"/>
|
|
||||||
<path d="M20.0002 24.6084C18.8528 23.1652 18.1779 21.9001 17.953 20.8178C17.728 19.9383 17.818 19.2393 18.2004 18.7206C18.6054 18.1118 19.2128 17.8186 20.0002 17.8186C20.7875 17.8186 21.3949 18.1118 21.7999 18.7206C22.1823 19.2393 22.2723 19.9383 22.0473 20.8178C21.7999 21.9227 21.125 23.1855 20.0002 24.6084ZM26.2812 27.9458C24.369 28.7801 22.477 27.4497 20.8573 25.6457C23.5366 22.2835 24.0315 19.6677 22.882 17.9742C22.2071 17.0046 21.2397 16.531 20.0002 16.531C17.503 16.531 16.1285 18.6507 16.6684 21.1109C16.9834 22.4413 17.8158 23.9544 19.143 25.6457C18.1475 26.7515 16.8414 28.0059 15.2736 28.1487C13.0015 28.487 11.222 26.2771 12.0319 23.9973L17.7933 12.0413C18.285 11.1425 18.8919 10.3726 19.9979 10.3726C20.8078 10.3726 21.4377 10.8462 21.7076 11.2295L27.9639 23.9973C28.5769 25.54 27.7996 27.2886 26.2812 27.9458ZM29.1832 23.5463L23.8268 12.3796C22.8145 10.305 22.0946 9.0625 20.0002 9.0625C17.9305 9.0625 17.0509 10.5057 16.151 12.3796L10.8171 23.5463C9.66976 26.7055 12.0319 29.4792 14.8912 29.4792C15.0712 29.4792 15.2499 29.4566 15.4311 29.4566C16.9159 29.2762 18.4479 28.3291 20.0002 26.6356C21.5524 28.3269 23.0844 29.2762 24.5692 29.4566C24.7505 29.4566 24.9291 29.4792 25.1091 29.4792C27.9684 29.4814 30.3306 26.7055 29.1832 23.5463Z" fill="white"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.5 KiB |
@@ -1,5 +0,0 @@
|
|||||||
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M20 2.5C10.3477 2.5 2.5 10.3477 2.5 20C2.5 29.6523 10.3477 37.5 20 37.5C29.6523 37.5 37.5 29.6523 37.5 20C37.5 10.3477 29.6523 2.5 20 2.5Z" fill="#87E64B"/>
|
|
||||||
<path d="M20.4865 30C20.9801 30 21.3803 29.5998 21.3803 29.1062C21.3803 28.6126 20.9801 28.2125 20.4865 28.2125C19.9929 28.2125 19.5928 28.6126 19.5928 29.1062C19.5928 29.5998 19.9929 30 20.4865 30Z" fill="white"/>
|
|
||||||
<path d="M25.6233 23.0134L20.5833 23.5539C20.4895 23.5633 20.4427 23.4477 20.5177 23.3883L25.4483 19.5482C25.767 19.2857 25.9732 18.8795 25.8857 18.4421C25.7982 17.7734 25.2452 17.336 24.5453 17.4235L19.1866 18.2077C19.0928 18.2202 19.0428 18.1015 19.1178 18.0421L24.4297 13.9864C25.4764 13.1709 25.5639 11.5711 24.6046 10.6399C23.7329 9.76817 22.3331 9.79629 21.4613 10.6681L12.903 19.3763C12.5843 19.7263 12.4374 20.1919 12.5249 20.6855C12.6718 21.4729 13.4561 21.9947 14.2435 21.851L18.8585 20.9105C18.9585 20.8886 19.0116 21.023 18.9272 21.0792L13.8091 24.3569C13.1686 24.7631 12.878 25.4912 13.0811 26.2192C13.2842 27.1785 14.2466 27.7315 15.1777 27.5003L22.8299 25.6162C22.9174 25.5943 22.9798 25.6943 22.9236 25.763L21.73 27.2378C21.4113 27.644 21.9331 28.1971 22.3705 27.8784L26.3013 24.6475C27.0012 24.0664 26.5356 22.929 25.6326 23.0165L25.6233 23.0134Z" fill="white"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.3 KiB |
@@ -1,6 +0,0 @@
|
|||||||
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<circle cx="20" cy="20" r="17.5" fill="#FF8C00"/>
|
|
||||||
<path d="M27.4363 25.1256C27.408 24.8784 27.2659 24.3115 26.8965 24.1659H26.897C26.712 24.0931 26.5558 24.151 26.4852 24.2821C26.3713 24.486 26.4565 24.8784 26.6697 25.2278C26.8824 25.5768 27.0814 25.7513 27.2235 25.7513C27.3656 25.7513 27.4932 25.5768 27.4363 25.1256Z" fill="white"/>
|
|
||||||
<path d="M25.4485 26.1147C25.2213 25.9113 24.9657 25.8096 24.7384 25.8096C24.5398 25.8096 24.3836 25.8819 24.2839 26.013C24.0288 26.3326 24.1422 26.9148 24.5398 27.2638C24.7102 27.4239 24.9657 27.5111 25.2071 27.5111C25.4485 27.5111 25.6613 27.4094 25.7892 27.2494C26.0165 26.9293 25.8885 26.4931 25.4485 26.1147Z" fill="white"/>
|
|
||||||
<path d="M10 20.0061C10 25.5328 14.3728 30.0125 19.7687 30.0128C21.203 30.0128 22.6227 29.7365 24.7102 29.7216C26.5704 29.7216 28.615 30.3907 30.8583 32.4272C31.0856 32.6306 31.3835 32.3688 31.1849 32.1216C28.984 29.2565 26.9393 28.7185 24.8947 28.2529C22.3959 27.6856 21.1178 26.2603 20.2233 24.6749C20.0529 24.3549 19.9677 24.4132 19.9532 24.8205C19.9371 25.4198 19.98 26.0192 20.0812 26.6097H19.7833C16.2191 26.6097 13.3223 23.6424 13.3223 19.9917C13.3223 16.3409 16.2191 13.3737 19.7828 13.3737C23.347 13.3737 26.2438 16.3409 26.2438 19.9917C26.2438 20.2534 26.2292 20.5152 26.201 20.7624C25.7182 20.6752 24.7954 20.6607 24.1422 20.719C23.9008 20.748 23.9291 20.8641 24.114 20.8935C26.2438 21.3004 27.7063 22.6534 28.047 25.1112C28.0612 25.1695 28.1322 25.1839 28.1609 25.1405C29.0268 23.6424 29.5378 21.8822 29.5378 20.0061C29.5378 14.4798 25.1643 10 19.7687 10C14.3735 10 10 14.4794 10 20.0061Z" fill="white"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.6 KiB |
@@ -1,4 +0,0 @@
|
|||||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M16 30C23.732 30 30 23.732 30 16C30 8.26801 23.732 2 16 2C8.26801 2 2 8.26801 2 16C2 23.732 8.26801 30 16 30Z" fill="#2BDE73"/>
|
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.357 13.4527L17.3808 9.20142C17.9525 8.40047 18.6904 8 19.5953 8C20.3334 8 20.972 8.25407 21.512 8.76237C22.0516 9.27068 22.3213 9.8791 22.3213 10.5876C22.3213 11.1114 22.1788 11.5736 21.8929 11.9741L19.1665 15.821L22.5 19.9222C22.8334 20.3303 23 20.808 23 21.3547C23 22.0788 22.7384 22.7004 22.2144 23.2203C21.6906 23.7403 21.0553 24 20.3096 24C19.492 24 18.8691 23.7423 18.4404 23.2261L14.357 18.2817V21.0082C14.357 21.7862 14.2182 22.3906 13.9403 22.8219C13.4324 23.6072 12.6943 24 11.7262 24C10.8452 24 10.1624 23.7112 9.67848 23.1337C9.22604 22.6022 9 21.8973 9 21.0198V10.9111C9 10.0793 9.22983 9.39412 9.69041 8.85495C10.1745 8.2851 10.841 8 11.6903 8C12.4999 8 13.1744 8.2851 13.7141 8.85495C14.0157 9.17056 14.2061 9.4902 14.2855 9.8137C14.3332 10.0141 14.357 10.3874 14.357 10.9343V13.4527Z" fill="white"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 227 KiB |
|
Before Width: | Height: | Size: 346 KiB |
|
Before Width: | Height: | Size: 162 KiB |
|
Before Width: | Height: | Size: 262 KiB |
|
Before Width: | Height: | Size: 189 KiB |
|
Before Width: | Height: | Size: 312 KiB |
|
Before Width: | Height: | Size: 869 KiB |
|
Before Width: | Height: | Size: 704 KiB |
|
Before Width: | Height: | Size: 453 KiB |
|
Before Width: | Height: | Size: 849 KiB |
@@ -1,15 +0,0 @@
|
|||||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<g clip-path="url(#clip0_1788_4304)">
|
|
||||||
<path d="M16 32C24.8366 32 32 24.8366 32 16C32 7.16344 24.8366 0 16 0C7.16344 0 0 7.16344 0 16C0 24.8366 7.16344 32 16 32Z" fill="#F0F0F0"/>
|
|
||||||
<path d="M15.3037 16H31.9993C31.9993 14.5559 31.8068 13.1569 31.4481 11.826H15.3037V16Z" fill="#D80027"/>
|
|
||||||
<path d="M15.3037 7.65135H29.651C28.6715 6.0531 27.4192 4.64042 25.9591 3.47742H15.3037V7.65135Z" fill="#D80027"/>
|
|
||||||
<path d="M15.9995 32C19.7651 32 23.2262 30.6985 25.9593 28.5217H6.03979C8.77292 30.6985 12.234 32 15.9995 32Z" fill="#D80027"/>
|
|
||||||
<path d="M2.34797 24.3465H29.6512C30.4375 23.0635 31.0473 21.661 31.4484 20.1726H0.550781C0.951844 21.661 1.56166 23.0635 2.34797 24.3465Z" fill="#D80027"/>
|
|
||||||
<path d="M7.4115 2.49863H8.86956L7.51331 3.48394L8.03137 5.07825L6.67519 4.09294L5.319 5.07825L5.7665 3.70094C4.57237 4.69562 3.52575 5.861 2.66325 7.1595H3.13044L2.26712 7.78669C2.13262 8.01106 2.00362 8.239 1.88 8.47031L2.29225 9.73913L1.52313 9.18031C1.33194 9.58537 1.15706 9.99956 0.999875 10.4224L1.45406 11.8204H3.13044L1.77419 12.8057L2.29225 14.4L0.936063 13.4147L0.123687 14.0049C0.042375 14.6586 0 15.3243 0 16H16C16 7.1635 16 6.12175 16 0C12.8393 0 9.89281 0.916875 7.4115 2.49863ZM8.03137 14.4L6.67519 13.4147L5.319 14.4L5.83706 12.8057L4.48081 11.8204H6.15719L6.67519 10.2261L7.19319 11.8204H8.86956L7.51331 12.8057L8.03137 14.4ZM7.51331 8.14481L8.03137 9.73913L6.67519 8.75381L5.319 9.73913L5.83706 8.14481L4.48081 7.1595H6.15719L6.67519 5.56519L7.19319 7.1595H8.86956L7.51331 8.14481ZM13.7705 14.4L12.4143 13.4147L11.0581 14.4L11.5762 12.8057L10.2199 11.8204H11.8963L12.4143 10.2261L12.9323 11.8204H14.6087L13.2524 12.8057L13.7705 14.4ZM13.2524 8.14481L13.7705 9.73913L12.4143 8.75381L11.0581 9.73913L11.5762 8.14481L10.2199 7.1595H11.8963L12.4143 5.56519L12.9323 7.1595H14.6087L13.2524 8.14481ZM13.2524 3.48394L13.7705 5.07825L12.4143 4.09294L11.0581 5.07825L11.5762 3.48394L10.2199 2.49863H11.8963L12.4143 0.904312L12.9323 2.49863H14.6087L13.2524 3.48394Z" fill="#0052B4"/>
|
|
||||||
</g>
|
|
||||||
<defs>
|
|
||||||
<clipPath id="clip0_1788_4304">
|
|
||||||
<rect width="32" height="32" rx="16" fill="white"/>
|
|
||||||
</clipPath>
|
|
||||||
</defs>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 2.1 KiB |
@@ -1,12 +0,0 @@
|
|||||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<g clip-path="url(#clip0_1788_4322)">
|
|
||||||
<path d="M16 32C24.8366 32 32 24.8366 32 16C32 7.16344 24.8366 0 16 0C7.16344 0 0 7.16344 0 16C0 24.8366 7.16344 32 16 32Z" fill="#F0F0F0"/>
|
|
||||||
<path d="M32 15.9999C32 9.12049 27.658 3.2558 21.5652 0.995117V31.0048C27.658 28.7441 32 22.8794 32 15.9999Z" fill="#D80027"/>
|
|
||||||
<path d="M0.000488281 16.001C0.000488281 22.8805 4.34255 28.7452 10.4353 31.0058V0.996216C4.34255 3.2569 0.000488281 9.12159 0.000488281 16.001Z" fill="#0052B4"/>
|
|
||||||
</g>
|
|
||||||
<defs>
|
|
||||||
<clipPath id="clip0_1788_4322">
|
|
||||||
<rect width="32" height="32" fill="white"/>
|
|
||||||
</clipPath>
|
|
||||||
</defs>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 680 B |
@@ -1,17 +0,0 @@
|
|||||||
<svg width="21" height="21" viewBox="0 0 21 21" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<g clip-path="url(#clip0_1589_35166)">
|
|
||||||
<path d="M10.875 20.5C16.3978 20.5 20.875 16.0228 20.875 10.5C20.875 4.97715 16.3978 0.5 10.875 0.5C5.35215 0.5 0.875 4.97715 0.875 10.5C0.875 16.0228 5.35215 20.5 10.875 20.5Z" fill="#F0F0F0"/>
|
|
||||||
<path d="M0.875 10.5C0.875 4.97719 5.35219 0.5 10.875 0.5C16.3978 0.5 20.875 4.97719 20.875 10.5" fill="#D80027"/>
|
|
||||||
<path d="M6.96163 5.71751C6.96163 4.26056 7.98558 3.04345 9.35292 2.74481C9.14276 2.69896 8.92475 2.67407 8.70073 2.67407C7.01983 2.67407 5.65726 4.03665 5.65726 5.71755C5.65726 7.39845 7.01983 8.76103 8.70073 8.76103C8.92468 8.76103 9.14272 8.73614 9.35292 8.69025C7.98558 8.39161 6.96163 7.1745 6.96163 5.71751Z" fill="#F0F0F0"/>
|
|
||||||
<path d="M10.8747 2.89172L11.0905 3.55598H11.789L11.2239 3.96657L11.4398 4.63083L10.8747 4.22032L10.3096 4.63083L10.5255 3.96657L9.96039 3.55598H10.6588L10.8747 2.89172Z" fill="#F0F0F0"/>
|
|
||||||
<path d="M9.18043 4.19556L9.39625 4.85985H10.0947L9.52964 5.2704L9.7455 5.93466L9.18043 5.52415L8.61527 5.93466L8.83117 5.2704L8.26605 4.85985H8.96453L9.18043 4.19556Z" fill="#F0F0F0"/>
|
|
||||||
<path d="M12.5691 4.19556L12.785 4.85985H13.4835L12.9184 5.2704L13.1343 5.93466L12.5691 5.52415L12.0041 5.93466L12.2199 5.2704L11.6548 4.85985H12.3533L12.5691 4.19556Z" fill="#F0F0F0"/>
|
|
||||||
<path d="M11.9171 6.15283L12.133 6.81713H12.8314L12.2663 7.22768L12.4822 7.89193L11.9171 7.48143L11.352 7.89193L11.5679 7.22768L11.0028 6.81713H11.7012L11.9171 6.15283Z" fill="#F0F0F0"/>
|
|
||||||
<path d="M9.83243 6.15283L10.0482 6.81713H10.7468L10.1816 7.22768L10.3975 7.89193L9.83243 7.48143L9.26731 7.89193L9.48317 7.22768L8.91809 6.81713H9.61657L9.83243 6.15283Z" fill="#F0F0F0"/>
|
|
||||||
</g>
|
|
||||||
<defs>
|
|
||||||
<clipPath id="clip0_1589_35166">
|
|
||||||
<rect x="0.875" y="0.5" width="20" height="20" rx="10" fill="white"/>
|
|
||||||
</clipPath>
|
|
||||||
</defs>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.8 KiB |
@@ -1,23 +0,0 @@
|
|||||||
<svg width="21" height="21" viewBox="0 0 21 21" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<g clip-path="url(#clip0_1589_35168)">
|
|
||||||
<path d="M10.875 20.5C16.3978 20.5 20.875 16.0228 20.875 10.5C20.875 4.97715 16.3978 0.5 10.875 0.5C5.35215 0.5 0.875 4.97715 0.875 10.5C0.875 16.0228 5.35215 20.5 10.875 20.5Z" fill="#F0F0F0"/>
|
|
||||||
<path d="M2.94224 4.41089C2.15673 5.43288 1.56443 6.61081 1.21954 7.89046H6.42181L2.94224 4.41089Z" fill="#0052B4"/>
|
|
||||||
<path d="M20.5309 7.89054C20.186 6.61093 19.5936 5.433 18.8082 4.41101L15.3287 7.89054H20.5309Z" fill="#0052B4"/>
|
|
||||||
<path d="M1.21954 13.1085C1.56447 14.3881 2.15677 15.5661 2.94224 16.588L6.42169 13.1085H1.21954Z" fill="#0052B4"/>
|
|
||||||
<path d="M16.9629 2.56649C15.9409 1.78098 14.763 1.18867 13.4834 0.84375V6.04598L16.9629 2.56649Z" fill="#0052B4"/>
|
|
||||||
<path d="M4.78662 18.4314C5.80861 19.2169 6.98655 19.8092 8.26616 20.1541V14.9519L4.78662 18.4314Z" fill="#0052B4"/>
|
|
||||||
<path d="M8.26611 0.84375C6.9865 1.18867 5.80857 1.78098 4.78662 2.56644L8.26611 6.04593V0.84375Z" fill="#0052B4"/>
|
|
||||||
<path d="M13.4834 20.1541C14.763 19.8092 15.9409 19.2169 16.9629 18.4314L13.4834 14.9519V20.1541Z" fill="#0052B4"/>
|
|
||||||
<path d="M15.3287 13.1085L18.8082 16.588C19.5936 15.5661 20.186 14.3881 20.5309 13.1085H15.3287Z" fill="#0052B4"/>
|
|
||||||
<path d="M20.7904 9.19566H12.1794H12.1794V0.584648C11.7524 0.529063 11.3171 0.5 10.875 0.5C10.4329 0.5 9.99762 0.529063 9.57066 0.584648V9.19559V9.19563H0.959648C0.904063 9.62262 0.875 10.0579 0.875 10.5C0.875 10.9421 0.904063 11.3774 0.959648 11.8043H9.57059H9.57063V20.4154C9.99762 20.4709 10.4329 20.5 10.875 20.5C11.3171 20.5 11.7524 20.471 12.1793 20.4154V11.8044V11.8044H20.7904C20.8459 11.3774 20.875 10.9421 20.875 10.5C20.875 10.0579 20.8459 9.62262 20.7904 9.19566Z" fill="#D80027"/>
|
|
||||||
<path d="M13.4837 13.1094L17.946 17.5718C18.1513 17.3666 18.3471 17.1521 18.5339 16.9298L14.7135 13.1094H13.4837V13.1094Z" fill="#D80027"/>
|
|
||||||
<path d="M8.26628 13.1094H8.2662L3.80389 17.5717C4.00905 17.7769 4.22354 17.9727 4.44589 18.1595L8.26628 14.339V13.1094Z" fill="#D80027"/>
|
|
||||||
<path d="M8.26616 7.89093V7.89085L3.80382 3.42847C3.59858 3.63362 3.4028 3.84812 3.216 4.07046L7.03643 7.89089H8.26616V7.89093Z" fill="#D80027"/>
|
|
||||||
<path d="M13.4837 7.89187L17.9461 3.42945C17.7409 3.22421 17.5264 3.02843 17.3041 2.84167L13.4837 6.6621V7.89187V7.89187Z" fill="#D80027"/>
|
|
||||||
</g>
|
|
||||||
<defs>
|
|
||||||
<clipPath id="clip0_1589_35168">
|
|
||||||
<rect x="0.875" y="0.5" width="20" height="20" rx="10" fill="white"/>
|
|
||||||
</clipPath>
|
|
||||||
</defs>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 2.4 KiB |
@@ -1,12 +0,0 @@
|
|||||||
<svg width="21" height="21" viewBox="0 0 21 21" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<g clip-path="url(#clip0_1589_35170)">
|
|
||||||
<path d="M10.875 20.5C16.3978 20.5 20.875 16.0228 20.875 10.5C20.875 4.97715 16.3978 0.5 10.875 0.5C5.35215 0.5 0.875 4.97715 0.875 10.5C0.875 16.0228 5.35215 20.5 10.875 20.5Z" fill="#F0F0F0"/>
|
|
||||||
<path d="M10.875 20.4993C15.1746 20.4993 18.84 17.7856 20.253 13.9775H1.49695C2.90988 17.7856 6.57531 20.4993 10.875 20.4993Z" fill="black"/>
|
|
||||||
<path d="M10.875 0.50061C6.57531 0.50061 2.90988 3.21436 1.49695 7.02237H20.253C18.84 3.21436 15.1746 0.50061 10.875 0.50061Z" fill="#D80027"/>
|
|
||||||
</g>
|
|
||||||
<defs>
|
|
||||||
<clipPath id="clip0_1589_35170">
|
|
||||||
<rect x="0.875" y="0.5" width="20" height="20" rx="10" fill="white"/>
|
|
||||||
</clipPath>
|
|
||||||
</defs>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 758 B |
@@ -1,11 +0,0 @@
|
|||||||
<svg width="21" height="21" viewBox="0 0 21 21" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<g clip-path="url(#clip0_1589_35172)">
|
|
||||||
<path d="M10.875 20.5C16.3978 20.5 20.875 16.0228 20.875 10.5C20.875 4.97715 16.3978 0.5 10.875 0.5C5.35215 0.5 0.875 4.97715 0.875 10.5C0.875 16.0228 5.35215 20.5 10.875 20.5Z" fill="#F0F0F0"/>
|
|
||||||
<path d="M20.7905 9.19564H8.70125H8.70122V0.737671C7.77708 0.942593 6.90094 1.27474 6.0925 1.71587V9.19556V9.1956H0.959771C0.904106 9.62259 0.875122 10.0579 0.875122 10.5C0.875122 10.942 0.904106 11.3774 0.959771 11.8043H6.09247H6.0925V19.284C6.90094 19.7251 7.77708 20.0574 8.70122 20.2622V11.8044V11.8044H20.7905C20.8461 11.3774 20.8751 10.942 20.8751 10.5C20.8751 10.0579 20.8461 9.62259 20.7905 9.19564Z" fill="#0052B4"/>
|
|
||||||
</g>
|
|
||||||
<defs>
|
|
||||||
<clipPath id="clip0_1589_35172">
|
|
||||||
<rect x="0.875" y="0.5" width="20" height="20" rx="10" fill="white"/>
|
|
||||||
</clipPath>
|
|
||||||
</defs>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 898 B |
@@ -1,12 +0,0 @@
|
|||||||
<svg width="21" height="21" viewBox="0 0 21 21" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<g clip-path="url(#clip0_1589_35174)">
|
|
||||||
<path d="M14.353 1.12211C13.2697 0.720161 12.0979 0.500122 10.8747 0.500122C9.65153 0.500122 8.47981 0.720161 7.39649 1.12211L6.52692 10.5001L7.39649 19.8781C8.47981 20.2801 9.65153 20.5001 10.8747 20.5001C12.0979 20.5001 13.2697 20.2801 14.353 19.8781L15.2225 10.5001L14.353 1.12211Z" fill="#FFDA44"/>
|
|
||||||
<path d="M20.8749 10.4994C20.8749 6.19982 18.1612 2.53435 14.3531 1.12146V19.8775C18.1612 18.4645 20.8749 14.7991 20.8749 10.4994Z" fill="#D80027"/>
|
|
||||||
<path d="M0.874664 10.5C0.874664 14.7997 3.58841 18.4651 7.39642 19.8781V1.12207C3.58841 2.53496 0.874664 6.20043 0.874664 10.5Z" fill="black"/>
|
|
||||||
</g>
|
|
||||||
<defs>
|
|
||||||
<clipPath id="clip0_1589_35174">
|
|
||||||
<rect x="0.875" y="0.5" width="20" height="20" rx="10" fill="white"/>
|
|
||||||
</clipPath>
|
|
||||||
</defs>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 874 B |
@@ -1,11 +0,0 @@
|
|||||||
<svg width="21" height="21" viewBox="0 0 21 21" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<g clip-path="url(#clip0_1589_35176)">
|
|
||||||
<path d="M10.875 20.5C16.3978 20.5 20.875 16.0228 20.875 10.5C20.875 4.97715 16.3978 0.5 10.875 0.5C5.35215 0.5 0.875 4.97715 0.875 10.5C0.875 16.0228 5.35215 20.5 10.875 20.5Z" fill="#496E2D"/>
|
|
||||||
<path d="M8.70105 14.8479C11.1023 14.8479 13.0489 12.9013 13.0489 10.5C13.0489 8.09881 11.1023 6.15222 8.70105 6.15222C6.29982 6.15222 4.35324 8.09881 4.35324 10.5C4.35324 12.9013 6.29982 14.8479 8.70105 14.8479Z" fill="#D80027"/>
|
|
||||||
</g>
|
|
||||||
<defs>
|
|
||||||
<clipPath id="clip0_1589_35176">
|
|
||||||
<rect x="0.875" y="0.5" width="20" height="20" rx="10" fill="white"/>
|
|
||||||
</clipPath>
|
|
||||||
</defs>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 703 B |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 692 B |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 119 KiB |
@@ -1,4 +0,0 @@
|
|||||||
<svg width="80" height="80" viewBox="0 0 80 80" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M5.61127 24.8528C5.93259 19.9058 9.90987 16.0289 14.8612 15.7836C22.031 15.4284 32.2787 15 40 15C47.7213 15 57.969 15.4284 65.1388 15.7836C70.0901 16.0289 74.0674 19.9058 74.3887 24.8528C74.6966 29.5923 75 35.4241 75 40C75 44.5759 74.6966 50.4077 74.3887 55.1472C74.0674 60.0942 70.0901 63.9711 65.1388 64.2164C57.969 64.5716 47.7213 65 40 65C32.2787 65 22.031 64.5716 14.8612 64.2164C9.90987 63.9711 5.93259 60.0942 5.61127 55.1472C5.30341 50.4077 5 44.5759 5 40C5 35.4241 5.30341 29.5923 5.61127 24.8528Z" fill="#FC0D1B"/>
|
|
||||||
<path d="M32.5 30V50L52.5 40L32.5 30Z" fill="white"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 691 B |
@@ -0,0 +1,284 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import html
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import unicodedata
|
||||||
|
from html.parser import HTMLParser
|
||||||
|
from pathlib import Path
|
||||||
|
from urllib.parse import unquote, urlparse
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
|
||||||
|
API_URL = "https://vi.wikipedia.org/w/api.php"
|
||||||
|
DEFAULT_OUTPUT_DIR = Path(__file__).resolve().parents[1] / "tmp" / "wiki"
|
||||||
|
USER_AGENT = "UltimateHistoryMapWikiImporter/1.0"
|
||||||
|
|
||||||
|
ALLOWED_TAGS = {
|
||||||
|
"p",
|
||||||
|
"blockquote",
|
||||||
|
"h2",
|
||||||
|
"h3",
|
||||||
|
"h4",
|
||||||
|
"h5",
|
||||||
|
"h6",
|
||||||
|
"ul",
|
||||||
|
"ol",
|
||||||
|
"li",
|
||||||
|
"b",
|
||||||
|
"strong",
|
||||||
|
"i",
|
||||||
|
"em",
|
||||||
|
"code",
|
||||||
|
"pre",
|
||||||
|
"a",
|
||||||
|
"br",
|
||||||
|
}
|
||||||
|
|
||||||
|
SKIP_TAGS = {
|
||||||
|
"audio",
|
||||||
|
"canvas",
|
||||||
|
"figure",
|
||||||
|
"form",
|
||||||
|
"iframe",
|
||||||
|
"img",
|
||||||
|
"input",
|
||||||
|
"map",
|
||||||
|
"math",
|
||||||
|
"meta",
|
||||||
|
"noscript",
|
||||||
|
"picture",
|
||||||
|
"script",
|
||||||
|
"style",
|
||||||
|
"svg",
|
||||||
|
"table",
|
||||||
|
"video",
|
||||||
|
}
|
||||||
|
|
||||||
|
SKIP_CLASS_PARTS = (
|
||||||
|
"ambox",
|
||||||
|
"authority-control",
|
||||||
|
"catlinks",
|
||||||
|
"error",
|
||||||
|
"hatnote",
|
||||||
|
"metadata",
|
||||||
|
"mw-editsection",
|
||||||
|
"mw-empty-elt",
|
||||||
|
"navbox",
|
||||||
|
"navigation-not-searchable",
|
||||||
|
"noprint",
|
||||||
|
"reference",
|
||||||
|
"reflist",
|
||||||
|
"shortdescription",
|
||||||
|
"sidebar",
|
||||||
|
"toc",
|
||||||
|
"vertical-navbox",
|
||||||
|
)
|
||||||
|
|
||||||
|
VOID_TAGS = {"br"}
|
||||||
|
|
||||||
|
|
||||||
|
class WikiHtmlSanitizer(HTMLParser):
|
||||||
|
def __init__(self) -> None:
|
||||||
|
super().__init__(convert_charrefs=False)
|
||||||
|
self.parts: list[str] = []
|
||||||
|
self.open_tags: list[str] = []
|
||||||
|
self.skip_depth = 0
|
||||||
|
|
||||||
|
def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
|
||||||
|
tag = tag.lower()
|
||||||
|
if self.skip_depth:
|
||||||
|
self.skip_depth += 1
|
||||||
|
return
|
||||||
|
|
||||||
|
attr_map = {name.lower(): value or "" for name, value in attrs}
|
||||||
|
if tag in SKIP_TAGS or self._has_skipped_class(attr_map.get("class", "")):
|
||||||
|
self.skip_depth = 1
|
||||||
|
return
|
||||||
|
|
||||||
|
if tag not in ALLOWED_TAGS:
|
||||||
|
return
|
||||||
|
|
||||||
|
if tag == "a":
|
||||||
|
self.parts.append('<a href="__missing__">')
|
||||||
|
elif tag == "br":
|
||||||
|
self.parts.append("<br>")
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
self.parts.append(f"<{tag}>")
|
||||||
|
self.open_tags.append(tag)
|
||||||
|
|
||||||
|
def handle_startendtag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
|
||||||
|
tag = tag.lower()
|
||||||
|
if self.skip_depth:
|
||||||
|
return
|
||||||
|
attr_map = {name.lower(): value or "" for name, value in attrs}
|
||||||
|
if tag in SKIP_TAGS or self._has_skipped_class(attr_map.get("class", "")):
|
||||||
|
return
|
||||||
|
if tag == "br":
|
||||||
|
self.parts.append("<br>")
|
||||||
|
|
||||||
|
def handle_endtag(self, tag: str) -> None:
|
||||||
|
tag = tag.lower()
|
||||||
|
if self.skip_depth:
|
||||||
|
self.skip_depth -= 1
|
||||||
|
return
|
||||||
|
if tag not in ALLOWED_TAGS or tag in VOID_TAGS:
|
||||||
|
return
|
||||||
|
|
||||||
|
for index in range(len(self.open_tags) - 1, -1, -1):
|
||||||
|
if self.open_tags[index] == tag:
|
||||||
|
while len(self.open_tags) > index:
|
||||||
|
closing_tag = self.open_tags.pop()
|
||||||
|
self.parts.append(f"</{closing_tag}>")
|
||||||
|
return
|
||||||
|
|
||||||
|
def handle_data(self, data: str) -> None:
|
||||||
|
if self.skip_depth:
|
||||||
|
return
|
||||||
|
if not data:
|
||||||
|
return
|
||||||
|
self.parts.append(html.escape(data, quote=False))
|
||||||
|
|
||||||
|
def handle_entityref(self, name: str) -> None:
|
||||||
|
if self.skip_depth:
|
||||||
|
return
|
||||||
|
self.parts.append(f"&{name};")
|
||||||
|
|
||||||
|
def handle_charref(self, name: str) -> None:
|
||||||
|
if self.skip_depth:
|
||||||
|
return
|
||||||
|
self.parts.append(f"&#{name};")
|
||||||
|
|
||||||
|
def get_html(self) -> str:
|
||||||
|
while self.open_tags:
|
||||||
|
self.parts.append(f"</{self.open_tags.pop()}>")
|
||||||
|
return "".join(self.parts)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _has_skipped_class(class_value: str) -> bool:
|
||||||
|
classes = class_value.lower().split()
|
||||||
|
return any(any(part in cls for part in SKIP_CLASS_PARTS) for cls in classes)
|
||||||
|
|
||||||
|
|
||||||
|
def title_from_source(source: str) -> str:
|
||||||
|
parsed = urlparse(source)
|
||||||
|
if parsed.scheme and parsed.netloc:
|
||||||
|
if "/wiki/" in parsed.path:
|
||||||
|
return unquote(parsed.path.rsplit("/wiki/", 1)[1]).replace("_", " ")
|
||||||
|
raise ValueError(f"Unsupported Wikipedia URL: {source}")
|
||||||
|
return source.replace("_", " ").strip()
|
||||||
|
|
||||||
|
|
||||||
|
def slugify_title(title: str) -> str:
|
||||||
|
text = unicodedata.normalize("NFD", title.strip().lower())
|
||||||
|
text = "".join(ch for ch in text if unicodedata.category(ch) != "Mn")
|
||||||
|
text = text.replace("đ", "d")
|
||||||
|
text = re.sub(r"[^a-z0-9]+", "-", text)
|
||||||
|
return text.strip("-") or "wiki"
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_wikipedia_html(title: str) -> tuple[str, str]:
|
||||||
|
response = requests.get(
|
||||||
|
API_URL,
|
||||||
|
params={
|
||||||
|
"action": "parse",
|
||||||
|
"page": title,
|
||||||
|
"prop": "text",
|
||||||
|
"format": "json",
|
||||||
|
"formatversion": "2",
|
||||||
|
"redirects": "1",
|
||||||
|
"disableeditsection": "1",
|
||||||
|
},
|
||||||
|
headers={"User-Agent": USER_AGENT},
|
||||||
|
timeout=30,
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
payload = response.json()
|
||||||
|
if "error" in payload:
|
||||||
|
raise RuntimeError(json.dumps(payload["error"], ensure_ascii=False))
|
||||||
|
parsed = payload.get("parse") or {}
|
||||||
|
fetched_title = str(parsed.get("title") or title).strip()
|
||||||
|
article_html = str(parsed.get("text") or "")
|
||||||
|
if not article_html.strip():
|
||||||
|
raise RuntimeError(f"No article HTML returned for title: {title}")
|
||||||
|
return fetched_title, article_html
|
||||||
|
|
||||||
|
|
||||||
|
def sanitize_wikipedia_html(article_html: str) -> str:
|
||||||
|
parser = WikiHtmlSanitizer()
|
||||||
|
parser.feed(article_html)
|
||||||
|
parser.close()
|
||||||
|
content = html.unescape(parser.get_html())
|
||||||
|
content = normalize_fragment(content)
|
||||||
|
return content
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_fragment(content: str) -> str:
|
||||||
|
content = re.sub(r"\r\n?", "\n", content)
|
||||||
|
content = re.sub(r"[ \t\f\v]+", " ", content)
|
||||||
|
content = re.sub(r"\s*\n\s*", "\n", content)
|
||||||
|
content = re.sub(r">\s+<", "><", content)
|
||||||
|
content = re.sub(r"<(p|li|h[2-6]|blockquote)>\s*</\1>", "", content)
|
||||||
|
content = re.sub(r"<(ul|ol)>\s*</\1>", "", content)
|
||||||
|
content = re.sub(r"(</(?:p|h[2-6]|ul|ol|li|blockquote|pre)>)", r"\1\n", content)
|
||||||
|
content = re.sub(r"\n{2,}", "\n", content)
|
||||||
|
return content.strip()
|
||||||
|
|
||||||
|
|
||||||
|
def put_first_paragraph_in_blockquote(content: str) -> str:
|
||||||
|
match = re.search(r"<p>(.*?)</p>", content, flags=re.S)
|
||||||
|
if not match:
|
||||||
|
return content
|
||||||
|
|
||||||
|
quote_inner = match.group(1).strip()
|
||||||
|
before = content[: match.start()].strip()
|
||||||
|
after = content[match.end() :].strip()
|
||||||
|
parts = []
|
||||||
|
if quote_inner:
|
||||||
|
parts.append(f"<blockquote>{quote_inner}</blockquote>")
|
||||||
|
if before:
|
||||||
|
parts.append(before)
|
||||||
|
if after:
|
||||||
|
parts.append(after)
|
||||||
|
return "\n".join(parts).strip()
|
||||||
|
|
||||||
|
|
||||||
|
def write_article(source: str, output_dir: Path, output_name: str | None = None) -> Path:
|
||||||
|
title = title_from_source(source)
|
||||||
|
fetched_title, article_html = fetch_wikipedia_html(title)
|
||||||
|
content = sanitize_wikipedia_html(article_html)
|
||||||
|
content = put_first_paragraph_in_blockquote(content)
|
||||||
|
|
||||||
|
filename = output_name or f"{slugify_title(fetched_title)}.html"
|
||||||
|
if not filename.endswith(".html"):
|
||||||
|
filename = f"{filename}.html"
|
||||||
|
|
||||||
|
output_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
output_path = output_dir / filename
|
||||||
|
output_path.write_text(content + "\n", encoding="utf-8")
|
||||||
|
return output_path
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser(description="Fetch a Vietnamese Wikipedia article into UHM wiki HTML format.")
|
||||||
|
parser.add_argument("source", help="Vietnamese Wikipedia URL or page title.")
|
||||||
|
parser.add_argument("--output-dir", type=Path, default=DEFAULT_OUTPUT_DIR)
|
||||||
|
parser.add_argument("--output-name", help="Output filename. Defaults to a slug from the fetched title.")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
output_path = write_article(args.source, args.output_dir, args.output_name)
|
||||||
|
print(output_path)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
raise SystemExit(main())
|
||||||
|
except Exception as exc:
|
||||||
|
print(f"error: {exc}", file=sys.stderr)
|
||||||
|
raise SystemExit(1)
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
import ComponentCard from "@/components/common/ComponentCard";
|
|
||||||
import PageBreadcrumb from "@/components/common/PageBreadCrumb";
|
|
||||||
import Alert from "@/components/ui/alert/Alert";
|
|
||||||
import { Metadata } from "next";
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
|
||||||
title: "Next.js Alerts | TailAdmin - Next.js Dashboard Template",
|
|
||||||
description:
|
|
||||||
"This is Next.js Alerts page for TailAdmin - Next.js Tailwind CSS Admin Dashboard Template",
|
|
||||||
// other metadata
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function Alerts() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<PageBreadcrumb pageTitle="Alerts" />
|
|
||||||
<div className="space-y-5 sm:space-y-6">
|
|
||||||
<ComponentCard title="Success Alert">
|
|
||||||
<Alert
|
|
||||||
variant="success"
|
|
||||||
title="Success Message"
|
|
||||||
message="Be cautious when performing this action."
|
|
||||||
showLink={true}
|
|
||||||
linkHref="/"
|
|
||||||
linkText="Learn more"
|
|
||||||
/>
|
|
||||||
<Alert
|
|
||||||
variant="success"
|
|
||||||
title="Success Message"
|
|
||||||
message="Be cautious when performing this action."
|
|
||||||
showLink={false}
|
|
||||||
/>
|
|
||||||
</ComponentCard>
|
|
||||||
<ComponentCard title="Warning Alert">
|
|
||||||
<Alert
|
|
||||||
variant="warning"
|
|
||||||
title="Warning Message"
|
|
||||||
message="Be cautious when performing this action."
|
|
||||||
showLink={true}
|
|
||||||
linkHref="/"
|
|
||||||
linkText="Learn more"
|
|
||||||
/>
|
|
||||||
<Alert
|
|
||||||
variant="warning"
|
|
||||||
title="Warning Message"
|
|
||||||
message="Be cautious when performing this action."
|
|
||||||
showLink={false}
|
|
||||||
/>
|
|
||||||
</ComponentCard>{" "}
|
|
||||||
<ComponentCard title="Error Alert">
|
|
||||||
<Alert
|
|
||||||
variant="error"
|
|
||||||
title="Error Message"
|
|
||||||
message="Be cautious when performing this action."
|
|
||||||
showLink={true}
|
|
||||||
linkHref="/"
|
|
||||||
linkText="Learn more"
|
|
||||||
/>
|
|
||||||
<Alert
|
|
||||||
variant="error"
|
|
||||||
title="Error Message"
|
|
||||||
message="Be cautious when performing this action."
|
|
||||||
showLink={false}
|
|
||||||
/>
|
|
||||||
</ComponentCard>{" "}
|
|
||||||
<ComponentCard title="Info Alert">
|
|
||||||
<Alert
|
|
||||||
variant="info"
|
|
||||||
title="Info Message"
|
|
||||||
message="Be cautious when performing this action."
|
|
||||||
showLink={true}
|
|
||||||
linkHref="/"
|
|
||||||
linkText="Learn more"
|
|
||||||
/>
|
|
||||||
<Alert
|
|
||||||
variant="info"
|
|
||||||
title="Info Message"
|
|
||||||
message="Be cautious when performing this action."
|
|
||||||
showLink={false}
|
|
||||||
/>
|
|
||||||
</ComponentCard>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,126 +0,0 @@
|
|||||||
import ComponentCard from "@/components/common/ComponentCard";
|
|
||||||
import PageBreadcrumb from "@/components/common/PageBreadCrumb";
|
|
||||||
import Avatar from "@/components/ui/avatar/Avatar";
|
|
||||||
import { Metadata } from "next";
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
|
||||||
title: "Next.js Avatars | TailAdmin - Next.js Dashboard Template",
|
|
||||||
description:
|
|
||||||
"This is Next.js Avatars page for TailAdmin - Next.js Tailwind CSS Admin Dashboard Template",
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function AvatarPage() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<PageBreadcrumb pageTitle="Avatar" />
|
|
||||||
<div className="space-y-5 sm:space-y-6">
|
|
||||||
<ComponentCard title="Default Avatar">
|
|
||||||
{/* Default Avatar (No Status) */}
|
|
||||||
<div className="flex flex-col items-center justify-center gap-5 sm:flex-row">
|
|
||||||
<Avatar src="/images/user/user-01.jpg" size="xsmall" />
|
|
||||||
<Avatar src="/images/user/user-01.jpg" size="small" />
|
|
||||||
<Avatar src="/images/user/user-01.jpg" size="medium" />
|
|
||||||
<Avatar src="/images/user/user-01.jpg" size="large" />
|
|
||||||
<Avatar src="/images/user/user-01.jpg" size="xlarge" />
|
|
||||||
<Avatar src="/images/user/user-01.jpg" size="xxlarge" />
|
|
||||||
</div>
|
|
||||||
</ComponentCard>
|
|
||||||
<ComponentCard title="Avatar with online indicator">
|
|
||||||
<div className="flex flex-col items-center justify-center gap-5 sm:flex-row">
|
|
||||||
<Avatar
|
|
||||||
src="/images/user/user-01.jpg"
|
|
||||||
size="xsmall"
|
|
||||||
status="online"
|
|
||||||
/>
|
|
||||||
<Avatar
|
|
||||||
src="/images/user/user-01.jpg"
|
|
||||||
size="small"
|
|
||||||
status="online"
|
|
||||||
/>
|
|
||||||
<Avatar
|
|
||||||
src="/images/user/user-01.jpg"
|
|
||||||
size="medium"
|
|
||||||
status="online"
|
|
||||||
/>
|
|
||||||
<Avatar
|
|
||||||
src="/images/user/user-01.jpg"
|
|
||||||
size="large"
|
|
||||||
status="online"
|
|
||||||
/>
|
|
||||||
<Avatar
|
|
||||||
src="/images/user/user-01.jpg"
|
|
||||||
size="xlarge"
|
|
||||||
status="online"
|
|
||||||
/>
|
|
||||||
<Avatar
|
|
||||||
src="/images/user/user-01.jpg"
|
|
||||||
size="xxlarge"
|
|
||||||
status="online"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</ComponentCard>
|
|
||||||
<ComponentCard title="Avatar with Offline indicator">
|
|
||||||
<div className="flex flex-col items-center justify-center gap-5 sm:flex-row">
|
|
||||||
<Avatar
|
|
||||||
src="/images/user/user-01.jpg"
|
|
||||||
size="xsmall"
|
|
||||||
status="offline"
|
|
||||||
/>
|
|
||||||
<Avatar
|
|
||||||
src="/images/user/user-01.jpg"
|
|
||||||
size="small"
|
|
||||||
status="offline"
|
|
||||||
/>
|
|
||||||
<Avatar
|
|
||||||
src="/images/user/user-01.jpg"
|
|
||||||
size="medium"
|
|
||||||
status="offline"
|
|
||||||
/>
|
|
||||||
<Avatar
|
|
||||||
src="/images/user/user-01.jpg"
|
|
||||||
size="large"
|
|
||||||
status="offline"
|
|
||||||
/>
|
|
||||||
<Avatar
|
|
||||||
src="/images/user/user-01.jpg"
|
|
||||||
size="xlarge"
|
|
||||||
status="offline"
|
|
||||||
/>
|
|
||||||
<Avatar
|
|
||||||
src="/images/user/user-01.jpg"
|
|
||||||
size="xxlarge"
|
|
||||||
status="offline"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</ComponentCard>{" "}
|
|
||||||
<ComponentCard title="Avatar with busy indicator">
|
|
||||||
<div className="flex flex-col items-center justify-center gap-5 sm:flex-row">
|
|
||||||
<Avatar
|
|
||||||
src="/images/user/user-01.jpg"
|
|
||||||
size="xsmall"
|
|
||||||
status="busy"
|
|
||||||
/>
|
|
||||||
<Avatar src="/images/user/user-01.jpg" size="small" status="busy" />
|
|
||||||
<Avatar
|
|
||||||
src="/images/user/user-01.jpg"
|
|
||||||
size="medium"
|
|
||||||
status="busy"
|
|
||||||
/>
|
|
||||||
<Avatar src="/images/user/user-01.jpg" size="large" status="busy" />
|
|
||||||
<Avatar
|
|
||||||
src="/images/user/user-01.jpg"
|
|
||||||
size="xlarge"
|
|
||||||
status="busy"
|
|
||||||
/>
|
|
||||||
<Avatar
|
|
||||||
src="/images/user/user-01.jpg"
|
|
||||||
size="xxlarge"
|
|
||||||
status="busy"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</ComponentCard>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,221 +0,0 @@
|
|||||||
import PageBreadcrumb from "@/components/common/PageBreadCrumb";
|
|
||||||
import Badge from "@/components/ui/badge/Badge";
|
|
||||||
import { PlusIcon } from "@/icons";
|
|
||||||
import { Metadata } from "next";
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
|
||||||
title: "Next.js Badge | TailAdmin - Next.js Dashboard Template",
|
|
||||||
description:
|
|
||||||
"This is Next.js Badge page for TailAdmin - Next.js Tailwind CSS Admin Dashboard Template",
|
|
||||||
// other metadata
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function BadgePage() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<PageBreadcrumb pageTitle="Badges" />
|
|
||||||
<div className="space-y-5 sm:space-y-6">
|
|
||||||
<div className="rounded-2xl border border-gray-200 bg-white dark:border-gray-800 dark:bg-white/[0.03]">
|
|
||||||
<div className="px-6 py-5">
|
|
||||||
<h3 className="text-base font-medium text-gray-800 dark:text-white/90">
|
|
||||||
With Light Background
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<div className="p-6 border-t border-gray-100 dark:border-gray-800 xl:p-10">
|
|
||||||
<div className="flex flex-wrap gap-4 sm:items-center sm:justify-center">
|
|
||||||
{/* Light Variant */}
|
|
||||||
<Badge variant="light" color="primary">
|
|
||||||
Primary
|
|
||||||
</Badge>
|
|
||||||
<Badge variant="light" color="success">
|
|
||||||
Success
|
|
||||||
</Badge>{" "}
|
|
||||||
<Badge variant="light" color="error">
|
|
||||||
Error
|
|
||||||
</Badge>{" "}
|
|
||||||
<Badge variant="light" color="warning">
|
|
||||||
Warning
|
|
||||||
</Badge>{" "}
|
|
||||||
<Badge variant="light" color="info">
|
|
||||||
Info
|
|
||||||
</Badge>
|
|
||||||
<Badge variant="light" color="light">
|
|
||||||
Light
|
|
||||||
</Badge>
|
|
||||||
<Badge variant="light" color="dark">
|
|
||||||
Dark
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="rounded-2xl border border-gray-200 bg-white dark:border-gray-800 dark:bg-white/[0.03]">
|
|
||||||
<div className="px-6 py-5">
|
|
||||||
<h3 className="text-base font-medium text-gray-800 dark:text-white/90">
|
|
||||||
With Solid Background
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<div className="p-6 border-t border-gray-100 dark:border-gray-800 xl:p-10">
|
|
||||||
<div className="flex flex-wrap gap-4 sm:items-center sm:justify-center">
|
|
||||||
{/* Light Variant */}
|
|
||||||
<Badge variant="solid" color="primary">
|
|
||||||
Primary
|
|
||||||
</Badge>
|
|
||||||
<Badge variant="solid" color="success">
|
|
||||||
Success
|
|
||||||
</Badge>{" "}
|
|
||||||
<Badge variant="solid" color="error">
|
|
||||||
Error
|
|
||||||
</Badge>{" "}
|
|
||||||
<Badge variant="solid" color="warning">
|
|
||||||
Warning
|
|
||||||
</Badge>{" "}
|
|
||||||
<Badge variant="solid" color="info">
|
|
||||||
Info
|
|
||||||
</Badge>
|
|
||||||
<Badge variant="solid" color="light">
|
|
||||||
Light
|
|
||||||
</Badge>
|
|
||||||
<Badge variant="solid" color="dark">
|
|
||||||
Dark
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="rounded-2xl border border-gray-200 bg-white dark:border-gray-800 dark:bg-white/[0.03]">
|
|
||||||
<div className="px-6 py-5">
|
|
||||||
<h3 className="text-base font-medium text-gray-800 dark:text-white/90">
|
|
||||||
Light Background with Left Icon
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<div className="p-6 border-t border-gray-100 dark:border-gray-800 xl:p-10">
|
|
||||||
<div className="flex flex-wrap gap-4 sm:items-center sm:justify-center">
|
|
||||||
<Badge variant="light" color="primary" startIcon={<PlusIcon />}>
|
|
||||||
Primary
|
|
||||||
</Badge>
|
|
||||||
<Badge variant="light" color="success" startIcon={<PlusIcon />}>
|
|
||||||
Success
|
|
||||||
</Badge>{" "}
|
|
||||||
<Badge variant="light" color="error" startIcon={<PlusIcon />}>
|
|
||||||
Error
|
|
||||||
</Badge>{" "}
|
|
||||||
<Badge variant="light" color="warning" startIcon={<PlusIcon />}>
|
|
||||||
Warning
|
|
||||||
</Badge>{" "}
|
|
||||||
<Badge variant="light" color="info" startIcon={<PlusIcon />}>
|
|
||||||
Info
|
|
||||||
</Badge>
|
|
||||||
<Badge variant="light" color="light" startIcon={<PlusIcon />}>
|
|
||||||
Light
|
|
||||||
</Badge>
|
|
||||||
<Badge variant="light" color="dark" startIcon={<PlusIcon />}>
|
|
||||||
Dark
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="rounded-2xl border border-gray-200 bg-white dark:border-gray-800 dark:bg-white/[0.03]">
|
|
||||||
<div className="px-6 py-5">
|
|
||||||
<h3 className="text-base font-medium text-gray-800 dark:text-white/90">
|
|
||||||
Solid Background with Left Icon
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<div className="p-6 border-t border-gray-100 dark:border-gray-800 xl:p-10">
|
|
||||||
<div className="flex flex-wrap gap-4 sm:items-center sm:justify-center">
|
|
||||||
<Badge variant="solid" color="primary" startIcon={<PlusIcon />}>
|
|
||||||
Primary
|
|
||||||
</Badge>
|
|
||||||
<Badge variant="solid" color="success" startIcon={<PlusIcon />}>
|
|
||||||
Success
|
|
||||||
</Badge>{" "}
|
|
||||||
<Badge variant="solid" color="error" startIcon={<PlusIcon />}>
|
|
||||||
Error
|
|
||||||
</Badge>{" "}
|
|
||||||
<Badge variant="solid" color="warning" startIcon={<PlusIcon />}>
|
|
||||||
Warning
|
|
||||||
</Badge>{" "}
|
|
||||||
<Badge variant="solid" color="info" startIcon={<PlusIcon />}>
|
|
||||||
Info
|
|
||||||
</Badge>
|
|
||||||
<Badge variant="solid" color="light" startIcon={<PlusIcon />}>
|
|
||||||
Light
|
|
||||||
</Badge>
|
|
||||||
<Badge variant="solid" color="dark" startIcon={<PlusIcon />}>
|
|
||||||
Dark
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="rounded-2xl border border-gray-200 bg-white dark:border-gray-800 dark:bg-white/[0.03]">
|
|
||||||
<div className="px-6 py-5">
|
|
||||||
<h3 className="text-base font-medium text-gray-800 dark:text-white/90">
|
|
||||||
Light Background with Right Icon
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<div className="p-6 border-t border-gray-100 dark:border-gray-800 xl:p-10">
|
|
||||||
<div className="flex flex-wrap gap-4 sm:items-center sm:justify-center">
|
|
||||||
<Badge variant="light" color="primary" endIcon={<PlusIcon />}>
|
|
||||||
Primary
|
|
||||||
</Badge>
|
|
||||||
<Badge variant="light" color="success" endIcon={<PlusIcon />}>
|
|
||||||
Success
|
|
||||||
</Badge>{" "}
|
|
||||||
<Badge variant="light" color="error" endIcon={<PlusIcon />}>
|
|
||||||
Error
|
|
||||||
</Badge>{" "}
|
|
||||||
<Badge variant="light" color="warning" endIcon={<PlusIcon />}>
|
|
||||||
Warning
|
|
||||||
</Badge>{" "}
|
|
||||||
<Badge variant="light" color="info" endIcon={<PlusIcon />}>
|
|
||||||
Info
|
|
||||||
</Badge>
|
|
||||||
<Badge variant="light" color="light" endIcon={<PlusIcon />}>
|
|
||||||
Light
|
|
||||||
</Badge>
|
|
||||||
<Badge variant="light" color="dark" endIcon={<PlusIcon />}>
|
|
||||||
Dark
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="rounded-2xl border border-gray-200 bg-white dark:border-gray-800 dark:bg-white/[0.03]">
|
|
||||||
<div className="px-6 py-5">
|
|
||||||
<h3 className="text-base font-medium text-gray-800 dark:text-white/90">
|
|
||||||
Solid Background with Right Icon
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<div className="p-6 border-t border-gray-100 dark:border-gray-800 xl:p-10">
|
|
||||||
<div className="flex flex-wrap gap-4 sm:items-center sm:justify-center">
|
|
||||||
<Badge variant="solid" color="primary" endIcon={<PlusIcon />}>
|
|
||||||
Primary
|
|
||||||
</Badge>
|
|
||||||
<Badge variant="solid" color="success" endIcon={<PlusIcon />}>
|
|
||||||
Success
|
|
||||||
</Badge>{" "}
|
|
||||||
<Badge variant="solid" color="error" endIcon={<PlusIcon />}>
|
|
||||||
Error
|
|
||||||
</Badge>{" "}
|
|
||||||
<Badge variant="solid" color="warning" endIcon={<PlusIcon />}>
|
|
||||||
Warning
|
|
||||||
</Badge>{" "}
|
|
||||||
<Badge variant="solid" color="info" endIcon={<PlusIcon />}>
|
|
||||||
Info
|
|
||||||
</Badge>
|
|
||||||
<Badge variant="solid" color="light" endIcon={<PlusIcon />}>
|
|
||||||
Light
|
|
||||||
</Badge>
|
|
||||||
<Badge variant="solid" color="dark" endIcon={<PlusIcon />}>
|
|
||||||
Dark
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
import ComponentCard from "@/components/common/ComponentCard";
|
|
||||||
import PageBreadcrumb from "@/components/common/PageBreadCrumb";
|
|
||||||
import Button from "@/components/ui/button/Button";
|
|
||||||
import { BoxIcon } from "@/icons";
|
|
||||||
import { Metadata } from "next";
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
|
||||||
title: "Next.js Buttons | TailAdmin - Next.js Dashboard Template",
|
|
||||||
description:
|
|
||||||
"This is Next.js Buttons page for TailAdmin - Next.js Tailwind CSS Admin Dashboard Template",
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function Buttons() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<PageBreadcrumb pageTitle="Buttons" />
|
|
||||||
<div className="space-y-5 sm:space-y-6">
|
|
||||||
{/* Primary Button */}
|
|
||||||
<ComponentCard title="Primary Button">
|
|
||||||
<div className="flex items-center gap-5">
|
|
||||||
<Button size="sm" variant="primary">
|
|
||||||
Button Text
|
|
||||||
</Button>
|
|
||||||
<Button size="md" variant="primary">
|
|
||||||
Button Text
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</ComponentCard>
|
|
||||||
{/* Primary Button with Start Icon */}
|
|
||||||
<ComponentCard title="Primary Button with Left Icon">
|
|
||||||
<div className="flex items-center gap-5">
|
|
||||||
<Button size="sm" variant="primary" startIcon={<BoxIcon />}>
|
|
||||||
Button Text
|
|
||||||
</Button>
|
|
||||||
<Button size="md" variant="primary" startIcon={<BoxIcon />}>
|
|
||||||
Button Text
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</ComponentCard>{" "}
|
|
||||||
{/* Primary Button with Start Icon */}
|
|
||||||
<ComponentCard title="Primary Button with Right Icon">
|
|
||||||
<div className="flex items-center gap-5">
|
|
||||||
<Button size="sm" variant="primary" endIcon={<BoxIcon />}>
|
|
||||||
Button Text
|
|
||||||
</Button>
|
|
||||||
<Button size="md" variant="primary" endIcon={<BoxIcon />}>
|
|
||||||
Button Text
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</ComponentCard>
|
|
||||||
{/* Outline Button */}
|
|
||||||
<ComponentCard title="Secondary Button">
|
|
||||||
<div className="flex items-center gap-5">
|
|
||||||
{/* Outline Button */}
|
|
||||||
<Button size="sm" variant="outline">
|
|
||||||
Button Text
|
|
||||||
</Button>
|
|
||||||
<Button size="md" variant="outline">
|
|
||||||
Button Text
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</ComponentCard>
|
|
||||||
{/* Outline Button with Start Icon */}
|
|
||||||
<ComponentCard title="Outline Button with Left Icon">
|
|
||||||
<div className="flex items-center gap-5">
|
|
||||||
<Button size="sm" variant="outline" startIcon={<BoxIcon />}>
|
|
||||||
Button Text
|
|
||||||
</Button>
|
|
||||||
<Button size="md" variant="outline" startIcon={<BoxIcon />}>
|
|
||||||
Button Text
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</ComponentCard>{" "}
|
|
||||||
{/* Outline Button with Start Icon */}
|
|
||||||
<ComponentCard title="Outline Button with Right Icon">
|
|
||||||
<div className="flex items-center gap-5">
|
|
||||||
<Button size="sm" variant="outline" endIcon={<BoxIcon />}>
|
|
||||||
Button Text
|
|
||||||
</Button>
|
|
||||||
<Button size="md" variant="outline" endIcon={<BoxIcon />}>
|
|
||||||
Button Text
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</ComponentCard>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
import ComponentCard from "@/components/common/ComponentCard";
|
|
||||||
import PageBreadcrumb from "@/components/common/PageBreadCrumb";
|
|
||||||
import ResponsiveImage from "@/components/ui/images/ResponsiveImage";
|
|
||||||
import ThreeColumnImageGrid from "@/components/ui/images/ThreeColumnImageGrid";
|
|
||||||
import TwoColumnImageGrid from "@/components/ui/images/TwoColumnImageGrid";
|
|
||||||
import { Metadata } from "next";
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
|
||||||
title: "Next.js Images | TailAdmin - Next.js Dashboard Template",
|
|
||||||
description:
|
|
||||||
"This is Next.js Images page for TailAdmin - Next.js Tailwind CSS Admin Dashboard Template",
|
|
||||||
// other metadata
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function Images() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<PageBreadcrumb pageTitle="Images" />
|
|
||||||
<div className="space-y-5 sm:space-y-6">
|
|
||||||
<ComponentCard title="Responsive image">
|
|
||||||
<ResponsiveImage />
|
|
||||||
</ComponentCard>
|
|
||||||
<ComponentCard title="Image in 2 Grid">
|
|
||||||
<TwoColumnImageGrid />
|
|
||||||
</ComponentCard>
|
|
||||||
<ComponentCard title="Image in 3 Grid">
|
|
||||||
<ThreeColumnImageGrid />
|
|
||||||
</ComponentCard>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
import PageBreadcrumb from "@/components/common/PageBreadCrumb";
|
|
||||||
import DefaultModal from "@/components/example/ModalExample/DefaultModal";
|
|
||||||
import FormInModal from "@/components/example/ModalExample/FormInModal";
|
|
||||||
import FullScreenModal from "@/components/example/ModalExample/FullScreenModal";
|
|
||||||
import ModalBasedAlerts from "@/components/example/ModalExample/ModalBasedAlerts";
|
|
||||||
import VerticallyCenteredModal from "@/components/example/ModalExample/VerticallyCenteredModal";
|
|
||||||
import { Metadata } from "next";
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
|
||||||
title: "Next.js Modals | TailAdmin - Next.js Dashboard Template",
|
|
||||||
description:
|
|
||||||
"This is Next.js Modals page for TailAdmin - Next.js Tailwind CSS Admin Dashboard Template",
|
|
||||||
// other metadata
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function Modals() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<PageBreadcrumb pageTitle="Modals" />
|
|
||||||
<div className="grid grid-cols-1 gap-5 xl:grid-cols-2 xl:gap-6">
|
|
||||||
<DefaultModal />
|
|
||||||
<VerticallyCenteredModal />
|
|
||||||
<FormInModal />
|
|
||||||
<FullScreenModal />
|
|
||||||
<ModalBasedAlerts />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import PageBreadcrumb from "@/components/common/PageBreadCrumb";
|
|
||||||
import VideosExample from "@/components/ui/video/VideosExample";
|
|
||||||
import { Metadata } from "next";
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
|
||||||
title: "Next.js Videos | TailAdmin - Next.js Dashboard Template",
|
|
||||||
description:
|
|
||||||
"This is Next.js Videos page for TailAdmin - Next.js Tailwind CSS Admin Dashboard Template",
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function VideoPage() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<PageBreadcrumb pageTitle="Videos" />
|
|
||||||
|
|
||||||
<VideosExample />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
@@ -65,6 +63,12 @@ export default function LandingPage() {
|
|||||||
<div className="text-xl font-bold tracking-widest text-[#2D3A3A] uppercase">
|
<div className="text-xl font-bold tracking-widest text-[#2D3A3A] uppercase">
|
||||||
<span className="text-[#A88B4C]">Geo</span>History
|
<span className="text-[#A88B4C]">Geo</span>History
|
||||||
</div>
|
</div>
|
||||||
|
<Link
|
||||||
|
href="/"
|
||||||
|
className="rounded-xl border border-[#A88B4C]/30 bg-[#FDFBF7]/80 px-4 py-2 text-sm font-bold text-[#2D3A3A] transition hover:bg-[#A88B4C] hover:text-white"
|
||||||
|
>
|
||||||
|
Quay lại bản đồ
|
||||||
|
</Link>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main className="max-w-6xl mx-auto px-6 pt-8 pb-24 flex flex-col gap-32 w-full relative">
|
<main className="max-w-6xl mx-auto px-6 pt-8 pb-24 flex flex-col gap-32 w-full relative">
|
||||||
@@ -96,6 +100,12 @@ export default function LandingPage() {
|
|||||||
>
|
>
|
||||||
Khám phá sứ mệnh
|
Khám phá sứ mệnh
|
||||||
</Link>
|
</Link>
|
||||||
|
<Link
|
||||||
|
href="/"
|
||||||
|
className="px-8 py-4 border border-[#A88B4C]/30 bg-[#FDFBF7]/80 text-[#2D3A3A] font-bold rounded-xl hover:bg-[#2D3A3A] hover:text-white"
|
||||||
|
>
|
||||||
|
Về bản đồ
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -1,142 +0,0 @@
|
|||||||
import type { ProjectCommit } from "@/uhm/api/projects";
|
|
||||||
import type { EntitySnapshot } from "@/uhm/types/entities";
|
|
||||||
import type { Feature, Geometry } from "@/uhm/types/geo";
|
|
||||||
import type { BattleReplay } from "@/uhm/types/projects";
|
|
||||||
import type { WikiSnapshot } from "@/uhm/types/wiki";
|
|
||||||
import { normalizeTimelineYearValue } from "@/uhm/lib/utils/timeline";
|
|
||||||
|
|
||||||
// Giới hạn kích thước panel khi drag resize để tránh layout bị vỡ.
|
|
||||||
export function clampNumber(value: number, min: number, max: number): number {
|
|
||||||
if (value < min) return min;
|
|
||||||
if (value > max) return max;
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tạo label ngắn cho commit history, ưu tiên summary người dùng nhập.
|
|
||||||
export function formatCommitTitle(commit: ProjectCommit): string {
|
|
||||||
return commit.edit_summary?.trim() || `Commit ${commit.id.slice(0, 8)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Kiểm tra feature có nằm trong năm timeline đang active hay không.
|
|
||||||
export function isFeatureVisibleAtYear(feature: Feature, year: number): boolean {
|
|
||||||
const start = normalizeTimelineYearValue(feature.properties.time_start);
|
|
||||||
const end = normalizeTimelineYearValue(feature.properties.time_end);
|
|
||||||
if (start !== null && year < start) return false;
|
|
||||||
if (end !== null && year > end) return false;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Chuẩn hóa wiki snapshot để so sánh dirty-state ổn định, không phụ thuộc thứ tự mảng.
|
|
||||||
export function normalizeWikisForCompare(input: WikiSnapshot[] | null | undefined) {
|
|
||||||
const list = Array.isArray(input) ? input : [];
|
|
||||||
return list
|
|
||||||
.filter((w) => w && typeof w.id === "string" && w.id.trim().length > 0)
|
|
||||||
.filter((w) => {
|
|
||||||
if (w.source === "ref") return true;
|
|
||||||
if (w.operation === "create" || w.operation === "update" || w.operation === "delete") return true;
|
|
||||||
const title = typeof w.title === "string" ? w.title.trim() : "";
|
|
||||||
const doc = typeof w.doc === "string" ? w.doc.trim() : "";
|
|
||||||
return title.length > 0 || (w.doc !== null && doc.length > 0);
|
|
||||||
})
|
|
||||||
.map((w) => ({
|
|
||||||
id: w.id,
|
|
||||||
source: w.source,
|
|
||||||
title: typeof w.title === "string" ? w.title.trim() : "",
|
|
||||||
slug: typeof w.slug === "string" ? w.slug : null,
|
|
||||||
doc: w.doc === null ? null : typeof w.doc === "string" ? w.doc.trim() : null,
|
|
||||||
}))
|
|
||||||
.sort((a, b) => a.id.localeCompare(b.id));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Chuẩn hóa entity snapshot để phát hiện thay đổi name/description/source.
|
|
||||||
export function normalizeEntitiesForCompare(input: EntitySnapshot[] | null | undefined) {
|
|
||||||
const list = Array.isArray(input) ? input : [];
|
|
||||||
return list
|
|
||||||
.filter((e) => e && (typeof e.id === "string" || typeof e.id === "number"))
|
|
||||||
.map((e) => ({
|
|
||||||
id: String(e.id),
|
|
||||||
source: e.source,
|
|
||||||
name: typeof e.name === "string" ? e.name.trim() : "",
|
|
||||||
description: e.description == null ? null : String(e.description),
|
|
||||||
time_start: normalizeTimelineYearValue(e.time_start),
|
|
||||||
time_end: normalizeTimelineYearValue(e.time_end),
|
|
||||||
}))
|
|
||||||
.sort((a, b) => a.id.localeCompare(b.id));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Chuẩn hóa binding entity-wiki để dirty check không bị nhiễu bởi thứ tự.
|
|
||||||
export function normalizeEntityWikiLinksForCompare(
|
|
||||||
input: Array<{ entity_id: string; wiki_id: string; operation?: string }> | null | undefined
|
|
||||||
) {
|
|
||||||
const list = Array.isArray(input) ? input : [];
|
|
||||||
return list
|
|
||||||
.filter((l) => l && typeof l.entity_id === "string" && typeof l.wiki_id === "string")
|
|
||||||
.map((l) => ({
|
|
||||||
entity_id: l.entity_id,
|
|
||||||
wiki_id: l.wiki_id,
|
|
||||||
operation: l.operation === "delete" ? "delete" : "binding",
|
|
||||||
}))
|
|
||||||
.sort((a, b) => (a.entity_id + a.wiki_id).localeCompare(b.entity_id + b.wiki_id));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Chuẩn hóa replay để phát hiện thay đổi script/target geometry.
|
|
||||||
export function normalizeReplaysForCompare(input: BattleReplay[] | null | undefined) {
|
|
||||||
const list = Array.isArray(input) ? input : [];
|
|
||||||
return list
|
|
||||||
.filter((replay) => replay && typeof replay.geometry_id === "string" && replay.geometry_id.trim().length > 0)
|
|
||||||
.map((replay) => ({
|
|
||||||
id: typeof replay.id === "string" ? replay.id : replay.geometry_id,
|
|
||||||
geometry_id: replay.geometry_id,
|
|
||||||
target_geometry_ids: normalizeReplayTargetGeometryIdsForCompare(
|
|
||||||
replay.target_geometry_ids,
|
|
||||||
replay.geometry_id
|
|
||||||
),
|
|
||||||
detail: Array.isArray(replay.detail) ? replay.detail : [],
|
|
||||||
}))
|
|
||||||
.sort((a, b) => a.geometry_id.localeCompare(b.geometry_id));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bảo toàn geometry chính ở vị trí đầu và loại bỏ id trùng trong replay target list.
|
|
||||||
function normalizeReplayTargetGeometryIdsForCompare(
|
|
||||||
input: string[] | null | undefined,
|
|
||||||
geometryId: string
|
|
||||||
) {
|
|
||||||
const orderedIds: string[] = [];
|
|
||||||
const seen = new Set<string>();
|
|
||||||
|
|
||||||
const pushId = (rawId: string | number | null | undefined) => {
|
|
||||||
if (rawId == null) return;
|
|
||||||
const id = String(rawId).trim();
|
|
||||||
if (!id || seen.has(id)) return;
|
|
||||||
seen.add(id);
|
|
||||||
orderedIds.push(id);
|
|
||||||
};
|
|
||||||
|
|
||||||
pushId(geometryId);
|
|
||||||
for (const rawId of input || []) pushId(rawId);
|
|
||||||
return orderedIds;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate tối thiểu geometry trả về từ search trước khi đưa vào draft.
|
|
||||||
export function normalizeGeoSearchGeometry(value: unknown): Geometry | null {
|
|
||||||
if (!value || typeof value !== "object") return null;
|
|
||||||
const geometry = value as Record<string, unknown>;
|
|
||||||
if (typeof geometry.type !== "string") return null;
|
|
||||||
if (!("coordinates" in geometry)) return null;
|
|
||||||
return value as Geometry;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Chuẩn hóa danh sách binding id từ API search GEO.
|
|
||||||
export function normalizeGeoSearchBindingIds(value: unknown): string[] {
|
|
||||||
if (!Array.isArray(value)) return [];
|
|
||||||
const deduped: string[] = [];
|
|
||||||
const seen = new Set<string>();
|
|
||||||
for (const rawId of value) {
|
|
||||||
if (typeof rawId !== "string" && typeof rawId !== "number") continue;
|
|
||||||
const id = String(rawId).trim();
|
|
||||||
if (!id || seen.has(id)) continue;
|
|
||||||
seen.add(id);
|
|
||||||
deduped.push(id);
|
|
||||||
}
|
|
||||||
return deduped;
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,280 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { useRef, useState } from 'react';
|
||||||
|
|
||||||
|
type GuideSection = {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
summary: string;
|
||||||
|
steps: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const guideSections: GuideSection[] = [
|
||||||
|
{
|
||||||
|
id: 'start',
|
||||||
|
title: 'Bắt đầu xem bản đồ',
|
||||||
|
summary: 'Route / là màn bản đồ lịch sử tương tác. Người dùng có thể xem dữ liệu theo năm, chọn lớp hiển thị, mở wiki và phát replay sự kiện.',
|
||||||
|
steps: [
|
||||||
|
'Khi vào trang chủ, chờ bản đồ tải xong hoặc nhấn vào màn hình chờ để vào nhanh hơn.',
|
||||||
|
'Dùng chuột để kéo bản đồ, cuộn để phóng to/thu nhỏ. Trên điện thoại, dùng một ngón để kéo và hai ngón để phóng to/thu nhỏ bản đồ.',
|
||||||
|
'Các vùng, đường, điểm hoặc biểu tượng trên bản đồ là dữ liệu lịch sử. Nhấn vào một đối tượng để xem thông tin liên quan.',
|
||||||
|
'Nếu bản đồ đang tải dữ liệu theo năm, chờ vài giây để các lớp lịch sử cập nhật.'
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'timeline',
|
||||||
|
title: 'Dùng thanh thời gian',
|
||||||
|
summary: 'Thanh thời gian ở cạnh dưới dùng để chuyển bản đồ về một mốc năm cụ thể.',
|
||||||
|
steps: [
|
||||||
|
'Kéo thước timeline để đổi năm nhanh.',
|
||||||
|
'Nhập trực tiếp năm vào ô số nếu đã biết mốc cần xem.',
|
||||||
|
'Nhấn nút - hoặc + để giảm/tăng từng năm. Giữ nút để chạy liên tục.',
|
||||||
|
'Bật lọc timeline để chỉ ưu tiên dữ liệu phù hợp với năm đang chọn. Tắt lọc nếu muốn xem nhiều dữ liệu hơn cùng lúc.',
|
||||||
|
'Trên màn hình desktop có ô Range. Tăng Range nếu muốn mở rộng khoảng năm gần mốc đang chọn, ví dụ xem thêm dữ liệu trong vài năm lân cận.'
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'search',
|
||||||
|
title: 'Tìm địa danh và dữ liệu lịch sử',
|
||||||
|
summary: 'Ô tìm kiếm giúp đi nhanh tới địa danh hiện tại, wiki hoặc geometry lịch sử.',
|
||||||
|
steps: [
|
||||||
|
'Nhập tên địa danh, nhân vật, sự kiện hoặc thực thể lịch sử vào ô tìm kiếm phía trên bản đồ.',
|
||||||
|
'Chọn kết quả phù hợp để bản đồ tự di chuyển tới vị trí liên quan.',
|
||||||
|
'Nếu kết quả là địa danh hiện tại, bản đồ sẽ focus tới tọa độ hiện nay.',
|
||||||
|
'Nếu kết quả có geometry lịch sử, hệ thống sẽ chọn đối tượng đó và có thể tự đổi timeline về năm bắt đầu của dữ liệu.'
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'layers',
|
||||||
|
title: 'Bật/tắt lớp bản đồ',
|
||||||
|
summary: 'Bảng lớp nằm bên trái, dùng để kiểm soát nền bản đồ và loại dữ liệu lịch sử đang hiển thị.',
|
||||||
|
steps: [
|
||||||
|
'Dùng nhóm lớp nền để đổi hoặc ẩn/hiện các nền như bản đồ cơ sở, vệ tinh hoặc các lớp tham chiếu.',
|
||||||
|
'Dùng nhóm geometry để bật/tắt từng loại dữ liệu như quốc gia, vùng, thành phố, tuyến đường, trận đánh, cảng, đền, pháo đài.',
|
||||||
|
'Nếu bản đồ quá nhiều chi tiết, hãy tắt bớt loại geometry chưa cần xem.',
|
||||||
|
'Có thể ẩn bảng lớp để mở rộng diện tích bản đồ. Mở menu tròn bên trái rồi nhấn nút hiện bảng lớp để bật lại.'
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'wiki',
|
||||||
|
title: 'Mở wiki từ bản đồ',
|
||||||
|
summary: 'Khi chọn một đối tượng lịch sử, panel bên phải hoặc phía dưới trên mobile sẽ hiển thị thông tin wiki liên quan.',
|
||||||
|
steps: [
|
||||||
|
'Nhấn vào vùng, điểm hoặc đường trên bản đồ để chọn đối tượng.',
|
||||||
|
'Nếu đối tượng có nhiều wiki liên quan, chọn bài viết phù hợp trong danh sách.',
|
||||||
|
'Trong nội dung wiki, nhấn các liên kết nội bộ để mở bài khác. Nếu bài đó có nhiều geometry, chọn geometry muốn focus.',
|
||||||
|
'Kéo cạnh panel để đổi kích thước trên desktop. Trên mobile, panel nằm phía dưới và có thể điều chỉnh chiều cao.',
|
||||||
|
'Nhấn nút đóng trong panel để quay lại chế độ xem bản đồ rộng hơn.'
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'replay',
|
||||||
|
title: 'Xem replay diễn biến lịch sử',
|
||||||
|
summary: 'Replay mô phỏng các bước diễn biến của một sự kiện hoặc trận đánh khi dữ liệu đó có kịch bản.',
|
||||||
|
steps: [
|
||||||
|
'Chọn một đối tượng trên bản đồ. Nếu đối tượng có replay, nút phát ở thanh thời gian sẽ khả dụng.',
|
||||||
|
'Nhấn nút phát để bắt đầu xem diễn biến.',
|
||||||
|
'Trong lúc replay chạy, bản đồ có thể tự di chuyển, đổi năm, làm nổi bật đường đi, vùng ảnh hưởng hoặc các điểm quan trọng.',
|
||||||
|
'Dùng các nút điều khiển replay để tạm dừng, phát lại, đổi tốc độ hoặc thoát khỏi chế độ replay.',
|
||||||
|
'Nếu không thấy nút phát hoạt động, đối tượng đang chọn có thể chưa có replay.'
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'menu',
|
||||||
|
title: 'Menu trợ giúp và tài khoản',
|
||||||
|
summary: 'Nút tròn bên trái mở các lối tắt tới tài khoản, chỉnh sửa, chatbot, FAQ và trang giới thiệu.',
|
||||||
|
steps: [
|
||||||
|
'Nhấn nút tròn avatar/người dùng ở bên trái để mở menu.',
|
||||||
|
'Nút bút chì đưa tới khu vực quản trị và chỉnh sửa. Tính năng này chỉ hỗ trợ tốt trên desktop.',
|
||||||
|
'Nút tia sét bật chế độ tải nhanh hơn cho lần vào sau.',
|
||||||
|
'Nút chat mở trợ lý AI lịch sử để hỏi nhanh về dữ liệu hoặc sự kiện.',
|
||||||
|
'Nút quyển sách mở trang hướng dẫn này. Nút thông tin mở trang giới thiệu dự án.'
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'mobile',
|
||||||
|
title: 'Lưu ý khi dùng trên điện thoại',
|
||||||
|
summary: 'Giao diện mobile ưu tiên bản đồ toàn màn hình, các panel và control sẽ được thu gọn.',
|
||||||
|
steps: [
|
||||||
|
'Thanh timeline trên mobile nằm phía dưới, gồm ô năm, nút -/+, công tắc lọc và thước kéo.',
|
||||||
|
'Bảng wiki mở ở phía dưới màn hình để không che toàn bộ bản đồ.',
|
||||||
|
'Một số thao tác quản trị hoặc chỉnh sửa bị khóa trên mobile để tránh lỗi thao tác.',
|
||||||
|
'Nếu khó chọn một đối tượng nhỏ, hãy phóng to bản đồ trước rồi nhấn lại.'
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const quickTips = [
|
||||||
|
'Muốn xem dữ liệu theo một năm cụ thể: nhập năm ở thanh timeline.',
|
||||||
|
'Muốn hiểu một vùng trên bản đồ: nhấn vào vùng đó để mở wiki.',
|
||||||
|
'Muốn bản đồ đỡ rối: tắt bớt lớp geometry bên trái.',
|
||||||
|
'Muốn xem diễn biến: chọn đối tượng có replay rồi nhấn nút phát.',
|
||||||
|
'Muốn hỏi nhanh: mở menu bên trái và chọn chatbot.'
|
||||||
|
];
|
||||||
|
|
||||||
|
const troubleshooting = [
|
||||||
|
{
|
||||||
|
question: 'Tại sao tôi không thấy dữ liệu sau khi đổi năm?',
|
||||||
|
answer: 'Có thể năm đang chọn không có geometry phù hợp hoặc bộ lọc timeline đang bật. Hãy thử tắt lọc timeline, tăng Range trên desktop hoặc chuyển sang năm gần hơn với sự kiện bạn đang xem.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: 'Tại sao nhấn vào bản đồ nhưng không mở wiki?',
|
||||||
|
answer: 'Một số geometry có thể chưa liên kết wiki hoặc bạn đang nhấn vào nền bản đồ. Hãy phóng to hơn, nhấn trực tiếp vào vùng/đường/biểu tượng, hoặc thử tìm bằng ô tìm kiếm.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: 'Tại sao không phát được replay?',
|
||||||
|
answer: 'Replay chỉ có ở những đối tượng đã được biên soạn kịch bản. Nếu nút phát không phản hồi, hãy chọn một đối tượng khác hoặc tìm các sự kiện/trận đánh đã có dữ liệu replay.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: 'Tại sao bản đồ tải chậm?',
|
||||||
|
answer: 'Bản đồ cần tải nền, geometry, wiki và quan hệ dữ liệu. Bạn có thể bật chế độ tải nhanh trong menu bên trái, giảm số lớp đang bật hoặc chờ bản đồ tải xong trước khi thao tác liên tục.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: 'Tôi muốn chỉnh sửa hoặc đóng góp dữ liệu thì làm thế nào?',
|
||||||
|
answer: 'Mở menu bên trái và vào khu vực quản trị/chỉnh sửa trên desktop. Nếu chưa có quyền phù hợp, hãy đăng nhập và gửi yêu cầu nâng quyền theo luồng trong tài khoản người dùng.',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
const [openSection, setOpenSection] = useState<string>('start');
|
||||||
|
const [openTrouble, setOpenTrouble] = useState<number | null>(0);
|
||||||
|
const sectionRefs = useRef<Record<string, HTMLDivElement | null>>({});
|
||||||
|
|
||||||
|
const handleGuideNavClick = (sectionId: string) => {
|
||||||
|
setOpenSection(sectionId);
|
||||||
|
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
sectionRefs.current[sectionId]?.scrollIntoView({
|
||||||
|
behavior: 'smooth',
|
||||||
|
block: 'start',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className="min-h-screen bg-slate-50 text-slate-950">
|
||||||
|
<section className="border-b border-slate-200 bg-white">
|
||||||
|
<div className="mx-auto flex max-w-6xl flex-col gap-6 px-4 py-10 sm:px-8 lg:flex-row lg:items-end lg:justify-between">
|
||||||
|
<div className="max-w-3xl">
|
||||||
|
<p className="mb-3 text-sm font-semibold uppercase tracking-wide text-blue-700">Hướng dẫn sử dụng</p>
|
||||||
|
<h1 className="text-3xl font-bold tracking-tight sm:text-4xl">Ultimate History Map FAQ</h1>
|
||||||
|
<p className="mt-4 text-base leading-7 text-slate-600">
|
||||||
|
Trang này hướng dẫn cách sử dụng màn bản đồ tại route <span className="font-semibold text-slate-900">/</span>: xem lịch sử theo timeline, tìm kiếm, bật/tắt lớp, đọc wiki và phát replay.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Link
|
||||||
|
href="/"
|
||||||
|
className="inline-flex h-11 items-center justify-center rounded-md bg-slate-900 px-5 text-sm font-semibold text-white transition hover:bg-slate-700"
|
||||||
|
>
|
||||||
|
Quay lại bản đồ
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="mx-auto grid max-w-6xl gap-6 px-4 py-8 sm:px-8 lg:grid-cols-[280px_minmax(0,1fr)]">
|
||||||
|
<nav className="lg:sticky lg:top-6 lg:self-start">
|
||||||
|
<div className="rounded-lg border border-slate-200 bg-white p-3 shadow-sm">
|
||||||
|
<p className="px-3 pb-2 text-xs font-semibold uppercase tracking-wide text-slate-500">Mục hướng dẫn</p>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
{guideSections.map((section) => (
|
||||||
|
<button
|
||||||
|
key={section.id}
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleGuideNavClick(section.id)}
|
||||||
|
className={`rounded-md px-3 py-2 text-left text-sm font-medium transition ${
|
||||||
|
openSection === section.id
|
||||||
|
? 'bg-blue-50 text-blue-700'
|
||||||
|
: 'text-slate-700 hover:bg-slate-100 hover:text-slate-950'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{section.title}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div className="space-y-6">
|
||||||
|
<section className="rounded-lg border border-slate-200 bg-white p-5 shadow-sm sm:p-6">
|
||||||
|
<h2 className="text-xl font-bold">Dùng nhanh trong 1 phút</h2>
|
||||||
|
<div className="mt-4 grid gap-3 sm:grid-cols-2">
|
||||||
|
{quickTips.map((tip, index) => (
|
||||||
|
<div key={tip} className="flex gap-3 rounded-md border border-slate-200 bg-slate-50 p-3">
|
||||||
|
<span className="flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-slate-900 text-sm font-bold text-white">
|
||||||
|
{index + 1}
|
||||||
|
</span>
|
||||||
|
<p className="text-sm leading-6 text-slate-700">{tip}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="rounded-lg border border-slate-200 bg-white shadow-sm">
|
||||||
|
{guideSections.map((section) => {
|
||||||
|
const isOpen = openSection === section.id;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={section.id}
|
||||||
|
ref={(element) => {
|
||||||
|
sectionRefs.current[section.id] = element;
|
||||||
|
}}
|
||||||
|
className="scroll-mt-6 border-b border-slate-200 last:border-b-0"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setOpenSection(isOpen ? '' : section.id)}
|
||||||
|
className="flex w-full items-center justify-between gap-4 px-5 py-5 text-left transition hover:bg-slate-50 sm:px-6"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
<span className="block text-lg font-bold text-slate-950">{section.title}</span>
|
||||||
|
<span className="mt-1 block text-sm leading-6 text-slate-600">{section.summary}</span>
|
||||||
|
</span>
|
||||||
|
<span className="shrink-0 text-2xl font-light text-blue-700">{isOpen ? '-' : '+'}</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className={`overflow-hidden transition-all duration-300 ${isOpen ? 'max-h-[620px] opacity-100' : 'max-h-0 opacity-0'}`}>
|
||||||
|
<ol className="space-y-3 px-5 pb-6 sm:px-6">
|
||||||
|
{section.steps.map((step, index) => (
|
||||||
|
<li key={step} className="flex gap-3 text-sm leading-6 text-slate-700">
|
||||||
|
<span className="mt-0.5 flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-blue-100 text-xs font-bold text-blue-700">
|
||||||
|
{index + 1}
|
||||||
|
</span>
|
||||||
|
<span>{step}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="rounded-lg border border-slate-200 bg-white p-5 shadow-sm sm:p-6">
|
||||||
|
<h2 className="text-xl font-bold">Lỗi thường gặp</h2>
|
||||||
|
<div className="mt-4 divide-y divide-slate-200">
|
||||||
|
{troubleshooting.map((item, index) => {
|
||||||
|
const isOpen = openTrouble === index;
|
||||||
|
return (
|
||||||
|
<div key={item.question}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setOpenTrouble(isOpen ? null : index)}
|
||||||
|
className="flex w-full items-center justify-between gap-4 py-4 text-left"
|
||||||
|
>
|
||||||
|
<span className="font-semibold text-slate-900">{item.question}</span>
|
||||||
|
<span className="shrink-0 text-2xl font-light text-blue-700">{isOpen ? '-' : '+'}</span>
|
||||||
|
</button>
|
||||||
|
<div className={`overflow-hidden transition-all duration-300 ${isOpen ? 'max-h-40 opacity-100 pb-4' : 'max-h-0 opacity-0'}`}>
|
||||||
|
<p className="text-sm leading-6 text-slate-600">{item.answer}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,13 +1,11 @@
|
|||||||
@import 'tailwindcss';
|
@import 'tailwindcss';
|
||||||
|
|
||||||
@import './fonts.css';
|
|
||||||
|
|
||||||
@custom-variant dark (&:is(.dark *));
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
@theme {
|
@theme {
|
||||||
--font-*: initial;
|
--font-*: initial;
|
||||||
--font-outfit: Outfit, sans-serif;
|
--font-outfit: Outfit, sans-serif;
|
||||||
--font-inter: "SF Pro Display", ui-sans-serif, system-ui, sans-serif;
|
--font-inter: var(--font-inter), ui-sans-serif, system-ui, sans-serif;
|
||||||
|
|
||||||
--breakpoint-*: initial;
|
--breakpoint-*: initial;
|
||||||
--breakpoint-2xsm: 375px;
|
--breakpoint-2xsm: 375px;
|
||||||
@@ -743,12 +741,7 @@ span.flatpickr-weekday,
|
|||||||
}
|
}
|
||||||
|
|
||||||
html {
|
html {
|
||||||
scrollbar-gutter: stable;
|
overflow-y: auto;
|
||||||
overflow-y: scroll;
|
|
||||||
}
|
|
||||||
|
|
||||||
html {
|
|
||||||
overflow-y: scroll;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
|
|||||||
@@ -1,47 +1,38 @@
|
|||||||
import localFont from 'next/font/local';
|
import type { Metadata } from 'next';
|
||||||
|
import { Inter } from 'next/font/google';
|
||||||
import './globals.css';
|
import './globals.css';
|
||||||
import "flatpickr/dist/flatpickr.css";
|
|
||||||
import { SidebarProvider } from '@/context/SidebarContext';
|
|
||||||
import { ThemeProvider } from '@/context/ThemeContext';
|
|
||||||
import { Toaster } from 'sonner';
|
import { Toaster } from 'sonner';
|
||||||
import StoreProvider from '@/store/StoreProvider';
|
import StoreProvider from '@/store/StoreProvider';
|
||||||
|
import { headers } from 'next/headers';
|
||||||
|
|
||||||
const sfPro = localFont({
|
export const metadata: Metadata = {
|
||||||
src: [
|
title: 'Ultimate History Map',
|
||||||
{
|
description: 'Bản đồ tương tác lịch sử thế giới qua các thời kỳ',
|
||||||
path: '../../public/font/SF-Pro-Display/SF-Pro-Display-Regular.otf',
|
};
|
||||||
weight: '400',
|
|
||||||
style: 'normal',
|
const inter = Inter({
|
||||||
},
|
subsets: ['latin', 'vietnamese'],
|
||||||
{
|
weight: ['400', '500', '600', '700'],
|
||||||
path: '../../public/font/SF-Pro-Display/SF-Pro-Display-Medium.otf',
|
variable: '--font-inter',
|
||||||
weight: '500',
|
display: 'swap',
|
||||||
style: 'normal',
|
preload: false,
|
||||||
},
|
});
|
||||||
{
|
|
||||||
path: '../../public/font/SF-Pro-Display/SF-Pro-Display-Semibold.otf',
|
export default async function RootLayout({
|
||||||
weight: '600',
|
|
||||||
style: 'normal',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '../../public/font/SF-Pro-Display/SF-Pro-Display-Bold.otf',
|
|
||||||
weight: '700',
|
|
||||||
style: 'normal',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
export default function RootLayout({
|
|
||||||
children,
|
children,
|
||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}>) {
|
}>) {
|
||||||
|
const headersList = await headers();
|
||||||
|
const pathname = headersList.get('x-pathname') || '/';
|
||||||
|
const isPublicRoot = pathname === '/';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en" className={isPublicRoot ? '' : inter.variable}>
|
||||||
<body className={`${sfPro.className} dark:bg-gray-900`}>
|
<body className={`${isPublicRoot ? 'font-sans' : inter.className} dark:bg-gray-900`}>
|
||||||
<StoreProvider>
|
<StoreProvider>
|
||||||
<ThemeProvider>
|
{children}
|
||||||
<SidebarProvider>{children} <Toaster closeButton richColors position="top-right" /> </SidebarProvider>
|
<Toaster closeButton richColors position="top-right" />
|
||||||
</ThemeProvider>
|
|
||||||
</StoreProvider>
|
</StoreProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
import BarChartOne from "@/components/charts/bar/BarChartOne";
|
|
||||||
import ComponentCard from "@/components/common/ComponentCard";
|
|
||||||
import PageBreadcrumb from "@/components/common/PageBreadCrumb";
|
|
||||||
import { Metadata } from "next";
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
|
||||||
title: "Next.js Bar Chart | TailAdmin - Next.js Dashboard Template",
|
|
||||||
description:
|
|
||||||
"This is Next.js Bar Chart page for TailAdmin - Next.js Tailwind CSS Admin Dashboard Template",
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function page() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<PageBreadcrumb pageTitle="Bar Chart" />
|
|
||||||
<div className="space-y-6">
|
|
||||||
<ComponentCard title="Bar Chart 1">
|
|
||||||
<BarChartOne />
|
|
||||||
</ComponentCard>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import LineChartOne from "@/components/charts/line/LineChartOne";
|
|
||||||
import ComponentCard from "@/components/common/ComponentCard";
|
|
||||||
import PageBreadcrumb from "@/components/common/PageBreadCrumb";
|
|
||||||
import { Metadata } from "next";
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
|
||||||
title: "Next.js Line Chart | TailAdmin - Next.js Dashboard Template",
|
|
||||||
description:
|
|
||||||
"This is Next.js Line Chart page for TailAdmin - Next.js Tailwind CSS Admin Dashboard Template",
|
|
||||||
};
|
|
||||||
export default function LineChart() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<PageBreadcrumb pageTitle="Line Chart" />
|
|
||||||
<div className="space-y-6">
|
|
||||||
<ComponentCard title="Line Chart 1">
|
|
||||||
<LineChartOne />
|
|
||||||
</ComponentCard>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import StickyHeader from "@/components/ui/StickyHeader";
|
|
||||||
import AccountDetails from "@/components/user-profile/AccountDetails";
|
|
||||||
import UserInfoCard from "@/components/user-profile/UserInfoCard";
|
|
||||||
import UserMetaCard from "@/components/user-profile/UserMetaCard";
|
|
||||||
import { UserMetaCardProps } from "@/interface/user";
|
|
||||||
import { apiGetCurrentUser } from "@/service/auth";
|
|
||||||
import { setUserData } from "@/store/features/userSlice";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { useDispatch } from "react-redux";
|
|
||||||
|
|
||||||
export default function Profile() {
|
|
||||||
const [user, setUser] = useState<UserMetaCardProps | null>(null);
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchUser = async () => {
|
|
||||||
try {
|
|
||||||
const userData = await apiGetCurrentUser();
|
|
||||||
dispatch(setUserData(userData.data));
|
|
||||||
setUser(userData);
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Lỗi:", err);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
fetchUser();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<StickyHeader header={`Thông tin tài khoản`} />
|
|
||||||
<div className="md:px-12 flex mx-auto ">
|
|
||||||
<div className="xl:max-w-82 pr-4 border-r border-gray-300 md:max-w-72">
|
|
||||||
<UserMetaCard data={user ?? {}} />
|
|
||||||
<UserInfoCard data={{ ...user, openEdit: true }} />
|
|
||||||
<AccountDetails data={user ?? {}} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useSidebar } from "@/context/SidebarContext";
|
import { SidebarProvider, useSidebar } from "@/context/SidebarContext";
|
||||||
import AppHeader from "@/layout/AppHeader";
|
import { ThemeProvider } from "@/context/ThemeContext";
|
||||||
import AppSidebar from "@/layout/AppSidebar";
|
import AppSidebar from "@/layout/AppSidebar";
|
||||||
import Backdrop from "@/layout/Backdrop";
|
import Backdrop from "@/layout/Backdrop";
|
||||||
import { apiGetCurrentUser } from "@/service/auth";
|
import { apiGetCurrentUser } from "@/service/auth";
|
||||||
@@ -13,6 +13,20 @@ export default function AdminLayout({
|
|||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<ThemeProvider>
|
||||||
|
<SidebarProvider>
|
||||||
|
<AdminLayoutContent>{children}</AdminLayoutContent>
|
||||||
|
</SidebarProvider>
|
||||||
|
</ThemeProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AdminLayoutContent({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
const { isExpanded, isHovered, isMobileOpen } = useSidebar();
|
const { isExpanded, isHovered, isMobileOpen } = useSidebar();
|
||||||
const dispatch = useDispatch()
|
const dispatch = useDispatch()
|
||||||
@@ -27,7 +41,7 @@ export default function AdminLayout({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
fetchUser();
|
fetchUser();
|
||||||
}, [])
|
}, [dispatch])
|
||||||
|
|
||||||
|
|
||||||
const mainContentMargin = isMobileOpen
|
const mainContentMargin = isMobileOpen
|
||||||
|
|||||||
@@ -1,41 +1,126 @@
|
|||||||
import type { Metadata } from "next";
|
"use client";
|
||||||
import { EcommerceMetrics } from "@/components/ecommerce/EcommerceMetrics";
|
|
||||||
import React from "react";
|
|
||||||
import MonthlyTarget from "@/components/ecommerce/MonthlyTarget";
|
|
||||||
import MonthlySalesChart from "@/components/ecommerce/MonthlySalesChart";
|
|
||||||
import StatisticsChart from "@/components/ecommerce/StatisticsChart";
|
|
||||||
import RecentOrders from "@/components/ecommerce/RecentOrders";
|
|
||||||
import DemographicCard from "@/components/ecommerce/DemographicCard";
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
import AccountDetails from "@/components/user-profile/AccountDetails";
|
||||||
title:
|
import UserInfoCard from "@/components/user-profile/UserInfoCard";
|
||||||
"Home Page",
|
import UserMetaCard from "@/components/user-profile/UserMetaCard";
|
||||||
description: "This is Dashboard Home for History Web",
|
import { UserMetaCardProps } from "@/interface/user";
|
||||||
|
import { apiGetCurrentUser } from "@/service/auth";
|
||||||
|
import { setUserData } from "@/store/features/userSlice";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
import { RootState } from "@/store/store";
|
||||||
|
import StickyHeader from "@/components/ui/StickyHeader";
|
||||||
|
import { SafeHTMLRenderer } from "@/components/ui/parse/SafeHTMLRenderer";
|
||||||
|
import { apiGetCurrentUserApplications } from "@/service/userService";
|
||||||
|
import Loading from "@/app/loading";
|
||||||
|
|
||||||
|
export default function Profile() {
|
||||||
|
const currentUser = useSelector((state: RootState) => state.user.data);
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const [application, setApplication] = useState<any>(null);
|
||||||
|
const [appLoading, setAppLoading] = useState(false);
|
||||||
|
|
||||||
|
const isHistorian = !!currentUser?.roles?.some(
|
||||||
|
(role: any) => role.name === "HISTORIAN"
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchUser = async () => {
|
||||||
|
try {
|
||||||
|
const userData = await apiGetCurrentUser();
|
||||||
|
dispatch(setUserData(userData.data));
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Lỗi:", err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
fetchUser();
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isHistorian) {
|
||||||
|
const fetchApp = async () => {
|
||||||
|
try {
|
||||||
|
setAppLoading(true);
|
||||||
|
const res = await apiGetCurrentUserApplications();
|
||||||
|
if (res?.data) {
|
||||||
|
const approvedApp =
|
||||||
|
res.data.find((app: any) => app.status === "APPROVED") ||
|
||||||
|
res.data[0];
|
||||||
|
setApplication(approvedApp);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Lỗi khi tải hồ sơ nhà sử học:", err);
|
||||||
|
} finally {
|
||||||
|
setAppLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
fetchApp();
|
||||||
|
}
|
||||||
|
}, [isHistorian]);
|
||||||
|
|
||||||
|
if (!currentUser) {
|
||||||
|
return <Loading />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const userMetaProps: UserMetaCardProps = {
|
||||||
|
data: currentUser
|
||||||
|
? {
|
||||||
|
id: currentUser.id,
|
||||||
|
email: currentUser.email,
|
||||||
|
profile: currentUser.profile,
|
||||||
|
roles: currentUser.roles?.map((role) => ({
|
||||||
|
id: Number(role.id) || undefined,
|
||||||
|
name: role.name,
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
status: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Ecommerce() {
|
// Nếu người dùng có role là HISTORIAN
|
||||||
|
if (isHistorian) {
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-12 gap-4 md:gap-6 2xl:gap-8">
|
<div>
|
||||||
<div className="col-span-12 space-y-6 xl:col-span-7 2xl:col-span-8">
|
<StickyHeader header={`Thông tin tài khoản`} />
|
||||||
<EcommerceMetrics />
|
<div className="md:px-12 flex flex-col md:flex-row mx-auto gap-6 w-full max-w-7xl items-start">
|
||||||
|
<div className="w-full md:max-w-72 xl:max-w-82 pr-0 md:pr-4 border-b md:border-b-0 md:border-r border-gray-300 pb-6 md:pb-0 shrink-0 space-y-6">
|
||||||
<MonthlySalesChart />
|
<UserMetaCard data={userMetaProps} />
|
||||||
|
<UserInfoCard data={{ ...userMetaProps, openEdit: true }} />
|
||||||
|
<AccountDetails data={userMetaProps} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="col-span-12 xl:col-span-5 2xl:col-span-4">
|
<div className="flex-1 min-w-0 w-full">
|
||||||
<MonthlyTarget />
|
{appLoading ? (
|
||||||
|
<div className="flex items-center justify-center p-20 w-full bg-zinc-50/50 dark:bg-zinc-950/30 rounded-2xl border border-zinc-200 dark:border-zinc-800">
|
||||||
|
<div className="w-8 h-8 border-4 border-zinc-200 border-t-blue-600 rounded-full animate-spin"></div>
|
||||||
</div>
|
</div>
|
||||||
|
) : application ? (
|
||||||
<div className="col-span-12">
|
<div className="">
|
||||||
<StatisticsChart />
|
<SafeHTMLRenderer html={application.content} />
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
<div className="col-span-12 xl:col-span-5 2xl:col-span-4">
|
<div className="p-10 text-center text-zinc-500 font-medium bg-zinc-50/50 dark:bg-zinc-950/30 rounded-2xl border-2 border-zinc-50 dark:border-zinc-800">
|
||||||
<DemographicCard />
|
Không tìm thấy thông tin hồ sơ nhà sử học.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="rounded-2xl border border-gray-200 bg-white p-5 dark:border-gray-800 dark:bg-white/[0.03] lg:p-6 mt-[100px]">
|
||||||
|
<h3 className="mb-5 text-lg font-semibold text-gray-800 dark:text-white/90 lg:mb-7">
|
||||||
|
Thông tin tài khoản
|
||||||
|
</h3>
|
||||||
|
<div className="space-y-6">
|
||||||
|
<UserMetaCard data={userMetaProps} />
|
||||||
|
<UserInfoCard data={{ ...userMetaProps, openEdit: true }} />
|
||||||
|
<AccountDetails data={userMetaProps} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="col-span-12 xl:col-span-7 2xl:col-span-8">
|
|
||||||
<RecentOrders />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -143,7 +143,6 @@ export default function ProjectDetailsPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleRemoveMember = async (userId: string) => {
|
const handleRemoveMember = async (userId: string) => {
|
||||||
// 1. Hiển thị hộp thoại xác nhận bằng SweetAlert2
|
|
||||||
const result = await Swal.fire({
|
const result = await Swal.fire({
|
||||||
title: "Xác nhận xóa?",
|
title: "Xác nhận xóa?",
|
||||||
text: "Bạn có chắc chắn muốn xóa thành viên này khỏi dự án?",
|
text: "Bạn có chắc chắn muốn xóa thành viên này khỏi dự án?",
|
||||||
@@ -263,18 +262,18 @@ export default function ProjectDetailsPage() {
|
|||||||
{[
|
{[
|
||||||
{
|
{
|
||||||
id: "overview",
|
id: "overview",
|
||||||
label: "Overview",
|
label: "Tổng quan",
|
||||||
icon: "M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z",
|
icon: "M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "members",
|
id: "members",
|
||||||
label: `Members`,
|
label: `Thành viên`,
|
||||||
count: project.members?.length || 0,
|
count: project.members?.length || 0,
|
||||||
icon: "M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z",
|
icon: "M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "settings",
|
id: "settings",
|
||||||
label: "Settings",
|
label: "Cài đặt",
|
||||||
icon: "M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z",
|
icon: "M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z",
|
||||||
},
|
},
|
||||||
].map((tab) => (
|
].map((tab) => (
|
||||||
@@ -324,7 +323,7 @@ export default function ProjectDetailsPage() {
|
|||||||
<div className="md:col-span-3">
|
<div className="md:col-span-3">
|
||||||
<div className="border border-gray-200 dark:border-[#30363d] rounded-xl overflow-hidden ">
|
<div className="border border-gray-200 dark:border-[#30363d] rounded-xl overflow-hidden ">
|
||||||
<div className="bg-gray-50 dark:bg-[#161b22] px-5 py-3 border-b border-gray-200 dark:border-[#30363d] font-semibold text-sm text-gray-800 dark:text-[#c9d1d9]">
|
<div className="bg-gray-50 dark:bg-[#161b22] px-5 py-3 border-b border-gray-200 dark:border-[#30363d] font-semibold text-sm text-gray-800 dark:text-[#c9d1d9]">
|
||||||
About
|
Thông tin
|
||||||
</div>
|
</div>
|
||||||
<div className="p-6 bg-white dark:bg-[#0d1117] text-[15px] leading-relaxed text-gray-700 dark:text-[#8b949e]">
|
<div className="p-6 bg-white dark:bg-[#0d1117] text-[15px] leading-relaxed text-gray-700 dark:text-[#8b949e]">
|
||||||
{project.description || (
|
{project.description || (
|
||||||
@@ -339,7 +338,7 @@ export default function ProjectDetailsPage() {
|
|||||||
<div className="md:col-span-1 space-y-6">
|
<div className="md:col-span-1 space-y-6">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="font-semibold text-sm mb-4 border-b border-gray-200 dark:border-[#30363d] pb-2 text-gray-800 dark:text-[#c9d1d9]">
|
<h3 className="font-semibold text-sm mb-4 border-b border-gray-200 dark:border-[#30363d] pb-2 text-gray-800 dark:text-[#c9d1d9]">
|
||||||
Owner
|
Chủ dự án
|
||||||
</h3>
|
</h3>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="w-8 h-8 shrink-0 flex items-center justify-center">
|
<div className="w-8 h-8 shrink-0 flex items-center justify-center">
|
||||||
@@ -379,7 +378,7 @@ export default function ProjectDetailsPage() {
|
|||||||
{activeTab === "members" && (
|
{activeTab === "members" && (
|
||||||
<div className="max-w-4xl">
|
<div className="max-w-4xl">
|
||||||
<h2 className="text-2xl font-normal mb-6 pb-2 border-b border-gray-200 dark:border-[#30363d]">
|
<h2 className="text-2xl font-normal mb-6 pb-2 border-b border-gray-200 dark:border-[#30363d]">
|
||||||
Manage access
|
Quản lý quyền truy cập
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<form
|
<form
|
||||||
@@ -403,14 +402,14 @@ export default function ProjectDetailsPage() {
|
|||||||
}
|
}
|
||||||
className="px-3 py-2 text-sm rounded-md border border-gray-300 dark:border-[#30363d] bg-white dark:bg-[#0d1117] outline-none focus:ring-2 focus:ring-blue-500/30 focus:border-blue-500 cursor-pointer"
|
className="px-3 py-2 text-sm rounded-md border border-gray-300 dark:border-[#30363d] bg-white dark:bg-[#0d1117] outline-none focus:ring-2 focus:ring-blue-500/30 focus:border-blue-500 cursor-pointer"
|
||||||
>
|
>
|
||||||
<option value="EDITOR">Editor</option>
|
<option value="EDITOR">Chỉnh sửa</option>
|
||||||
<option value="VIEWER">Viewer</option>
|
<option value="VIEWER">Xem</option>
|
||||||
</select>
|
</select>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="px-5 py-2 text-sm font-medium text-white bg-[#238636] border border-transparent rounded-md hover:bg-[#2ea043] transition-colors "
|
className="px-5 py-2 text-sm font-medium text-white bg-[#238636] border border-transparent rounded-md hover:bg-[#2ea043] transition-colors "
|
||||||
>
|
>
|
||||||
Add member
|
Thêm thành viên
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@@ -449,8 +448,8 @@ export default function ProjectDetailsPage() {
|
|||||||
}
|
}
|
||||||
className="text-sm px-3 py-1.5 rounded-md border border-gray-300 dark:border-[#30363d] bg-white dark:bg-[#21262d] outline-none hover:bg-gray-50 dark:hover:bg-[#30363d] cursor-pointer transition-colors"
|
className="text-sm px-3 py-1.5 rounded-md border border-gray-300 dark:border-[#30363d] bg-white dark:bg-[#21262d] outline-none hover:bg-gray-50 dark:hover:bg-[#30363d] cursor-pointer transition-colors"
|
||||||
>
|
>
|
||||||
<option value="EDITOR">Editor</option>
|
<option value="EDITOR">Chỉnh sửa</option>
|
||||||
<option value="VIEWER">Viewer</option>
|
<option value="VIEWER">Xem</option>
|
||||||
</select>
|
</select>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleRemoveMember(member.user_id)}
|
onClick={() => handleRemoveMember(member.user_id)}
|
||||||
@@ -488,12 +487,12 @@ export default function ProjectDetailsPage() {
|
|||||||
<div className="max-w-3xl space-y-10">
|
<div className="max-w-3xl space-y-10">
|
||||||
<section>
|
<section>
|
||||||
<h2 className="text-2xl font-normal mb-4 pb-2 border-b border-gray-200 dark:border-[#30363d]">
|
<h2 className="text-2xl font-normal mb-4 pb-2 border-b border-gray-200 dark:border-[#30363d]">
|
||||||
General
|
Chung
|
||||||
</h2>
|
</h2>
|
||||||
<form onSubmit={handleUpdateInfo} className="space-y-5">
|
<form onSubmit={handleUpdateInfo} className="space-y-5">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-semibold mb-1.5 text-gray-800 dark:text-[#c9d1d9]">
|
<label className="block text-sm font-semibold mb-1.5 text-gray-800 dark:text-[#c9d1d9]">
|
||||||
Project name
|
Tên dự án
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@@ -506,7 +505,7 @@ export default function ProjectDetailsPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-semibold mb-1.5 text-gray-800 dark:text-[#c9d1d9]">
|
<label className="block text-sm font-semibold mb-1.5 text-gray-800 dark:text-[#c9d1d9]">
|
||||||
Description
|
Mô tả
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
rows={4}
|
rows={4}
|
||||||
@@ -519,7 +518,7 @@ export default function ProjectDetailsPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-semibold mb-1.5 text-gray-800 dark:text-[#c9d1d9]">
|
<label className="block text-sm font-semibold mb-1.5 text-gray-800 dark:text-[#c9d1d9]">
|
||||||
Status
|
Nhãn
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={editForm.status}
|
value={editForm.status}
|
||||||
@@ -531,32 +530,32 @@ export default function ProjectDetailsPage() {
|
|||||||
}
|
}
|
||||||
className="w-48 px-3 py-2 text-sm rounded-md border border-gray-300 dark:border-[#30363d] bg-white dark:bg-[#0d1117] outline-none focus:ring-2 focus:ring-blue-500/30 focus:border-blue-500 cursor-pointer"
|
className="w-48 px-3 py-2 text-sm rounded-md border border-gray-300 dark:border-[#30363d] bg-white dark:bg-[#0d1117] outline-none focus:ring-2 focus:ring-blue-500/30 focus:border-blue-500 cursor-pointer"
|
||||||
>
|
>
|
||||||
<option value="PUBLIC">Public</option>
|
<option value="PUBLIC">Công khai</option>
|
||||||
<option value="PRIVATE">Private</option>
|
<option value="PRIVATE">Riêng tư</option>
|
||||||
<option value="ARCHIVE">Archive</option>
|
<option value="ARCHIVE">Lưu trữ</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="px-5 py-2 text-sm font-medium rounded-md bg-[#21262d] text-[#c9d1d9] border border-[#30363d] hover:bg-[#30363d] transition-colors "
|
className="px-5 py-2 text-sm font-medium rounded-md bg-[#21262d] text-[#c9d1d9] border border-[#30363d] hover:bg-[#30363d] transition-colors "
|
||||||
>
|
>
|
||||||
Update settings
|
Cập nhật
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h2 className="text-2xl font-normal text-red-500 mb-4 pb-2 border-b border-red-500/30">
|
<h2 className="text-2xl font-normal text-red-500 mb-4 pb-2 border-b border-red-500/30">
|
||||||
Danger Zone
|
Vùng nguy hiểm
|
||||||
</h2>
|
</h2>
|
||||||
<div className="border border-red-500/30 rounded-xl overflow-hidden">
|
<div className="border border-red-500/30 rounded-xl overflow-hidden">
|
||||||
<div className="p-5 flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
<div className="p-5 flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<div className="font-semibold text-sm text-gray-800 dark:text-[#c9d1d9]">
|
<div className="font-semibold text-sm text-gray-800 dark:text-[#c9d1d9]">
|
||||||
Transfer ownership
|
Chuyển quyền sở hữu
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-gray-500 dark:text-[#8b949e] mt-1">
|
<div className="text-xs text-gray-500 dark:text-[#8b949e] mt-1">
|
||||||
Transfer this project to another member in the project.
|
Chuyển quyền sở hữu dự án này sang một thành viên khác trong dự án.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<form
|
<form
|
||||||
@@ -593,18 +592,17 @@ export default function ProjectDetailsPage() {
|
|||||||
}
|
}
|
||||||
className="shrink-0 px-4 py-2 text-sm font-medium text-red-500 bg-transparent border border-red-500/50 rounded-md hover:bg-red-500 hover:text-white dark:hover:bg-red-900/30 transition-colors disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-transparent disabled:hover:text-red-500 dark:disabled:hover:bg-transparent"
|
className="shrink-0 px-4 py-2 text-sm font-medium text-red-500 bg-transparent border border-red-500/50 rounded-md hover:bg-red-500 hover:text-white dark:hover:bg-red-900/30 transition-colors disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-transparent disabled:hover:text-red-500 dark:disabled:hover:bg-transparent"
|
||||||
>
|
>
|
||||||
Transfer
|
Chuyển quyền
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-5 flex flex-col sm:flex-row sm:items-center justify-between gap-4 hover:bg-red-50/30 dark:hover:bg-red-900/10 transition-colors">
|
<div className="p-5 flex flex-col sm:flex-row sm:items-center justify-between gap-4 hover:bg-red-50/30 dark:hover:bg-red-900/10 transition-colors">
|
||||||
<div>
|
<div>
|
||||||
<div className="font-semibold text-sm text-gray-800 dark:text-[#c9d1d9]">
|
<div className="font-semibold text-sm text-gray-800 dark:text-[#c9d1d9]">
|
||||||
Delete this project
|
Xóa dự án
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-gray-500 dark:text-[#8b949e] mt-1">
|
<div className="text-xs text-gray-500 dark:text-[#8b949e] mt-1">
|
||||||
Once you delete a project, there is no going back. Please
|
Thận trọng với hành động này! Xóa dự án sẽ xóa vĩnh viễn tất cả dữ liệu liên quan và không thể khôi phục.
|
||||||
be certain.
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
@@ -612,7 +610,7 @@ export default function ProjectDetailsPage() {
|
|||||||
onClick={handleDeleteProject}
|
onClick={handleDeleteProject}
|
||||||
className="shrink-0 px-4 py-2 text-sm font-medium text-red-500 bg-transparent border border-red-500/50 rounded-md hover:bg-red-500 hover:text-white dark:hover:bg-red-900/30 transition-colors"
|
className="shrink-0 px-4 py-2 text-sm font-medium text-red-500 bg-transparent border border-red-500/50 rounded-md hover:bg-red-500 hover:text-white dark:hover:bg-red-900/30 transition-colors"
|
||||||
>
|
>
|
||||||
Delete project
|
Xóa dự án
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -23,6 +23,9 @@ import StickyHeader from "@/components/ui/StickyHeader";
|
|||||||
|
|
||||||
export type ProjectSortColumn = "created_at" | "updated_at" | "title";
|
export type ProjectSortColumn = "created_at" | "updated_at" | "title";
|
||||||
|
|
||||||
|
const IMPORT_JSON_MAX_BYTES = 2 * 1024 * 1024;
|
||||||
|
const IMPORT_JSON_MAX_LABEL = "2MB";
|
||||||
|
|
||||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||||
return !!value && typeof value === "object" && !Array.isArray(value);
|
return !!value && typeof value === "object" && !Array.isArray(value);
|
||||||
}
|
}
|
||||||
@@ -193,6 +196,14 @@ export default function ProjectsPage() {
|
|||||||
|
|
||||||
const handleImportJsonFile = async (file: File | null) => {
|
const handleImportJsonFile = async (file: File | null) => {
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
|
if (file.size > IMPORT_JSON_MAX_BYTES) {
|
||||||
|
setImportSnapshot(null);
|
||||||
|
setImportSnapshotName(null);
|
||||||
|
if (importJsonInputRef.current) importJsonInputRef.current.value = "";
|
||||||
|
toast.error(`File JSON tối đa ${IMPORT_JSON_MAX_LABEL}.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const text = await file.text();
|
const text = await file.text();
|
||||||
const raw = JSON.parse(text) as unknown;
|
const raw = JSON.parse(text) as unknown;
|
||||||
@@ -591,6 +602,9 @@ export default function ProjectsPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label>Khởi tạo từ JSON</Label>
|
<Label>Khởi tạo từ JSON</Label>
|
||||||
|
<p className="mb-2 text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
Chỉ hỗ trợ JSON snapshot tối đa {IMPORT_JSON_MAX_LABEL}.
|
||||||
|
</p>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -607,7 +621,7 @@ export default function ProjectsPage() {
|
|||||||
<input
|
<input
|
||||||
ref={importJsonInputRef}
|
ref={importJsonInputRef}
|
||||||
type="file"
|
type="file"
|
||||||
accept="application/json"
|
accept="application/json,.json"
|
||||||
className="hidden"
|
className="hidden"
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
handleImportJsonFile(e.target.files?.[0] || null)
|
handleImportJsonFile(e.target.files?.[0] || null)
|
||||||
|
|||||||
@@ -1,82 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useState } from 'react';
|
|
||||||
|
|
||||||
const faqData = [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
question: "1. Phần mềm này tương thích với những hệ điều hành nào?",
|
|
||||||
answer: "Hệ thống tương thích hoàn toàn với Windows 10/11, macOS 12 trở lên. Đối với môi trường máy chủ, chúng tôi hỗ trợ các bản phân phối Linux phổ biến như Ubuntu và Debian."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
question: "2. Sự khác biệt giữa phiên bản Miễn phí và Trả phí là gì?",
|
|
||||||
answer: "Phiên bản trả phí cung cấp băng thông không giới hạn, hỗ trợ kỹ thuật ưu tiên 24/7, và quyền truy cập sớm vào các tính năng nâng cao. Bản miễn phí sẽ giới hạn một số tính năng xuất dữ liệu."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
question: "3. Hệ thống hỗ trợ những phương thức kết nối nào?",
|
|
||||||
answer: "Chúng tôi hỗ trợ kết nối qua REST API, WebSocket cho dữ liệu thời gian thực và cung cấp sẵn SDK cho các ngôn ngữ phổ biến như TypeScript, Go."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 4,
|
|
||||||
question: "4. Làm thế nào để tôi có thể tích hợp vào dự án Next.js hiện tại?",
|
|
||||||
answer: "Bạn chỉ cần cài đặt package qua npm/yarn, thêm API Key vào file .env và gọi component Provider ở file layout.tsx gốc. Tài liệu chi tiết có sẵn trong mục Developer Docs."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 5,
|
|
||||||
question: "5. Dữ liệu của tôi được bảo mật như thế nào?",
|
|
||||||
answer: "Toàn bộ dữ liệu được mã hóa đầu cuối (End-to-End Encryption). Chúng tôi tuân thủ nghiêm ngặt các tiêu chuẩn bảo mật quốc tế và thường xuyên rà soát hệ thống để phòng chống các lỗ hổng bảo mật."
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
export default function Page() {
|
|
||||||
// Lưu index của câu hỏi đang được mở. Mặc định mở câu đầu tiên (index 0).
|
|
||||||
const [openIndex, setOpenIndex] = useState<number | null>(0);
|
|
||||||
|
|
||||||
const toggleFAQ = (index: number) => {
|
|
||||||
// Nếu click lại vào câu đang mở thì đóng nó, ngược lại thì mở câu mới
|
|
||||||
setOpenIndex(openIndex === index ? null : index);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-white text-slate-900 py-16 px-4 sm:px-8">
|
|
||||||
<div className="max-w-4xl mx-auto">
|
|
||||||
<h1 className="text-4xl font-bold mb-10">FAQs</h1>
|
|
||||||
|
|
||||||
<div className="border-t border-slate-200">
|
|
||||||
{faqData.map((faq, index) => {
|
|
||||||
const isOpen = openIndex === index;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div key={faq.id} className="border-b border-slate-200">
|
|
||||||
<button
|
|
||||||
onClick={() => toggleFAQ(index)}
|
|
||||||
className="w-full py-6 flex justify-between items-center text-left focus:outline-none group"
|
|
||||||
>
|
|
||||||
<span className="text-lg font-bold group-hover:text-indigo-600 transition-colors">
|
|
||||||
{faq.question}
|
|
||||||
</span>
|
|
||||||
<span className="text-3xl font-light ml-4 text-indigo-600 shrink-0 leading-none">
|
|
||||||
{isOpen ? '−' : '+'}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* Phần nội dung có hiệu ứng trượt */}
|
|
||||||
<div
|
|
||||||
className={`overflow-hidden transition-all duration-300 ease-in-out ${
|
|
||||||
isOpen ? 'max-h-96 opacity-100 pb-6' : 'max-h-0 opacity-0'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<p className="text-slate-600 text-base leading-relaxed pr-8">
|
|
||||||
{faq.answer}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -2,38 +2,14 @@ export type StoredTokens = {
|
|||||||
access_token: string;
|
access_token: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const LS_KEY = "uhm_auth_tokens_v1";
|
|
||||||
|
|
||||||
let cached: StoredTokens | null = null;
|
let cached: StoredTokens | null = null;
|
||||||
|
|
||||||
function safeParseTokens(raw: string | null): StoredTokens | null {
|
|
||||||
if (!raw) return null;
|
|
||||||
try {
|
|
||||||
const v = JSON.parse(raw) as Partial<StoredTokens>;
|
|
||||||
if (!v || typeof v !== "object") return null;
|
|
||||||
if (typeof v.access_token !== "string") return null;
|
|
||||||
if (!v.access_token.trim()) return null;
|
|
||||||
return { access_token: v.access_token };
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getStoredTokens(): StoredTokens | null {
|
export function getStoredTokens(): StoredTokens | null {
|
||||||
if (cached) return cached;
|
|
||||||
if (typeof window === "undefined") return null;
|
|
||||||
cached = safeParseTokens(window.localStorage.getItem(LS_KEY));
|
|
||||||
return cached;
|
return cached;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setStoredTokens(tokens: StoredTokens | null): void {
|
export function setStoredTokens(tokens: StoredTokens | null): void {
|
||||||
cached = tokens;
|
cached = tokens;
|
||||||
if (typeof window === "undefined") return;
|
|
||||||
if (!tokens) {
|
|
||||||
window.localStorage.removeItem(LS_KEY);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
window.localStorage.setItem(LS_KEY, JSON.stringify(tokens));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getAccessToken(): string | null {
|
export function getAccessToken(): string | null {
|
||||||
@@ -47,11 +23,6 @@ export function clearStoredTokens(): void {
|
|||||||
// Helper for dealing with CommonResponse where token payload shape is not strictly typed.
|
// Helper for dealing with CommonResponse where token payload shape is not strictly typed.
|
||||||
export function extractTokensFromResponsePayload(payload: any): StoredTokens | null {
|
export function extractTokensFromResponsePayload(payload: any): StoredTokens | null {
|
||||||
const data = payload?.data ?? payload;
|
const data = payload?.data ?? payload;
|
||||||
// Common shapes observed in various backends:
|
|
||||||
// - { status: true, data: { access_token, refresh_token } }
|
|
||||||
// - { data: { tokens: { access_token, refresh_token } } }
|
|
||||||
// - { data: { token: <access>, refresh_token } }
|
|
||||||
// - { accessToken, refreshToken }
|
|
||||||
const tokenContainer = data?.tokens ?? data?.token_set ?? data;
|
const tokenContainer = data?.tokens ?? data?.token_set ?? data;
|
||||||
|
|
||||||
const access =
|
const access =
|
||||||
@@ -62,11 +33,6 @@ export function extractTokensFromResponsePayload(payload: any): StoredTokens | n
|
|||||||
tokenContainer?.jwt ??
|
tokenContainer?.jwt ??
|
||||||
null;
|
null;
|
||||||
|
|
||||||
const refresh =
|
|
||||||
tokenContainer?.refresh_token ??
|
|
||||||
tokenContainer?.refreshToken ??
|
|
||||||
tokenContainer?.refresh ??
|
|
||||||
null;
|
|
||||||
if (typeof access === "string" && access.trim()) {
|
if (typeof access === "string" && access.trim()) {
|
||||||
return { access_token: access };
|
return { access_token: access };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,293 +0,0 @@
|
|||||||
"use client";
|
|
||||||
import React, { useState, useRef, useEffect } from "react";
|
|
||||||
import FullCalendar from "@fullcalendar/react";
|
|
||||||
import dayGridPlugin from "@fullcalendar/daygrid";
|
|
||||||
import timeGridPlugin from "@fullcalendar/timegrid";
|
|
||||||
import interactionPlugin from "@fullcalendar/interaction";
|
|
||||||
import {
|
|
||||||
EventInput,
|
|
||||||
DateSelectArg,
|
|
||||||
EventClickArg,
|
|
||||||
EventContentArg,
|
|
||||||
} from "@fullcalendar/core";
|
|
||||||
import { useModal } from "@/hooks/useModal";
|
|
||||||
import { Modal } from "@/components/ui/modal";
|
|
||||||
import { newId } from "@/uhm/lib/utils/id";
|
|
||||||
|
|
||||||
interface CalendarEvent extends EventInput {
|
|
||||||
extendedProps: {
|
|
||||||
calendar: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const Calendar: React.FC = () => {
|
|
||||||
const [selectedEvent, setSelectedEvent] = useState<CalendarEvent | null>(
|
|
||||||
null
|
|
||||||
);
|
|
||||||
const [eventTitle, setEventTitle] = useState("");
|
|
||||||
const [eventStartDate, setEventStartDate] = useState("");
|
|
||||||
const [eventEndDate, setEventEndDate] = useState("");
|
|
||||||
const [eventLevel, setEventLevel] = useState("");
|
|
||||||
const [events, setEvents] = useState<CalendarEvent[]>([]);
|
|
||||||
const calendarRef = useRef<FullCalendar>(null);
|
|
||||||
const { isOpen, openModal, closeModal } = useModal();
|
|
||||||
|
|
||||||
const calendarsEvents = {
|
|
||||||
Danger: "danger",
|
|
||||||
Success: "success",
|
|
||||||
Primary: "primary",
|
|
||||||
Warning: "warning",
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// Initialize with some events
|
|
||||||
setEvents([
|
|
||||||
{
|
|
||||||
id: "1",
|
|
||||||
title: "Event Conf.",
|
|
||||||
start: new Date().toISOString().split("T")[0],
|
|
||||||
extendedProps: { calendar: "Danger" },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "2",
|
|
||||||
title: "Meeting",
|
|
||||||
start: new Date(Date.now() + 86400000).toISOString().split("T")[0],
|
|
||||||
extendedProps: { calendar: "Success" },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "3",
|
|
||||||
title: "Workshop",
|
|
||||||
start: new Date(Date.now() + 172800000).toISOString().split("T")[0],
|
|
||||||
end: new Date(Date.now() + 259200000).toISOString().split("T")[0],
|
|
||||||
extendedProps: { calendar: "Primary" },
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleDateSelect = (selectInfo: DateSelectArg) => {
|
|
||||||
resetModalFields();
|
|
||||||
setEventStartDate(selectInfo.startStr);
|
|
||||||
setEventEndDate(selectInfo.endStr || selectInfo.startStr);
|
|
||||||
openModal();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleEventClick = (clickInfo: EventClickArg) => {
|
|
||||||
const event = clickInfo.event;
|
|
||||||
setSelectedEvent({
|
|
||||||
id: event.id,
|
|
||||||
title: event.title,
|
|
||||||
start: event.startStr,
|
|
||||||
end: event.endStr,
|
|
||||||
extendedProps: {
|
|
||||||
calendar: event.extendedProps.calendar,
|
|
||||||
},
|
|
||||||
} as CalendarEvent);
|
|
||||||
|
|
||||||
setEventTitle(event.title);
|
|
||||||
setEventStartDate(event.start?.toISOString().split("T")[0] || "");
|
|
||||||
setEventEndDate(event.end?.toISOString().split("T")[0] || "");
|
|
||||||
setEventLevel(event.extendedProps.calendar);
|
|
||||||
openModal();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAddOrUpdateEvent = () => {
|
|
||||||
if (selectedEvent) {
|
|
||||||
// Update existing event
|
|
||||||
setEvents((prevEvents) =>
|
|
||||||
prevEvents.map((event) =>
|
|
||||||
event.id === selectedEvent.id
|
|
||||||
? {
|
|
||||||
...event,
|
|
||||||
title: eventTitle,
|
|
||||||
start: eventStartDate,
|
|
||||||
end: eventEndDate,
|
|
||||||
extendedProps: { calendar: eventLevel },
|
|
||||||
}
|
|
||||||
: event
|
|
||||||
)
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// Add new event
|
|
||||||
const newEvent: CalendarEvent = {
|
|
||||||
id: newId(),
|
|
||||||
title: eventTitle,
|
|
||||||
start: eventStartDate,
|
|
||||||
end: eventEndDate,
|
|
||||||
allDay: true,
|
|
||||||
extendedProps: { calendar: eventLevel },
|
|
||||||
};
|
|
||||||
setEvents((prevEvents) => [...prevEvents, newEvent]);
|
|
||||||
}
|
|
||||||
closeModal();
|
|
||||||
resetModalFields();
|
|
||||||
};
|
|
||||||
|
|
||||||
const resetModalFields = () => {
|
|
||||||
setEventTitle("");
|
|
||||||
setEventStartDate("");
|
|
||||||
setEventEndDate("");
|
|
||||||
setEventLevel("");
|
|
||||||
setSelectedEvent(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="rounded-2xl border border-gray-200 bg-white dark:border-gray-800 dark:bg-white/[0.03]">
|
|
||||||
<div className="custom-calendar">
|
|
||||||
<FullCalendar
|
|
||||||
ref={calendarRef}
|
|
||||||
plugins={[dayGridPlugin, timeGridPlugin, interactionPlugin]}
|
|
||||||
initialView="dayGridMonth"
|
|
||||||
headerToolbar={{
|
|
||||||
left: "prev,next addEventButton",
|
|
||||||
center: "title",
|
|
||||||
right: "dayGridMonth,timeGridWeek,timeGridDay",
|
|
||||||
}}
|
|
||||||
events={events}
|
|
||||||
selectable={true}
|
|
||||||
select={handleDateSelect}
|
|
||||||
eventClick={handleEventClick}
|
|
||||||
eventContent={renderEventContent}
|
|
||||||
customButtons={{
|
|
||||||
addEventButton: {
|
|
||||||
text: "Add Event +",
|
|
||||||
click: openModal,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<Modal
|
|
||||||
isOpen={isOpen}
|
|
||||||
onClose={closeModal}
|
|
||||||
className="max-w-[700px] p-6 lg:p-10"
|
|
||||||
>
|
|
||||||
<div className="flex flex-col px-2 overflow-y-auto custom-scrollbar">
|
|
||||||
<div>
|
|
||||||
<h5 className="mb-2 font-semibold text-gray-800 modal-title text-theme-xl dark:text-white/90 lg:text-2xl">
|
|
||||||
{selectedEvent ? "Edit Event" : "Add Event"}
|
|
||||||
</h5>
|
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
|
||||||
Plan your next big moment: schedule or edit an event to stay on
|
|
||||||
track
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="mt-8">
|
|
||||||
<div>
|
|
||||||
<div>
|
|
||||||
<label className="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-400">
|
|
||||||
Event Title
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="event-title"
|
|
||||||
type="text"
|
|
||||||
value={eventTitle}
|
|
||||||
onChange={(e) => setEventTitle(e.target.value)}
|
|
||||||
className="dark:bg-dark-900 h-11 w-full rounded-lg border border-gray-300 bg-transparent px-4 py-2.5 text-sm text-gray-800 shadow-theme-xs placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="mt-6">
|
|
||||||
<label className="block mb-4 text-sm font-medium text-gray-700 dark:text-gray-400">
|
|
||||||
Event Color
|
|
||||||
</label>
|
|
||||||
<div className="flex flex-wrap items-center gap-4 sm:gap-5">
|
|
||||||
{Object.entries(calendarsEvents).map(([key, value]) => (
|
|
||||||
<div key={key} className="n-chk">
|
|
||||||
<div
|
|
||||||
className={`form-check form-check-${value} form-check-inline`}
|
|
||||||
>
|
|
||||||
<label
|
|
||||||
className="flex items-center text-sm text-gray-700 form-check-label dark:text-gray-400"
|
|
||||||
htmlFor={`modal${key}`}
|
|
||||||
>
|
|
||||||
<span className="relative">
|
|
||||||
<input
|
|
||||||
className="sr-only form-check-input"
|
|
||||||
type="radio"
|
|
||||||
name="event-level"
|
|
||||||
value={key}
|
|
||||||
id={`modal${key}`}
|
|
||||||
checked={eventLevel === key}
|
|
||||||
onChange={() => setEventLevel(key)}
|
|
||||||
/>
|
|
||||||
<span className="flex items-center justify-center w-5 h-5 mr-2 border border-gray-300 rounded-full box dark:border-gray-700">
|
|
||||||
<span
|
|
||||||
className={`h-2 w-2 rounded-full bg-white ${
|
|
||||||
eventLevel === key ? "block" : "hidden"
|
|
||||||
}`}
|
|
||||||
></span>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
{key}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-6">
|
|
||||||
<label className="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-400">
|
|
||||||
Enter Start Date
|
|
||||||
</label>
|
|
||||||
<div className="relative">
|
|
||||||
<input
|
|
||||||
id="event-start-date"
|
|
||||||
type="date"
|
|
||||||
value={eventStartDate}
|
|
||||||
onChange={(e) => setEventStartDate(e.target.value)}
|
|
||||||
className="dark:bg-dark-900 h-11 w-full appearance-none rounded-lg border border-gray-300 bg-transparent bg-none px-4 py-2.5 pl-4 pr-11 text-sm text-gray-800 shadow-theme-xs placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-6">
|
|
||||||
<label className="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-400">
|
|
||||||
Enter End Date
|
|
||||||
</label>
|
|
||||||
<div className="relative">
|
|
||||||
<input
|
|
||||||
id="event-end-date"
|
|
||||||
type="date"
|
|
||||||
value={eventEndDate}
|
|
||||||
onChange={(e) => setEventEndDate(e.target.value)}
|
|
||||||
className="dark:bg-dark-900 h-11 w-full appearance-none rounded-lg border border-gray-300 bg-transparent bg-none px-4 py-2.5 pl-4 pr-11 text-sm text-gray-800 shadow-theme-xs placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-3 mt-6 modal-footer sm:justify-end">
|
|
||||||
<button
|
|
||||||
onClick={closeModal}
|
|
||||||
type="button"
|
|
||||||
className="flex w-full justify-center rounded-lg border border-gray-300 bg-white px-4 py-2.5 text-sm font-medium text-gray-700 hover:bg-gray-50 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-white/[0.03] sm:w-auto"
|
|
||||||
>
|
|
||||||
Close
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={handleAddOrUpdateEvent}
|
|
||||||
type="button"
|
|
||||||
className="btn btn-success btn-update-event flex w-full justify-center rounded-lg bg-brand-500 px-4 py-2.5 text-sm font-medium text-white hover:bg-brand-600 sm:w-auto"
|
|
||||||
>
|
|
||||||
{selectedEvent ? "Update Changes" : "Add Event"}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderEventContent = (eventInfo: EventContentArg) => {
|
|
||||||
const colorClass = `fc-bg-${eventInfo.event.extendedProps.calendar.toLowerCase()}`;
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={`event-fc-color flex fc-event-main ${colorClass} p-1 rounded-sm`}
|
|
||||||
>
|
|
||||||
<div className="fc-daygrid-event-dot"></div>
|
|
||||||
<div className="fc-event-time">{eventInfo.timeText}</div>
|
|
||||||
<div className="fc-event-title">{eventInfo.event.title}</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Calendar;
|
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
"use client";
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
import { ApexOptions } from "apexcharts";
|
|
||||||
|
|
||||||
import dynamic from "next/dynamic";
|
|
||||||
// Dynamically import the ReactApexChart component
|
|
||||||
const ReactApexChart = dynamic(() => import("react-apexcharts"), {
|
|
||||||
ssr: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
export default function BarChartOne() {
|
|
||||||
const options: ApexOptions = {
|
|
||||||
colors: ["#465fff"],
|
|
||||||
chart: {
|
|
||||||
fontFamily: "Outfit, sans-serif",
|
|
||||||
type: "bar",
|
|
||||||
height: 180,
|
|
||||||
toolbar: {
|
|
||||||
show: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
plotOptions: {
|
|
||||||
bar: {
|
|
||||||
horizontal: false,
|
|
||||||
columnWidth: "39%",
|
|
||||||
borderRadius: 5,
|
|
||||||
borderRadiusApplication: "end",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
dataLabels: {
|
|
||||||
enabled: false,
|
|
||||||
},
|
|
||||||
stroke: {
|
|
||||||
show: true,
|
|
||||||
width: 4,
|
|
||||||
colors: ["transparent"],
|
|
||||||
},
|
|
||||||
xaxis: {
|
|
||||||
categories: [
|
|
||||||
"Jan",
|
|
||||||
"Feb",
|
|
||||||
"Mar",
|
|
||||||
"Apr",
|
|
||||||
"May",
|
|
||||||
"Jun",
|
|
||||||
"Jul",
|
|
||||||
"Aug",
|
|
||||||
"Sep",
|
|
||||||
"Oct",
|
|
||||||
"Nov",
|
|
||||||
"Dec",
|
|
||||||
],
|
|
||||||
axisBorder: {
|
|
||||||
show: false,
|
|
||||||
},
|
|
||||||
axisTicks: {
|
|
||||||
show: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
legend: {
|
|
||||||
show: true,
|
|
||||||
position: "top",
|
|
||||||
horizontalAlign: "left",
|
|
||||||
fontFamily: "Outfit",
|
|
||||||
},
|
|
||||||
yaxis: {
|
|
||||||
title: {
|
|
||||||
text: undefined,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
grid: {
|
|
||||||
yaxis: {
|
|
||||||
lines: {
|
|
||||||
show: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
fill: {
|
|
||||||
opacity: 1,
|
|
||||||
},
|
|
||||||
|
|
||||||
tooltip: {
|
|
||||||
x: {
|
|
||||||
show: false,
|
|
||||||
},
|
|
||||||
y: {
|
|
||||||
formatter: (val: number) => `${val}`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const series = [
|
|
||||||
{
|
|
||||||
name: "Sales",
|
|
||||||
data: [168, 385, 201, 298, 187, 195, 291, 110, 215, 390, 280, 112],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
return (
|
|
||||||
<div className="max-w-full overflow-x-auto custom-scrollbar">
|
|
||||||
<div id="chartOne" className="min-w-[1000px]">
|
|
||||||
<ReactApexChart
|
|
||||||
options={options}
|
|
||||||
series={series}
|
|
||||||
type="bar"
|
|
||||||
height={180}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,133 +0,0 @@
|
|||||||
"use client";
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
import { ApexOptions } from "apexcharts";
|
|
||||||
|
|
||||||
import dynamic from "next/dynamic";
|
|
||||||
// Dynamically import the ReactApexChart component
|
|
||||||
const ReactApexChart = dynamic(() => import("react-apexcharts"), {
|
|
||||||
ssr: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
export default function LineChartOne() {
|
|
||||||
const options: ApexOptions = {
|
|
||||||
legend: {
|
|
||||||
show: false, // Hide legend
|
|
||||||
position: "top",
|
|
||||||
horizontalAlign: "left",
|
|
||||||
},
|
|
||||||
colors: ["#465FFF", "#9CB9FF"], // Define line colors
|
|
||||||
chart: {
|
|
||||||
fontFamily: "Outfit, sans-serif",
|
|
||||||
height: 310,
|
|
||||||
type: "line", // Set the chart type to 'line'
|
|
||||||
toolbar: {
|
|
||||||
show: false, // Hide chart toolbar
|
|
||||||
},
|
|
||||||
},
|
|
||||||
stroke: {
|
|
||||||
curve: "straight", // Define the line style (straight, smooth, or step)
|
|
||||||
width: [2, 2], // Line width for each dataset
|
|
||||||
},
|
|
||||||
|
|
||||||
fill: {
|
|
||||||
type: "gradient",
|
|
||||||
gradient: {
|
|
||||||
opacityFrom: 0.55,
|
|
||||||
opacityTo: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
markers: {
|
|
||||||
size: 0, // Size of the marker points
|
|
||||||
strokeColors: "#fff", // Marker border color
|
|
||||||
strokeWidth: 2,
|
|
||||||
hover: {
|
|
||||||
size: 6, // Marker size on hover
|
|
||||||
},
|
|
||||||
},
|
|
||||||
grid: {
|
|
||||||
xaxis: {
|
|
||||||
lines: {
|
|
||||||
show: false, // Hide grid lines on x-axis
|
|
||||||
},
|
|
||||||
},
|
|
||||||
yaxis: {
|
|
||||||
lines: {
|
|
||||||
show: true, // Show grid lines on y-axis
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
dataLabels: {
|
|
||||||
enabled: false, // Disable data labels
|
|
||||||
},
|
|
||||||
tooltip: {
|
|
||||||
enabled: true, // Enable tooltip
|
|
||||||
x: {
|
|
||||||
format: "dd MMM yyyy", // Format for x-axis tooltip
|
|
||||||
},
|
|
||||||
},
|
|
||||||
xaxis: {
|
|
||||||
type: "category", // Category-based x-axis
|
|
||||||
categories: [
|
|
||||||
"Jan",
|
|
||||||
"Feb",
|
|
||||||
"Mar",
|
|
||||||
"Apr",
|
|
||||||
"May",
|
|
||||||
"Jun",
|
|
||||||
"Jul",
|
|
||||||
"Aug",
|
|
||||||
"Sep",
|
|
||||||
"Oct",
|
|
||||||
"Nov",
|
|
||||||
"Dec",
|
|
||||||
],
|
|
||||||
axisBorder: {
|
|
||||||
show: false, // Hide x-axis border
|
|
||||||
},
|
|
||||||
axisTicks: {
|
|
||||||
show: false, // Hide x-axis ticks
|
|
||||||
},
|
|
||||||
tooltip: {
|
|
||||||
enabled: false, // Disable tooltip for x-axis points
|
|
||||||
},
|
|
||||||
},
|
|
||||||
yaxis: {
|
|
||||||
labels: {
|
|
||||||
style: {
|
|
||||||
fontSize: "12px", // Adjust font size for y-axis labels
|
|
||||||
colors: ["#6B7280"], // Color of the labels
|
|
||||||
},
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
text: "", // Remove y-axis title
|
|
||||||
style: {
|
|
||||||
fontSize: "0px",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const series = [
|
|
||||||
{
|
|
||||||
name: "Sales",
|
|
||||||
data: [180, 190, 170, 160, 175, 165, 170, 205, 230, 210, 240, 235],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Revenue",
|
|
||||||
data: [40, 30, 50, 40, 55, 40, 70, 100, 110, 120, 150, 140],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
return (
|
|
||||||
<div className="max-w-full overflow-x-auto custom-scrollbar">
|
|
||||||
<div id="chartEight" className="min-w-[1000px]">
|
|
||||||
<ReactApexChart
|
|
||||||
options={options}
|
|
||||||
series={series}
|
|
||||||
type="area"
|
|
||||||
height={310}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
import React, { useState } from "react";
|
|
||||||
|
|
||||||
const ChartTab: React.FC = () => {
|
|
||||||
const [selected, setSelected] = useState<
|
|
||||||
"optionOne" | "optionTwo" | "optionThree"
|
|
||||||
>("optionOne");
|
|
||||||
|
|
||||||
const getButtonClass = (option: "optionOne" | "optionTwo" | "optionThree") =>
|
|
||||||
selected === option
|
|
||||||
? "shadow-theme-xs text-gray-900 dark:text-white bg-white dark:bg-gray-800"
|
|
||||||
: "text-gray-500 dark:text-gray-400";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex items-center gap-0.5 rounded-lg bg-gray-100 p-0.5 dark:bg-gray-900">
|
|
||||||
<button
|
|
||||||
onClick={() => setSelected("optionOne")}
|
|
||||||
className={`px-3 py-2 font-medium w-full rounded-md text-theme-sm hover:text-gray-900 dark:hover:text-white ${getButtonClass(
|
|
||||||
"optionOne"
|
|
||||||
)}`}
|
|
||||||
>
|
|
||||||
Monthly
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={() => setSelected("optionTwo")}
|
|
||||||
className={`px-3 py-2 font-medium w-full rounded-md text-theme-sm hover:text-gray-900 dark:hover:text-white ${getButtonClass(
|
|
||||||
"optionTwo"
|
|
||||||
)}`}
|
|
||||||
>
|
|
||||||
Quarterly
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={() => setSelected("optionThree")}
|
|
||||||
className={`px-3 py-2 font-medium w-full rounded-md text-theme-sm hover:text-gray-900 dark:hover:text-white ${getButtonClass(
|
|
||||||
"optionThree"
|
|
||||||
)}`}
|
|
||||||
>
|
|
||||||
Annually
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ChartTab;
|
|
||||||
@@ -1,123 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
// import { VectorMap } from "@react-jvectormap/core";
|
|
||||||
import { worldMill } from "@react-jvectormap/world";
|
|
||||||
import dynamic from "next/dynamic";
|
|
||||||
|
|
||||||
const VectorMap = dynamic(
|
|
||||||
() => import("@react-jvectormap/core").then((mod) => mod.VectorMap),
|
|
||||||
{ ssr: false }
|
|
||||||
);
|
|
||||||
|
|
||||||
// Define the component props
|
|
||||||
interface CountryMapProps {
|
|
||||||
mapColor?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
type MarkerStyle = {
|
|
||||||
initial: {
|
|
||||||
fill: string;
|
|
||||||
r: number; // Radius for markers
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
type Marker = {
|
|
||||||
latLng: [number, number];
|
|
||||||
name: string;
|
|
||||||
style?: {
|
|
||||||
fill: string;
|
|
||||||
borderWidth: number;
|
|
||||||
borderColor: string;
|
|
||||||
stroke?: string;
|
|
||||||
strokeOpacity?: number;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const CountryMap: React.FC<CountryMapProps> = ({ mapColor }) => {
|
|
||||||
return (
|
|
||||||
<VectorMap
|
|
||||||
map={worldMill}
|
|
||||||
backgroundColor="transparent"
|
|
||||||
markerStyle={
|
|
||||||
{
|
|
||||||
initial: {
|
|
||||||
fill: "#465FFF",
|
|
||||||
r: 4, // Custom radius for markers
|
|
||||||
}, // Type assertion to bypass strict CSS property checks
|
|
||||||
} as MarkerStyle
|
|
||||||
}
|
|
||||||
markersSelectable={true}
|
|
||||||
markers={
|
|
||||||
[
|
|
||||||
{
|
|
||||||
latLng: [37.2580397, -104.657039],
|
|
||||||
name: "United States",
|
|
||||||
style: {
|
|
||||||
fill: "#465FFF",
|
|
||||||
borderWidth: 1,
|
|
||||||
borderColor: "white",
|
|
||||||
stroke: "#383f47",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
latLng: [20.7504374, 73.7276105],
|
|
||||||
name: "India",
|
|
||||||
style: { fill: "#465FFF", borderWidth: 1, borderColor: "white" },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
latLng: [53.613, -11.6368],
|
|
||||||
name: "United Kingdom",
|
|
||||||
style: { fill: "#465FFF", borderWidth: 1, borderColor: "white" },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
latLng: [-25.0304388, 115.2092761],
|
|
||||||
name: "Sweden",
|
|
||||||
style: {
|
|
||||||
fill: "#465FFF",
|
|
||||||
borderWidth: 1,
|
|
||||||
borderColor: "white",
|
|
||||||
strokeOpacity: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
] as Marker[]
|
|
||||||
}
|
|
||||||
zoomOnScroll={false}
|
|
||||||
zoomMax={12}
|
|
||||||
zoomMin={1}
|
|
||||||
zoomAnimate={true}
|
|
||||||
zoomStep={1.5}
|
|
||||||
regionStyle={{
|
|
||||||
initial: {
|
|
||||||
fill: mapColor || "#D0D5DD",
|
|
||||||
fillOpacity: 1,
|
|
||||||
fontFamily: "Outfit",
|
|
||||||
stroke: "none",
|
|
||||||
strokeWidth: 0,
|
|
||||||
strokeOpacity: 0,
|
|
||||||
},
|
|
||||||
hover: {
|
|
||||||
fillOpacity: 0.7,
|
|
||||||
cursor: "pointer",
|
|
||||||
fill: "#465fff",
|
|
||||||
stroke: "none",
|
|
||||||
},
|
|
||||||
selected: {
|
|
||||||
fill: "#465FFF",
|
|
||||||
},
|
|
||||||
selectedHover: {},
|
|
||||||
}}
|
|
||||||
regionLabelStyle={{
|
|
||||||
initial: {
|
|
||||||
fill: "#35373e",
|
|
||||||
fontWeight: 500,
|
|
||||||
fontSize: "13px",
|
|
||||||
stroke: "none",
|
|
||||||
},
|
|
||||||
hover: {},
|
|
||||||
selected: {},
|
|
||||||
selectedHover: {},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default CountryMap;
|
|
||||||
@@ -1,131 +0,0 @@
|
|||||||
"use client";
|
|
||||||
import Image from "next/image";
|
|
||||||
|
|
||||||
import CountryMap from "./CountryMap";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { MoreDotIcon } from "@/icons";
|
|
||||||
import { Dropdown } from "../ui/dropdown/Dropdown";
|
|
||||||
import { DropdownItem } from "../ui/dropdown/DropdownItem";
|
|
||||||
|
|
||||||
export default function DemographicCard() {
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
|
||||||
|
|
||||||
function toggleDropdown() {
|
|
||||||
setIsOpen(!isOpen);
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeDropdown() {
|
|
||||||
setIsOpen(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="rounded-2xl border border-gray-200 bg-white p-5 dark:border-gray-800 dark:bg-white/[0.03] sm:p-6">
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<div>
|
|
||||||
<h3 className="text-lg font-semibold text-gray-800 dark:text-white/90">
|
|
||||||
Customers Demographic
|
|
||||||
</h3>
|
|
||||||
<p className="mt-1 text-gray-500 text-theme-sm dark:text-gray-400">
|
|
||||||
Number of customer based on country
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="relative inline-block">
|
|
||||||
<button onClick={toggleDropdown} className="dropdown-toggle">
|
|
||||||
<MoreDotIcon className="text-gray-400 hover:text-gray-700 dark:hover:text-gray-300" />
|
|
||||||
</button>
|
|
||||||
<Dropdown
|
|
||||||
isOpen={isOpen}
|
|
||||||
onClose={closeDropdown}
|
|
||||||
className="w-40 p-2"
|
|
||||||
>
|
|
||||||
<DropdownItem
|
|
||||||
onItemClick={closeDropdown}
|
|
||||||
className="flex w-full font-normal text-left text-gray-500 rounded-lg hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-gray-300"
|
|
||||||
>
|
|
||||||
View More
|
|
||||||
</DropdownItem>
|
|
||||||
<DropdownItem
|
|
||||||
onItemClick={closeDropdown}
|
|
||||||
className="flex w-full font-normal text-left text-gray-500 rounded-lg hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-gray-300"
|
|
||||||
>
|
|
||||||
Delete
|
|
||||||
</DropdownItem>
|
|
||||||
</Dropdown>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="px-4 py-6 my-6 overflow-hidden border border-gary-200 rounded-2xl bg-gray-50 dark:border-gray-800 dark:bg-gray-900 sm:px-6">
|
|
||||||
<div
|
|
||||||
id="mapOne"
|
|
||||||
className="mapOne map-btn -mx-4 -my-6 h-[212px] w-[252px] 2xsm:w-[307px] xsm:w-[358px] sm:-mx-6 md:w-[668px] lg:w-[634px] xl:w-[393px] 2xl:w-[554px]"
|
|
||||||
>
|
|
||||||
<CountryMap />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-5">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<div className="items-center w-full rounded-full max-w-8">
|
|
||||||
<Image
|
|
||||||
width={48}
|
|
||||||
height={48}
|
|
||||||
src="/images/country/country-01.svg"
|
|
||||||
alt="usa"
|
|
||||||
className="w-full"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="font-semibold text-gray-800 text-theme-sm dark:text-white/90">
|
|
||||||
USA
|
|
||||||
</p>
|
|
||||||
<span className="block text-gray-500 text-theme-xs dark:text-gray-400">
|
|
||||||
2,379 Customers
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex w-full max-w-[140px] items-center gap-3">
|
|
||||||
<div className="relative block h-2 w-full max-w-[100px] rounded-sm bg-gray-200 dark:bg-gray-800">
|
|
||||||
<div className="absolute left-0 top-0 flex h-full w-[79%] items-center justify-center rounded-sm bg-brand-500 text-xs font-medium text-white"></div>
|
|
||||||
</div>
|
|
||||||
<p className="font-medium text-gray-800 text-theme-sm dark:text-white/90">
|
|
||||||
79%
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<div className="items-center w-full rounded-full max-w-8">
|
|
||||||
<Image
|
|
||||||
width={48}
|
|
||||||
height={48}
|
|
||||||
className="w-full"
|
|
||||||
src="/images/country/country-02.svg"
|
|
||||||
alt="france"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="font-semibold text-gray-800 text-theme-sm dark:text-white/90">
|
|
||||||
France
|
|
||||||
</p>
|
|
||||||
<span className="block text-gray-500 text-theme-xs dark:text-gray-400">
|
|
||||||
589 Customers
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex w-full max-w-[140px] items-center gap-3">
|
|
||||||
<div className="relative block h-2 w-full max-w-[100px] rounded-sm bg-gray-200 dark:bg-gray-800">
|
|
||||||
<div className="absolute left-0 top-0 flex h-full w-[23%] items-center justify-center rounded-sm bg-brand-500 text-xs font-medium text-white"></div>
|
|
||||||
</div>
|
|
||||||
<p className="font-medium text-gray-800 text-theme-sm dark:text-white/90">
|
|
||||||
23%
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
"use client";
|
|
||||||
import React from "react";
|
|
||||||
import Badge from "../ui/badge/Badge";
|
|
||||||
import { ArrowDownIcon, ArrowUpIcon, BoxIconLine, GroupIcon } from "@/icons";
|
|
||||||
|
|
||||||
export const EcommerceMetrics = () => {
|
|
||||||
return (
|
|
||||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:gap-6">
|
|
||||||
{/* <!-- Metric Item Start --> */}
|
|
||||||
<div className="rounded-2xl border border-gray-200 bg-white p-5 dark:border-gray-800 dark:bg-white/[0.03] md:p-6">
|
|
||||||
<div className="flex items-center justify-center w-12 h-12 bg-gray-100 rounded-xl dark:bg-gray-800">
|
|
||||||
<GroupIcon className="text-gray-800 size-6 dark:text-white/90" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-end justify-between mt-5">
|
|
||||||
<div>
|
|
||||||
<span className="text-sm text-gray-500 dark:text-gray-400">
|
|
||||||
Customers
|
|
||||||
</span>
|
|
||||||
<h4 className="mt-2 font-bold text-gray-800 text-title-sm dark:text-white/90">
|
|
||||||
3,782
|
|
||||||
</h4>
|
|
||||||
</div>
|
|
||||||
<Badge color="success">
|
|
||||||
<ArrowUpIcon />
|
|
||||||
11.01%
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/* <!-- Metric Item End --> */}
|
|
||||||
|
|
||||||
{/* <!-- Metric Item Start --> */}
|
|
||||||
<div className="rounded-2xl border border-gray-200 bg-white p-5 dark:border-gray-800 dark:bg-white/[0.03] md:p-6">
|
|
||||||
<div className="flex items-center justify-center w-12 h-12 bg-gray-100 rounded-xl dark:bg-gray-800">
|
|
||||||
<BoxIconLine className="text-gray-800 dark:text-white/90" />
|
|
||||||
</div>
|
|
||||||
<div className="flex items-end justify-between mt-5">
|
|
||||||
<div>
|
|
||||||
<span className="text-sm text-gray-500 dark:text-gray-400">
|
|
||||||
Orders
|
|
||||||
</span>
|
|
||||||
<h4 className="mt-2 font-bold text-gray-800 text-title-sm dark:text-white/90">
|
|
||||||
5,359
|
|
||||||
</h4>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Badge color="error">
|
|
||||||
<ArrowDownIcon className="text-error-500" />
|
|
||||||
9.05%
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/* <!-- Metric Item End --> */}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,154 +0,0 @@
|
|||||||
"use client";
|
|
||||||
import { ApexOptions } from "apexcharts";
|
|
||||||
import dynamic from "next/dynamic";
|
|
||||||
import { MoreDotIcon } from "@/icons";
|
|
||||||
import { DropdownItem } from "../ui/dropdown/DropdownItem";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { Dropdown } from "../ui/dropdown/Dropdown";
|
|
||||||
|
|
||||||
// Dynamically import the ReactApexChart component
|
|
||||||
const ReactApexChart = dynamic(() => import("react-apexcharts"), {
|
|
||||||
ssr: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
export default function MonthlySalesChart() {
|
|
||||||
const options: ApexOptions = {
|
|
||||||
colors: ["#465fff"],
|
|
||||||
chart: {
|
|
||||||
fontFamily: "Outfit, sans-serif",
|
|
||||||
type: "bar",
|
|
||||||
height: 180,
|
|
||||||
toolbar: {
|
|
||||||
show: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
plotOptions: {
|
|
||||||
bar: {
|
|
||||||
horizontal: false,
|
|
||||||
columnWidth: "39%",
|
|
||||||
borderRadius: 5,
|
|
||||||
borderRadiusApplication: "end",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
dataLabels: {
|
|
||||||
enabled: false,
|
|
||||||
},
|
|
||||||
stroke: {
|
|
||||||
show: true,
|
|
||||||
width: 4,
|
|
||||||
colors: ["transparent"],
|
|
||||||
},
|
|
||||||
xaxis: {
|
|
||||||
categories: [
|
|
||||||
"Jan",
|
|
||||||
"Feb",
|
|
||||||
"Mar",
|
|
||||||
"Apr",
|
|
||||||
"May",
|
|
||||||
"Jun",
|
|
||||||
"Jul",
|
|
||||||
"Aug",
|
|
||||||
"Sep",
|
|
||||||
"Oct",
|
|
||||||
"Nov",
|
|
||||||
"Dec",
|
|
||||||
],
|
|
||||||
axisBorder: {
|
|
||||||
show: false,
|
|
||||||
},
|
|
||||||
axisTicks: {
|
|
||||||
show: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
legend: {
|
|
||||||
show: true,
|
|
||||||
position: "top",
|
|
||||||
horizontalAlign: "left",
|
|
||||||
fontFamily: "Outfit",
|
|
||||||
},
|
|
||||||
yaxis: {
|
|
||||||
title: {
|
|
||||||
text: undefined,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
grid: {
|
|
||||||
yaxis: {
|
|
||||||
lines: {
|
|
||||||
show: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
fill: {
|
|
||||||
opacity: 1,
|
|
||||||
},
|
|
||||||
|
|
||||||
tooltip: {
|
|
||||||
x: {
|
|
||||||
show: false,
|
|
||||||
},
|
|
||||||
y: {
|
|
||||||
formatter: (val: number) => `${val}`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const series = [
|
|
||||||
{
|
|
||||||
name: "Sales",
|
|
||||||
data: [168, 385, 201, 298, 187, 195, 291, 110, 215, 390, 280, 112],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
|
||||||
|
|
||||||
function toggleDropdown() {
|
|
||||||
setIsOpen(!isOpen);
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeDropdown() {
|
|
||||||
setIsOpen(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="overflow-hidden rounded-2xl border border-gray-200 bg-white px-5 pt-5 dark:border-gray-800 dark:bg-white/[0.03] sm:px-6 sm:pt-6">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<h3 className="text-lg font-semibold text-gray-800 dark:text-white/90">
|
|
||||||
Monthly Sales
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<div className="relative inline-block">
|
|
||||||
<button onClick={toggleDropdown} className="dropdown-toggle">
|
|
||||||
<MoreDotIcon className="text-gray-400 hover:text-gray-700 dark:hover:text-gray-300" />
|
|
||||||
</button>
|
|
||||||
<Dropdown
|
|
||||||
isOpen={isOpen}
|
|
||||||
onClose={closeDropdown}
|
|
||||||
className="w-40 p-2"
|
|
||||||
>
|
|
||||||
<DropdownItem
|
|
||||||
onItemClick={closeDropdown}
|
|
||||||
className="flex w-full font-normal text-left text-gray-500 rounded-lg hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-gray-300"
|
|
||||||
>
|
|
||||||
View More
|
|
||||||
</DropdownItem>
|
|
||||||
<DropdownItem
|
|
||||||
onItemClick={closeDropdown}
|
|
||||||
className="flex w-full font-normal text-left text-gray-500 rounded-lg hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-gray-300"
|
|
||||||
>
|
|
||||||
Delete
|
|
||||||
</DropdownItem>
|
|
||||||
</Dropdown>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="max-w-full overflow-x-auto custom-scrollbar">
|
|
||||||
<div className="-ml-5 min-w-[650px] xl:min-w-full pl-2">
|
|
||||||
<ReactApexChart
|
|
||||||
options={options}
|
|
||||||
series={series}
|
|
||||||
type="bar"
|
|
||||||
height={180}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,209 +0,0 @@
|
|||||||
"use client";
|
|
||||||
// import Chart from "react-apexcharts";
|
|
||||||
import { ApexOptions } from "apexcharts";
|
|
||||||
|
|
||||||
import dynamic from "next/dynamic";
|
|
||||||
import { Dropdown } from "../ui/dropdown/Dropdown";
|
|
||||||
import { MoreDotIcon } from "@/icons";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { DropdownItem } from "../ui/dropdown/DropdownItem";
|
|
||||||
// Dynamically import the ReactApexChart component
|
|
||||||
const ReactApexChart = dynamic(() => import("react-apexcharts"), {
|
|
||||||
ssr: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
export default function MonthlyTarget() {
|
|
||||||
const series = [75.55];
|
|
||||||
const options: ApexOptions = {
|
|
||||||
colors: ["#465FFF"],
|
|
||||||
chart: {
|
|
||||||
fontFamily: "Outfit, sans-serif",
|
|
||||||
type: "radialBar",
|
|
||||||
height: 330,
|
|
||||||
sparkline: {
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
plotOptions: {
|
|
||||||
radialBar: {
|
|
||||||
startAngle: -85,
|
|
||||||
endAngle: 85,
|
|
||||||
hollow: {
|
|
||||||
size: "80%",
|
|
||||||
},
|
|
||||||
track: {
|
|
||||||
background: "#E4E7EC",
|
|
||||||
strokeWidth: "100%",
|
|
||||||
margin: 5, // margin is in pixels
|
|
||||||
},
|
|
||||||
dataLabels: {
|
|
||||||
name: {
|
|
||||||
show: false,
|
|
||||||
},
|
|
||||||
value: {
|
|
||||||
fontSize: "36px",
|
|
||||||
fontWeight: "600",
|
|
||||||
offsetY: -40,
|
|
||||||
color: "#1D2939",
|
|
||||||
formatter: function (val) {
|
|
||||||
return val + "%";
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
fill: {
|
|
||||||
type: "solid",
|
|
||||||
colors: ["#465FFF"],
|
|
||||||
},
|
|
||||||
stroke: {
|
|
||||||
lineCap: "round",
|
|
||||||
},
|
|
||||||
labels: ["Progress"],
|
|
||||||
};
|
|
||||||
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
|
||||||
|
|
||||||
function toggleDropdown() {
|
|
||||||
setIsOpen(!isOpen);
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeDropdown() {
|
|
||||||
setIsOpen(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="rounded-2xl border border-gray-200 bg-gray-100 dark:border-gray-800 dark:bg-white/[0.03]">
|
|
||||||
<div className="px-5 pt-5 bg-white shadow-default rounded-2xl pb-11 dark:bg-gray-900 sm:px-6 sm:pt-6">
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<div>
|
|
||||||
<h3 className="text-lg font-semibold text-gray-800 dark:text-white/90">
|
|
||||||
Monthly Target
|
|
||||||
</h3>
|
|
||||||
<p className="mt-1 font-normal text-gray-500 text-theme-sm dark:text-gray-400">
|
|
||||||
Target you’ve set for each month
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="relative inline-block">
|
|
||||||
<button onClick={toggleDropdown} className="dropdown-toggle">
|
|
||||||
<MoreDotIcon className="text-gray-400 hover:text-gray-700 dark:hover:text-gray-300" />
|
|
||||||
</button>
|
|
||||||
<Dropdown
|
|
||||||
isOpen={isOpen}
|
|
||||||
onClose={closeDropdown}
|
|
||||||
className="w-40 p-2"
|
|
||||||
>
|
|
||||||
<DropdownItem
|
|
||||||
tag="a"
|
|
||||||
onItemClick={closeDropdown}
|
|
||||||
className="flex w-full font-normal text-left text-gray-500 rounded-lg hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-gray-300"
|
|
||||||
>
|
|
||||||
View More
|
|
||||||
</DropdownItem>
|
|
||||||
<DropdownItem
|
|
||||||
tag="a"
|
|
||||||
onItemClick={closeDropdown}
|
|
||||||
className="flex w-full font-normal text-left text-gray-500 rounded-lg hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-gray-300"
|
|
||||||
>
|
|
||||||
Delete
|
|
||||||
</DropdownItem>
|
|
||||||
</Dropdown>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="relative ">
|
|
||||||
<div className="max-h-[330px]">
|
|
||||||
<ReactApexChart
|
|
||||||
options={options}
|
|
||||||
series={series}
|
|
||||||
type="radialBar"
|
|
||||||
height={330}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<span className="absolute left-1/2 top-full -translate-x-1/2 -translate-y-[95%] rounded-full bg-success-50 px-3 py-1 text-xs font-medium text-success-600 dark:bg-success-500/15 dark:text-success-500">
|
|
||||||
+10%
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<p className="mx-auto mt-10 w-full max-w-[380px] text-center text-sm text-gray-500 sm:text-base">
|
|
||||||
You earn $3287 today, it's higher than last month. Keep up your
|
|
||||||
good work!
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center justify-center gap-5 px-6 py-3.5 sm:gap-8 sm:py-5">
|
|
||||||
<div>
|
|
||||||
<p className="mb-1 text-center text-gray-500 text-theme-xs dark:text-gray-400 sm:text-sm">
|
|
||||||
Target
|
|
||||||
</p>
|
|
||||||
<p className="flex items-center justify-center gap-1 text-base font-semibold text-gray-800 dark:text-white/90 sm:text-lg">
|
|
||||||
$20K
|
|
||||||
<svg
|
|
||||||
width="16"
|
|
||||||
height="16"
|
|
||||||
viewBox="0 0 16 16"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fillRule="evenodd"
|
|
||||||
clipRule="evenodd"
|
|
||||||
d="M7.26816 13.6632C7.4056 13.8192 7.60686 13.9176 7.8311 13.9176C7.83148 13.9176 7.83187 13.9176 7.83226 13.9176C8.02445 13.9178 8.21671 13.8447 8.36339 13.6981L12.3635 9.70076C12.6565 9.40797 12.6567 8.9331 12.3639 8.6401C12.0711 8.34711 11.5962 8.34694 11.3032 8.63973L8.5811 11.36L8.5811 2.5C8.5811 2.08579 8.24531 1.75 7.8311 1.75C7.41688 1.75 7.0811 2.08579 7.0811 2.5L7.0811 11.3556L4.36354 8.63975C4.07055 8.34695 3.59568 8.3471 3.30288 8.64009C3.01008 8.93307 3.01023 9.40794 3.30321 9.70075L7.26816 13.6632Z"
|
|
||||||
fill="#D92D20"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="w-px bg-gray-200 h-7 dark:bg-gray-800"></div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<p className="mb-1 text-center text-gray-500 text-theme-xs dark:text-gray-400 sm:text-sm">
|
|
||||||
Revenue
|
|
||||||
</p>
|
|
||||||
<p className="flex items-center justify-center gap-1 text-base font-semibold text-gray-800 dark:text-white/90 sm:text-lg">
|
|
||||||
$20K
|
|
||||||
<svg
|
|
||||||
width="16"
|
|
||||||
height="16"
|
|
||||||
viewBox="0 0 16 16"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fillRule="evenodd"
|
|
||||||
clipRule="evenodd"
|
|
||||||
d="M7.60141 2.33683C7.73885 2.18084 7.9401 2.08243 8.16435 2.08243C8.16475 2.08243 8.16516 2.08243 8.16556 2.08243C8.35773 2.08219 8.54998 2.15535 8.69664 2.30191L12.6968 6.29924C12.9898 6.59203 12.9899 7.0669 12.6971 7.3599C12.4044 7.6529 11.9295 7.65306 11.6365 7.36027L8.91435 4.64004L8.91435 13.5C8.91435 13.9142 8.57856 14.25 8.16435 14.25C7.75013 14.25 7.41435 13.9142 7.41435 13.5L7.41435 4.64442L4.69679 7.36025C4.4038 7.65305 3.92893 7.6529 3.63613 7.35992C3.34333 7.06693 3.34348 6.59206 3.63646 6.29926L7.60141 2.33683Z"
|
|
||||||
fill="#039855"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="w-px bg-gray-200 h-7 dark:bg-gray-800"></div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<p className="mb-1 text-center text-gray-500 text-theme-xs dark:text-gray-400 sm:text-sm">
|
|
||||||
Today
|
|
||||||
</p>
|
|
||||||
<p className="flex items-center justify-center gap-1 text-base font-semibold text-gray-800 dark:text-white/90 sm:text-lg">
|
|
||||||
$20K
|
|
||||||
<svg
|
|
||||||
width="16"
|
|
||||||
height="16"
|
|
||||||
viewBox="0 0 16 16"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fillRule="evenodd"
|
|
||||||
clipRule="evenodd"
|
|
||||||
d="M7.60141 2.33683C7.73885 2.18084 7.9401 2.08243 8.16435 2.08243C8.16475 2.08243 8.16516 2.08243 8.16556 2.08243C8.35773 2.08219 8.54998 2.15535 8.69664 2.30191L12.6968 6.29924C12.9898 6.59203 12.9899 7.0669 12.6971 7.3599C12.4044 7.6529 11.9295 7.65306 11.6365 7.36027L8.91435 4.64004L8.91435 13.5C8.91435 13.9142 8.57856 14.25 8.16435 14.25C7.75013 14.25 7.41435 13.9142 7.41435 13.5L7.41435 4.64442L4.69679 7.36025C4.4038 7.65305 3.92893 7.6529 3.63613 7.35992C3.34333 7.06693 3.34348 6.59206 3.63646 6.29926L7.60141 2.33683Z"
|
|
||||||
fill="#039855"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,211 +0,0 @@
|
|||||||
import {
|
|
||||||
Table,
|
|
||||||
TableBody,
|
|
||||||
TableCell,
|
|
||||||
TableHeader,
|
|
||||||
TableRow,
|
|
||||||
} from "../ui/table";
|
|
||||||
import Badge from "../ui/badge/Badge";
|
|
||||||
import Image from "next/image";
|
|
||||||
|
|
||||||
// Define the TypeScript interface for the table rows
|
|
||||||
interface Product {
|
|
||||||
id: number; // Unique identifier for each product
|
|
||||||
name: string; // Product name
|
|
||||||
variants: string; // Number of variants (e.g., "1 Variant", "2 Variants")
|
|
||||||
category: string; // Category of the product
|
|
||||||
price: string; // Price of the product (as a string with currency symbol)
|
|
||||||
// status: string; // Status of the product
|
|
||||||
image: string; // URL or path to the product image
|
|
||||||
status: "Delivered" | "Pending" | "Canceled"; // Status of the product
|
|
||||||
}
|
|
||||||
|
|
||||||
// Define the table data using the interface
|
|
||||||
const tableData: Product[] = [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
name: "MacBook Pro 13”",
|
|
||||||
variants: "2 Variants",
|
|
||||||
category: "Laptop",
|
|
||||||
price: "$2399.00",
|
|
||||||
status: "Delivered",
|
|
||||||
image: "/images/product/product-01.jpg", // Replace with actual image URL
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
name: "Apple Watch Ultra",
|
|
||||||
variants: "1 Variant",
|
|
||||||
category: "Watch",
|
|
||||||
price: "$879.00",
|
|
||||||
status: "Pending",
|
|
||||||
image: "/images/product/product-02.jpg", // Replace with actual image URL
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
name: "iPhone 15 Pro Max",
|
|
||||||
variants: "2 Variants",
|
|
||||||
category: "SmartPhone",
|
|
||||||
price: "$1869.00",
|
|
||||||
status: "Delivered",
|
|
||||||
image: "/images/product/product-03.jpg", // Replace with actual image URL
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 4,
|
|
||||||
name: "iPad Pro 3rd Gen",
|
|
||||||
variants: "2 Variants",
|
|
||||||
category: "Electronics",
|
|
||||||
price: "$1699.00",
|
|
||||||
status: "Canceled",
|
|
||||||
image: "/images/product/product-04.jpg", // Replace with actual image URL
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 5,
|
|
||||||
name: "AirPods Pro 2nd Gen",
|
|
||||||
variants: "1 Variant",
|
|
||||||
category: "Accessories",
|
|
||||||
price: "$240.00",
|
|
||||||
status: "Delivered",
|
|
||||||
image: "/images/product/product-05.jpg", // Replace with actual image URL
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export default function RecentOrders() {
|
|
||||||
return (
|
|
||||||
<div className="overflow-hidden rounded-2xl border border-gray-200 bg-white px-4 pb-3 pt-4 dark:border-gray-800 dark:bg-white/[0.03] sm:px-6">
|
|
||||||
<div className="flex flex-col gap-2 mb-4 sm:flex-row sm:items-center sm:justify-between">
|
|
||||||
<div>
|
|
||||||
<h3 className="text-lg font-semibold text-gray-800 dark:text-white/90">
|
|
||||||
Recent Orders
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<button className="inline-flex items-center gap-2 rounded-lg border border-gray-300 bg-white px-4 py-2.5 text-theme-sm font-medium text-gray-700 shadow-theme-xs hover:bg-gray-50 hover:text-gray-800 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-white/[0.03] dark:hover:text-gray-200">
|
|
||||||
<svg
|
|
||||||
className="stroke-current fill-white dark:fill-gray-800"
|
|
||||||
width="20"
|
|
||||||
height="20"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M2.29004 5.90393H17.7067"
|
|
||||||
stroke=""
|
|
||||||
strokeWidth="1.5"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M17.7075 14.0961H2.29085"
|
|
||||||
stroke=""
|
|
||||||
strokeWidth="1.5"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M12.0826 3.33331C13.5024 3.33331 14.6534 4.48431 14.6534 5.90414C14.6534 7.32398 13.5024 8.47498 12.0826 8.47498C10.6627 8.47498 9.51172 7.32398 9.51172 5.90415C9.51172 4.48432 10.6627 3.33331 12.0826 3.33331Z"
|
|
||||||
fill=""
|
|
||||||
stroke=""
|
|
||||||
strokeWidth="1.5"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M7.91745 11.525C6.49762 11.525 5.34662 12.676 5.34662 14.0959C5.34661 15.5157 6.49762 16.6667 7.91745 16.6667C9.33728 16.6667 10.4883 15.5157 10.4883 14.0959C10.4883 12.676 9.33728 11.525 7.91745 11.525Z"
|
|
||||||
fill=""
|
|
||||||
stroke=""
|
|
||||||
strokeWidth="1.5"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
Filter
|
|
||||||
</button>
|
|
||||||
<button className="inline-flex items-center gap-2 rounded-lg border border-gray-300 bg-white px-4 py-2.5 text-theme-sm font-medium text-gray-700 shadow-theme-xs hover:bg-gray-50 hover:text-gray-800 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-white/[0.03] dark:hover:text-gray-200">
|
|
||||||
See all
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="max-w-full overflow-x-auto">
|
|
||||||
<Table>
|
|
||||||
{/* Table Header */}
|
|
||||||
<TableHeader className="border-gray-100 dark:border-gray-800 border-y">
|
|
||||||
<TableRow>
|
|
||||||
<TableCell
|
|
||||||
isHeader
|
|
||||||
className="py-3 font-medium text-gray-500 text-start text-theme-xs dark:text-gray-400"
|
|
||||||
>
|
|
||||||
Products
|
|
||||||
</TableCell>
|
|
||||||
<TableCell
|
|
||||||
isHeader
|
|
||||||
className="py-3 font-medium text-gray-500 text-start text-theme-xs dark:text-gray-400"
|
|
||||||
>
|
|
||||||
Category
|
|
||||||
</TableCell>
|
|
||||||
<TableCell
|
|
||||||
isHeader
|
|
||||||
className="py-3 font-medium text-gray-500 text-start text-theme-xs dark:text-gray-400"
|
|
||||||
>
|
|
||||||
Price
|
|
||||||
</TableCell>
|
|
||||||
<TableCell
|
|
||||||
isHeader
|
|
||||||
className="py-3 font-medium text-gray-500 text-start text-theme-xs dark:text-gray-400"
|
|
||||||
>
|
|
||||||
Status
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
|
|
||||||
{/* Table Body */}
|
|
||||||
|
|
||||||
<TableBody className="divide-y divide-gray-100 dark:divide-gray-800">
|
|
||||||
{tableData.map((product) => (
|
|
||||||
<TableRow key={product.id} className="">
|
|
||||||
<TableCell className="py-3">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<div className="h-[50px] w-[50px] overflow-hidden rounded-md">
|
|
||||||
<Image
|
|
||||||
width={50}
|
|
||||||
height={50}
|
|
||||||
src={product.image}
|
|
||||||
className="h-[50px] w-[50px]"
|
|
||||||
alt={product.name}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="font-medium text-gray-800 text-theme-sm dark:text-white/90">
|
|
||||||
{product.name}
|
|
||||||
</p>
|
|
||||||
<span className="text-gray-500 text-theme-xs dark:text-gray-400">
|
|
||||||
{product.variants}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="py-3 text-gray-500 text-theme-sm dark:text-gray-400">
|
|
||||||
{product.price}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="py-3 text-gray-500 text-theme-sm dark:text-gray-400">
|
|
||||||
{product.category}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="py-3 text-gray-500 text-theme-sm dark:text-gray-400">
|
|
||||||
<Badge
|
|
||||||
size="sm"
|
|
||||||
color={
|
|
||||||
product.status === "Delivered"
|
|
||||||
? "success"
|
|
||||||
: product.status === "Pending"
|
|
||||||
? "warning"
|
|
||||||
: "error"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{product.status}
|
|
||||||
</Badge>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
))}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,180 +0,0 @@
|
|||||||
"use client";
|
|
||||||
import { useEffect, useRef } from "react";
|
|
||||||
import dynamic from "next/dynamic";
|
|
||||||
import { ApexOptions } from "apexcharts";
|
|
||||||
import flatpickr from "flatpickr";
|
|
||||||
import ChartTab from "../common/ChartTab";
|
|
||||||
import { CalenderIcon } from "../../icons";
|
|
||||||
|
|
||||||
const Chart = dynamic(() => import("react-apexcharts"), { ssr: false });
|
|
||||||
|
|
||||||
export default function StatisticsChart() {
|
|
||||||
const datePickerRef = useRef<HTMLInputElement>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!datePickerRef.current) return;
|
|
||||||
|
|
||||||
const today = new Date();
|
|
||||||
const sevenDaysAgo = new Date();
|
|
||||||
sevenDaysAgo.setDate(today.getDate() - 6);
|
|
||||||
|
|
||||||
const fp = flatpickr(datePickerRef.current, {
|
|
||||||
mode: "range",
|
|
||||||
static: true,
|
|
||||||
monthSelectorType: "static",
|
|
||||||
dateFormat: "M d",
|
|
||||||
defaultDate: [sevenDaysAgo, today],
|
|
||||||
clickOpens: true,
|
|
||||||
prevArrow:
|
|
||||||
'<svg class="stroke-current" width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M12.5 15L7.5 10L12.5 5" stroke="" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>',
|
|
||||||
nextArrow:
|
|
||||||
'<svg class="stroke-current" width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M7.5 15L12.5 10L7.5 5" stroke="" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>',
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
if (!Array.isArray(fp)) {
|
|
||||||
fp.destroy();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const options: ApexOptions = {
|
|
||||||
legend: {
|
|
||||||
show: false, // Hide legend
|
|
||||||
position: "top",
|
|
||||||
horizontalAlign: "left",
|
|
||||||
},
|
|
||||||
colors: ["#465FFF", "#9CB9FF"], // Define line colors
|
|
||||||
chart: {
|
|
||||||
fontFamily: "Outfit, sans-serif",
|
|
||||||
height: 310,
|
|
||||||
type: "line", // Set the chart type to 'line'
|
|
||||||
toolbar: {
|
|
||||||
show: false, // Hide chart toolbar
|
|
||||||
},
|
|
||||||
},
|
|
||||||
stroke: {
|
|
||||||
curve: "straight", // Define the line style (straight, smooth, or step)
|
|
||||||
width: [2, 2], // Line width for each dataset
|
|
||||||
},
|
|
||||||
|
|
||||||
fill: {
|
|
||||||
type: "gradient",
|
|
||||||
gradient: {
|
|
||||||
opacityFrom: 0.55,
|
|
||||||
opacityTo: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
markers: {
|
|
||||||
size: 0, // Size of the marker points
|
|
||||||
strokeColors: "#fff", // Marker border color
|
|
||||||
strokeWidth: 2,
|
|
||||||
hover: {
|
|
||||||
size: 6, // Marker size on hover
|
|
||||||
},
|
|
||||||
},
|
|
||||||
grid: {
|
|
||||||
xaxis: {
|
|
||||||
lines: {
|
|
||||||
show: false, // Hide grid lines on x-axis
|
|
||||||
},
|
|
||||||
},
|
|
||||||
yaxis: {
|
|
||||||
lines: {
|
|
||||||
show: true, // Show grid lines on y-axis
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
dataLabels: {
|
|
||||||
enabled: false, // Disable data labels
|
|
||||||
},
|
|
||||||
tooltip: {
|
|
||||||
enabled: true, // Enable tooltip
|
|
||||||
x: {
|
|
||||||
format: "dd MMM yyyy", // Format for x-axis tooltip
|
|
||||||
},
|
|
||||||
},
|
|
||||||
xaxis: {
|
|
||||||
type: "category", // Category-based x-axis
|
|
||||||
categories: [
|
|
||||||
"Jan",
|
|
||||||
"Feb",
|
|
||||||
"Mar",
|
|
||||||
"Apr",
|
|
||||||
"May",
|
|
||||||
"Jun",
|
|
||||||
"Jul",
|
|
||||||
"Aug",
|
|
||||||
"Sep",
|
|
||||||
"Oct",
|
|
||||||
"Nov",
|
|
||||||
"Dec",
|
|
||||||
],
|
|
||||||
axisBorder: {
|
|
||||||
show: false, // Hide x-axis border
|
|
||||||
},
|
|
||||||
axisTicks: {
|
|
||||||
show: false, // Hide x-axis ticks
|
|
||||||
},
|
|
||||||
tooltip: {
|
|
||||||
enabled: false, // Disable tooltip for x-axis points
|
|
||||||
},
|
|
||||||
},
|
|
||||||
yaxis: {
|
|
||||||
labels: {
|
|
||||||
style: {
|
|
||||||
fontSize: "12px", // Adjust font size for y-axis labels
|
|
||||||
colors: ["#6B7280"], // Color of the labels
|
|
||||||
},
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
text: "", // Remove y-axis title
|
|
||||||
style: {
|
|
||||||
fontSize: "0px",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const series = [
|
|
||||||
{
|
|
||||||
name: "Sales",
|
|
||||||
data: [180, 190, 170, 160, 175, 165, 170, 205, 230, 210, 240, 235],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Revenue",
|
|
||||||
data: [40, 30, 50, 40, 55, 40, 70, 100, 110, 120, 150, 140],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
return (
|
|
||||||
<div className="rounded-2xl border border-gray-200 bg-white px-5 pb-5 pt-5 dark:border-gray-800 dark:bg-white/[0.03] sm:px-6 sm:pt-6">
|
|
||||||
<div className="flex flex-col gap-5 mb-6 sm:flex-row sm:justify-between">
|
|
||||||
<div className="w-full">
|
|
||||||
<h3 className="text-lg font-semibold text-gray-800 dark:text-white/90">
|
|
||||||
Statistics
|
|
||||||
</h3>
|
|
||||||
<p className="mt-1 text-gray-500 text-theme-sm dark:text-gray-400">
|
|
||||||
Target you've set for each month
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-3 sm:justify-end">
|
|
||||||
<ChartTab />
|
|
||||||
<div className="relative inline-flex items-center">
|
|
||||||
<CalenderIcon className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 lg:left-3 lg:top-1/2 lg:translate-x-0 lg:-translate-y-1/2 text-gray-500 dark:text-gray-400 pointer-events-none z-10" />
|
|
||||||
<input
|
|
||||||
ref={datePickerRef}
|
|
||||||
className="h-10 w-10 lg:w-40 lg:h-auto lg:pl-10 lg:pr-3 lg:py-2 rounded-lg border border-gray-200 bg-white text-sm font-medium text-transparent lg:text-gray-700 outline-none dark:border-gray-700 dark:bg-gray-800 dark:lg:text-gray-300 cursor-pointer"
|
|
||||||
placeholder="Select date range"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="max-w-full overflow-x-auto custom-scrollbar">
|
|
||||||
<div className="min-w-[1000px] xl:min-w-full">
|
|
||||||
<Chart options={options} series={series} type="area" height={310} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
"use client";
|
|
||||||
import React from "react";
|
|
||||||
import ComponentCard from "../../common/ComponentCard";
|
|
||||||
|
|
||||||
import { Modal } from "../../ui/modal";
|
|
||||||
import Button from "../../ui/button/Button";
|
|
||||||
import { useModal } from "@/hooks/useModal";
|
|
||||||
|
|
||||||
export default function DefaultModal() {
|
|
||||||
const { isOpen, openModal, closeModal } = useModal();
|
|
||||||
const handleSave = () => {
|
|
||||||
// Handle save logic here
|
|
||||||
console.log("Saving changes...");
|
|
||||||
closeModal();
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<ComponentCard title="Default Modal">
|
|
||||||
<Button size="sm" onClick={openModal}>
|
|
||||||
Open Modal
|
|
||||||
</Button>
|
|
||||||
<Modal
|
|
||||||
isOpen={isOpen}
|
|
||||||
onClose={closeModal}
|
|
||||||
className="max-w-[600px] p-5 lg:p-10"
|
|
||||||
>
|
|
||||||
<h4 className="font-semibold text-gray-800 mb-7 text-title-sm dark:text-white/90">
|
|
||||||
Modal Heading
|
|
||||||
</h4>
|
|
||||||
<p className="text-sm leading-6 text-gray-500 dark:text-gray-400">
|
|
||||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
|
|
||||||
Pellentesque euismod est quis mauris lacinia pharetra. Sed a ligula
|
|
||||||
ac odio condimentum aliquet a nec nulla. Aliquam bibendum ex sit
|
|
||||||
amet ipsum rutrum feugiat ultrices enim quam.
|
|
||||||
</p>
|
|
||||||
<p className="mt-5 text-sm leading-6 text-gray-500 dark:text-gray-400">
|
|
||||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
|
|
||||||
Pellentesque euismod est quis mauris lacinia pharetra. Sed a ligula
|
|
||||||
ac odio.
|
|
||||||
</p>
|
|
||||||
<div className="flex items-center justify-end w-full gap-3 mt-8">
|
|
||||||
<Button size="sm" variant="outline" onClick={closeModal}>
|
|
||||||
Close
|
|
||||||
</Button>
|
|
||||||
<Button size="sm" onClick={handleSave}>
|
|
||||||
Save Changes
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
</ComponentCard>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
"use client";
|
|
||||||
import React from "react";
|
|
||||||
import ComponentCard from "../../common/ComponentCard";
|
|
||||||
import Button from "../../ui/button/Button";
|
|
||||||
import { Modal } from "../../ui/modal";
|
|
||||||
import Label from "../../form/Label";
|
|
||||||
import Input from "../../form/input/InputField";
|
|
||||||
import { useModal } from "@/hooks/useModal";
|
|
||||||
|
|
||||||
export default function FormInModal() {
|
|
||||||
const { isOpen, openModal, closeModal } = useModal();
|
|
||||||
const handleSave = () => {
|
|
||||||
// Handle save logic here
|
|
||||||
console.log("Saving changes...");
|
|
||||||
closeModal();
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<ComponentCard title="Form In Modal">
|
|
||||||
<Button size="sm" onClick={openModal}>
|
|
||||||
Open Modal
|
|
||||||
</Button>
|
|
||||||
<Modal
|
|
||||||
isOpen={isOpen}
|
|
||||||
onClose={closeModal}
|
|
||||||
className="max-w-[584px] p-5 lg:p-10"
|
|
||||||
>
|
|
||||||
<form className="">
|
|
||||||
<h4 className="mb-6 text-lg font-medium text-gray-800 dark:text-white/90">
|
|
||||||
Personal Information
|
|
||||||
</h4>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 gap-x-6 gap-y-5 sm:grid-cols-2">
|
|
||||||
<div className="col-span-1">
|
|
||||||
<Label>First Name</Label>
|
|
||||||
<Input type="text" placeholder="Emirhan" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="col-span-1">
|
|
||||||
<Label>Last Name</Label>
|
|
||||||
<Input type="text" placeholder="Boruch" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="col-span-1">
|
|
||||||
<Label>Last Name</Label>
|
|
||||||
<Input type="email" placeholder="emirhanboruch55@gmail.com" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="col-span-1">
|
|
||||||
<Label>Phone</Label>
|
|
||||||
<Input type="text" placeholder="+09 363 398 46" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="col-span-1 sm:col-span-2">
|
|
||||||
<Label>Bio</Label>
|
|
||||||
<Input type="text" placeholder="Team Manager" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center justify-end w-full gap-3 mt-6">
|
|
||||||
<Button size="sm" variant="outline" onClick={closeModal}>
|
|
||||||
Close
|
|
||||||
</Button>
|
|
||||||
<Button size="sm" onClick={handleSave}>
|
|
||||||
Save Changes
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</Modal>
|
|
||||||
</ComponentCard>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
"use client";
|
|
||||||
import { useModal } from "@/hooks/useModal";
|
|
||||||
import ComponentCard from "../../common/ComponentCard";
|
|
||||||
|
|
||||||
import Button from "../../ui/button/Button";
|
|
||||||
import { Modal } from "../../ui/modal";
|
|
||||||
|
|
||||||
export default function FullScreenModal() {
|
|
||||||
const {
|
|
||||||
isOpen: isFullscreenModalOpen,
|
|
||||||
openModal: openFullscreenModal,
|
|
||||||
closeModal: closeFullscreenModal,
|
|
||||||
} = useModal();
|
|
||||||
const handleSave = () => {
|
|
||||||
// Handle save logic here
|
|
||||||
console.log("Saving changes...");
|
|
||||||
closeFullscreenModal();
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<ComponentCard title="Full Screen Modal">
|
|
||||||
<Button size="sm" onClick={openFullscreenModal}>
|
|
||||||
Open Modal
|
|
||||||
</Button>
|
|
||||||
<Modal
|
|
||||||
isOpen={isFullscreenModalOpen}
|
|
||||||
onClose={closeFullscreenModal}
|
|
||||||
isFullscreen={true}
|
|
||||||
showCloseButton={true}
|
|
||||||
>
|
|
||||||
<div className="fixed top-0 left-0 flex flex-col justify-between w-full h-screen p-6 overflow-x-hidden overflow-y-auto bg-white dark:bg-gray-900 lg:p-10">
|
|
||||||
<div>
|
|
||||||
<h4 className="font-semibold text-gray-800 mb-7 text-title-sm dark:text-white/90">
|
|
||||||
Modal Heading
|
|
||||||
</h4>
|
|
||||||
<p className="text-sm leading-6 text-gray-500 dark:text-gray-400">
|
|
||||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
|
|
||||||
Pellentesque euismod est quis mauris lacinia pharetra. Sed a
|
|
||||||
ligula ac odio condimentum aliquet a nec nulla. Aliquam bibendum
|
|
||||||
ex sit amet ipsum rutrum feugiat ultrices enim quam.
|
|
||||||
</p>
|
|
||||||
<p className="mt-5 text-sm leading-6 text-gray-500 dark:text-gray-400">
|
|
||||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
|
|
||||||
Pellentesque euismod est quis mauris lacinia pharetra. Sed a
|
|
||||||
ligula ac odio condimentum aliquet a nec nulla. Aliquam bibendum
|
|
||||||
ex sit amet ipsum rutrum feugiat ultrices enim quam odio
|
|
||||||
condimentum aliquet a nec nulla pellentesque euismod est quis
|
|
||||||
mauris lacinia pharetra.
|
|
||||||
</p>
|
|
||||||
<p className="mt-5 text-sm leading-6 text-gray-500 dark:text-gray-400">
|
|
||||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
|
|
||||||
Pellentesque euismod est quis mauris lacinia pharetra.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-end w-full gap-3 mt-8">
|
|
||||||
<Button size="sm" variant="outline" onClick={closeFullscreenModal}>
|
|
||||||
Close
|
|
||||||
</Button>
|
|
||||||
<Button size="sm" onClick={handleSave}>
|
|
||||||
Save Changes
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
</ComponentCard>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,282 +0,0 @@
|
|||||||
"use client";
|
|
||||||
import React from "react";
|
|
||||||
import ComponentCard from "../../common/ComponentCard";
|
|
||||||
|
|
||||||
import { Modal } from "../../ui/modal";
|
|
||||||
import { useModal } from "@/hooks/useModal";
|
|
||||||
|
|
||||||
export default function ModalBasedAlerts() {
|
|
||||||
const successModal = useModal();
|
|
||||||
const infoModal = useModal();
|
|
||||||
const warningModal = useModal();
|
|
||||||
const errorModal = useModal();
|
|
||||||
return (
|
|
||||||
<ComponentCard title="Modal Based Alerts">
|
|
||||||
<div className="flex flex-wrap items-center gap-3">
|
|
||||||
<button
|
|
||||||
onClick={successModal.openModal}
|
|
||||||
className="px-4 py-3 text-sm font-medium text-white rounded-lg bg-success-500 shadow-theme-xs hover:bg-success-600"
|
|
||||||
>
|
|
||||||
Success Alert
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={infoModal.openModal}
|
|
||||||
className="px-4 py-3 text-sm font-medium text-white rounded-lg bg-blue-light-500 shadow-theme-xs hover:bg-blue-light-600"
|
|
||||||
>
|
|
||||||
Info Alert
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={warningModal.openModal}
|
|
||||||
className="px-4 py-3 text-sm font-medium text-white rounded-lg bg-warning-500 shadow-theme-xs hover:bg-warning-600"
|
|
||||||
>
|
|
||||||
Warning Alert
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={errorModal.openModal}
|
|
||||||
className="px-4 py-3 text-sm font-medium text-white rounded-lg bg-error-500 shadow-theme-xs hover:bg-error-600"
|
|
||||||
>
|
|
||||||
Danger Alert
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{/* Success Modal */}
|
|
||||||
<Modal
|
|
||||||
isOpen={successModal.isOpen}
|
|
||||||
onClose={successModal.closeModal}
|
|
||||||
className="max-w-[600px] p-5 lg:p-10"
|
|
||||||
>
|
|
||||||
<div className="text-center">
|
|
||||||
<div className="relative flex items-center justify-center z-1 mb-7">
|
|
||||||
<svg
|
|
||||||
className="fill-success-50 dark:fill-success-500/15"
|
|
||||||
width="90"
|
|
||||||
height="90"
|
|
||||||
viewBox="0 0 90 90"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M34.364 6.85053C38.6205 -2.28351 51.3795 -2.28351 55.636 6.85053C58.0129 11.951 63.5594 14.6722 68.9556 13.3853C78.6192 11.0807 86.5743 21.2433 82.2185 30.3287C79.7862 35.402 81.1561 41.5165 85.5082 45.0122C93.3019 51.2725 90.4628 63.9451 80.7747 66.1403C75.3648 67.3661 71.5265 72.2695 71.5572 77.9156C71.6123 88.0265 60.1169 93.6664 52.3918 87.3184C48.0781 83.7737 41.9219 83.7737 37.6082 87.3184C29.8831 93.6664 18.3877 88.0266 18.4428 77.9156C18.4735 72.2695 14.6352 67.3661 9.22531 66.1403C-0.462787 63.9451 -3.30193 51.2725 4.49185 45.0122C8.84391 41.5165 10.2138 35.402 7.78151 30.3287C3.42572 21.2433 11.3808 11.0807 21.0444 13.3853C26.4406 14.6722 31.9871 11.951 34.364 6.85053Z"
|
|
||||||
fill=""
|
|
||||||
fillOpacity=""
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
|
|
||||||
<span className="absolute -translate-x-1/2 -translate-y-1/2 left-1/2 top-1/2">
|
|
||||||
<svg
|
|
||||||
className="fill-success-600 dark:fill-success-500"
|
|
||||||
width="38"
|
|
||||||
height="38"
|
|
||||||
viewBox="0 0 38 38"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fillRule="evenodd"
|
|
||||||
clipRule="evenodd"
|
|
||||||
d="M5.9375 19.0004C5.9375 11.7854 11.7864 5.93652 19.0014 5.93652C26.2164 5.93652 32.0653 11.7854 32.0653 19.0004C32.0653 26.2154 26.2164 32.0643 19.0014 32.0643C11.7864 32.0643 5.9375 26.2154 5.9375 19.0004ZM19.0014 2.93652C10.1296 2.93652 2.9375 10.1286 2.9375 19.0004C2.9375 27.8723 10.1296 35.0643 19.0014 35.0643C27.8733 35.0643 35.0653 27.8723 35.0653 19.0004C35.0653 10.1286 27.8733 2.93652 19.0014 2.93652ZM24.7855 17.0575C25.3713 16.4717 25.3713 15.522 24.7855 14.9362C24.1997 14.3504 23.25 14.3504 22.6642 14.9362L17.7177 19.8827L15.3387 17.5037C14.7529 16.9179 13.8031 16.9179 13.2173 17.5037C12.6316 18.0894 12.6316 19.0392 13.2173 19.625L16.657 23.0647C16.9383 23.346 17.3199 23.504 17.7177 23.504C18.1155 23.504 18.4971 23.346 18.7784 23.0647L24.7855 17.0575Z"
|
|
||||||
fill=""
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<h4 className="mb-2 text-2xl font-semibold text-gray-800 dark:text-white/90 sm:text-title-sm">
|
|
||||||
Well Done!
|
|
||||||
</h4>
|
|
||||||
<p className="text-sm leading-6 text-gray-500 dark:text-gray-400">
|
|
||||||
Lorem ipsum dolor sit amet consectetur. Feugiat ipsum libero tempor
|
|
||||||
felis risus nisi non. Quisque eu ut tempor curabitur.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="flex items-center justify-center w-full gap-3 mt-7">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="flex justify-center w-full px-4 py-3 text-sm font-medium text-white rounded-lg bg-success-500 shadow-theme-xs hover:bg-success-600 sm:w-auto"
|
|
||||||
>
|
|
||||||
Okay, Got It
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
{/* Info Modal */}
|
|
||||||
<Modal
|
|
||||||
isOpen={infoModal.isOpen}
|
|
||||||
onClose={infoModal.closeModal}
|
|
||||||
className="max-w-[600px] p-5 lg:p-10"
|
|
||||||
>
|
|
||||||
<div className="text-center">
|
|
||||||
<div className="relative flex items-center justify-center z-1 mb-7">
|
|
||||||
<svg
|
|
||||||
className="fill-blue-light-50 dark:fill-blue-light-500/15"
|
|
||||||
width="90"
|
|
||||||
height="90"
|
|
||||||
viewBox="0 0 90 90"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M34.364 6.85053C38.6205 -2.28351 51.3795 -2.28351 55.636 6.85053C58.0129 11.951 63.5594 14.6722 68.9556 13.3853C78.6192 11.0807 86.5743 21.2433 82.2185 30.3287C79.7862 35.402 81.1561 41.5165 85.5082 45.0122C93.3019 51.2725 90.4628 63.9451 80.7747 66.1403C75.3648 67.3661 71.5265 72.2695 71.5572 77.9156C71.6123 88.0265 60.1169 93.6664 52.3918 87.3184C48.0781 83.7737 41.9219 83.7737 37.6082 87.3184C29.8831 93.6664 18.3877 88.0266 18.4428 77.9156C18.4735 72.2695 14.6352 67.3661 9.22531 66.1403C-0.462787 63.9451 -3.30193 51.2725 4.49185 45.0122C8.84391 41.5165 10.2138 35.402 7.78151 30.3287C3.42572 21.2433 11.3808 11.0807 21.0444 13.3853C26.4406 14.6722 31.9871 11.951 34.364 6.85053Z"
|
|
||||||
fill=""
|
|
||||||
fillOpacity=""
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
|
|
||||||
<span className="absolute -translate-x-1/2 -translate-y-1/2 left-1/2 top-1/2">
|
|
||||||
<svg
|
|
||||||
className="fill-blue-light-500 dark:fill-blue-light-500"
|
|
||||||
width="38"
|
|
||||||
height="38"
|
|
||||||
viewBox="0 0 38 38"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fillRule="evenodd"
|
|
||||||
clipRule="evenodd"
|
|
||||||
d="M5.85547 18.9998C5.85547 11.7396 11.7411 5.854 19.0013 5.854C26.2615 5.854 32.1471 11.7396 32.1471 18.9998C32.1471 26.2601 26.2615 32.1457 19.0013 32.1457C11.7411 32.1457 5.85547 26.2601 5.85547 18.9998ZM19.0013 2.854C10.0842 2.854 2.85547 10.0827 2.85547 18.9998C2.85547 27.9169 10.0842 35.1457 19.0013 35.1457C27.9184 35.1457 35.1471 27.9169 35.1471 18.9998C35.1471 10.0827 27.9184 2.854 19.0013 2.854ZM16.9999 11.9145C16.9999 13.0191 17.8953 13.9145 18.9999 13.9145H19.0015C20.106 13.9145 21.0015 13.0191 21.0015 11.9145C21.0015 10.81 20.106 9.91454 19.0015 9.91454H18.9999C17.8953 9.91454 16.9999 10.81 16.9999 11.9145ZM19.0014 27.8171C18.173 27.8171 17.5014 27.1455 17.5014 26.3171V17.3293C17.5014 16.5008 18.173 15.8293 19.0014 15.8293C19.8299 15.8293 20.5014 16.5008 20.5014 17.3293L20.5014 26.3171C20.5014 27.1455 19.8299 27.8171 19.0014 27.8171Z"
|
|
||||||
fill=""
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h4 className="mb-2 text-2xl font-semibold text-gray-800 dark:text-white/90 sm:text-title-sm">
|
|
||||||
Information Alert!
|
|
||||||
</h4>
|
|
||||||
<p className="text-sm leading-6 text-gray-500 dark:text-gray-400">
|
|
||||||
Lorem ipsum dolor sit amet consectetur. Feugiat ipsum libero tempor
|
|
||||||
felis risus nisi non. Quisque eu ut tempor curabitur.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="flex items-center justify-center w-full gap-3 mt-7">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="flex justify-center w-full px-4 py-3 text-sm font-medium text-white rounded-lg bg-blue-light-500 shadow-theme-xs hover:bg-blue-light-600 sm:w-auto"
|
|
||||||
>
|
|
||||||
Okay, Got It
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
{/* Warning Modal */}
|
|
||||||
<Modal
|
|
||||||
isOpen={warningModal.isOpen}
|
|
||||||
onClose={warningModal.closeModal}
|
|
||||||
className="max-w-[600px] p-5 lg:p-10"
|
|
||||||
>
|
|
||||||
<div className="text-center">
|
|
||||||
<div className="relative flex items-center justify-center z-1 mb-7">
|
|
||||||
<svg
|
|
||||||
className="fill-warning-50 dark:fill-warning-500/15"
|
|
||||||
width="90"
|
|
||||||
height="90"
|
|
||||||
viewBox="0 0 90 90"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M34.364 6.85053C38.6205 -2.28351 51.3795 -2.28351 55.636 6.85053C58.0129 11.951 63.5594 14.6722 68.9556 13.3853C78.6192 11.0807 86.5743 21.2433 82.2185 30.3287C79.7862 35.402 81.1561 41.5165 85.5082 45.0122C93.3019 51.2725 90.4628 63.9451 80.7747 66.1403C75.3648 67.3661 71.5265 72.2695 71.5572 77.9156C71.6123 88.0265 60.1169 93.6664 52.3918 87.3184C48.0781 83.7737 41.9219 83.7737 37.6082 87.3184C29.8831 93.6664 18.3877 88.0266 18.4428 77.9156C18.4735 72.2695 14.6352 67.3661 9.22531 66.1403C-0.462787 63.9451 -3.30193 51.2725 4.49185 45.0122C8.84391 41.5165 10.2138 35.402 7.78151 30.3287C3.42572 21.2433 11.3808 11.0807 21.0444 13.3853C26.4406 14.6722 31.9871 11.951 34.364 6.85053Z"
|
|
||||||
fill=""
|
|
||||||
fillOpacity=""
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
|
|
||||||
<span className="absolute -translate-x-1/2 -translate-y-1/2 left-1/2 top-1/2">
|
|
||||||
<svg
|
|
||||||
className="fill-warning-600 dark:fill-orange-400"
|
|
||||||
width="38"
|
|
||||||
height="38"
|
|
||||||
viewBox="0 0 38 38"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fillRule="evenodd"
|
|
||||||
clipRule="evenodd"
|
|
||||||
d="M32.1445 19.0002C32.1445 26.2604 26.2589 32.146 18.9987 32.146C11.7385 32.146 5.85287 26.2604 5.85287 19.0002C5.85287 11.7399 11.7385 5.85433 18.9987 5.85433C26.2589 5.85433 32.1445 11.7399 32.1445 19.0002ZM18.9987 35.146C27.9158 35.146 35.1445 27.9173 35.1445 19.0002C35.1445 10.0831 27.9158 2.85433 18.9987 2.85433C10.0816 2.85433 2.85287 10.0831 2.85287 19.0002C2.85287 27.9173 10.0816 35.146 18.9987 35.146ZM21.0001 26.0855C21.0001 24.9809 20.1047 24.0855 19.0001 24.0855L18.9985 24.0855C17.894 24.0855 16.9985 24.9809 16.9985 26.0855C16.9985 27.19 17.894 28.0855 18.9985 28.0855L19.0001 28.0855C20.1047 28.0855 21.0001 27.19 21.0001 26.0855ZM18.9986 10.1829C19.827 10.1829 20.4986 10.8545 20.4986 11.6829L20.4986 20.6707C20.4986 21.4992 19.827 22.1707 18.9986 22.1707C18.1701 22.1707 17.4986 21.4992 17.4986 20.6707L17.4986 11.6829C17.4986 10.8545 18.1701 10.1829 18.9986 10.1829Z"
|
|
||||||
fill=""
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h4 className="mb-2 text-2xl font-semibold text-gray-800 dark:text-white/90 sm:text-title-sm">
|
|
||||||
Warning Alert!
|
|
||||||
</h4>
|
|
||||||
<p className="text-sm leading-6 text-gray-500 dark:text-gray-400">
|
|
||||||
Lorem ipsum dolor sit amet consectetur. Feugiat ipsum libero tempor
|
|
||||||
felis risus nisi non. Quisque eu ut tempor curabitur.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="flex items-center justify-center w-full gap-3 mt-7">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="flex justify-center w-full px-4 py-3 text-sm font-medium text-white rounded-lg bg-warning-500 shadow-theme-xs hover:bg-warning-600 sm:w-auto"
|
|
||||||
>
|
|
||||||
Okay, Got It
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
{/* Error Modal */}
|
|
||||||
<Modal
|
|
||||||
isOpen={errorModal.isOpen}
|
|
||||||
onClose={errorModal.closeModal}
|
|
||||||
className="max-w-[600px] p-5 lg:p-10"
|
|
||||||
>
|
|
||||||
<div className="text-center">
|
|
||||||
<div className="relative flex items-center justify-center z-1 mb-7">
|
|
||||||
<svg
|
|
||||||
className="fill-error-50 dark:fill-error-500/15"
|
|
||||||
width="90"
|
|
||||||
height="90"
|
|
||||||
viewBox="0 0 90 90"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M34.364 6.85053C38.6205 -2.28351 51.3795 -2.28351 55.636 6.85053C58.0129 11.951 63.5594 14.6722 68.9556 13.3853C78.6192 11.0807 86.5743 21.2433 82.2185 30.3287C79.7862 35.402 81.1561 41.5165 85.5082 45.0122C93.3019 51.2725 90.4628 63.9451 80.7747 66.1403C75.3648 67.3661 71.5265 72.2695 71.5572 77.9156C71.6123 88.0265 60.1169 93.6664 52.3918 87.3184C48.0781 83.7737 41.9219 83.7737 37.6082 87.3184C29.8831 93.6664 18.3877 88.0266 18.4428 77.9156C18.4735 72.2695 14.6352 67.3661 9.22531 66.1403C-0.462787 63.9451 -3.30193 51.2725 4.49185 45.0122C8.84391 41.5165 10.2138 35.402 7.78151 30.3287C3.42572 21.2433 11.3808 11.0807 21.0444 13.3853C26.4406 14.6722 31.9871 11.951 34.364 6.85053Z"
|
|
||||||
fill=""
|
|
||||||
fillOpacity=""
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
|
|
||||||
<span className="absolute -translate-x-1/2 -translate-y-1/2 left-1/2 top-1/2">
|
|
||||||
<svg
|
|
||||||
className="fill-error-600 dark:fill-error-500"
|
|
||||||
width="38"
|
|
||||||
height="38"
|
|
||||||
viewBox="0 0 38 38"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fillRule="evenodd"
|
|
||||||
clipRule="evenodd"
|
|
||||||
d="M9.62684 11.7496C9.04105 11.1638 9.04105 10.2141 9.62684 9.6283C10.2126 9.04252 11.1624 9.04252 11.7482 9.6283L18.9985 16.8786L26.2485 9.62851C26.8343 9.04273 27.7841 9.04273 28.3699 9.62851C28.9556 10.2143 28.9556 11.164 28.3699 11.7498L21.1198 18.9999L28.3699 26.25C28.9556 26.8358 28.9556 27.7855 28.3699 28.3713C27.7841 28.9571 26.8343 28.9571 26.2485 28.3713L18.9985 21.1212L11.7482 28.3715C11.1624 28.9573 10.2126 28.9573 9.62684 28.3715C9.04105 27.7857 9.04105 26.836 9.62684 26.2502L16.8771 18.9999L9.62684 11.7496Z"
|
|
||||||
fill=""
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h4 className="mb-2 text-2xl font-semibold text-gray-800 dark:text-white/90 sm:text-title-sm">
|
|
||||||
Danger Alert!
|
|
||||||
</h4>
|
|
||||||
<p className="text-sm leading-6 text-gray-500 dark:text-gray-400">
|
|
||||||
Lorem ipsum dolor sit amet consectetur. Feugiat ipsum libero tempor
|
|
||||||
felis risus nisi non. Quisque eu ut tempor curabitur.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="flex items-center justify-center w-full gap-3 mt-7">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="flex justify-center w-full px-4 py-3 text-sm font-medium text-white rounded-lg bg-error-500 shadow-theme-xs hover:bg-error-600 sm:w-auto"
|
|
||||||
>
|
|
||||||
Okay, Got It
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
</ComponentCard>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
"use client";
|
|
||||||
import React from "react";
|
|
||||||
import ComponentCard from "../../common/ComponentCard";
|
|
||||||
import Button from "../../ui/button/Button";
|
|
||||||
import { Modal } from "../../ui/modal";
|
|
||||||
import { useModal } from "@/hooks/useModal";
|
|
||||||
|
|
||||||
export default function VerticallyCenteredModal() {
|
|
||||||
const { isOpen, openModal, closeModal } = useModal();
|
|
||||||
const handleSave = () => {
|
|
||||||
// Handle save logic here
|
|
||||||
console.log("Saving changes...");
|
|
||||||
closeModal();
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<ComponentCard title="Vertically Centered Modal">
|
|
||||||
<Button size="sm" onClick={openModal}>
|
|
||||||
Open Modal
|
|
||||||
</Button>
|
|
||||||
<Modal
|
|
||||||
isOpen={isOpen}
|
|
||||||
onClose={closeModal}
|
|
||||||
showCloseButton={false}
|
|
||||||
className="max-w-[507px] p-6 lg:p-10"
|
|
||||||
>
|
|
||||||
<div className="text-center">
|
|
||||||
<h4 className="mb-2 text-2xl font-semibold text-gray-800 dark:text-white/90 sm:text-title-sm">
|
|
||||||
All Done! Success Confirmed
|
|
||||||
</h4>
|
|
||||||
<p className="text-sm leading-6 text-gray-500 dark:text-gray-400">
|
|
||||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
|
|
||||||
Pellentesque euismod est quis mauris lacinia pharetra.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="flex items-center justify-center w-full gap-3 mt-8">
|
|
||||||
<Button size="sm" variant="outline" onClick={closeModal}>
|
|
||||||
Close
|
|
||||||
</Button>
|
|
||||||
<Button size="sm" onClick={handleSave}>
|
|
||||||
Save Changes
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
</ComponentCard>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import React, { FC, ReactNode, FormEvent } from "react";
|
|
||||||
|
|
||||||
interface FormProps {
|
|
||||||
onSubmit: (event: FormEvent<HTMLFormElement>) => void;
|
|
||||||
children: ReactNode;
|
|
||||||
className?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Form: FC<FormProps> = ({ onSubmit, children, className }) => {
|
|
||||||
return (
|
|
||||||
<form
|
|
||||||
onSubmit={(event) => {
|
|
||||||
event.preventDefault(); // Prevent default form submission
|
|
||||||
onSubmit(event);
|
|
||||||
}}
|
|
||||||
className={` ${className}`} // Default spacing between form fields
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Form;
|
|
||||||