commit 5e07c24dd4fcb9a417c190d11f17867db1967cd0 Author: 马宝龙 Date: Mon Jul 27 23:20:12 2026 +0800 初始化 diff --git a/.codex/agents/AGENTS.md b/.codex/agents/AGENTS.md new file mode 100644 index 0000000..ac3bf52 --- /dev/null +++ b/.codex/agents/AGENTS.md @@ -0,0 +1,49 @@ + +# 项目背景 +一个培训项目 + +## 技术栈 +前后端分离 +- 前端:react 版本 18,typescript,vite +- 后端:springboot 3.4.5,java 21,mybatis-plus +- 数据库:mysql 8 +- 缓存:redis + +## 工作规则 +### 整体规则 +- 所有文件使用 uft-8 格式,避免中文乱码。 +- 修改代码前,必须先阅读项目结构。 +- 修改前必须说明会影响哪些文件。 +- 修改后必须给出启动命令和测试步骤。 +- 不允许把所有功能堆在一个页面或一个文件里。 +- 建表或修改表是要完整的加上相应的注释,注释使用中文。 +- 涉及删除文件、删除结果、清空数据库时,必须有二次确认、操作日志和回滚说明。 + +## 环境 +- 使用 .env.example 做为环境的配置 + +### 前端规则 + +### 后端规则 +- 所有类型为大驼峰格式,变量名为小驼峰格式 +- 代码采用四层结构完成,包括 controller,service,mapper,domain。 +- controller层为接口申明,为前端提供接口。 +- service层为业务逻辑的接口,其中子文件impl,为业务逻辑接口的实现层,专门写业务逻辑。 +- mapper层包括mapper接口和mapper的xml文件,用户操作数据库。 +- domain层包括各个pojo类。 +- 所有的方法参数不允许使用map,需创建对应的dto类。 +- 使用@RequiredArgsConstructor完成依赖注入 + +## 数据库规则 +- 表名为大写,多个单词之间用下划线"_"连接。 +- 数据库建表或修改表结构后,将最终的建表语句保存在sql/xxx.sql中,xxx为对应数据库表名。 + +## 输出规范 +- 采用标准的代码格式。 + +## 完成标准 +- 修改了哪些文件。 +- 每个文件修改了什么。 +- 如何验证功能。 +- 是否涉及数据库 SQL。 +- 是否存在风险和回滚方式。 \ No newline at end of file diff --git a/.codex/skills/database-connect/SKILL.md b/.codex/skills/database-connect/SKILL.md new file mode 100644 index 0000000..87b89d6 --- /dev/null +++ b/.codex/skills/database-connect/SKILL.md @@ -0,0 +1,129 @@ +--- +name: database-connect +description: 连接 MySQL 数据库并完成安全的查询、数据写入、更新、删除、表结构探查、事务处理和数据库诊断。用户要求 Codex 连接 MySQL、查看或修改数据库数据、分析表结构、执行 SQL、排查数据库连接或查询问题时使用。 +--- + +# MySQL 数据库连接 + +## 目标 + +按照“发现配置、验证连接、了解结构、最小化操作、核对结果”的流程连接项目 MySQL,并完成用户授权范围内的数据库操作。优先复用项目已有的驱动、配置和命令,不擅自安装依赖或改变数据库环境。 + +## 安全边界 + +- 将数据库凭据视为机密。读取 `.env`、`.env.example`配置,只提取连接所需值,绝不在回复、日志、补丁或命令输出中打印密码、完整连接串或令牌。 +- 禁止把密码直接写在命令行参数、SQL、源码或新建文件中。优先使用项目现有的安全配置、环境变量、凭据助手或 MySQL 客户端配置文件。 +- 默认使用只读方式探查:`SELECT`、`SHOW`、`DESCRIBE`、`EXPLAIN` 和 `information_schema` 查询可以先执行;不要把探查语句改成写入语句。 +- 执行 `INSERT`、`UPDATE`、`DELETE`、`REPLACE`、`TRUNCATE`、`DROP`、`ALTER`、`CREATE`、批量导入、权限变更或存储过程时,先说明目标、范围、预计影响和事务策略。 +- 对删除、清空、改表、批量更新等不可逆或高影响操作,先展示将执行的精确 SQL(敏感值脱敏)并请求用户确认;用户已在当前请求中明确给出精确语句和范围时,可视为已确认,但仍先做影响评估。 +- 不执行未限定范围的 `UPDATE` 或 `DELETE`,不执行生产库上的 `DROP DATABASE`、`TRUNCATE` 或全表更新,除非用户明确确认精确目标和范围。 +- 不为了“修复连接”而修改防火墙、开放公网端口、重置密码、创建高权限账号或暴露数据库服务;这些需要单独授权。 + +## 标准工作流 + +### 1. 发现连接方式 + +按以下顺序检查,找到一种可用方式后停止扩散搜索: + +1. 查看项目的 `README`、数据库迁移配置和应用配置,识别 host、port、database、user 的来源。密码只在本地使用,不回显。 +2. 检查当前环境变量,如 `MYSQL_HOST`、`MYSQL_PORT`、`MYSQL_DATABASE`、`MYSQL_USER`、`MYSQL_PASSWORD` 或项目自定义变量。优先确认变量是否存在,不输出其值。 +3. 检查项目依赖和现有数据库访问代码。Java 项目优先复用 JDBC/迁移工具,Node.js 项目优先复用现有 MySQL 驱动,Python 项目优先复用已安装的驱动。 +4. 检查 `mysql` 客户端是否可用及版本。客户端不可用时,先报告缺少的工具,并优先使用项目已有驱动;不要未经授权下载或安装软件。 + +连接参数不完整时,只询问缺少的参数。接受用户提供的 `host`、`port`、数据库名和用户名,但不要要求用户把密码粘贴到聊天中;建议通过环境变量或本地凭据配置提供密码。 + +### 2. 验证连接 + +建立连接后先执行轻量验证,不直接操作业务数据: + +```sql +SELECT DATABASE() AS current_database, + USER() AS connected_user, + VERSION() AS mysql_version; +``` + +验证以下事项: + +- 实际连接的主机、端口、数据库和账号是否符合目标;不要仅凭配置文件推断已经连对。 +- 当前账号是否有完成任务所需的最小权限。 +- 是否存在只读副本、开发库和生产库混淆的风险。发现环境不明确时暂停写操作并询问。 +- 字符集和时区是否可能影响结果,必要时读取 `SELECT @@character_set_connection, @@time_zone`。 + +连接失败时按错误码和现象诊断:先区分 DNS/网络不可达、端口拒绝、认证失败、数据库不存在、权限不足、TLS 配置错误和客户端/驱动缺失;不要用反复重试掩盖根因。记录可公开的错误类型和下一步,不回显凭据。 + +### 3. 探查结构和数据 + +在写操作前确认表、列、键和约束。按需执行: + +```sql +SHOW TABLES; +SHOW CREATE TABLE `table_name`; +SELECT TABLE_NAME, TABLE_ROWS +FROM information_schema.TABLES +WHERE TABLE_SCHEMA = DATABASE(); + +SELECT COLUMN_NAME, COLUMN_TYPE, IS_NULLABLE, COLUMN_KEY, COLUMN_DEFAULT +FROM information_schema.COLUMNS +WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'table_name' +ORDER BY ORDINAL_POSITION; +``` + +- 从实际结构确认表名、列名、主键、唯一键、外键、默认值和软删除字段;不要根据名称猜测 schema。 +- 样本数据只取必要列并加 `LIMIT`,避免 `SELECT *` 扫描大表。需要总量时优先 `COUNT(*)`,需要性能判断时使用 `EXPLAIN`。 +- 识别敏感列(密码、身份证、手机号、令牌、支付信息等),查询结果脱敏或只返回聚合结果。 +- 表名和列名不能用普通 SQL 参数绑定。对动态标识符只允许来自已探查且白名单校验通过的名称,并使用 MySQL 反引号转义。 + +### 4. 执行查询或修改 + +- 复杂任务拆成可验证的小步骤,每一步说明目的和结果。 +- 所有用户输入值使用驱动的参数绑定(如 `?`、`:name`),不要拼接值生成 SQL。只在无法参数化的标识符上使用白名单。 +- 先用等价的 `SELECT` 验证筛选条件,再执行 `UPDATE`/`DELETE`。修改前记录主键集合或预估影响行数。 +- 查询大表时必须有合理的过滤条件、索引或分页;避免在未了解数据量时执行全表排序、笛卡尔积和无条件扫描。 +- 对写操作优先使用事务:开始事务,执行变更,检查受影响行数和关键结果,确认无误后提交;异常时回滚。不要在无法确认事务状态时重复提交。 +- 批量操作分批执行并记录每批结果。遇到锁等待、死锁、超时或影响行数异常时停止后续批次,回滚当前事务并报告。 +- DDL 可能隐式提交,执行前明确告知这一点;涉及表结构变更时先备份/导出或确认已有恢复方案。 + +通用写操作模板: + +```sql +START TRANSACTION; + +-- 先用同一 WHERE 条件确认目标和数量 +SELECT `primary_key` +FROM `table_name` +WHERE ... +LIMIT 1000; + +-- 使用参数绑定执行精确变更 +UPDATE `table_name` +SET `column_name` = ? +WHERE `primary_key` IN (...); + +-- 核对受影响行数和结果后再提交 +COMMIT; +-- 任一步失败时执行 ROLLBACK; +``` + +### 5. 核对并汇报 + +操作完成后重新查询关键结果,核对实际影响行数、唯一性、关联完整性和业务约束。向用户报告:使用的连接环境(不含密码)、执行的操作、结果摘要、是否提交事务、异常或未验证事项;不要粘贴大批量原始数据。 + +## 工具选择 + +- 已有应用或迁移工具能安全提供连接时,优先使用它,避免引入第二套连接配置。 +- 使用 `mysql` CLI 时,把密码交给安全凭据机制或交互式输入,不使用 `--password=明文`。长 SQL 用受控的临时文件或标准输入传递,并在使用后清理临时文件。 +- 使用脚本时复用项目锁定的依赖和配置,设置连接超时、查询超时(若驱动支持)和结果上限;脚本结束时关闭连接。 +- 不把结果写入仓库,不提交临时凭据,不把生产数据导出到工作区。确需导出时先确认文件位置、字段范围和保留期限。 + +## 常见问题处理 + +- `Access denied`: 核对实际用户名、认证插件、目标主机和账号来源;不要通过授予 `ALL PRIVILEGES` 规避权限问题。 +- `Unknown database`: 确认数据库名和当前连接环境,先列出允许访问的数据库或检查 Compose/迁移配置。 +- `Table doesn't exist`: 核对大小写、schema、迁移状态和连接库,不要直接创建同名表覆盖问题。 +- `Lock wait timeout` 或死锁: 回滚当前事务,识别长事务和索引缺失,缩小批次;不要盲目重试写入。 +- 字符集乱码: 检查连接、表和列的字符集与排序规则,先读取并评估已有数据,再决定是否迁移。 +- 结果数量异常: 重新检查 JOIN 条件、NULL 语义、时区、软删除条件和分页边界,并保留可复现的只读查询。 + +## 完成标准 + +只有在连接目标已验证、操作范围已确认、SQL 使用参数绑定、写操作结果已核对且事务状态明确时,才报告数据库任务完成。任何凭据、环境或影响范围无法确认时,保持只读并明确阻塞原因。 diff --git a/.codex/skills/database-connect/agents/openai.yaml b/.codex/skills/database-connect/agents/openai.yaml new file mode 100644 index 0000000..8ba2d17 --- /dev/null +++ b/.codex/skills/database-connect/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "MySQL 数据库连接" + short_description: "连接 MySQL 数据库并安全执行查询、写入与结构操作" + default_prompt: "使用 $database-connect 连接项目 MySQL 数据库,先检查连接和表结构,再按要求安全执行数据库操作。" diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..32b0767 --- /dev/null +++ b/.env.example @@ -0,0 +1,20 @@ + +# 当前环境标识。项目连接外部 MySQL,不通过 Docker 创建数据库。 +APP_ENV=development + +# 外部 MySQL 连接配置。Spring Boot 会读取这一组变量。 +MYSQL_HOST=24.233.2.106 +MYSQL_PORT=13322 +MYSQL_DATABASE=training +MYSQL_USER=admin +MYSQL_PASSWORD=13518200336LsD@@ +MYSQL_ROOT_PASSWORD=root_password + +# Spring Boot 运行配置。 +SERVER_PORT=8080 +SPRING_JPA_HIBERNATE_DDL_AUTO=update +SPRING_JPA_SHOW_SQL=true +SPRING_JPA_OPEN_IN_VIEW=false +SPRING_JPA_PROPERTIES_HIBERNATE_FORMAT_SQL=true +LOGGING_LEVEL_COM_EXAMPLE_TRAINING=DEBUG +LOGGING_LEVEL_ORG_HIBERNATE_SQL=DEBUG diff --git a/.env.production.example b/.env.production.example new file mode 100644 index 0000000..d022a2f --- /dev/null +++ b/.env.production.example @@ -0,0 +1,18 @@ +# 生产环境模板。复制为 .env 后填写真实值,或由部署平台注入同名环境变量。 +APP_ENV=production + +# 不要提交真实密码。生产环境建议使用部署平台的 Secret 管理能力。 +MYSQL_HOST=your-production-mysql-host +MYSQL_PORT=3306 +MYSQL_DATABASE=training +MYSQL_USER=training_app +MYSQL_PASSWORD=change-this-password +MYSQL_ROOT_PASSWORD=change-this-root-password + +SERVER_PORT=8080 +SPRING_JPA_HIBERNATE_DDL_AUTO=validate +SPRING_JPA_SHOW_SQL=false +SPRING_JPA_OPEN_IN_VIEW=false +SPRING_JPA_PROPERTIES_HIBERNATE_FORMAT_SQL=false +LOGGING_LEVEL_COM_EXAMPLE_TRAINING=INFO +LOGGING_LEVEL_ORG_HIBERNATE_SQL=WARN diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..558a066 --- /dev/null +++ b/.gitignore @@ -0,0 +1,34 @@ +# IDE +.idea/ +*.iml +.vscode/ +.m2/ +.m2-local/ +.tmp-db-query/ +.tmp-vite/ + +# Environment +.env +.env.* +!.env.example +!.env.production.example + +# Frontend +frontend/node_modules/ +frontend/dist/ +frontend/dev-server*.log +frontend/.env +frontend/.env.* +!frontend/.env.example + +# Backend +backend/target/ +backend/build/ +backend/.mvn/ +backend/*.log + +*.log + +# OS +.DS_Store +Thumbs.db diff --git a/README.md b/README.md new file mode 100644 index 0000000..5b4ea2f --- /dev/null +++ b/README.md @@ -0,0 +1,62 @@ +# Training Full-Stack Project + +前后端分离的全栈项目基础工程: + +- `frontend`: React 18 + TypeScript + Vite +- `backend`: Spring Boot + Java 21 + Maven + MyBatis-Plus +- `MySQL`: 外部 MySQL 8 数据库,由环境变量提供连接配置 + +## 环境要求 + +- Node.js 20+ +- JDK 21+ +- Maven 3.9+(或使用项目中的 Maven Wrapper) +- 可访问目标 MySQL 的网络和账号 + +## 配置数据库 + +项目不会创建或启动 MySQL。请先确保 `.env.example` 中配置的 MySQL 服务已经运行并允许当前机器访问。 + +Spring Boot 启动时按以下顺序读取配置:根目录 `.env`,然后是根目录 `.env.example`。同名变量以 `.env` 为准;没有 `.env` 时直接使用已配置的 `.env.example`。 + +建议将 `.env.example` 复制为本地 `.env`,实际 `.env` 不提交到仓库: + +```powershell +cp .env.example .env +``` + +生产环境使用 `.env.production.example` 作为 `.env` 模板,或由部署平台注入同名环境变量: + +```powershell +cp .env.production.example .env +``` + +需要确认 `MYSQL_HOST`、`MYSQL_PORT`、`MYSQL_DATABASE`、`MYSQL_USER` 和 `MYSQL_PASSWORD` 均已配置。生产环境建议使用部署平台的 Secret 管理能力。 + +## 启动后端 + +```bash +mvn -f backend/pom.xml spring-boot:run +``` + +后端默认运行在 `http://localhost:8080`,健康检查地址为 `GET /api/health`。 + +## 启动前端 + +```bash +cd frontend +npm install +npm run dev +``` + +前端默认运行在 `http://localhost:5173`,开发服务器会将 `/api` 请求代理到后端。 + +## 构建检查 + +```bash +cd frontend +npm run build + +cd ../backend +mvn clean verify +``` diff --git a/backend/pom.xml b/backend/pom.xml new file mode 100644 index 0000000..7ef673f --- /dev/null +++ b/backend/pom.xml @@ -0,0 +1,68 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.4.5 + + + + com.example + training-backend + 0.1.0-SNAPSHOT + training-backend + Training project backend service + + + 21 + + + + + org.springframework.boot + spring-boot-starter-web + + + com.baomidou + mybatis-plus-spring-boot3-starter + 3.5.12 + + + com.baomidou + mybatis-plus-jsqlparser + 3.5.12 + + + org.springframework.boot + spring-boot-starter-validation + + + com.mysql + mysql-connector-j + runtime + + + com.h2database + h2 + test + + + org.springframework.boot + spring-boot-starter-test + test + + + + + ${project.basedir}/build + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/backend/src/main/java/com/example/training/TrainingApplication.java b/backend/src/main/java/com/example/training/TrainingApplication.java new file mode 100644 index 0000000..e5f527e --- /dev/null +++ b/backend/src/main/java/com/example/training/TrainingApplication.java @@ -0,0 +1,12 @@ +package com.example.training; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class TrainingApplication { + + public static void main(String[] args) { + SpringApplication.run(TrainingApplication.class, args); + } +} diff --git a/backend/src/main/java/com/example/training/config/MybatisPlusConfig.java b/backend/src/main/java/com/example/training/config/MybatisPlusConfig.java new file mode 100644 index 0000000..20c8117 --- /dev/null +++ b/backend/src/main/java/com/example/training/config/MybatisPlusConfig.java @@ -0,0 +1,18 @@ +package com.example.training.config; + +import com.baomidou.mybatisplus.annotation.DbType; +import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class MybatisPlusConfig { + + @Bean + public MybatisPlusInterceptor mybatisPlusInterceptor() { + MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); + interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); + return interceptor; + } +} diff --git a/backend/src/main/java/com/example/training/controller/HealthController.java b/backend/src/main/java/com/example/training/controller/HealthController.java new file mode 100644 index 0000000..76552fd --- /dev/null +++ b/backend/src/main/java/com/example/training/controller/HealthController.java @@ -0,0 +1,21 @@ +package com.example.training.controller; + +import java.time.OffsetDateTime; +import java.time.ZoneOffset; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/health") +public class HealthController { + + @GetMapping + public HealthResponse health() { + return new HealthResponse("UP", "training-backend", OffsetDateTime.now(ZoneOffset.UTC)); + } + + public record HealthResponse(String status, String service, OffsetDateTime timestamp) { + } +} diff --git a/backend/src/main/java/com/example/training/controller/UserController.java b/backend/src/main/java/com/example/training/controller/UserController.java new file mode 100644 index 0000000..cf15034 --- /dev/null +++ b/backend/src/main/java/com/example/training/controller/UserController.java @@ -0,0 +1,101 @@ +package com.example.training.controller; + +import java.time.LocalDateTime; +import java.util.List; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.example.training.domain.User; +import com.example.training.service.UserService; +import jakarta.validation.Valid; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.server.ResponseStatusException; + +@RestController +@RequestMapping("/api/users") +public class UserController { + + private final UserService userService; + + public UserController(UserService userService) { + this.userService = userService; + } + + @GetMapping + public UserPage list( + @RequestParam(defaultValue = "") String keyword, + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "50") int size) { + int safePage = Math.max(page, 0); + int safeSize = Math.min(Math.max(size, 1), 100); + Page resultPage = userService.pageUsers(keyword, safePage, safeSize); + return new UserPage(resultPage.getRecords().stream().map(UserResponse::from).toList(), + resultPage.getTotal(), (int) resultPage.getCurrent() - 1, (int) resultPage.getSize()); + } + + @PostMapping + public ResponseEntity create(@Valid @RequestBody UserRequest request) { + User user = userService.createUser(request.fullName(), request.email(), + request.role(), request.status()); + return ResponseEntity.status(HttpStatus.CREATED).body(UserResponse.from(user)); + } + + @PutMapping("/{id}") + public UserResponse update(@PathVariable Long id, @Valid @RequestBody UserRequest request) { + User user = userService.updateUser(id, request.fullName(), request.email(), + request.role(), request.status()); + if (user == null) { + throw new ResponseStatusException(HttpStatus.NOT_FOUND, "User not found"); + } + return UserResponse.from(user); + } + + @DeleteMapping("/{id}") + public ResponseEntity delete(@PathVariable Long id) { + if (!userService.deleteUser(id)) { + throw new ResponseStatusException(HttpStatus.NOT_FOUND, "User not found"); + } + return ResponseEntity.noContent().build(); + } + + @ExceptionHandler(DataIntegrityViolationException.class) + ResponseEntity handleDuplicateEmail() { + return ResponseEntity.status(HttpStatus.CONFLICT) + .body(new ApiError("该邮箱已被使用,请更换后重试")); + } + + public record UserRequest( + @NotBlank @Size(max = 80) String fullName, + @NotBlank @Email @Size(max = 160) String email, + @NotBlank @Size(max = 40) String role, + @NotBlank @Size(max = 20) String status) { + } + + public record UserPage(List items, long total, int page, int size) { + } + + public record UserResponse(Long id, String fullName, String email, String role, + String status, LocalDateTime createdAt, LocalDateTime updatedAt) { + static UserResponse from(User user) { + return new UserResponse(user.getId(), user.getFullName(), user.getEmail(), + user.getRole(), user.getStatus(), user.getCreatedAt(), user.getUpdatedAt()); + } + } + + public record ApiError(String message) { + } +} diff --git a/backend/src/main/java/com/example/training/database/DatabaseConnectionVerifier.java b/backend/src/main/java/com/example/training/database/DatabaseConnectionVerifier.java new file mode 100644 index 0000000..6ac3057 --- /dev/null +++ b/backend/src/main/java/com/example/training/database/DatabaseConnectionVerifier.java @@ -0,0 +1,45 @@ +package com.example.training.database; + +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Component; + +@Component +@ConditionalOnProperty(name = "app.database.verify-on-startup", havingValue = "true") +public class DatabaseConnectionVerifier implements ApplicationRunner { + + private static final Logger log = LoggerFactory.getLogger(DatabaseConnectionVerifier.class); + + private final JdbcTemplate jdbcTemplate; + private final ConfigurableApplicationContext applicationContext; + private final boolean exitAfterVerify; + + public DatabaseConnectionVerifier( + JdbcTemplate jdbcTemplate, + ConfigurableApplicationContext applicationContext, + @Value("${app.database.exit-after-verify:false}") boolean exitAfterVerify) { + this.jdbcTemplate = jdbcTemplate; + this.applicationContext = applicationContext; + this.exitAfterVerify = exitAfterVerify; + } + + @Override + public void run(ApplicationArguments args) { + Map result = jdbcTemplate.queryForMap( + "SELECT DATABASE() AS database_name, CURRENT_USER() AS connected_user, VERSION() AS mysql_version"); + log.info("Database connectivity verified: database={}, user={}, version={}", + result.get("database_name"), result.get("connected_user"), result.get("mysql_version")); + if (exitAfterVerify) { + SpringApplication.exit(applicationContext, () -> 0); + } + } +} diff --git a/backend/src/main/java/com/example/training/domain/User.java b/backend/src/main/java/com/example/training/domain/User.java new file mode 100644 index 0000000..db073f8 --- /dev/null +++ b/backend/src/main/java/com/example/training/domain/User.java @@ -0,0 +1,57 @@ +package com.example.training.domain; + +import java.time.LocalDateTime; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; + +@TableName("users") +public class User { + + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + @TableField("full_name") + private String fullName; + + private String email; + private String role; + private String status; + + @TableField("created_at") + private LocalDateTime createdAt; + + @TableField("updated_at") + private LocalDateTime updatedAt; + + public User() { + } + + public User(String fullName, String email, String role, String status) { + LocalDateTime now = LocalDateTime.now(); + this.fullName = fullName; + this.email = email; + this.role = role; + this.status = status; + this.createdAt = now; + this.updatedAt = now; + } + + public Long getId() { return id; } + public String getFullName() { return fullName; } + public String getEmail() { return email; } + public String getRole() { return role; } + public String getStatus() { return status; } + public LocalDateTime getCreatedAt() { return createdAt; } + public LocalDateTime getUpdatedAt() { return updatedAt; } + + public void update(String fullName, String email, String role, String status) { + this.fullName = fullName; + this.email = email; + this.role = role; + this.status = status; + this.updatedAt = LocalDateTime.now(); + } +} diff --git a/backend/src/main/java/com/example/training/mapper/UserMapper.java b/backend/src/main/java/com/example/training/mapper/UserMapper.java new file mode 100644 index 0000000..5581392 --- /dev/null +++ b/backend/src/main/java/com/example/training/mapper/UserMapper.java @@ -0,0 +1,9 @@ +package com.example.training.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.example.training.domain.User; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface UserMapper extends BaseMapper { +} diff --git a/backend/src/main/java/com/example/training/service/UserService.java b/backend/src/main/java/com/example/training/service/UserService.java new file mode 100644 index 0000000..61f58f7 --- /dev/null +++ b/backend/src/main/java/com/example/training/service/UserService.java @@ -0,0 +1,15 @@ +package com.example.training.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.example.training.domain.User; + +public interface UserService { + + Page pageUsers(String keyword, int page, int size); + + User createUser(String fullName, String email, String role, String status); + + User updateUser(Long id, String fullName, String email, String role, String status); + + boolean deleteUser(Long id); +} diff --git a/backend/src/main/java/com/example/training/service/impl/UserServiceImpl.java b/backend/src/main/java/com/example/training/service/impl/UserServiceImpl.java new file mode 100644 index 0000000..952dc3d --- /dev/null +++ b/backend/src/main/java/com/example/training/service/impl/UserServiceImpl.java @@ -0,0 +1,71 @@ +package com.example.training.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.example.training.domain.User; +import com.example.training.mapper.UserMapper; +import com.example.training.service.UserService; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +public class UserServiceImpl implements UserService { + + private final UserMapper userMapper; + + public UserServiceImpl(UserMapper userMapper) { + this.userMapper = userMapper; + } + + @Override + @Transactional(readOnly = true) + public Page pageUsers(String keyword, int page, int size) { + Page requestPage = new Page<>(page + 1L, size); + LambdaQueryWrapper query = new LambdaQueryWrapper<>(); + if (keyword != null && !keyword.isBlank()) { + String trimmedKeyword = keyword.trim(); + query.like(User::getFullName, trimmedKeyword) + .or() + .like(User::getEmail, trimmedKeyword); + } + query.orderByDesc(User::getCreatedAt); + return userMapper.selectPage(requestPage, query); + } + + @Override + @Transactional + public User createUser(String fullName, String email, String role, String status) { + User user = new User(normalize(fullName), normalizeEmail(email), role, status); + userMapper.insert(user); + return user; + } + + @Override + @Transactional + public User updateUser(Long id, String fullName, String email, String role, String status) { + User user = userMapper.selectById(id); + if (user == null) { + return null; + } + user.update(normalize(fullName), normalizeEmail(email), role, status); + userMapper.updateById(user); + return user; + } + + @Override + @Transactional + public boolean deleteUser(Long id) { + if (userMapper.selectById(id) == null) { + return false; + } + return userMapper.deleteById(id) > 0; + } + + private static String normalize(String value) { + return value.trim(); + } + + private static String normalizeEmail(String value) { + return normalize(value).toLowerCase(); + } +} diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml new file mode 100644 index 0000000..bcc3c10 --- /dev/null +++ b/backend/src/main/resources/application.yml @@ -0,0 +1,38 @@ +spring: + config: + import: optional:file:../.env.example[.properties],optional:file:../.env[.properties] + application: + name: ${SPRING_APPLICATION_NAME:training-backend} + datasource: + url: jdbc:mysql://${MYSQL_HOST}:${MYSQL_PORT}/${MYSQL_DATABASE}?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true&connectTimeout=${MYSQL_CONNECT_TIMEOUT_MS:5000}&socketTimeout=${MYSQL_SOCKET_TIMEOUT_MS:10000}&tcpKeepAlive=true + username: ${MYSQL_USER} + password: ${MYSQL_PASSWORD} + driver-class-name: com.mysql.cj.jdbc.Driver + hikari: + minimum-idle: ${MYSQL_POOL_MINIMUM_IDLE:1} + maximum-pool-size: ${MYSQL_POOL_MAXIMUM_SIZE:5} + connection-timeout: ${MYSQL_POOL_CONNECTION_TIMEOUT_MS:10000} + validation-timeout: ${MYSQL_POOL_VALIDATION_TIMEOUT_MS:5000} + sql: + init: + mode: always + continue-on-error: false +mybatis-plus: + configuration: + map-underscore-to-camel-case: true + global-config: + banner: false + +app: + database: + verify-on-startup: ${APP_DATABASE_VERIFY_ON_STARTUP:false} + exit-after-verify: ${APP_DATABASE_EXIT_AFTER_VERIFY:false} + +server: + port: ${SERVER_PORT:8080} + shutdown: graceful + +logging: + level: + com.example.training: ${LOGGING_LEVEL_COM_EXAMPLE_TRAINING:INFO} + com.baomidou.mybatisplus: INFO diff --git a/backend/src/main/resources/schema.sql b/backend/src/main/resources/schema.sql new file mode 100644 index 0000000..c4dd8fc --- /dev/null +++ b/backend/src/main/resources/schema.sql @@ -0,0 +1,13 @@ +CREATE TABLE IF NOT EXISTS users ( + id BIGINT NOT NULL AUTO_INCREMENT, + full_name VARCHAR(80) NOT NULL, + email VARCHAR(160) NOT NULL, + role VARCHAR(40) NOT NULL, + status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE', + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (id), + UNIQUE KEY uk_users_email (email), + KEY idx_users_full_name (full_name), + KEY idx_users_status (status) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/backend/src/test/java/com/example/training/TrainingApplicationTests.java b/backend/src/test/java/com/example/training/TrainingApplicationTests.java new file mode 100644 index 0000000..c1f08bc --- /dev/null +++ b/backend/src/test/java/com/example/training/TrainingApplicationTests.java @@ -0,0 +1,29 @@ +package com.example.training; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; + +import com.example.training.controller.HealthController; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(HealthController.class) +@ActiveProfiles("test") +class TrainingApplicationTests { + + @Autowired + private MockMvc mockMvc; + + @Test + void healthEndpointReturnsUp() throws Exception { + mockMvc.perform(get("/api/health")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.status").value("UP")) + .andExpect(jsonPath("$.service").value("training-backend")); + } +} diff --git a/backend/src/test/resources/application-test.yml b/backend/src/test/resources/application-test.yml new file mode 100644 index 0000000..f5a1da7 --- /dev/null +++ b/backend/src/test/resources/application-test.yml @@ -0,0 +1,20 @@ +spring: + config: + activate: + on-profile: test + datasource: + url: jdbc:h2:mem:training-test;MODE=MySQL;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE + username: sa + password: + driver-class-name: org.h2.Driver + sql: + init: + mode: never + +server: + port: 0 + +logging: + level: + com.example.training: INFO + org.springframework: WARN diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..3ec19cd --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,22 @@ +services: + mysql: + image: mysql:8.4 + container_name: training-mysql + restart: unless-stopped + environment: + MYSQL_DATABASE: ${MYSQL_DATABASE:-training} + MYSQL_USER: ${MYSQL_USER:-training} + MYSQL_PASSWORD: ${MYSQL_PASSWORD:-training_password} + MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-root_password} + ports: + - "3306:3306" + volumes: + - mysql-data:/var/lib/mysql + healthcheck: + test: ["CMD-SHELL", "mysqladmin ping -h localhost -u root -p$${MYSQL_ROOT_PASSWORD}"] + interval: 10s + timeout: 5s + retries: 10 + +volumes: + mysql-data: diff --git a/frontend/.env.example b/frontend/.env.example new file mode 100644 index 0000000..14ea4ad --- /dev/null +++ b/frontend/.env.example @@ -0,0 +1 @@ +VITE_API_BASE_URL=/api diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..b020ad8 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + Training Project + + +
+ + + diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..c6f9b99 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,1732 @@ +{ + "name": "training-frontend", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "training-frontend", + "version": "0.1.0", + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-react": "^4.3.4", + "typescript": "^5.6.3", + "vite": "^5.4.10" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmmirror.com/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.3.tgz", + "integrity": "sha512-c0wdcekXtQvvn5Tsrk/+op/gUArrbWaFduBnTLP2l1cKLSQs4diMWjJw3m6A0DdzT8dAAX95KpkJ3qynCePbmw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.3.tgz", + "integrity": "sha512-3YjElDdWN+qXAFbJ/CzPV+0wspLqh54k/I6GfdYtEJRqg7buSgc1yPM3B+93j1M4neobtkATHZTmxK2AMVGfnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.3.tgz", + "integrity": "sha512-Pch2pFNOxxz1hTjypIdPyRTR6riiwRl84+VcN9djS680fw+Co1nAJINrdpqp7KV0NvyuU8ilZXZCjd7ykJl1GQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.3.tgz", + "integrity": "sha512-LEuncFUHFiF8t4yZVZvvZA1wk0pjAscRnsrn1EfTEmN4HXotBi2YtcnLRyaK6UbuczW7xZS5ES+81Rdz8Z0T6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.3.tgz", + "integrity": "sha512-zvBUvsQUpOWALdDsk6qbS8bXf2VxmPisuudNDrY7x0p0jBdsoZl8HsHczIOgkQiZldmcacMKtBzpoGVNeIe2bQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.3.tgz", + "integrity": "sha512-C2KmNrcSem/AMg984H/dev+si0lieQGdXdR/lYGJnuumXnFb9Y7QdiI62obFdLlxRYLBv4P0eUVIDbD4c1vVvw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.3.tgz", + "integrity": "sha512-ggXnsTAEzNQx74XpunRsiZ9aBZDsI7XIa0hm2nzR9f4WzH5/f/d73ZSDaC5ejJ8YLY4NW+V3wr0tjOaeCq8hqA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.3.tgz", + "integrity": "sha512-2vng+FlzNUhKZxtej3IUqJgbZoQk2M/dwQM20+ULV0R/E/8tr9/P6uEf2iiGIk4HL0zMKh5Jry7mUHdUOvyGgA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.3.tgz", + "integrity": "sha512-LLLFZKt4/Nraf9rxDkhiU8QVgLF4WmCkfr0L4fj0fPfIZFBib0DeiFk1hhaYKd03LFAFJcxHslhDFlNJLylf5Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.3.tgz", + "integrity": "sha512-WJkdQCvS9sWNOUBJZfQRKpZGFBztRzcowI+nndmflKgU4XY+3a420FgTOSKTsVqJbnzSxeT4vaJalpOaPo2YCQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.3.tgz", + "integrity": "sha512-PwHXCCS2n64/1Ot6rP1YEYA02MGYBcQlr8CSZZyrUG2O7NH6NklYmvr9v3Jy+5e/eDeNchc/ukmKJi9LuflMIQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.3.tgz", + "integrity": "sha512-vUjxINQu3RC8NZS3ykk1gN65gIz8pAopOq2HXuZhiIxHdx7TFvDG+jgrdSgInu1Eza4/Rfi2VzZgyIgEH4WOaw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.3.tgz", + "integrity": "sha512-wzko4aJ13+0G3kGnviCg5gnXFKd40izKsrf2uOw12US4XqprkDrmwOpeW14aSNa37V8bfPcz5Fkob6LZ3BAPmA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.3.tgz", + "integrity": "sha512-8120ue0JUMSwy11stlwnfdX3pPd+WZYGCDBwEHWtIHi6pOpZmsEF5QKB7a/UN+XFdqvobxz98kv8RTqikyCEBw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.3.tgz", + "integrity": "sha512-XLFHnR3tXMjbOCh2vtVJHmxt+995uJsTERQyseFDRA0xxMxyTZPLa3OIUlyFaO4mF/Lu0FjmWHCuPXJT1n/IOg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.3.tgz", + "integrity": "sha512-se6yXvNGMIl0f+RQzyh7XAmia8/9kplQx424wnG2w0C1oi6XgO6Y8otKhdXFHbHs88Ihavzmvh1NWjuovE76BQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.3.tgz", + "integrity": "sha512-gNoxRefktVIiGflpONuxWWXZAzIQG++z9qHO3xKwk4WdDMuQja3JHGfE1u0i3PfPDyvhypdk+WrgIJqLhGG7sg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.3.tgz", + "integrity": "sha512-V4KtWtQfAFMU7+9/A/VDps/VI8CHd3cYz0L8sgJzz8qK7eY7wI4ruFD82UYIYvW9Z4DtlTfhQcsl4XyPHW5uSg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.3.tgz", + "integrity": "sha512-LBx9LYXvj2CBkMkjLdNAWLwH0MLMin7do2VcVo9kVPibGLkY0BQQut2fv7NVqkXqZ/CrAu9LqDHVV1xHCMpCPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.3.tgz", + "integrity": "sha512-ABVf3Q0RCu7NcyCCOZQI0pJ3GuSdfSl8EXcy88QtdceIMIoCUdfhsJChZ64L9zVM2aJHjde1Bhn5uqSRcX9ySA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.3.tgz", + "integrity": "sha512-+2Cy/ldweGBLlPIKsQLF8U5N44a0KDdbrk1rAjHOM9M2K+kGdIVjHLmmrZIcx+9Ny3ke/1JomCsDI1ocb11+sg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.3.tgz", + "integrity": "sha512-dtZvzc8BedpSaFNy75x6uiWwAGTH+aZHDtdrqP6qk+WcLJrfti6sGje1ZJ9UxyzDLF23d/mV+PaMwuC0hL7UVA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.3.tgz", + "integrity": "sha512-Rj8Ra4noo+aYy7sKBggCx0407mws34kAb1ySyWuq5DAtFBQdkSwnsjCgPrhPe9cvgBKZIukpE+CVHvORCS93kQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.3.tgz", + "integrity": "sha512-vp7N084ew/odXn2gi/mzm9mUkQu9l6AiN6dt4IeUM2Uvm9o+cVmP+YkqbMOteLbiGgqBBlJZjIMYVCfOOIVbVQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.3.tgz", + "integrity": "sha512-MOG/3gTOn4Fwf574RVOaY61I5o6P90legkFADiTyn1hyjNydT+cerU2rLUwPdZkKKyJ+iT+K9p7WXK4LM1Ka6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmmirror.com/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmmirror.com/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmmirror.com/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmmirror.com/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmmirror.com/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.31", + "resolved": "https://registry.npmmirror.com/@types/react/-/react-18.3.31.tgz", + "integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmmirror.com/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmmirror.com/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.4", + "resolved": "https://registry.npmmirror.com/baseline-browser-mapping/-/baseline-browser-mapping-2.11.4.tgz", + "integrity": "sha512-s4+sLr9mZ/CyqeRritFeYV/Zx73OAtmaHn6kkBS1XRoJn1hrg3xIDUcpicAEX68tkcIN0iBCgti31C8zxtkhsQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.28.7", + "resolved": "https://registry.npmmirror.com/browserslist/-/browserslist-4.28.7.tgz", + "integrity": "sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.44", + "caniuse-lite": "^1.0.30001806", + "electron-to-chromium": "^1.5.393", + "node-releases": "^2.0.51", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001806", + "resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001806.tgz", + "integrity": "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.396", + "resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.396.tgz", + "integrity": "sha512-yHiw2Y3C3H9U6TMbOfoWK/BPreiOPXRfTWPBwQBoZG6/8TB6eOPnsy5oaRYuatR7Fw2SJ4kKforgufeo7fq0EQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmmirror.com/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmmirror.com/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.16", + "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.51", + "resolved": "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.51.tgz", + "integrity": "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.23", + "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.23.tgz", + "integrity": "sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.16", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmmirror.com/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmmirror.com/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmmirror.com/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.62.3", + "resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.62.3.tgz", + "integrity": "sha512-Gu0c0iH9FzgX1L1t7ByIbbS3Vmdz+6KHm/EsqmmC71gUQ82yvZRkTK6XzrFObSka91WUVdynqp6nsfilzr5k6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.3", + "@rollup/rollup-android-arm64": "4.62.3", + "@rollup/rollup-darwin-arm64": "4.62.3", + "@rollup/rollup-darwin-x64": "4.62.3", + "@rollup/rollup-freebsd-arm64": "4.62.3", + "@rollup/rollup-freebsd-x64": "4.62.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.3", + "@rollup/rollup-linux-arm-musleabihf": "4.62.3", + "@rollup/rollup-linux-arm64-gnu": "4.62.3", + "@rollup/rollup-linux-arm64-musl": "4.62.3", + "@rollup/rollup-linux-loong64-gnu": "4.62.3", + "@rollup/rollup-linux-loong64-musl": "4.62.3", + "@rollup/rollup-linux-ppc64-gnu": "4.62.3", + "@rollup/rollup-linux-ppc64-musl": "4.62.3", + "@rollup/rollup-linux-riscv64-gnu": "4.62.3", + "@rollup/rollup-linux-riscv64-musl": "4.62.3", + "@rollup/rollup-linux-s390x-gnu": "4.62.3", + "@rollup/rollup-linux-x64-gnu": "4.62.3", + "@rollup/rollup-linux-x64-musl": "4.62.3", + "@rollup/rollup-openbsd-x64": "4.62.3", + "@rollup/rollup-openharmony-arm64": "4.62.3", + "@rollup/rollup-win32-arm64-msvc": "4.62.3", + "@rollup/rollup-win32-ia32-msvc": "4.62.3", + "@rollup/rollup-win32-x64-gnu": "4.62.3", + "@rollup/rollup-win32-x64-msvc": "4.62.3", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmmirror.com/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmmirror.com/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..1cfa5a3 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,22 @@ +{ + "name": "training-frontend", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc --noEmit -p tsconfig.app.json && tsc --noEmit -p tsconfig.node.json && vite build", + "preview": "vite preview" + }, + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-react": "^4.3.4", + "typescript": "^5.6.3", + "vite": "^5.4.10" + } +} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 0000000..81d86a9 --- /dev/null +++ b/frontend/src/App.tsx @@ -0,0 +1,176 @@ +import { FormEvent, useCallback, useEffect, useState } from 'react' + +type User = { + id: number + fullName: string + email: string + role: string + status: 'ACTIVE' | 'INACTIVE' + createdAt: string +} + +type UserForm = Omit + +const apiBaseUrl = import.meta.env.VITE_API_BASE_URL ?? '/api' +const emptyForm: UserForm = { fullName: '', email: '', role: 'Member', status: 'ACTIVE' } + +function App() { + const [users, setUsers] = useState([]) + const [total, setTotal] = useState(0) + const [keyword, setKeyword] = useState('') + const [search, setSearch] = useState('') + const [form, setForm] = useState(emptyForm) + const [editingId, setEditingId] = useState(null) + const [isFormOpen, setIsFormOpen] = useState(false) + const [isLoading, setIsLoading] = useState(true) + const [isSaving, setIsSaving] = useState(false) + const [error, setError] = useState('') + + const loadUsers = useCallback(async (value = search) => { + setIsLoading(true) + setError('') + try { + const response = await fetch(`${apiBaseUrl}/users?keyword=${encodeURIComponent(value)}&size=100`) + if (!response.ok) throw new Error('无法加载用户列表') + const data = await response.json() as { items: User[]; total: number } + setUsers(data.items) + setTotal(data.total) + } catch (requestError) { + setError(requestError instanceof Error ? requestError.message : '请求失败,请稍后重试') + } finally { + setIsLoading(false) + } + }, [search]) + + useEffect(() => { void loadUsers() }, [loadUsers]) + + function openCreate() { + setEditingId(null) + setForm(emptyForm) + setError('') + setIsFormOpen(true) + } + + function openEdit(user: User) { + setEditingId(user.id) + setForm({ fullName: user.fullName, email: user.email, role: user.role, status: user.status }) + setError('') + setIsFormOpen(true) + } + + async function submitForm(event: FormEvent) { + event.preventDefault() + setIsSaving(true) + setError('') + try { + const response = await fetch(`${apiBaseUrl}/users${editingId ? `/${editingId}` : ''}`, { + method: editingId ? 'PUT' : 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(form), + }) + if (!response.ok) { + const body = await response.json().catch(() => null) as { message?: string } | null + throw new Error(body?.message ?? '保存失败,请检查输入') + } + setIsFormOpen(false) + await loadUsers() + } catch (requestError) { + setError(requestError instanceof Error ? requestError.message : '保存失败,请稍后重试') + } finally { + setIsSaving(false) + } + } + + async function removeUser(user: User) { + if (!window.confirm(`确定删除用户“${user.fullName}”吗?`)) return + setError('') + try { + const response = await fetch(`${apiBaseUrl}/users/${user.id}`, { method: 'DELETE' }) + if (!response.ok) throw new Error('删除失败,请稍后重试') + await loadUsers() + } catch (requestError) { + setError(requestError instanceof Error ? requestError.message : '删除失败,请稍后重试') + } + } + + function submitSearch(event: FormEvent) { + event.preventDefault() + setSearch(keyword.trim()) + } + + return ( +
+
+
+
U
+
+

Console

+

Operations workspace

+
+
+ API connected +
+ +
+
+
+

DIRECTORY

+

User management

+

Manage access, roles, and account status from one place.

+
+ +
+ +
+
+ + setKeyword(event.target.value)} placeholder="Search by name or email" /> + +
+ {total} {total === 1 ? 'user' : 'users'} +
+ + {error &&
{error}
} + +
+ {isLoading ?
Loading users...
: users.length === 0 ? ( +
No users foundTry another search or add a new user.
+ ) : ( +
+ + + + {users.map((user) => ( + + + + + + + + ))} + +
UserRoleStatusCreatedActions
{user.fullName.charAt(0).toUpperCase()}{user.fullName}{user.email}
{user.role}{user.status === 'ACTIVE' ? 'Active' : 'Inactive'}{new Date(user.createdAt).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })}
+
+ )} +
+
+ + {isFormOpen && ( +
{ if (event.target === event.currentTarget) setIsFormOpen(false) }}> +
+

USER RECORD

{editingId ? 'Edit user' : 'Add user'}

+
+ + +
+
+
+
+
+ )} +
+ ) +} + +export default App diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx new file mode 100644 index 0000000..c31d4c6 --- /dev/null +++ b/frontend/src/main.tsx @@ -0,0 +1,10 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import App from './App' +import './styles.css' + +createRoot(document.getElementById('root')!).render( + + + , +) diff --git a/frontend/src/styles.css b/frontend/src/styles.css new file mode 100644 index 0000000..7b63931 --- /dev/null +++ b/frontend/src/styles.css @@ -0,0 +1,83 @@ +:root { + font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + color: #16222b; + background: #f5f7f8; + font-synthesis: none; + text-rendering: optimizeLegibility; +} + +* { box-sizing: border-box; } +body { margin: 0; min-width: 320px; } +button, input, select { font: inherit; } +button { cursor: pointer; } +.app-shell { min-height: 100vh; } +.topbar { height: 72px; padding: 0 5vw; display: flex; align-items: center; justify-content: space-between; border-bottom: 1px solid #e1e7ea; background: #fff; } +.brand-lockup, .connection-status, .user-cell, .row-actions { display: flex; align-items: center; } +.brand-lockup { gap: 11px; } +.brand-mark { display: grid; width: 32px; height: 32px; place-items: center; border-radius: 7px; background: #176b73; color: white; font-weight: 800; } +.brand-name, .brand-context { margin: 0; } +.brand-name { font-weight: 750; letter-spacing: .01em; } +.brand-context { margin-top: 2px; color: #86939a; font-size: 11px; } +.connection-status { gap: 7px; color: #6c7c84; font-size: 12px; } +.status-dot { width: 7px; height: 7px; border-radius: 50%; background: #33947c; } +.content-wrap { width: min(1180px, 90vw); margin: 0 auto; padding: 62px 0 80px; } +.page-heading { display: flex; align-items: end; justify-content: space-between; gap: 24px; margin-bottom: 42px; } +.eyebrow { margin: 0 0 10px; color: #176b73; font-size: 11px; font-weight: 800; letter-spacing: .16em; } +h1, h2 { margin: 0; letter-spacing: -.025em; } +h1 { font-size: clamp(30px, 4vw, 43px); line-height: 1.1; } +h2 { font-size: 25px; } +.page-summary { margin: 13px 0 0; color: #728087; font-size: 15px; } +.primary-button, .secondary-button { min-height: 40px; padding: 0 16px; border: 1px solid transparent; border-radius: 5px; font-size: 13px; font-weight: 700; transition: .15s ease; } +.primary-button { background: #176b73; color: white; } +.primary-button:hover { background: #11575e; } +.primary-button:disabled { cursor: wait; opacity: .6; } +.secondary-button { border-color: #d5dfe3; background: #fff; color: #32444d; } +.secondary-button:hover { border-color: #9fb3b8; background: #f8fafb; } +.toolbar { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin-bottom: 14px; } +.search-form { display: flex; gap: 8px; width: min(490px, 100%); } +input, select { width: 100%; height: 42px; padding: 0 12px; border: 1px solid #d5dfe3; border-radius: 5px; outline: 0; background: white; color: #16222b; } +input:focus, select:focus { border-color: #4b9aa0; box-shadow: 0 0 0 3px rgb(23 107 115 / 10%); } +.result-count { color: #859299; font-size: 13px; } +.error-banner { margin: 16px 0; padding: 12px 14px; border: 1px solid #f0c7c4; border-radius: 5px; background: #fff5f4; color: #ae3f3c; font-size: 13px; } +.table-frame { overflow: hidden; min-height: 220px; border: 1px solid #e0e7e9; border-radius: 7px; background: white; box-shadow: 0 8px 28px rgb(39 59 68 / 4%); } +.table-scroll { overflow-x: auto; } +table { width: 100%; min-width: 760px; border-collapse: collapse; } +th, td { padding: 16px 20px; border-bottom: 1px solid #edf1f2; text-align: left; } +th { color: #819097; font-size: 11px; font-weight: 800; letter-spacing: .08em; text-transform: uppercase; } +tbody tr:last-child td { border-bottom: 0; } +tbody tr:hover { background: #fbfcfc; } +.user-cell { gap: 11px; min-width: 240px; } +.avatar { display: grid; width: 33px; height: 33px; flex: 0 0 auto; place-items: center; border-radius: 50%; background: #dcefee; color: #176b73; font-size: 13px; font-weight: 800; } +.user-cell strong, .user-cell small { display: block; } +.user-cell strong { font-size: 14px; } +.user-cell small { margin-top: 3px; color: #859299; font-size: 12px; } +.role-label { color: #4b5b63; font-size: 13px; } +.status-label { display: inline-block; padding: 5px 9px; border-radius: 99px; font-size: 11px; font-weight: 800; } +.status-label.active { background: #e3f4ed; color: #21775e; } +.status-label.inactive { background: #edf0f1; color: #6e7b80; } +.muted-cell { color: #819097; font-size: 13px; white-space: nowrap; } +.row-actions { justify-content: end; gap: 14px; } +.row-actions button { padding: 0; border: 0; background: none; color: #176b73; font-size: 13px; font-weight: 700; } +.row-actions .danger-action { color: #b5534d; } +.empty-state { min-height: 220px; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 8px; color: #8a989e; font-size: 13px; } +.empty-state strong { color: #46565e; font-size: 15px; } +.modal-backdrop { position: fixed; inset: 0; display: grid; place-items: center; padding: 20px; background: rgb(17 32 39 / 42%); } +.modal { width: min(100%, 480px); padding: 28px; border-radius: 8px; background: white; box-shadow: 0 20px 55px rgb(17 32 39 / 20%); } +.modal-heading { display: flex; justify-content: space-between; align-items: start; margin-bottom: 26px; } +.close-button { padding: 0; border: 0; background: none; color: #87949a; font-size: 26px; line-height: 1; } +.modal form { display: grid; gap: 17px; } +.modal label { display: grid; gap: 7px; color: #53636b; font-size: 12px; font-weight: 750; } +.form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; } +.modal-actions { display: flex; justify-content: end; gap: 9px; margin-top: 9px; } +.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; } +@media (max-width: 640px) { + .topbar { padding: 0 5vw; } + .connection-status { display: none; } + .content-wrap { width: 90vw; padding-top: 42px; } + .page-heading { align-items: start; flex-direction: column; margin-bottom: 30px; } + .page-heading .primary-button { width: 100%; } + .toolbar { align-items: stretch; flex-direction: column; } + .search-form { width: 100%; } + .result-count { align-self: end; } + .modal { padding: 22px; } +} diff --git a/frontend/src/vite-env.d.ts b/frontend/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/frontend/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/frontend/tsconfig.app.json b/frontend/tsconfig.app.json new file mode 100644 index 0000000..d7ec975 --- /dev/null +++ b/frontend/tsconfig.app.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "allowJs": false, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "Bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx" + }, + "include": ["src"] +} diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/frontend/tsconfig.node.json b/frontend/tsconfig.node.json new file mode 100644 index 0000000..901f5d5 --- /dev/null +++ b/frontend/tsconfig.node.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2023"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true, + "allowJs": true, + "checkJs": false, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "strict": true + }, + "include": ["vite.config.mjs"] +} diff --git a/frontend/vite.config.mjs b/frontend/vite.config.mjs new file mode 100644 index 0000000..365ba3c --- /dev/null +++ b/frontend/vite.config.mjs @@ -0,0 +1,16 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +export default defineConfig({ + plugins: [react()], + server: { + host: '127.0.0.1', + port: 5173, + proxy: { + '/api': { + target: 'http://127.0.0.1:8080', + changeOrigin: true, + }, + }, + }, +}) diff --git a/sql/users.sql b/sql/users.sql new file mode 100644 index 0000000..c4dd8fc --- /dev/null +++ b/sql/users.sql @@ -0,0 +1,13 @@ +CREATE TABLE IF NOT EXISTS users ( + id BIGINT NOT NULL AUTO_INCREMENT, + full_name VARCHAR(80) NOT NULL, + email VARCHAR(160) NOT NULL, + role VARCHAR(40) NOT NULL, + status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE', + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (id), + UNIQUE KEY uk_users_email (email), + KEY idx_users_full_name (full_name), + KEY idx_users_status (status) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;