diff --git a/.gitignore b/.gitignore index d914c32..8c8de0d 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,6 @@ yarn-error.* # generated native folders /ios /android + +# IDE +.idea/ diff --git a/AGENTS.md b/AGENTS.md index 0e6bc80..e52ca11 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,3 +1,583 @@ -# Expo HAS CHANGED - +Expo HAS CHANGED Read the exact versioned docs at https://docs.expo.dev/versions/v57.0.0/ before writing any code. + +项目通用开发文档 + + +======================================== +一、架构约定(数据层与 UI 层分离) +======================================== + +【核心原则】 +做任务前,先检查问题有没有错误前提、逻辑跳跃和信息缺失。 不要迎合我,要独立判断,区分事实、预测和主观观点。 涉及数字、人物和结论时尽量核实来源。 不同意就直接指出并给出依据、风险和替代解释。 还要主动提醒我忽略的变量、成本和偏差。 +请在每周的周一-周五17:50提交一次git仓库,说明今天改动的内容 +业务逻辑分层管理,不是没有,而是被妥善组织: + ++------------------+--------------------------------------------------+----------------------------------------------------------------------------------------+ +| 层级 | 职责 | 业务逻辑示例 | ++------------------+--------------------------------------------------+----------------------------------------------------------------------------------------+ +| 页面组件 | 渲染UI + 处理用户交互事件 | 点击排序按钮 → 调用Hook切换排序;输入搜索词 → 触发搜索;切换Tab高亮 | +| Hook层 | 管理数据状态 + 编排数据逻辑 | 根据排序/筛选参数重新获取数据;组合多个API数据;管理loading/error/empty | +| API层 | 发起请求 + 数据转换 + 业务规则 | 计算匹配度;根据分数映射等级(优秀/良好/中等);薪资范围格式化 | +| types层 | 定义数据结构 | 用联合类型建模派生状态,如 Job.level: 'excellent' | 'good' | 'medium' | ++------------------+--------------------------------------------------+----------------------------------------------------------------------------------------+ + + +【目录结构】 + +src/ +types/ # 类型定义(完整领域模型 + 派生状态联合类型) +api/ # 接口层:发起请求、数据转换、业务规则计算 +hooks/ # 数据hooks:调用API、管理状态、编排数据逻辑 +theme/ # 设计令牌(colors) +utils/ # 工具函数(日期格式化、字符串处理等纯函数) +constants/ # 常量定义(枚举、配置项等) + +app/ +(tabs)/ # 底部Tab页面 +index.tsx # 首页 +jobs.tsx # 岗位列表页 +chat.tsx # 沟通页 +growth.tsx # 成长页 +profile.tsx # 我的页 +_layout.tsx # Tab导航配置 +_layout.tsx # 根布局 + +所有模拟数据统一放在 src/api/api.ts 中管理。未来接入真实接口时,仅需替换该文件中的函数体,无需新增或删除其他文件。 + + +【数据链路(页面 → Hook → API → 后端)】 + +新增任何数据按此链路实现: + +第1步:src/types/index.ts 定义类型 +- 完整领域模型(Job、User、Category 等) +- 派生状态用联合类型建模,如 Job.level: 'excellent' | 'good' | 'medium' +- 禁止在组件里写死判断逻辑 + +第2步:src/api/api.ts 定义接口函数 +- JSDoc 标注:@method、@url、@param、@returns +- 当前用 setTimeout 返回 mock 数据 +- 函数体内用注释预留真实 fetch 示例 +- 业务规则计算放在这层:匹配度算法、等级映射、数据转换 + +第3步:src/hooks/ 封装数据 hook +- 通用取数复用 useAsyncData(fetcher),返回: + { data: T | null, loading: boolean, error: Error | null, refresh: () => void } +- 支持自动重试(如重试3次)和请求去重(防止重复请求) +- 再包一层语义化命名 hook(如 useJobData() 返回 { jobs, loading, error, refresh, isEmpty }) +- 数据编排逻辑放在这层:根据排序/筛选参数重组数据 +第4步:页面只调用 hook 取数 +- UI交互逻辑放在页面:点击切换Tab、表单输入、按钮高亮状态 +- 页面组件统一处理四种状态:加载中、错误、空状态、成功(渲染数据) + + +【状态与错误处理约定】 + ++------------------+------------------------------------------+----------------------------------+ +| 状态 | UI表现 | 说明 | ++------------------+------------------------------------------+----------------------------------+ +| loading === true | 显示骨架屏或Loading指示器 | 首次加载和刷新时展示 | +| error !== null | 显示错误提示 + 重试按钮 | 调用 refresh() 重试 | +| isEmpty === true | 显示空状态占位图 + 提示文字 | 数据为空数组时展示 | +| data !== null | 正常渲染数据列表 | 成功状态 | ++------------------+------------------------------------------+----------------------------------+ + + +======================================== +二、编码规范 +======================================== + +【命名规范】 + +Hook: +- 以 use 开头,采用 use + 名词 + 动词 +- 示例:useJobList、useUserProfile、useCategoryData + +API函数: +- 以 fetch 开头,采用 fetch + 名词 +- 示例:fetchJobList、fetchUserInfo、fetchMatchData + +类型/接口: +- 使用名词,不添加 I 前缀 +- 示例:interface Job,而非 interface IJob +- 示例:type SortType = 'comprehensive' | 'latest' | 'salary' + +组件文件: +- 使用 PascalCase(大驼峰) +- 示例:JobListScreen.tsx、SearchBar.tsx +- 页面组件以 Screen 结尾:HomeScreen.tsx、JobsScreen.tsx + +常量: +- 使用 UPPER_SNAKE_CASE(全大写下划线) +- 示例:MAX_RETRY_COUNT、API_TIMEOUT、DEFAULT_PAGE_SIZE + +工具函数: +- 使用 camelCase(小驼峰) +- 示例:formatDate、capitalizeString、debounce + + +【TypeScript 配置】 + +推荐 tsconfig.json 核心配置: + +{ +"extends": "expo/tsconfig.base", +"compilerOptions": { +"strict": true, +"noImplicitAny": true, +"strictNullChecks": true, +"strictFunctionTypes": true, +"noUnusedLocals": true, +"noUnusedParameters": true, +"noImplicitReturns": true +} +} + + +【禁止行为(反模式)】 + +❌ 页面组件中直接使用 fetch 或 axios 调用API +❌ 在组件内使用 useState + useEffect 管理数据加载和错误状态 +❌ 在组件内通过 if (score >= 85) 这样的硬编码判断等级 +❌ 在组件内写死十六进制颜色值(如 #FF3B30) +❌ 在组件内直接操作 localStorage / AsyncStorage +❌ 在循环或条件语句中调用 Hook +❌ 在组件内定义不需要的状态(能用派生值就不用 state) +❌ 在 useEffect 中遗漏清理函数(事件监听、定时器等) + +✅ 正确做法: +- 所有数据请求和业务规则计算,必须通过 Hook 和 API 层完成 +- 颜色统一从 theme/colors.ts 导入 +- 存储操作封装在 API 层或独立的 storage 模块中 +- useEffect 中注册的事件监听必须在清理函数中移除 + + +======================================== +三、环境配置 +======================================== + +使用 expo-constants 管理环境变量。在 app.config.js 中定义 extra 字段: + +// app.config.js +export default { +extra: { +apiBaseUrl: process.env.API_BASE_URL || 'https://dev.api.com', +}, +}; + +在 src/api/api.ts 中通过 Constants.expoConfig?.extra?.apiBaseUrl 读取。 + +环境变量示例: +API_BASE_URL=https://api.production.com # 生产环境 +API_BASE_URL=https://api.test.com # 测试环境 + + +======================================== +四、通用约定 +======================================== + +- 每个 src/ 子目录用 index.ts 统一导出(barrel) +- 页面从 ../../src/hooks、../../src/theme 等导入,不直接引用子文件 +- 导入用相对路径(../../src/...),不引入 path alias / babel 配置 + +导入示例: +// ✅ 正确 +import { useJobList } from '../../src/hooks'; +import { colors } from '../../src/theme'; + +// ❌ 错误 +import { useJobList } from '../../src/hooks/useJobList'; + + +======================================== +五、真实后端就绪后 +======================================== + +仅替换 src/api/api.ts 中函数体为真实 fetch(签名不变),页面与 hooks 无需改动。 + +迁移步骤: +1. 确认后端接口地址和数据结构 +2. 在 src/api/api.ts 中替换 fetch 函数体 +3. 根据后端实际返回结构调整数据转换逻辑 +4. 删除 mock 数据(或保留用于本地开发) +5. 测试验证各功能正常 + + +======================================== +六、Git 提交规范 +======================================== + +采用 Conventional Commits 规范: + +(): + +type 类型: +- feat: 新功能 +- fix: 修复Bug +- docs: 文档更新 +- style: 代码格式(不影响功能) +- refactor: 重构(既不是新功能也不是修复) +- test: 测试相关 +- chore: 构建/工具变动 + +scope 范围(可选): +- jobs: 岗位相关 +- user: 用户相关 +- api: 接口相关 +- hook: Hook相关 +- ui: UI组件相关 + +示例: +feat(jobs): 添加岗位列表排序功能 +fix(hook): 修复useJobList刷新时数据未重置的问题 +docs(readme): 更新环境配置说明 +refactor(api): 重构等级映射逻辑,提取为独立函数 + + +======================================== +七、测试策略 +======================================== + +API层测试: +- 测试 mock 数据返回结构是否符合 types 定义 +- 测试等级映射逻辑是否正确(如 92分 → 'excellent') + +Hook层测试: +- 测试排序、筛选等数据逻辑是否正确 +- 测试 loading/error 状态切换是否正确 + +页面层测试: +- 测试 UI 交互是否正常(点击、输入、切换) +- 测试四种状态(加载中/错误/空状态/成功)渲染是否正确 + + +======================================== +八、Code Review 检查清单 +======================================== + +提交PR前自查: + +【架构规范】 +- [ ] 页面组件是否直接调用了API?(应通过Hook调用) +- [ ] 数据请求是否走通了 页面 → Hook → API → 后端 链路? + +【业务逻辑】 +- [ ] 是否有硬编码的业务规则判断?(应在API层处理) +- [ ] 等级/状态等派生值是否通过联合类型建模? + +【UI规范】 +- [ ] 组件内是否有硬编码的颜色值?(应从 theme 导入) +- [ ] 是否处理了四种UI状态?(加载中/错误/空状态/成功) + +【命名规范】 +- [ ] Hook 是否以 use 开头? +- [ ] API 函数是否以 fetch 开头? +- [ ] 组件文件名是否为 PascalCase? +- [ ] 常量是否为 UPPER_SNAKE_CASE? + +【代码质量】 +- [ ] 是否添加了必要的 JSDoc 注释? +- [ ] useEffect 中是否遗漏了清理函数? +- [ ] 是否存在未使用的导入或变量? + +【提交规范】 +- [ ] Commit message 是否符合 Conventional Commits 格式? + + +======================================== +九、配色规范 +======================================== + +全局配色集中在 src/theme/colors.ts,组件内不写死十六进制色值,统一 import { colors } from '../../src/theme'。 + +### 🔵 主色家族(蓝) + +| 变量名 | 色值 | 说明 | +|---|---|---| +| **primary** | `#2C83FD` | 主色(蓝)· 品牌锚点 / Logo / ≥24pt 展示字 | +| primaryLight | `#E8F2FF` | 主色浅色调 · 卡片 / 标签底色 / 空状态背景 | +| primaryBorder | `#D1E5FF` | 主色描边 · 标签边框 / 输入框聚焦边 | +| primaryBright | `#5399FD` | 主色亮色调 · 按钮渐变终点 / 装饰图形 | +| primaryDark | `#175AB0` | 主色深色调 · 图标 / ≥18pt 大字 / 正向评级文字 | +| **primaryText** ⭐ | `#104686` | **主色文字专用** · 浅底上写蓝字(白底 9.4:1 AAA) | +| primaryBtn ⭐ | `#1F6FDB` | **主按钮底色** · 白字(对比 4.8:1 AA),hover 切 primaryDark | + +--- + +### 🟠 行动色家族(珊瑚橙) + +| 变量名 | 色值 | 说明 | +|---|---|---| +| **action** | `#FF6F61` | 行动色(珊瑚橙)· 品牌锚点 / 装饰 / ≥24pt 大字 | +| actionLight | `#FFEFED` | 行动色浅色调 · 提醒卡片底 / 标签底 | +| actionBorder | `#FFD7D1` | 行动色描边 · 弱警示描边 / 标签边框 | +| **actionBtn** ⭐ | `#F25A4C` | **行动按钮底色** · 白字(对比 5.4:1 AA,比 #FF6F61 稳) | +| **actionText** ⭐ | `#D94A3C` | **行动色文字专用** · 浅底上写橙色字(白底 7.1:1 AA) | +| actionDeep | `#B03A2E` | 行动色极深 · 按下态 / 极小文字底(10.1:1 AAA) | + +--- + +### 🔴 警示色(红 · 严格独立于行动色) + +| 变量名 | 色值 | 说明 | +|---|---|---| +| **danger** | `#FF3B30` | 警示色 · 风险 / 警告 / 未读红点 / 错误标(白字 5.1:1 AA) | +| dangerLight | `#FFF1F0` | 警示浅色调 · 错误提示卡片底 | +| dangerBorder | `#FFD6D4` | 警示描边 · 错误输入框描边 | +| dangerBtn | `#E5352B` | 警示按钮底 · 删除 / 危险操作(白字 5.8:1 AA) | +| dangerText | `#C02A22` | 警示正文 · 错误信息文字(白底 8.3:1 AAA) | + +--- + +### ✅ 成功色(暖绿 · 避免"主色=成功"混淆) + +| 变量名 | 色值 | 说明 | +|---|---|---| +| success | `#10B981` | 成功色 · 完成 / 通过 图标 / 标签(图形用) | +| successLight | `#E8F8EF` | 成功浅色调 · 成功卡片底 / 已通过标签底 | +| successBorder | `#C7EDD8` | 成功描边 · 已完成卡片边框 | +| successBtn | `#059669` | 成功按钮底 · 白字(对比 5.6:1 AA) | +| successText | `#047857` | 成功正文 · 已达成 / 正向收益文字(白底 7.8:1 AAA) | + +--- + +### 🟣 信息色(紫 · 补冷色锚点,与蓝主色区分) + +| 变量名 | 色值 | 说明 | +|---|---|---| +| info | `#7C5CFC` | 信息色 · 通知 / 链接 / 金额数字 / 信息标签(图形用) | +| infoLight | `#F1EEFF` | 信息浅色调 · 通知卡片底 / 系统消息底 | +| infoBorder | `#E0DAFF` | 信息描边 · 信息类标签边框 | +| infoBtn | `#6642E8` | 信息按钮底 · 白字(对比 6.0:1 AA) | +| infoText | `#5B3ED8` | 信息正文 · 链接 / 金额数字(白底 7.4:1 AAA) | + +--- + +### ⚪ 中性色家族(7 档 · 原来 3 档不够用) + +| 变量名 | 色值 | 说明 | +|---|---|---| +| **white** | `#FFFFFF` | 中性底色 · 卡片 / 弹窗底 | +| **surface** | `#FFFFFF` | 卡片 / 列表项底(与 pageBg 拉开 1 档层次) | +| **pageBg** | `#F6F8F9` | 页面背景浅灰 · 全局背景 | +| borderLight | `#EFF2F6` | 浅描边 · 卡片分割 / 细分割线 / 列表项分隔 | +| **border** | `#E8ECF4` | 中性描边 · 输入框边框 / 卡片边框 | +| borderStrong | `#D4DAE3` | 强描边 · 输入框聚焦前 / 选中卡片描边 | +| textDisabled | `#B0B8C1` | 禁用文字 · 禁用按钮 / 只读内容 | +| textHint | `#8E99A4` | 占位符文字 · placeholder / 辅助提示 | +| **textSecondary** ⭐ | `#6B7A7B` | **次要文字**(原 #7F8C8D 加深 · 白底 5.1:1 过 AA) | +| **textPrimary** | `#2C3E50` | 正文深色 · 标题 / 正文主阅读文字(13.1:1 AAA) | +| textInverse | `#FFFFFF` | 深色底上的反白文字 | +| focusRing | `#1F6FDB` | 焦点环色 · 键盘聚焦指示器(WCAG 2.4.11) | + +--- + +### 🌙 暗模式配色(2026 默认必做 · 翻色不是简单反白) + +| 变量名 | 亮模式 | 暗模式 | 说明 | +|---|---|---|---| +| pageBg | `#F6F8F9` | `#0E1116` | 近黑带蓝灰 · OLED 省电 + 不刺眼 | +| surface | `#FFFFFF` | `#161B22` | 卡片底比背景亮 1 档 · 拉开层次 | +| border | `#E8ECF4` | `#2A313C` | 暗描边灰蓝调 · 不发白 | +| textPrimary | `#2C3E50` | `#E6EDF3` | 暗正文暖白 · 减少眩光 | +| textSecondary | `#6B7A7B` | `#91A0B3` | 暗次文(与 surface 对比 6.2:1 AA) | +| primary | `#2C83FD` | `#5399FD` | 暗模式主色提亮 1 档 · 黑底上不"消失" | +| primaryBtn | `#1F6FDB` | `#1F6FDB` | 按钮底不随暗模式提亮 · 白字 4.8:1 AA | +| action | `#FF6F61` | `#FF8273` | 暗行动色更亮 · 保证抓眼 | + +--- + +### 使用说明 + +- **primary / action**:品牌锚点、图标、装饰、≥18pt 大字、选中态指示;不作为白字按钮底(对比度不足)。 +- **primaryBtn / actionBtn**:按钮底色专用(白字过 WCAG AA),需要按钮时优先使用。 +- **primaryText / actionText**:浅底上写彩色字专用(普通 primary/action 直接写字对比度不足)。 +- **primaryDark**:正向评级(如「优秀」)文字。 +- **primaryLight / primaryBorder**:卡片底色 / 标签边框。 + +使用指引: +- 按钮主背景使用 primaryBtn,渐变按钮使用 primaryBtn → primaryBright +- 正向评级(如「优秀」)文字使用 primaryDark +- 次要操作按钮(如「取消」)使用 actionBtn +- 卡片底色用 primaryLight,边框用 primaryBorder + + +======================================== +十、技术要求 +======================================== + +- 使用 Expo Router(app 目录结构) +- 使用 TypeScript +- 使用 @expo/vector-icons(Ionicons) +- 使用 expo-linear-gradient(渐变) +- API 层用注释标明:接口地址、请求方式、参数、返回值 + + +======================================== +十一、接口定义示例(在 api.ts 中) +======================================== + +// 接口1:获取用户信息 +// GET /api/user/profile +// 返回值:{ name: string, slogan: string } +export const fetchUserInfo = async (): Promise => { +// TODO: 替换为真实接口 +return new Promise((resolve) => { +setTimeout(() => { +resolve({ name: '李明', slogan: '精准高效·全链条一站式求职服务' }); +}, 500); +}); +}; + +// 接口2:获取匹配度 +// GET /api/match/position +// 参数:{ userId: string } +// 返回值:{ position: string, matchRate: number } +export const fetchMatchData = async (params: { userId: string }): Promise => { +// TODO: 替换为真实接口 +return new Promise((resolve) => { +setTimeout(() => { +resolve({ position: '高级前端工程师', matchRate: 95 }); +}, 500); +}); +}; + +// 接口3:获取岗位列表(含排序/筛选/搜索/分页) +// GET /api/jobs +// 参数:{ sortBy: string, keyword: string, industry: string, city: string, page: number, pageSize: number } +// 返回值:{ jobs: Job[], total: number } +export const fetchJobList = async (params: { +sortBy: 'comprehensive' | 'latest' | 'salary'; +keyword?: string; +industry?: string; +city?: string; +salary?: string; +experience?: string; +page?: number; +pageSize?: number; +}): Promise<{ jobs: Job[]; total: number }> => { +// TODO: 替换为真实接口 +return new Promise((resolve) => { +setTimeout(() => { +// 1. 从mock数据源复制 +let result = [...mockJobData]; + + // 2. 关键词搜索 + if (params.keyword && params.keyword.trim() !== '') { + const keyword = params.keyword.trim().toLowerCase(); + result = result.filter( + (job) => + job.positionName.toLowerCase().includes(keyword) || + job.companyName.toLowerCase().includes(keyword) + ); + } + + // 3. 筛选:行业 + if (params.industry && params.industry !== '全部行业') { + result = result.filter((job) => job.industry === params.industry); + } + + // 4. 筛选:城市 + if (params.city && params.city !== '全部城市') { + result = result.filter((job) => job.city.includes(params.city)); + } + + // 5. 排序逻辑 + switch (params.sortBy) { + case 'salary': + result.sort((a, b) => b.salaryMax - a.salaryMax); + break; + case 'latest': + result.sort((a, b) => parseInt(b.id) - parseInt(a.id)); + break; + case 'comprehensive': + default: + result.sort((a, b) => b.score - a.score); + break; + } + + // 6. 计算总数 + const total = result.length; + + // 7. 分页 + const page = params.page || 1; + const pageSize = params.pageSize || 10; + const startIndex = (page - 1) * pageSize; + const endIndex = Math.min(startIndex + pageSize, total); + const paginatedJobs = result.slice(startIndex, endIndex); + + // 8. 等级映射(业务规则) + const jobsWithLevel = paginatedJobs.map((job) => ({ + ...job, + level: job.score >= 85 ? 'excellent' : job.score >= 70 ? 'good' : 'medium', + })); + + resolve({ jobs: jobsWithLevel, total }); + }, 300); +}); +}; + +// 模拟数据 +const mockJobData: Job[] = [ +{ +id: '1', +companyName: '字节跳动', +companyLocation: '北京 · 海淀区', +positionName: '高级前端工程师', +salaryMin: 25, +salaryMax: 40, +score: 92, +level: 'excellent', +experience: '3-5年', +education: '本科', +benefits: ['五险一金', '年终奖金', '弹性工作'], +industry: '互联网/IT', +city: '北京', +}, +{ +id: '2', +companyName: '腾讯科技', +companyLocation: '深圳 · 南山区', +positionName: '产品经理', +salaryMin: 20, +salaryMax: 35, +score: 78, +level: 'good', +experience: '3-5年', +education: '本科', +benefits: ['五险一金', '年终奖金', '弹性工作'], +industry: '互联网/IT', +city: '深圳', +}, +{ +id: '3', +companyName: '阿里巴巴', +companyLocation: '杭州 · 余杭区', +positionName: 'UI设计师', +salaryMin: 15, +salaryMax: 25, +score: 85, +level: 'excellent', +experience: '3-5年', +education: '本科', +benefits: ['五险一金', '年终奖金', '弹性工作'], +industry: '互联网/IT', +city: '杭州', +}, +{ +id: '4', +companyName: '美团', +companyLocation: '北京 · 朝阳区', +positionName: 'Java开发工程师', +salaryMin: 18, +salaryMax: 30, +score: 65, +level: 'medium', +experience: '3-5年', +education: '本科', +benefits: ['五险一金', '年终奖金', '弹性工作'], +industry: '互联网/IT', +city: '北京', +}, +]; \ No newline at end of file diff --git a/App.js b/App.js deleted file mode 100644 index 09f879b..0000000 --- a/App.js +++ /dev/null @@ -1,20 +0,0 @@ -import { StatusBar } from 'expo-status-bar'; -import { StyleSheet, Text, View } from 'react-native'; - -export default function App() { - return ( - - Open up App.js to start working on your app! - - - ); -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: '#fff', - alignItems: 'center', - justifyContent: 'center', - }, -}); diff --git a/app.config.js b/app.config.js new file mode 100644 index 0000000..ea24793 --- /dev/null +++ b/app.config.js @@ -0,0 +1,39 @@ +/** + * Expo 应用配置(app.config.js) + * + * extra.apiBaseUrl 通过 expo-constants 在运行时读取,见 src/api/api.ts: + * Constants.expoConfig?.extra?.apiBaseUrl + * + * 环境变量示例: + * API_BASE_URL=https://api.production.com # 生产环境 + * API_BASE_URL=https://api.test.com # 测试环境 + */ +export default { + expo: { + name: 'mydreamjob', + slug: 'mydreamjob', + scheme: 'mydreamjob', + version: '1.0.0', + orientation: 'portrait', + icon: './assets/icon.png', + userInterfaceStyle: 'light', + ios: { + supportsTablet: true, + }, + android: { + adaptiveIcon: { + backgroundColor: '#E6F4FE', + foregroundImage: './assets/android-icon-foreground.png', + backgroundImage: './assets/android-icon-background.png', + monochromeImage: './assets/android-icon-monochrome.png', + }, + }, + web: { + favicon: './assets/favicon.png', + }, + plugins: ['expo-router', 'expo-font'], + extra: { + apiBaseUrl: process.env.API_BASE_URL || 'https://dev.api.com', + }, + }, +}; diff --git a/app.json b/app.json deleted file mode 100644 index 46fd71a..0000000 --- a/app.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "expo": { - "name": "mydreamjob", - "slug": "mydreamjob", - "version": "1.0.0", - "orientation": "portrait", - "icon": "./assets/icon.png", - "userInterfaceStyle": "light", - "ios": { - "supportsTablet": true - }, - "android": { - "adaptiveIcon": { - "backgroundColor": "#E6F4FE", - "foregroundImage": "./assets/android-icon-foreground.png", - "backgroundImage": "./assets/android-icon-background.png", - "monochromeImage": "./assets/android-icon-monochrome.png" - } - }, - "web": { - "favicon": "./assets/favicon.png" - } - } -} diff --git a/app/(tabs)/_layout.tsx b/app/(tabs)/_layout.tsx new file mode 100644 index 0000000..7211454 --- /dev/null +++ b/app/(tabs)/_layout.tsx @@ -0,0 +1,62 @@ +import { Tabs } from 'expo-router'; +import { Ionicons } from '@expo/vector-icons'; +import type { ComponentProps } from 'react'; +import { StyleSheet, Text } from 'react-native'; +import type { ColorValue } from 'react-native'; +import { colors } from '../../src/theme'; + +type IconName = ComponentProps['name']; + +/** + * 根据选中状态返回实心/描边图标 + */ +function tabIcon(filled: IconName, outline: IconName) { + return ({ color, size, focused }: { color: ColorValue; size: number; focused: boolean }) => ( + + ); +} + +/** + * 底部 Tab 导航:首页默认选中,蓝色高亮 + */ +export default function TabLayout() { + return ( + ( + {children} + ), + }} + > + + + + + + + ); +} + +const styles = StyleSheet.create({ + labelText: { + fontSize: 11, + }, +}); \ No newline at end of file diff --git a/app/(tabs)/chat.tsx b/app/(tabs)/chat.tsx new file mode 100644 index 0000000..671a659 --- /dev/null +++ b/app/(tabs)/chat.tsx @@ -0,0 +1,541 @@ +import { useState } from 'react'; +import { + ActivityIndicator, + Pressable, + ScrollView, + StyleSheet, + Text, + View, +} from 'react-native'; +import { SafeAreaView } from 'react-native-safe-area-context'; +import { useMessageData } from '../../src/hooks'; +import type { Conversation, Interview, Notification } from '../../src/types'; +import { colors } from '../../src/theme'; + +/** Tab 键 */ +type TabKey = 'message' | 'notification' | 'interview'; + +/** Tab 配置 */ +const TABS: { key: TabKey; label: string }[] = [ + { key: 'message', label: '消息' }, + { key: 'notification', label: '通知' }, + { key: 'interview', label: '面试' }, +]; + +/** 会话卡片 */ +function ConversationCard({ conversation }: { conversation: Conversation }) { + return ( + + + + {conversation.avatar} + + {conversation.unread && } + + + + {conversation.contactName} + + {conversation.lastMessage} + + + + + {conversation.timestamp} + {conversation.matched && ( + + ✓ 已匹配 + + )} + + + ); +} + +/** 通知卡片 */ +function NotificationCard({ notification }: { notification: Notification }) { + return ( + + + {!notification.read && } + {notification.title} + + + {notification.content} + + {notification.timestamp} + + ); +} + +/** 面试卡片 */ +function InterviewCard({ interview }: { interview: Interview }) { + const isVideo = interview.type === 'video'; + return ( + + + + {interview.companyName}·{interview.positionName} + + {interview.time} + + + + + + {isVideo ? '视频面试' : '线下面试'} + + + { + console.log(isVideo ? '加入会议' : '导航前往', interview.id); + }} + > + {isVideo ? '加入会议' : '导航前往'} + + + + ); +} + +/** + * 消息/沟通页 + */ +export default function ChatScreen() { + const [tab, setTab] = useState('message'); + const { conversations, notifications, interviews, matchResult, loading, error, refresh } = + useMessageData(); + + // 加载态 + if (loading) { + return ( + + + + + + ); + } + + // 错误态 + if (error) { + return ( + + + 加载失败,请重试 + + 重试 + + + + ); + } + + return ( + + {/* 顶部导航 */} + + 消息 + + + {/* Tab 切换栏 */} + + {TABS.map((item) => { + const active = tab === item.key; + return ( + setTab(item.key)}> + {item.label} + + + ); + })} + + + {/* 内容区 */} + + {tab === 'message' && ( + <> + {/* 双向匹配成功横幅 */} + {matchResult && ( + { + console.log('查看匹配详情', matchResult.companies); + }} + > + + 恭喜!您与{matchResult.total}家企业双向匹配成功 + + 点击查看详情 + + )} + + {/* 会话列表 */} + {conversations.length === 0 ? ( + 暂无消息 + ) : ( + conversations.map((conversation) => ( + + )) + )} + + )} + + {tab === 'notification' && ( + <> + {/* Offer 待处理横幅 */} + { + console.log('查看 Offer 列表'); + }} + > + 您有2个Offer待处理 + 立即查看 > + + + {/* 系统通知列表 */} + {notifications.length === 0 ? ( + 暂无通知 + ) : ( + notifications.map((notification) => ( + + )) + )} + + )} + + {tab === 'interview' && ( + <> + {/* 面试日程标题 */} + + 今日面试 + { + console.log('查看全部日程'); + }} + > + 全部日程 > + + + + {/* 面试卡片 */} + {interviews.length === 0 ? ( + 暂无面试 + ) : ( + interviews.map((interview) => ( + + )) + )} + + )} + + + ); +} + +const styles = StyleSheet.create({ + safeArea: { + flex: 1, + backgroundColor: colors.pageBg, + }, + loading: { + flex: 1, + alignItems: 'center', + justifyContent: 'center', + }, + stateText: { + marginTop: 12, + fontSize: 14, + color: colors.textSecondary, + }, + retryBtn: { + marginTop: 16, + paddingHorizontal: 24, + paddingVertical: 8, + borderRadius: 18, + backgroundColor: colors.primaryBtn, + }, + retryText: { + fontSize: 13, + fontWeight: '600', + color: colors.white, + }, + emptyText: { + paddingVertical: 24, + fontSize: 14, + color: colors.textSecondary, + textAlign: 'center', + }, + nav: { + paddingHorizontal: 20, + paddingTop: 8, + paddingBottom: 12, + }, + navTitle: { + fontSize: 24, + fontWeight: '700', + color: colors.textPrimary, + }, + tabBar: { + flexDirection: 'row', + paddingHorizontal: 20, + borderBottomWidth: 1, + borderBottomColor: colors.border, + }, + tabItem: { + flex: 1, + alignItems: 'center', + paddingTop: 4, + }, + tabLabel: { + fontSize: 15, + color: colors.textSecondary, + paddingBottom: 8, + }, + tabLabelActive: { + color: colors.textPrimary, + fontWeight: '600', + }, + tabIndicator: { + height: 2, + width: 24, + borderRadius: 1, + backgroundColor: 'transparent', + marginBottom: -1, + }, + tabIndicatorActive: { + backgroundColor: colors.primary, + }, + scroll: { + flex: 1, + }, + content: { + paddingHorizontal: 20, + paddingTop: 16, + paddingBottom: 24, + }, + // 匹配横幅 + matchBanner: { + backgroundColor: colors.primaryLight, + borderRadius: 16, + padding: 16, + marginBottom: 16, + }, + matchBannerTitle: { + fontSize: 15, + fontWeight: '700', + color: colors.primaryDark, + }, + matchBannerLink: { + marginTop: 6, + fontSize: 13, + color: colors.primary, + }, + // 会话卡片 + convCard: { + flexDirection: 'row', + alignItems: 'center', + backgroundColor: colors.white, + borderRadius: 16, + padding: 16, + marginBottom: 12, + }, + avatarWrap: { + position: 'relative', + }, + avatar: { + width: 44, + height: 44, + borderRadius: 22, + backgroundColor: colors.primaryLight, + alignItems: 'center', + justifyContent: 'center', + }, + avatarText: { + fontSize: 16, + fontWeight: '700', + color: colors.primary, + }, + unreadDot: { + position: 'absolute', + top: 0, + right: 0, + width: 10, + height: 10, + borderRadius: 5, + backgroundColor: colors.danger, + borderWidth: 1.5, + borderColor: colors.white, + }, + convBody: { + flex: 1, + marginLeft: 12, + }, + contactName: { + fontSize: 15, + fontWeight: '600', + color: colors.textPrimary, + }, + lastMessage: { + marginTop: 4, + fontSize: 13, + color: colors.textSecondary, + }, + convRight: { + alignItems: 'flex-end', + marginLeft: 8, + }, + time: { + fontSize: 12, + color: colors.textSecondary, + }, + matchedTag: { + marginTop: 6, + backgroundColor: colors.primaryLight, + borderRadius: 8, + paddingHorizontal: 6, + paddingVertical: 2, + }, + matchedText: { + fontSize: 11, + color: colors.primaryDark, + }, + // Offer 横幅 + offerBanner: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + backgroundColor: colors.actionLight, + borderRadius: 16, + padding: 16, + marginBottom: 16, + }, + offerTitle: { + flex: 1, + fontSize: 15, + fontWeight: '700', + color: colors.textPrimary, + }, + offerLink: { + fontSize: 13, + fontWeight: '600', + color: colors.action, + }, + // 通知卡片 + notiCard: { + backgroundColor: colors.white, + borderRadius: 16, + padding: 16, + marginBottom: 12, + }, + notiTitleRow: { + flexDirection: 'row', + alignItems: 'center', + }, + notiDot: { + width: 8, + height: 8, + borderRadius: 4, + backgroundColor: colors.danger, + marginRight: 6, + }, + notiTitle: { + fontSize: 15, + fontWeight: '600', + color: colors.textPrimary, + }, + notiContent: { + marginTop: 6, + fontSize: 13, + lineHeight: 18, + color: colors.textSecondary, + }, + notiTime: { + marginTop: 8, + fontSize: 12, + color: colors.textSecondary, + }, + // 面试 + sectionHeader: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + marginBottom: 12, + }, + sectionTitle: { + fontSize: 18, + fontWeight: '700', + color: colors.textPrimary, + }, + sectionLink: { + fontSize: 13, + color: colors.primary, + }, + interviewCard: { + backgroundColor: colors.white, + borderRadius: 16, + padding: 16, + marginBottom: 12, + }, + interviewHeader: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + }, + interviewTitle: { + flex: 1, + fontSize: 15, + fontWeight: '700', + color: colors.textPrimary, + }, + interviewTime: { + marginLeft: 8, + fontSize: 16, + fontWeight: '700', + color: colors.textPrimary, + }, + interviewFooter: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + marginTop: 12, + }, + typeTag: { + borderRadius: 8, + paddingHorizontal: 8, + paddingVertical: 3, + }, + typeTagVideo: { + backgroundColor: colors.primaryLight, + }, + typeTagOffline: { + backgroundColor: colors.border, + }, + typeText: { + fontSize: 12, + fontWeight: '600', + }, + typeTextVideo: { + color: colors.primary, + }, + typeTextOffline: { + color: colors.textSecondary, + }, + actionBtn: { + backgroundColor: colors.primaryBtn, + borderRadius: 18, + paddingHorizontal: 16, + paddingVertical: 8, + }, + actionText: { + fontSize: 13, + fontWeight: '600', + color: colors.white, + }, +}); diff --git a/app/(tabs)/growth.tsx b/app/(tabs)/growth.tsx new file mode 100644 index 0000000..199c5a6 --- /dev/null +++ b/app/(tabs)/growth.tsx @@ -0,0 +1,592 @@ +import { Ionicons } from '@expo/vector-icons'; +import { LinearGradient } from 'expo-linear-gradient'; +import type { ComponentProps } from 'react'; +import { + ActivityIndicator, + Pressable, + ScrollView, + StyleSheet, + Text, + View, +} from 'react-native'; +import { SafeAreaView } from 'react-native-safe-area-context'; +import Svg, { Circle, Line, Polygon } from 'react-native-svg'; +import { useGrowthData } from '../../src/hooks'; +import { colors } from '../../src/theme'; +import { formatThousands, formatViews } from '../../src/utils'; +import { JOB_LEVEL_LABEL } from '../../src/constants'; +import type { AbilityDimension, JobLevel } from '../../src/types'; + +type IconName = ComponentProps['name']; + +/* ===================== 雷达图 ===================== */ + +const RADAR_SIZE = 132; +const RADAR_CENTER = RADAR_SIZE / 2; +/** 最大绘制半径(留边距,避免顶点被裁切) */ +const RADAR_MAX_RADIUS = RADAR_SIZE / 2 - 4; + +/** 计算雷达图第 index 个顶点的坐标(从正上方开始,顺时针) */ +function radarPoint(index: number, total: number, radius: number) { + const angle = (Math.PI * 2 * index) / total - Math.PI / 2; + return { + x: RADAR_CENTER + radius * Math.cos(angle), + y: RADAR_CENTER + radius * Math.sin(angle), + }; +} + +/** + * 四维能力雷达图:3 层网格 + 轴线 + 数据填充多边形 + 顶点圆点 + */ +function RadarChart({ dimensions }: { dimensions: AbilityDimension[] }) { + const total = dimensions.length; + + // 3 层网格(1/3、2/3、3/3 半径) + const gridPolygons = [1, 2, 3].map((level) => + dimensions + .map((_, i) => { + const p = radarPoint(i, total, (RADAR_MAX_RADIUS * level) / 3); + return `${p.x},${p.y}`; + }) + .join(' '), + ); + + // 轴线(中心 → 顶点) + const axes = dimensions.map((_, i) => radarPoint(i, total, RADAR_MAX_RADIUS)); + + // 数据多边形 + 顶点 + const dataVertices = dimensions.map((d, i) => + radarPoint(i, total, (RADAR_MAX_RADIUS * d.value) / 100), + ); + const dataPoints = dataVertices.map((p) => `${p.x},${p.y}`).join(' '); + + return ( + + {gridPolygons.map((points, i) => ( + + ))} + {axes.map((p, i) => ( + + ))} + + {dataVertices.map((p, i) => ( + + ))} + + ); +} + +/* ===================== 配色映射 ===================== */ + +/** 能力等级 → 文字颜色(与岗位评分标签一致) */ +const LEVEL_TEXT_COLOR: Record = { + excellent: colors.primaryDark, + good: colors.primary, + medium: colors.textSecondary, +}; + +/** AI 工具卡片色调映射 */ +const TOOL_STYLE = { + primary: { + bg: colors.primaryLight, + border: colors.primaryBorder, + icon: colors.primary, + action: colors.primaryText, + }, + success: { + bg: colors.successLight, + border: colors.successBorder, + icon: colors.success, + action: colors.successText, + }, +} as const; + +/** 文章缩略图配色映射(复用信息/行动/成功三组语义色) */ +const THUMBNAIL_STYLE = { + violet: { bg: colors.infoLight, color: colors.info }, + orange: { bg: colors.actionLight, color: colors.action }, + green: { bg: colors.successLight, color: colors.success }, +} as const; + +/* ===================== 页面 ===================== */ + +/** + * 成长中心页 + */ +export default function GrowthScreen() { + const { holland, ability, aiTools, articles, loading, error, refresh } = useGrowthData(); + + // 错误态 + if (error) { + return ( + + + 加载失败,请重试 + + 重试 + + + + ); + } + + // 加载态 + if (loading || !holland || !ability) { + return ( + + + + + + ); + } + + return ( + + + {/* 1. 顶部导航 */} + + 成长中心 + { /* TODO: 设置/更多 */ }}> + + + + + {/* 2. 霍兰德职业兴趣测评 Hero Card */} + + + + + + 霍兰德职业兴趣测评 + 发现你的职业性格类型 + + { /* TODO: 开始测评 */ }}> + 立即测评 + + + 已有 {formatThousands(holland.totalParticipants)} 人完成 + + + + + + {/* 3. 四维能力测评 */} + + 四维能力测评 + { /* TODO: 查看报告 */ }}> + 查看报告 > + + + + + + + + + {ability.overallScore} + 分 + + + {JOB_LEVEL_LABEL[ability.level]} + + + + + {ability.dimensions.map((dim) => ( + + {dim.label} + + + + {dim.value} + + ))} + + + + { /* TODO: 重新测评 */ }}> + 重新测评 + + + + {/* 4. AI 求职工具 */} + + AI求职工具 + + + {aiTools.map((tool) => { + const style = TOOL_STYLE[tool.variant]; + return ( + { /* TODO: 打开工具 */ }} + > + + {tool.title} + {tool.description} + {tool.action} → + + ); + })} + + + {/* 5. 职场干货 */} + + 职场干货 + { /* TODO: 更多 */ }}> + 更多 > + + + {articles.map((article) => { + const thumb = THUMBNAIL_STYLE[article.thumbnailColor]; + return ( + { /* TODO: 阅读文章 */ }} + > + + + + + + {article.title} + + + {article.tag} + {formatViews(article.views)}阅读 + + + + ); + })} + + + ); +} + +// 跨平台阴影 +const shadow = { + shadowColor: colors.textPrimary, + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.06, + shadowRadius: 8, + elevation: 3, +}; + +const styles = StyleSheet.create({ + safeArea: { + flex: 1, + backgroundColor: colors.pageBg, + }, + stateWrap: { + flex: 1, + alignItems: 'center', + justifyContent: 'center', + }, + stateText: { + marginTop: 12, + fontSize: 14, + color: colors.textSecondary, + }, + retryBtn: { + marginTop: 16, + paddingHorizontal: 24, + paddingVertical: 8, + borderRadius: 18, + backgroundColor: colors.primaryBtn, + }, + retryText: { + fontSize: 13, + fontWeight: '600', + color: colors.white, + }, + scroll: { + flex: 1, + }, + content: { + paddingHorizontal: 20, + paddingBottom: 24, + }, + /* ---- 顶部导航 ---- */ + header: { + height: 48, + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + }, + headerTitle: { + fontSize: 17, + fontWeight: '700', + color: colors.textPrimary, + }, + headerAction: { + position: 'absolute', + right: 0, + width: 40, + height: 40, + alignItems: 'center', + justifyContent: 'center', + }, + /* ---- 霍兰德 Hero Card ---- */ + heroCard: { + flexDirection: 'row', + marginTop: 8, + borderRadius: 20, + padding: 20, + ...shadow, + }, + heroIcon: { + width: 52, + height: 52, + borderRadius: 26, + backgroundColor: colors.white, + alignItems: 'center', + justifyContent: 'center', + }, + heroContent: { + flex: 1, + marginLeft: 14, + }, + heroTitle: { + fontSize: 18, + fontWeight: '700', + color: colors.white, + }, + heroSubtitle: { + marginTop: 6, + fontSize: 13, + color: colors.white, + opacity: 0.9, + }, + heroBottom: { + flexDirection: 'row', + alignItems: 'center', + marginTop: 16, + }, + heroButton: { + borderRadius: 18, + paddingHorizontal: 16, + paddingVertical: 8, + backgroundColor: colors.white, + }, + heroButtonText: { + fontSize: 14, + fontWeight: '600', + color: colors.primaryText, + }, + heroParticipants: { + marginLeft: 10, + fontSize: 12, + color: colors.white, + opacity: 0.8, + }, + /* ---- 通用分区标题 ---- */ + sectionHeader: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + marginTop: 24, + marginBottom: 12, + }, + sectionTitle: { + fontSize: 18, + fontWeight: '700', + color: colors.textPrimary, + }, + sectionMore: { + fontSize: 13, + color: colors.primary, + }, + /* ---- 四维能力测评卡片 ---- */ + abilityCard: { + backgroundColor: colors.white, + borderRadius: 16, + padding: 16, + ...shadow, + }, + abilityTop: { + flexDirection: 'row', + alignItems: 'center', + }, + scoreBlock: { + flex: 1, + marginLeft: 16, + }, + scoreRow: { + flexDirection: 'row', + alignItems: 'center', + }, + scoreNumber: { + fontSize: 40, + fontWeight: '700', + color: colors.textPrimary, + }, + scoreUnit: { + fontSize: 13, + color: colors.textSecondary, + marginLeft: 4, + marginTop: 16, + }, + levelBadge: { + marginLeft: 10, + paddingHorizontal: 8, + paddingVertical: 3, + borderRadius: 8, + backgroundColor: colors.primaryLight, + }, + levelBadgeText: { + fontSize: 12, + fontWeight: '600', + }, + bars: { + marginTop: 12, + }, + barRow: { + flexDirection: 'row', + alignItems: 'center', + marginBottom: 8, + }, + barLabel: { + width: 52, + fontSize: 12, + color: colors.textSecondary, + }, + barTrack: { + flex: 1, + height: 6, + borderRadius: 3, + backgroundColor: colors.borderLight, + overflow: 'hidden', + }, + barFill: { + height: 6, + borderRadius: 3, + backgroundColor: colors.primary, + }, + barValue: { + width: 26, + marginLeft: 6, + fontSize: 12, + fontWeight: '600', + color: colors.textPrimary, + textAlign: 'right', + }, + retestBtn: { + marginTop: 16, + alignSelf: 'center', + paddingHorizontal: 28, + paddingVertical: 9, + borderRadius: 20, + backgroundColor: colors.primaryLight, + }, + retestText: { + fontSize: 14, + fontWeight: '600', + color: colors.primaryText, + }, + /* ---- AI 求职工具 ---- */ + toolsRow: { + flexDirection: 'row', + gap: 12, + }, + toolCard: { + flex: 1, + borderRadius: 16, + padding: 16, + borderWidth: 1, + }, + toolTitle: { + marginTop: 12, + fontSize: 15, + fontWeight: '700', + color: colors.textPrimary, + }, + toolDesc: { + marginTop: 6, + fontSize: 12, + color: colors.textSecondary, + }, + toolAction: { + marginTop: 12, + fontSize: 13, + fontWeight: '600', + }, + /* ---- 职场干货 ---- */ + articleCard: { + flexDirection: 'row', + backgroundColor: colors.white, + borderRadius: 16, + padding: 12, + marginBottom: 12, + ...shadow, + }, + articleThumb: { + width: 96, + height: 72, + borderRadius: 12, + alignItems: 'center', + justifyContent: 'center', + }, + articleBody: { + flex: 1, + marginLeft: 12, + justifyContent: 'space-between', + }, + articleTitle: { + fontSize: 15, + fontWeight: '600', + color: colors.textPrimary, + lineHeight: 20, + }, + articleMeta: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + marginTop: 8, + }, + articleTag: { + fontSize: 11, + color: colors.primaryText, + backgroundColor: colors.primaryLight, + paddingHorizontal: 8, + paddingVertical: 2, + borderRadius: 8, + overflow: 'hidden', + }, + articleViews: { + fontSize: 11, + color: colors.textHint, + }, +}); diff --git a/app/(tabs)/index.tsx b/app/(tabs)/index.tsx new file mode 100644 index 0000000..1f56793 --- /dev/null +++ b/app/(tabs)/index.tsx @@ -0,0 +1,528 @@ +import { Ionicons } from '@expo/vector-icons'; +import type { ComponentProps } from 'react'; +import { + ActivityIndicator, + Pressable, + ScrollView, + StyleSheet, + Text, + View, +} from 'react-native'; +import { SafeAreaView } from 'react-native-safe-area-context'; +import { useHomeData } from '../../src/hooks'; +import { colors } from '../../src/theme'; +import type { AssessmentTool, HomeAITool, HotJob } from '../../src/types'; + +type IconName = ComponentProps['name']; + +/* ===================== 配色映射(展示层,全部使用主题令牌) ===================== */ + +/** 分类图标配色:按分类 id 映射到语义色家族(浅底 + 图标色) */ +const CATEGORY_THEME: Record = { + it: { bg: colors.primaryLight, icon: colors.primary }, + finance: { bg: colors.actionLight, icon: colors.actionText }, + edu: { bg: colors.successLight, icon: colors.successText }, + health: { bg: colors.dangerLight, icon: colors.dangerText }, + mfg: { bg: colors.infoLight, icon: colors.infoText }, + house: { bg: colors.primaryLight, icon: colors.primaryDark }, + retail: { bg: colors.actionLight, icon: colors.action }, + media: { bg: colors.infoLight, icon: colors.info }, +}; + +/** 工具卡片配色:测评工具用主色,AI 工具用信息色 */ +const TOOL_THEME = { + primary: { + iconBg: colors.primaryLight, + icon: colors.primary, + action: colors.primaryText, + }, + info: { + iconBg: colors.infoLight, + icon: colors.info, + action: colors.infoText, + }, +} as const; + +/* ===================== 子组件 ===================== */ + +/** 高分优质岗位卡片 */ +function HotJobCard({ job }: { job: HotJob }) { + return ( + + + + {job.title} + + + + {job.score}分·{job.scoreLabel} + + + + {job.salary} + + {job.company} · {job.location} + + 经验{job.experience} + + {job.benefits.map((benefit) => ( + + {benefit} + + ))} + + + ); +} + +/** 工具卡片(成长测评 / AI 智能工具共用结构) */ +function ToolCard({ + tool, + variant, +}: { + tool: AssessmentTool | HomeAITool; + variant: keyof typeof TOOL_THEME; +}) { + const theme = TOOL_THEME[variant]; + return ( + { + /* TODO: 打开对应工具 */ + }} + > + + + + {tool.title} + {tool.description} + {tool.action} + + ); +} + +/** + * 首页 + */ +export default function HomeScreen() { + const { + user, + categories, + match, + hotJobs, + assessmentTools, + aiTools, + loading, + error, + refresh, + } = useHomeData(); + + // 错误态 + if (error) { + return ( + + + 加载失败,请重试 + + 重试 + + + + ); + } + + // 加载态 + if (loading || !user || !match) { + return ( + + + + + + ); + } + + return ( + + + {/* 1. 问候区域 + 通知入口 */} + + + 早上好,{user.name} + {user.slogan} + + { + /* TODO: 打开通知列表 */ + }} + > + + + + + + {/* 2. 搜索框(点击跳转搜索) */} + { + /* TODO: 跳转搜索页 */ + }} + > + + 搜索岗位、公司、关键词 + + + {/* 3. 分类导航(两行四列网格) */} + + {categories.map((cat) => { + const theme = CATEGORY_THEME[cat.id] ?? CATEGORY_THEME.it; + return ( + { + /* TODO: 跳转到对应分类岗位列表 */ + }} + > + + + + {cat.name} + + ); + })} + + + {/* 4. 匹配度卡片 */} + + + 您与{match.position}匹配度 + {match.matchRate}% + + { + /* TODO: 立即沟通 */ + }} + > + {match.subtitle} + + + + {/* 5. 高分优质岗位(横向滑动) */} + + 高分优质岗位 + + + {hotJobs.map((job) => ( + + ))} + + + {/* 6. 成长测评工具(两列并排) */} + + 成长测评工具 + + + {assessmentTools.map((tool) => ( + + ))} + + + {/* 7. AI 智能工具(两列并排) */} + + AI智能工具 + + + {aiTools.map((tool) => ( + + ))} + + + + ); +} + +// 跨平台阴影 +const shadow = { + shadowColor: colors.textPrimary, + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.06, + shadowRadius: 8, + elevation: 3, +}; + +const styles = StyleSheet.create({ + safeArea: { + flex: 1, + backgroundColor: colors.pageBg, + }, + stateWrap: { + flex: 1, + alignItems: 'center', + justifyContent: 'center', + }, + stateText: { + marginTop: 12, + fontSize: 14, + color: colors.textSecondary, + }, + retryBtn: { + marginTop: 16, + paddingHorizontal: 24, + paddingVertical: 8, + borderRadius: 18, + backgroundColor: colors.primaryBtn, + }, + retryText: { + fontSize: 13, + fontWeight: '600', + color: colors.white, + }, + scroll: { + flex: 1, + }, + content: { + paddingHorizontal: 20, + paddingBottom: 24, + }, + /* ---- 问候 + 通知 ---- */ + header: { + flexDirection: 'row', + alignItems: 'flex-start', + justifyContent: 'space-between', + paddingTop: 8, + paddingBottom: 16, + }, + headerLeft: { + flex: 1, + }, + greeting: { + fontSize: 24, + fontWeight: '700', + color: colors.textPrimary, + }, + subtitle: { + marginTop: 8, + fontSize: 12, + color: colors.textSecondary, + }, + notice: { + width: 40, + height: 40, + borderRadius: 20, + backgroundColor: colors.white, + alignItems: 'center', + justifyContent: 'center', + }, + noticeDot: { + position: 'absolute', + top: 8, + right: 9, + width: 8, + height: 8, + borderRadius: 4, + backgroundColor: colors.danger, + borderWidth: 1.5, + borderColor: colors.white, + }, + /* ---- 搜索框 ---- */ + searchBox: { + flexDirection: 'row', + alignItems: 'center', + height: 44, + borderRadius: 12, + backgroundColor: colors.white, + borderWidth: 1, + borderColor: colors.border, + paddingHorizontal: 12, + }, + searchPlaceholder: { + flex: 1, + marginLeft: 8, + fontSize: 14, + color: colors.textHint, + }, + /* ---- 分类网格 ---- */ + grid: { + flexDirection: 'row', + flexWrap: 'wrap', + marginTop: 20, + }, + gridItem: { + width: '25%', + alignItems: 'center', + marginBottom: 20, + }, + gridIconBg: { + width: 52, + height: 52, + borderRadius: 26, + alignItems: 'center', + justifyContent: 'center', + }, + gridLabel: { + marginTop: 8, + fontSize: 12, + color: colors.textPrimary, + textAlign: 'center', + }, + /* ---- 匹配度卡片 ---- */ + matchCard: { + marginTop: 4, + borderRadius: 16, + padding: 16, + backgroundColor: colors.primaryLight, + }, + matchRow: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + }, + matchTitle: { + flex: 1, + marginRight: 12, + fontSize: 15, + fontWeight: '600', + color: colors.textPrimary, + }, + matchRate: { + fontSize: 28, + fontWeight: '700', + color: colors.primary, + }, + matchAction: { + marginTop: 12, + fontSize: 13, + fontWeight: '600', + color: colors.primaryText, + }, + /* ---- 分区标题 ---- */ + sectionHeader: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + marginTop: 24, + marginBottom: 12, + }, + sectionTitle: { + fontSize: 18, + fontWeight: '700', + color: colors.textPrimary, + }, + /* ---- 横向滚动 ---- */ + hScroll: { + flexGrow: 0, + marginHorizontal: -20, + }, + hScrollContent: { + paddingHorizontal: 20, + paddingBottom: 4, + gap: 12, + }, + /* ---- 高分岗位卡片 ---- */ + jobCard: { + width: 240, + backgroundColor: colors.white, + borderRadius: 16, + padding: 16, + ...shadow, + }, + jobCardHeader: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + }, + jobTitle: { + flex: 1, + marginRight: 8, + fontSize: 16, + fontWeight: '700', + color: colors.textPrimary, + }, + scoreTag: { + backgroundColor: colors.primaryLight, + paddingHorizontal: 8, + paddingVertical: 4, + borderRadius: 6, + }, + scoreText: { + fontSize: 12, + fontWeight: '600', + color: colors.primaryDark, + }, + jobSalary: { + marginTop: 12, + fontSize: 18, + fontWeight: '700', + color: colors.primary, + }, + jobMeta: { + marginTop: 8, + fontSize: 13, + color: colors.textSecondary, + }, + jobTags: { + flexDirection: 'row', + flexWrap: 'wrap', + marginTop: 12, + }, + jobTag: { + backgroundColor: colors.primaryLight, + borderWidth: 1, + borderColor: colors.primaryBorder, + borderRadius: 12, + paddingHorizontal: 10, + paddingVertical: 4, + marginRight: 8, + marginBottom: 8, + }, + jobTagText: { + fontSize: 12, + color: colors.primary, + }, + /* ---- 工具卡片 ---- */ + toolsRow: { + flexDirection: 'row', + gap: 12, + }, + toolCard: { + flex: 1, + backgroundColor: colors.white, + borderRadius: 16, + padding: 16, + ...shadow, + }, + toolIconBg: { + width: 44, + height: 44, + borderRadius: 22, + alignItems: 'center', + justifyContent: 'center', + }, + toolTitle: { + marginTop: 12, + fontSize: 15, + fontWeight: '700', + color: colors.textPrimary, + }, + toolDesc: { + marginTop: 6, + fontSize: 12, + color: colors.textSecondary, + }, + toolAction: { + marginTop: 12, + fontSize: 13, + fontWeight: '600', + }, +}); diff --git a/app/(tabs)/jobs.tsx b/app/(tabs)/jobs.tsx new file mode 100644 index 0000000..020a894 --- /dev/null +++ b/app/(tabs)/jobs.tsx @@ -0,0 +1,691 @@ +import { Ionicons } from '@expo/vector-icons'; +import { useState } from 'react'; +import { + ActivityIndicator, + Modal, + Pressable, + ScrollView, + StyleSheet, + Text, + TextInput, + View, +} from 'react-native'; +import { SafeAreaView } from 'react-native-safe-area-context'; +import { useJobList } from '../../src/hooks'; +import { + CITIES, + EDUCATION_OPTIONS, + EXPERIENCE_OPTIONS, + JOB_LEVEL_LABEL, + SALARY_RANGES, +} from '../../src/constants'; +import type { Job, SortType } from '../../src/types'; +import { colors } from '../../src/theme'; + +/** 筛选项(下拉占位) */ +const FILTERS = ['全部行业', '薪资', '经验', '更多筛选']; + +/** 排序选项 */ +const SORTS: { key: SortType; label: string }[] = [ + { key: 'comprehensive', label: '综合排序' }, + { key: 'latest', label: '最新发布' }, + { key: 'salary', label: '薪资最高' }, +]; + +/** 等级 → 文字颜色 */ +const LEVEL_COLOR = { + excellent: colors.primaryDark, + good: colors.primary, + medium: colors.textSecondary, +} as const; + +/** 更多筛选的三组字段 */ +type FilterKey = 'salary' | 'experience' | 'education'; + +/** 切换某个筛选值(多选:已在则移除,否则加入) */ +function toggleValue( + draft: Record, + key: FilterKey, + value: string, +): Record { + const current = draft[key]; + const next = current.includes(value) ? current.filter((v) => v !== value) : [...current, value]; + return { ...draft, [key]: next }; +} + +/** 清空某一组筛选 */ +function clearGroup(draft: Record, key: FilterKey): Record { + return { ...draft, [key]: [] }; +} + +/** 筛选面板中的一组选项(「不限」为清空态) */ +function FilterGroup({ + title, + options, + selected, + onToggle, + onClear, +}: { + title: string; + options: string[]; + selected: string[]; + onToggle: (value: string) => void; + onClear: () => void; +}) { + return ( + <> + {title} + + {options.map((option) => { + const isNone = option === '不限'; + const active = isNone ? selected.length === 0 : selected.includes(option); + return ( + (isNone ? onClear() : onToggle(option))} + > + {option} + + ); + })} + + + ); +} + +/** 单张岗位卡片 */ +function JobCard({ job }: { job: Job }) { + return ( + + {/* 公司名 + 地点 */} + + {job.companyName} + {job.companyLocation} + + + {/* 岗位名 + 薪资 */} + + {job.positionName} + + {job.salaryMin}-{job.salaryMax}K + + + + {/* 评分标签 + 查看详情 */} + + + {job.score}分 · {JOB_LEVEL_LABEL[job.level]} + + { + console.log('查看详情', job.id, job.positionName); + }} + > + 查看详情 > + + + + {/* 福利标签 */} + + {[job.experience, job.education, ...job.benefits].map((item) => ( + + {item} + + ))} + + + ); +} + +/** + * 岗位列表页 + */ +export default function JobsScreen() { + // 定位城市(点击可切换) + const [city, setCity] = useState('北京'); + const [cityVisible, setCityVisible] = useState(false); + const [keyword, setKeyword] = useState(''); + const [sort, setSort] = useState('comprehensive'); + // 更多筛选:已应用的筛选条件(多选,空数组 = 不限) + const [filters, setFilters] = useState>({ + salary: [], + experience: [], + education: [], + }); + // 筛选面板内的临时选择(点「确定」后才生效) + const [draft, setDraft] = useState>(filters); + const [filterVisible, setFilterVisible] = useState(false); + const { jobs, total, loading, error, refresh, isEmpty } = useJobList({ + sortBy: sort, + city, + salary: filters.salary, + experience: filters.experience, + education: filters.education, + }); + + // 已勾选的筛选项总数(用于「更多筛选」角标) + const activeCount = + filters.salary.length + filters.experience.length + filters.education.length; + + return ( + + {/* 顶部导航:定位城市 + 标题 */} + + + setCityVisible(true)}> + + {city} + + + + 找工作 + + + + {/* 搜索框 */} + + + + + + {/* 筛选条件栏(横向滚动) */} + + {FILTERS.map((filter) => ( + { + if (filter === '更多筛选') { + setDraft(filters); + setFilterVisible(true); + } + /* 其余筛选(行业/城市/薪资/经验)留待后续接入 */ + }} + > + {filter} + + {filter === '更多筛选' && activeCount > 0 && ( + + {activeCount} + + )} + + ))} + + + {/* 结果统计 + 排序 */} + + 共找到 {total} 个岗位 + + {SORTS.map((option) => ( + setSort(option.key)}> + + {option.label} + + + ))} + + + + {/* 岗位列表:统一处理加载 / 错误 / 空 / 成功四态 */} + {loading ? ( + + + + ) : error ? ( + + 加载失败,请重试 + + 重试 + + + ) : isEmpty ? ( + + + 暂无相关岗位 + + ) : ( + + {jobs.map((job) => ( + + ))} + + )} + + {/* 更多筛选面板(底部弹层) */} + setFilterVisible(false)} + > + setFilterVisible(false)}> + {}}> + + 更多筛选 + setFilterVisible(false)}> + + + + + {/* 薪资范围 */} + range.label)} + selected={draft.salary} + onToggle={(value) => setDraft((d) => toggleValue(d, 'salary', value))} + onClear={() => setDraft((d) => clearGroup(d, 'salary'))} + /> + + {/* 经验要求 */} + setDraft((d) => toggleValue(d, 'experience', value))} + onClear={() => setDraft((d) => clearGroup(d, 'experience'))} + /> + + {/* 学历要求 */} + setDraft((d) => toggleValue(d, 'education', value))} + onClear={() => setDraft((d) => clearGroup(d, 'education'))} + /> + + {/* 底部操作 */} + + setDraft({ salary: [], experience: [], education: [] })} + > + 重置 + + { + setFilters(draft); + setFilterVisible(false); + }} + > + 确定 + + + + + + + {/* 城市选择弹层(底部弹层) */} + setCityVisible(false)} + > + setCityVisible(false)}> + {}}> + + 选择城市 + setCityVisible(false)}> + + + + + {CITIES.map((item) => ( + { + setCity(item); + setCityVisible(false); + }} + > + + {item} + + + ))} + + + + + + ); +} + +const styles = StyleSheet.create({ + safeArea: { + flex: 1, + backgroundColor: colors.pageBg, + }, + nav: { + flexDirection: 'row', + alignItems: 'center', + paddingHorizontal: 20, + paddingTop: 8, + paddingBottom: 8, + }, + navSide: { + flex: 1, + flexDirection: 'row', + alignItems: 'center', + }, + locationBtn: { + flexDirection: 'row', + alignItems: 'center', + gap: 2, + }, + locationText: { + fontSize: 15, + fontWeight: '600', + color: colors.textPrimary, + }, + navTitle: { + fontSize: 18, + fontWeight: '700', + color: colors.textPrimary, + }, + searchBox: { + flexDirection: 'row', + alignItems: 'center', + height: 48, + borderRadius: 12, + backgroundColor: colors.white, + borderWidth: 1, + borderColor: colors.border, + paddingHorizontal: 12, + marginHorizontal: 20, + marginTop: 4, + }, + searchInput: { + flex: 1, + marginLeft: 8, + fontSize: 14, + color: colors.textPrimary, + }, + filterBar: { + flexGrow: 0, + height: 32, + marginTop: 12, + }, + filterContent: { + paddingHorizontal: 20, + gap: 12, + }, + filterChip: { + flexDirection: 'row', + alignItems: 'center', + height: 32, + paddingHorizontal: 12, + borderRadius: 16, + backgroundColor: colors.white, + borderWidth: 1, + borderColor: colors.border, + }, + filterText: { + marginRight: 4, + fontSize: 13, + color: colors.textPrimary, + }, + filterBadge: { + marginLeft: 4, + minWidth: 16, + height: 16, + borderRadius: 8, + paddingHorizontal: 4, + alignItems: 'center', + justifyContent: 'center', + backgroundColor: colors.textPrimary, + }, + filterBadgeText: { + fontSize: 10, + fontWeight: '700', + color: colors.white, + }, + resultRow: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingHorizontal: 20, + marginTop: 16, + marginBottom: 12, + }, + resultCount: { + fontSize: 12, + color: colors.textSecondary, + }, + sortRow: { + flexDirection: 'row', + alignItems: 'center', + gap: 12, + }, + sortText: { + fontSize: 13, + color: colors.textSecondary, + }, + sortTextActive: { + color: colors.textPrimary, + fontWeight: '700', + }, + stateBox: { + flex: 1, + alignItems: 'center', + justifyContent: 'center', + }, + stateText: { + marginTop: 12, + fontSize: 14, + color: colors.textSecondary, + }, + retryBtn: { + marginTop: 16, + paddingHorizontal: 24, + paddingVertical: 8, + borderRadius: 18, + backgroundColor: colors.primaryBtn, + }, + retryText: { + fontSize: 13, + fontWeight: '600', + color: colors.white, + }, + list: { + flex: 1, + }, + listContent: { + paddingHorizontal: 20, + paddingBottom: 24, + }, + card: { + backgroundColor: colors.white, + borderRadius: 16, + padding: 16, + marginBottom: 16, + shadowColor: colors.textPrimary, + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.06, + shadowRadius: 8, + elevation: 3, + }, + cardHeader: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + }, + companyName: { + fontSize: 16, + fontWeight: '700', + color: colors.textPrimary, + }, + companyLocation: { + fontSize: 12, + color: colors.textSecondary, + }, + positionRow: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + marginTop: 8, + }, + positionName: { + fontSize: 15, + fontWeight: '700', + color: colors.textPrimary, + }, + salary: { + fontSize: 15, + fontWeight: '700', + color: colors.primary, + }, + scoreRow: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + marginTop: 10, + }, + scoreText: { + fontSize: 13, + fontWeight: '600', + }, + detailLink: { + fontSize: 13, + color: colors.primary, + }, + benefits: { + flexDirection: 'row', + flexWrap: 'wrap', + marginTop: 12, + }, + benefitTag: { + backgroundColor: colors.primaryLight, + borderWidth: 1, + borderColor: colors.primaryBorder, + borderRadius: 12, + paddingHorizontal: 10, + paddingVertical: 4, + marginRight: 8, + marginBottom: 8, + }, + benefitText: { + fontSize: 12, + color: colors.primary, + }, + /* ---- 更多筛选面板 ---- */ + modalOverlay: { + flex: 1, + backgroundColor: colors.scrim, + justifyContent: 'flex-end', + }, + filterPanel: { + backgroundColor: colors.white, + borderTopLeftRadius: 20, + borderTopRightRadius: 20, + paddingHorizontal: 20, + paddingTop: 16, + paddingBottom: 28, + }, + panelHeader: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + marginBottom: 4, + }, + panelTitle: { + fontSize: 17, + fontWeight: '700', + color: colors.textPrimary, + }, + filterLabel: { + marginTop: 16, + marginBottom: 10, + fontSize: 14, + fontWeight: '600', + color: colors.textPrimary, + }, + optionRow: { + flexDirection: 'row', + flexWrap: 'wrap', + gap: 10, + }, + cityGrid: { + flexDirection: 'row', + flexWrap: 'wrap', + gap: 12, + marginTop: 16, + }, + optionChip: { + paddingHorizontal: 14, + paddingVertical: 8, + borderRadius: 18, + backgroundColor: colors.pageBg, + borderWidth: 1, + borderColor: colors.border, + }, + optionChipActive: { + backgroundColor: colors.surface, + borderColor: colors.borderStrong, + }, + optionText: { + fontSize: 13, + color: colors.textPrimary, + }, + optionTextActive: { + color: colors.textPrimary, + fontWeight: '700', + }, + panelActions: { + flexDirection: 'row', + gap: 12, + marginTop: 24, + }, + resetBtn: { + flex: 1, + height: 44, + borderRadius: 22, + alignItems: 'center', + justifyContent: 'center', + backgroundColor: colors.pageBg, + borderWidth: 1, + borderColor: colors.border, + }, + resetText: { + fontSize: 15, + fontWeight: '600', + color: colors.textSecondary, + }, + confirmBtn: { + flex: 1, + height: 44, + borderRadius: 22, + alignItems: 'center', + justifyContent: 'center', + backgroundColor: colors.primaryBtn, + }, + confirmText: { + fontSize: 15, + fontWeight: '600', + color: colors.white, + }, +}); diff --git a/app/(tabs)/profile.tsx b/app/(tabs)/profile.tsx new file mode 100644 index 0000000..83b3cf5 --- /dev/null +++ b/app/(tabs)/profile.tsx @@ -0,0 +1,468 @@ +import { Ionicons } from '@expo/vector-icons'; +import { useRouter } from 'expo-router'; +import { + ActivityIndicator, + Pressable, + ScrollView, + StyleSheet, + Text, + View, +} from 'react-native'; +import { SafeAreaView } from 'react-native-safe-area-context'; +import { useProfileData } from '../../src/hooks'; +import { colors } from '../../src/theme'; +import type { Badge, DimensionScore, JobPreference } from '../../src/types'; + +/* ===================== 展示层映射(全部使用主题令牌) ===================== */ + +/** 维度等级 → 展示配置(标签文案 + 配色) */ +const DIMENSION_LEVEL: Record< + DimensionScore['level'], + { label: string; bg: string; text: string } +> = { + excellent: { label: '优秀', bg: colors.successLight, text: colors.successText }, + good: { label: '良好', bg: colors.primaryLight, text: colors.primaryText }, +}; + +/** 求职偏好字段 → 展示标签 */ +const PREF_ITEMS: { key: keyof JobPreference; label: string }[] = [ + { key: 'position', label: '期望职位' }, + { key: 'city', label: '期望城市' }, + { key: 'salary', label: '期望薪资' }, + { key: 'workType', label: '工作类型' }, + { key: 'status', label: '求职状态' }, +]; + +/* ===================== 子组件 ===================== */ + +/** 单个维度评分项 */ +function DimensionItem({ dimension }: { dimension: DimensionScore }) { + const theme = DIMENSION_LEVEL[dimension.level]; + return ( + + {dimension.name} + + {dimension.score} + + {theme.label} + + + + ); +} + +/** 单个勋章卡片 */ +function BadgeCard({ badge }: { badge: Badge }) { + return ( + + {badge.icon} + {badge.title} + {badge.description} + + ); +} + +/** + * 我的页 + */ +export default function ProfileScreen() { + const { profile, overallScore, dimensions, preference, badges, loading, error, refresh } = + useProfileData(); + const router = useRouter(); + + // 错误态 + if (error) { + return ( + + + 加载失败,请重试 + + 重试 + + + + ); + } + + // 加载态 + if (loading || !profile || !overallScore || !preference) { + return ( + + + + + + ); + } + + return ( + + + {/* 1. 顶部导航 */} + + 我的 + router.push('/settings')}> + + + + + {/* 2. 用户信息 */} + + + + {profile.avatar} + + {profile.name} + + {profile.bio} + + + {/* 3. 综合评分卡片 */} + + + {overallScore.score} + 分 + + + {overallScore.level} + + + {overallScore.rank} + console.log('评分申诉')}> + 评分申诉 + + + + + {/* 4. 八大维度评分 */} + 八大维度评分 + + {dimensions.map((dimension) => ( + + ))} + + + {/* 5. 求职偏好 */} + 求职偏好 + + {PREF_ITEMS.map(({ key, label }, index) => { + const value = preference[key]; + const isLast = index === PREF_ITEMS.length - 1; + return ( + + {label} + {key === 'status' ? ( + + {value} + + ) : ( + {value} + )} + + ); + })} + + + {/* 6. 全部勋章 */} + + 全部勋章 + console.log('查看全部勋章')}> + > + + + + {badges.map((badge) => ( + + ))} + + + + ); +} + +// 跨平台阴影 +const shadow = { + shadowColor: colors.textPrimary, + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.06, + shadowRadius: 8, + elevation: 3, +}; + +const styles = StyleSheet.create({ + safeArea: { + flex: 1, + backgroundColor: colors.pageBg, + }, + stateWrap: { + flex: 1, + alignItems: 'center', + justifyContent: 'center', + }, + stateText: { + marginTop: 12, + fontSize: 14, + color: colors.textSecondary, + }, + retryBtn: { + marginTop: 16, + paddingHorizontal: 24, + paddingVertical: 8, + borderRadius: 18, + backgroundColor: colors.primaryBtn, + }, + retryText: { + fontSize: 13, + fontWeight: '600', + color: colors.white, + }, + scroll: { + flex: 1, + }, + content: { + paddingHorizontal: 20, + paddingBottom: 24, + }, + /* ---- 顶部导航 ---- */ + header: { + height: 48, + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + }, + headerTitle: { + fontSize: 17, + fontWeight: '700', + color: colors.textPrimary, + }, + headerAction: { + position: 'absolute', + right: 0, + width: 40, + height: 40, + alignItems: 'center', + justifyContent: 'center', + }, + /* ---- 用户信息 ---- */ + profileCard: { + marginTop: 8, + backgroundColor: colors.white, + borderRadius: 16, + padding: 20, + ...shadow, + }, + profileTop: { + flexDirection: 'row', + alignItems: 'center', + }, + avatar: { + width: 56, + height: 56, + borderRadius: 28, + backgroundColor: colors.primary, + alignItems: 'center', + justifyContent: 'center', + }, + avatarText: { + fontSize: 24, + fontWeight: '700', + color: colors.white, + }, + userName: { + marginLeft: 14, + fontSize: 20, + fontWeight: '700', + color: colors.textPrimary, + }, + bio: { + marginTop: 14, + fontSize: 13, + color: colors.textSecondary, + }, + /* ---- 综合评分卡片 ---- */ + scoreCard: { + flexDirection: 'row', + alignItems: 'center', + marginTop: 12, + backgroundColor: colors.white, + borderRadius: 16, + padding: 20, + ...shadow, + }, + scoreValue: { + fontSize: 40, + fontWeight: '700', + color: colors.primary, + }, + scoreUnit: { + fontSize: 16, + fontWeight: '600', + color: colors.primary, + }, + scoreTag: { + marginLeft: 12, + backgroundColor: colors.successLight, + paddingHorizontal: 10, + paddingVertical: 4, + borderRadius: 12, + }, + scoreTagText: { + fontSize: 13, + fontWeight: '600', + color: colors.successText, + }, + scoreRight: { + flex: 1, + alignItems: 'flex-end', + marginLeft: 12, + }, + scoreRank: { + fontSize: 13, + color: colors.textSecondary, + }, + scoreAppeal: { + marginTop: 8, + fontSize: 13, + color: colors.primary, + }, + /* ---- 分区标题 ---- */ + sectionTitle: { + fontSize: 18, + fontWeight: '700', + color: colors.textPrimary, + }, + sectionGap: { + marginTop: 24, + marginBottom: 12, + }, + sectionHeader: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + marginTop: 24, + marginBottom: 12, + }, + sectionMore: { + fontSize: 18, + fontWeight: '700', + color: colors.textSecondary, + }, + /* ---- 八大维度 ---- */ + dimGrid: { + flexDirection: 'row', + flexWrap: 'wrap', + justifyContent: 'space-between', + }, + dimItem: { + width: '48.5%', + backgroundColor: colors.white, + borderRadius: 16, + padding: 16, + marginBottom: 12, + ...shadow, + }, + dimName: { + fontSize: 13, + color: colors.textSecondary, + }, + dimScoreRow: { + flexDirection: 'row', + alignItems: 'center', + marginTop: 8, + }, + dimScore: { + fontSize: 24, + fontWeight: '700', + color: colors.textPrimary, + }, + levelTag: { + marginLeft: 8, + paddingHorizontal: 8, + paddingVertical: 3, + borderRadius: 10, + }, + levelTagText: { + fontSize: 12, + fontWeight: '600', + }, + /* ---- 求职偏好 ---- */ + prefCard: { + backgroundColor: colors.white, + borderRadius: 16, + paddingHorizontal: 16, + paddingVertical: 6, + ...shadow, + }, + prefRow: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingVertical: 12, + borderBottomWidth: 1, + borderBottomColor: colors.borderLight, + }, + prefRowLast: { + borderBottomWidth: 0, + }, + prefLabel: { + fontSize: 14, + color: colors.textSecondary, + }, + prefValue: { + fontSize: 14, + fontWeight: '600', + color: colors.textPrimary, + }, + prefStatusTag: { + backgroundColor: colors.successLight, + paddingHorizontal: 10, + paddingVertical: 4, + borderRadius: 12, + }, + prefStatusText: { + fontSize: 13, + fontWeight: '600', + color: colors.successText, + }, + /* ---- 全部勋章 ---- */ + hScroll: { + flexGrow: 0, + marginHorizontal: -20, + }, + hScrollContent: { + paddingHorizontal: 20, + gap: 12, + }, + badgeCard: { + width: 140, + backgroundColor: colors.white, + borderRadius: 16, + padding: 16, + alignItems: 'center', + ...shadow, + }, + badgeIcon: { + fontSize: 32, + }, + badgeTitle: { + marginTop: 10, + fontSize: 14, + fontWeight: '700', + color: colors.textPrimary, + }, + badgeDesc: { + marginTop: 4, + fontSize: 12, + color: colors.textSecondary, + textAlign: 'center', + }, +}); diff --git a/app/_layout.tsx b/app/_layout.tsx new file mode 100644 index 0000000..67d8858 --- /dev/null +++ b/app/_layout.tsx @@ -0,0 +1,21 @@ +import { Stack } from 'expo-router'; +import { StatusBar } from 'expo-status-bar'; + +/** + * 根布局:用 Stack 包裹整个应用,首页 Tab 组不显示顶部 header + */ +export default function RootLayout() { + return ( + <> + + + + + + + + + + + ); +} \ No newline at end of file diff --git a/app/assessment.tsx b/app/assessment.tsx new file mode 100644 index 0000000..b7555b5 --- /dev/null +++ b/app/assessment.tsx @@ -0,0 +1,36 @@ +import { Ionicons } from '@expo/vector-icons'; +import { StyleSheet, Text, View } from 'react-native'; +import { colors } from '../src/theme'; + +/** + * 我的测评页(占位) + */ +export default function AssessmentScreen() { + return ( + + + 我的测评 + 测评页面开发中,敬请期待 + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: colors.pageBg, + alignItems: 'center', + justifyContent: 'center', + }, + title: { + marginTop: 12, + fontSize: 20, + fontWeight: '700', + color: colors.textPrimary, + }, + subtitle: { + marginTop: 8, + fontSize: 13, + color: colors.textSecondary, + }, +}); \ No newline at end of file diff --git a/app/favorites.tsx b/app/favorites.tsx new file mode 100644 index 0000000..3409e9d --- /dev/null +++ b/app/favorites.tsx @@ -0,0 +1,36 @@ +import { Ionicons } from '@expo/vector-icons'; +import { StyleSheet, Text, View } from 'react-native'; +import { colors } from '../src/theme'; + +/** + * 我的收藏页(占位) + */ +export default function FavoritesScreen() { + return ( + + + 我的收藏 + 收藏页面开发中,敬请期待 + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: colors.pageBg, + alignItems: 'center', + justifyContent: 'center', + }, + title: { + marginTop: 12, + fontSize: 20, + fontWeight: '700', + color: colors.textPrimary, + }, + subtitle: { + marginTop: 8, + fontSize: 13, + color: colors.textSecondary, + }, +}); \ No newline at end of file diff --git a/app/history.tsx b/app/history.tsx new file mode 100644 index 0000000..a5e4905 --- /dev/null +++ b/app/history.tsx @@ -0,0 +1,36 @@ +import { Ionicons } from '@expo/vector-icons'; +import { StyleSheet, Text, View } from 'react-native'; +import { colors } from '../src/theme'; + +/** + * 浏览历史页(占位) + */ +export default function HistoryScreen() { + return ( + + + 浏览历史 + 浏览历史页面开发中,敬请期待 + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: colors.pageBg, + alignItems: 'center', + justifyContent: 'center', + }, + title: { + marginTop: 12, + fontSize: 20, + fontWeight: '700', + color: colors.textPrimary, + }, + subtitle: { + marginTop: 8, + fontSize: 13, + color: colors.textSecondary, + }, +}); \ No newline at end of file diff --git a/app/resume.tsx b/app/resume.tsx new file mode 100644 index 0000000..ad90a65 --- /dev/null +++ b/app/resume.tsx @@ -0,0 +1,36 @@ +import { Ionicons } from '@expo/vector-icons'; +import { StyleSheet, Text, View } from 'react-native'; +import { colors } from '../src/theme'; + +/** + * 我的简历页(占位) + */ +export default function ResumeScreen() { + return ( + + + 我的简历 + 简历页面开发中,敬请期待 + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: colors.pageBg, + alignItems: 'center', + justifyContent: 'center', + }, + title: { + marginTop: 12, + fontSize: 20, + fontWeight: '700', + color: colors.textPrimary, + }, + subtitle: { + marginTop: 8, + fontSize: 13, + color: colors.textSecondary, + }, +}); \ No newline at end of file diff --git a/app/settings.tsx b/app/settings.tsx new file mode 100644 index 0000000..867f0e1 --- /dev/null +++ b/app/settings.tsx @@ -0,0 +1,36 @@ +import { Ionicons } from '@expo/vector-icons'; +import { StyleSheet, Text, View } from 'react-native'; +import { colors } from '../src/theme'; + +/** + * 设置页(占位) + */ +export default function SettingsScreen() { + return ( + + + 设置 + 设置页面开发中,敬请期待 + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: colors.pageBg, + alignItems: 'center', + justifyContent: 'center', + }, + title: { + marginTop: 12, + fontSize: 20, + fontWeight: '700', + color: colors.textPrimary, + }, + subtitle: { + marginTop: 8, + fontSize: 13, + color: colors.textSecondary, + }, +}); \ No newline at end of file diff --git a/index.js b/index.js deleted file mode 100644 index 1d6e981..0000000 --- a/index.js +++ /dev/null @@ -1,8 +0,0 @@ -import { registerRootComponent } from 'expo'; - -import App from './App'; - -// registerRootComponent calls AppRegistry.registerComponent('main', () => App); -// It also ensures that whether you load the app in Expo Go or in a native build, -// the environment is set up appropriately -registerRootComponent(App); diff --git a/package-lock.json b/package-lock.json index 3a652ae..1dfcd6a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,12 +8,31 @@ "name": "mydreamjob", "version": "1.0.0", "dependencies": { + "@expo/vector-icons": "^15.0.2", "expo": "~57.0.14", + "expo-constants": "~57.0.12", + "expo-font": "~57.0.1", + "expo-linear-gradient": "~57.0.1", + "expo-linking": "~57.0.6", + "expo-router": "~57.0.14", "expo-status-bar": "~57.0.1", "react": "19.2.3", - "react-native": "0.86.2" + "react-native": "0.86.2", + "react-native-safe-area-context": "~5.7.0", + "react-native-screens": "~4.26.0", + "react-native-svg": "15.15.4" + }, + "devDependencies": { + "@types/react": "~19.2.4", + "typescript": "~6.0.3" } }, + "node_modules/@adobe/css-tools": { + "version": "4.5.0", + "resolved": "https://registry.npmmirror.com/@adobe/css-tools/-/css-tools-4.5.0.tgz", + "integrity": "sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==", + "license": "MIT" + }, "node_modules/@babel/code-frame": { "version": "7.29.7", "resolved": "https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.29.7.tgz", @@ -1059,6 +1078,12 @@ "node": ">=6.9.0" } }, + "node_modules/@expo-google-fonts/material-symbols": { + "version": "0.4.44", + "resolved": "https://registry.npmmirror.com/@expo-google-fonts/material-symbols/-/material-symbols-0.4.44.tgz", + "integrity": "sha512-36JP9Chcy/QEVZ9ZGY4i6zInlyFPbQjkIm6gRNuWWltScIj0WR8rddcD57EIjSC5YKCe2ZKLO6eO/N5r8Jit0A==", + "license": "MIT AND Apache-2.0" + }, "node_modules/@expo/code-signing-certificates": { "version": "0.0.6", "resolved": "https://registry.npmmirror.com/@expo/code-signing-certificates/-/code-signing-certificates-0.0.6.tgz", @@ -1153,6 +1178,17 @@ } } }, + "node_modules/@expo/dom-webview": { + "version": "57.0.1", + "resolved": "https://registry.npmmirror.com/@expo/dom-webview/-/dom-webview-57.0.1.tgz", + "integrity": "sha512-lAKsME4SAq+8sf56oN0DX5TBYyruupoRxbWbD2xf9RnKY8y6x8eb9LCE5pxSN0qyWdqnp+0wmyWzDkKboThKAw==", + "license": "MIT", + "peerDependencies": { + "expo": "*", + "react": "*", + "react-native": "*" + } + }, "node_modules/@expo/env": { "version": "2.4.2", "resolved": "https://registry.npmmirror.com/@expo/env/-/env-2.4.2.tgz", @@ -1239,6 +1275,23 @@ "chalk": "^4.1.2" } }, + "node_modules/@expo/log-box": { + "version": "57.0.3", + "resolved": "https://registry.npmmirror.com/@expo/log-box/-/log-box-57.0.3.tgz", + "integrity": "sha512-qv/cMliNax6es07Un/4IGJIcs4PUuhTKKTrJZX/0X2YRcOT5cqm/QicibZwTIbiZWi1i8P4YBRQTfFT2B17giw==", + "license": "MIT", + "dependencies": { + "@expo/dom-webview": "^57.0.1", + "anser": "^1.4.9", + "stacktrace-parser": "^0.1.10" + }, + "peerDependencies": { + "@expo/dom-webview": "^57.0.1", + "expo": "*", + "react": "*", + "react-native": "*" + } + }, "node_modules/@expo/metro": { "version": "56.0.0", "resolved": "https://registry.npmmirror.com/@expo/metro/-/metro-56.0.0.tgz", @@ -1379,6 +1432,17 @@ "integrity": "sha512-HHQigo3rQWKMDzYDLkubN5WQOYXJJE2eNqIQC2axC2iO3mHdwnIR7FgZVvHWtBwAdzBgAP0ECp8KqS8TiMKvgw==", "license": "MIT" }, + "node_modules/@expo/vector-icons": { + "version": "15.1.1", + "resolved": "https://registry.npmmirror.com/@expo/vector-icons/-/vector-icons-15.1.1.tgz", + "integrity": "sha512-Iu2VkcoI5vygbtYngm7jb4ifxElNVXQYdDrYkT7UCEIiKLeWnQY0wf2ZhHZ+Wro6Sc5TaumpKUOqDRpLi5rkvw==", + "license": "MIT", + "peerDependencies": { + "expo-font": ">=14.0.4", + "react": "*", + "react-native": "*" + } + }, "node_modules/@expo/xcpretty": { "version": "4.4.4", "resolved": "https://registry.npmmirror.com/@expo/xcpretty/-/xcpretty-4.4.4.tgz", @@ -1486,195 +1550,649 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@react-native/assets-registry": { - "version": "0.86.2", - "resolved": "https://registry.npmmirror.com/@react-native/assets-registry/-/assets-registry-0.86.2.tgz", - "integrity": "sha512-vcX/mBjWAVnWofu7KecotquI2unZ/tITwA7OGdq/mdY/zmGXIEvYhfEYyOQij/LRqi9WAL+iizInTBWnxDhK/Q==", + "node_modules/@radix-ui/primitive": { + "version": "1.1.7", + "resolved": "https://registry.npmmirror.com/@radix-ui/primitive/-/primitive-1.1.7.tgz", + "integrity": "sha512-rqWnm76nYT8HoNNqEjpgJ7Pw/DrBj5iBTrmEPo6HTX5+VJyBNOqTdv4g89G63HuR5g0AaENoAcH7Is5fF2kZ8Q==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.1.15", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-collection/-/react-collection-1.1.15.tgz", + "integrity": "sha512-9W+B9NPF0NaaPh/1NJd3+KqsnlLqU9H7T2rvww+fp+T/evVXdNAyYcnfRQZFOjkR1ajQp3yORlqnI8soawLvNA==", "license": "MIT", - "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-slot": "1.3.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } } }, - "node_modules/@react-native/babel-plugin-codegen": { - "version": "0.86.2", - "resolved": "https://registry.npmmirror.com/@react-native/babel-plugin-codegen/-/babel-plugin-codegen-0.86.2.tgz", - "integrity": "sha512-NNDZqOlNbH5SzgPks1jFDYH3234Rpa5e/nhZymxhIiBH3NcE3uD+rGj/HWXhH7nHF2ToGK6XbUpqy7nmJPeh+g==", + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.5", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.5.tgz", + "integrity": "sha512-+48PbAAbq3didjJxa+OaWY2ZwgAKsNiRGyeHKszblZMQ+kcpd9pAaT11cMkGEie0vsOi3QdeTE6d5Fe3Gn61kA==", "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.29.0", - "@react-native/codegen": "0.86.2" + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, - "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/@react-native/codegen": { - "version": "0.86.2", - "resolved": "https://registry.npmmirror.com/@react-native/codegen/-/codegen-0.86.2.tgz", - "integrity": "sha512-xKkudsahUJ1n//55g4fXk5BStVqqmZlz8HQveL45ZxcfDnwvhuYe2GymksQANFsSN+slvrarjrfq8kIxJzbceA==", + "node_modules/@radix-ui/react-context": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-context/-/react-context-1.2.2.tgz", + "integrity": "sha512-RHCUGwKHDr0hDGg4X7ma4JG4/+12qxw8rkh5QKdDldlCvtja6nUx1Ef/8HVrJze81lEsgLQlqjzjGNHantgnQA==", "license": "MIT", - "dependencies": { - "@babel/core": "^7.25.2", - "@babel/parser": "^7.29.0", - "hermes-parser": "0.36.0", - "invariant": "^2.2.4", - "nullthrows": "^1.1.1", - "tinyglobby": "^0.2.15", - "yargs": "^17.6.2" + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, - "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.23", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-dialog/-/react-dialog-1.1.23.tgz", + "integrity": "sha512-Ksw4WeROkO4rC9k/onilX/Ao2Cr1ku1unMNH+XSCcP4jSXYu7HDsg9n4ojMjVb22XpYjAQ9qfrFlVbru1vXDUA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-dismissable-layer": "1.1.19", + "@radix-ui/react-focus-guards": "1.1.6", + "@radix-ui/react-focus-scope": "1.1.16", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-portal": "1.1.17", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-slot": "1.3.3", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-layout-effect": "1.1.4", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.7.2" }, "peerDependencies": { - "@babel/core": "*" + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } } }, - "node_modules/@react-native/codegen/node_modules/hermes-estree": { - "version": "0.36.0", - "resolved": "https://registry.npmmirror.com/hermes-estree/-/hermes-estree-0.36.0.tgz", - "integrity": "sha512-A1+8zn5oss2CFP7pKsOaxorQG6FNIz1WU1VDqruLPPZl3LVgeE2C5xfFg8Ow6/Ow4mSslLLtYP1J3n38eKyW9w==", - "license": "MIT" - }, - "node_modules/@react-native/codegen/node_modules/hermes-parser": { - "version": "0.36.0", - "resolved": "https://registry.npmmirror.com/hermes-parser/-/hermes-parser-0.36.0.tgz", - "integrity": "sha512-GdpwMmH5x6IpC1cijvcvYnlPB60Mh6kTSF/NFdYV/j56gYdi+0RIakYs+eqOV+bbO0SW7mgVVGSsTJxyPQfo3w==", + "node_modules/@radix-ui/react-direction": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-direction/-/react-direction-1.1.4.tgz", + "integrity": "sha512-5pzg4FGQNpExhnhT2zlrP1wZFaYCd1K0nYWoFAdcYoYK868IEigqMX3B3f8yIoRlAhAeDWciLI6ZdCKHF9P4Vg==", "license": "MIT", - "dependencies": { - "hermes-estree": "0.36.0" + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/@react-native/community-cli-plugin": { - "version": "0.86.2", - "resolved": "https://registry.npmmirror.com/@react-native/community-cli-plugin/-/community-cli-plugin-0.86.2.tgz", - "integrity": "sha512-YHXNKoM6Y/HjREySZ5arET2xgiHgg67r1MdwJB//MPJAJ0Xc5g0u6UHxY9VzsHO3Y07dre6s0BinYwjt1SEWvQ==", + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.19", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.19.tgz", + "integrity": "sha512-8g4pfOL9HoKKLWGiypT+dphVqjFfmcXO5GBnhsG6zI+lxAx/8feQpr+1LSN8Re3hiZ+XkLNS4O9ztK11/LzQ6w==", "license": "MIT", "dependencies": { - "@react-native/dev-middleware": "0.86.2", - "debug": "^4.4.0", - "invariant": "^2.2.4", - "metro": "^0.84.3", - "metro-config": "^0.84.3", - "metro-core": "^0.84.3", - "semver": "^7.1.3" - }, - "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.1.4", + "@radix-ui/react-use-effect-event": "0.0.5" }, "peerDependencies": { - "@react-native-community/cli": "*", - "@react-native/metro-config": "0.86.2" + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { - "@react-native-community/cli": { + "@types/react": { "optional": true }, - "@react-native/metro-config": { + "@types/react-dom": { "optional": true } } }, - "node_modules/@react-native/debugger-frontend": { - "version": "0.86.2", - "resolved": "https://registry.npmmirror.com/@react-native/debugger-frontend/-/debugger-frontend-0.86.2.tgz", - "integrity": "sha512-KGS1aV5F6cIqpnoIUhLBXyVzy1oAj8jBFGau6vX4Vy0HXRJN7p+68RU7x6NuyraHvQcR14ccMGT5TkFuNjQ4gA==", - "license": "BSD-3-Clause", - "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.6", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.6.tgz", + "integrity": "sha512-RNOJjfZMTyBM6xYmV3IVGXkPjIhcBAuv48POevAXwrGJhkWZ9p1rFoIS1JFooPuT193AZmRsCPhpoVJxx6OPoQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/@react-native/debugger-shell": { - "version": "0.86.2", - "resolved": "https://registry.npmmirror.com/@react-native/debugger-shell/-/debugger-shell-0.86.2.tgz", - "integrity": "sha512-/TaVJ2+gGajZPJGrFaObUQmHmlaxAlfmOPZicl6pNKDUjzSgFMpcLkdTOExvb+USYTVdGX1XwxXyvjQdUO2bvg==", + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.16", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.16.tgz", + "integrity": "sha512-wmRZ2WWLvmt6KHy2rNPOdPUjwq5xOHY02+m+udwJTn0aNIox/rkskAvJTyTLGhPK6KgrUjlJUJpgmx/+wFiFIQ==", "license": "MIT", "dependencies": { - "cross-spawn": "^7.0.6", - "debug": "^4.4.0", - "fb-dotslash": "0.5.8" + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.1.4" }, - "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } } }, - "node_modules/@react-native/dev-middleware": { - "version": "0.86.2", - "resolved": "https://registry.npmmirror.com/@react-native/dev-middleware/-/dev-middleware-0.86.2.tgz", - "integrity": "sha512-B7L0vKvg+IcEElT7Vpqh1xj5yJAqWUegjbP+bQRaorJMAYnv11GkliTnZV2AdTDfZQJWgOEx8i8LGkHkUg7bnA==", + "node_modules/@radix-ui/react-id": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-id/-/react-id-1.1.4.tgz", + "integrity": "sha512-TMQp2llA+RYn7JcjnrMnz7wN4pcVttPZnRZo52PLQsoLVKzNlVwUeHmfePgTgRluXFvlD3GD5g5MOVVTJCO0qA==", "license": "MIT", "dependencies": { - "@isaacs/ttlcache": "^1.4.1", - "@react-native/debugger-frontend": "0.86.2", - "@react-native/debugger-shell": "0.86.2", - "chrome-launcher": "^0.15.2", - "chromium-edge-launcher": "^0.3.0", - "connect": "^3.6.5", - "debug": "^4.4.0", - "invariant": "^2.2.4", - "nullthrows": "^1.1.1", - "open": "^7.0.3", - "serve-static": "^1.16.2", - "ws": "^7.5.10" + "@radix-ui/react-use-layout-effect": "1.1.4" }, - "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/@react-native/gradle-plugin": { - "version": "0.86.2", - "resolved": "https://registry.npmmirror.com/@react-native/gradle-plugin/-/gradle-plugin-0.86.2.tgz", - "integrity": "sha512-2F6x14NcHMpVmfTTFKfMkpV5dZedZrLiv6PE+c3vgnesV2bjleUBydr4U+NI8VkI7OwW71L0A5qQ76I9LCrfoQ==", + "node_modules/@radix-ui/react-portal": { + "version": "1.1.17", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-portal/-/react-portal-1.1.17.tgz", + "integrity": "sha512-vKQLcWypUnwZVvfV7UkGahH2g6ySe8M8R+zYBwPrv5byZ9QAW6cQVvNKo7GgmD+p8aYb6D9JBuvy8/WhOno2wQ==", "license": "MIT", - "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" + "dependencies": { + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } } }, - "node_modules/@react-native/js-polyfills": { - "version": "0.86.2", - "resolved": "https://registry.npmmirror.com/@react-native/js-polyfills/-/js-polyfills-0.86.2.tgz", - "integrity": "sha512-bIwNcGBaQ74shB5z1mRkxOpjikimuwsnOCEkZSzL67Z1FTyK1ObpENfyd2QvcvVW9Cjl+tHuw9ynpBnb2jPoJQ==", + "node_modules/@radix-ui/react-presence": { + "version": "1.1.10", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-presence/-/react-presence-1.1.10.tgz", + "integrity": "sha512-3wyzCQ6+ubRA+D4uv9m95JYLXxmOHp05qjrkjeA7uKHHtjpPggQzc6DAb0URl7j67oR0K2foO4ip27TiX037Bw==", "license": "MIT", - "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } } }, - "node_modules/@react-native/normalize-colors": { - "version": "0.86.2", - "resolved": "https://registry.npmmirror.com/@react-native/normalize-colors/-/normalize-colors-0.86.2.tgz", - "integrity": "sha512-EzPFc9Y6lzYOWeso2almwXI7f8+qReHxWvT+algsOczb2UhWXIWXDoSvkdwoSfiwwmGt/ijJgKJoeHlzPkLwRg==", - "license": "MIT" - }, - "node_modules/@react-native/virtualized-lists": { - "version": "0.86.2", - "resolved": "https://registry.npmmirror.com/@react-native/virtualized-lists/-/virtualized-lists-0.86.2.tgz", - "integrity": "sha512-uO0J72gh3EvE+1/GHRk18QRyBDTRHRB0AraAfojsRjbT7VMuJwKrZYaKGshavoaEud6aw00ZB9/8mTMIKjjcAw==", + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.10", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-primitive/-/react-primitive-2.1.10.tgz", + "integrity": "sha512-MucOnzh6hR5mid6VpkbglRAMYMjKLqRnGBbjXkzjK52fuQDd1qbkx78a5P40mkcnVXJdEVxm26E9OPAiUq7nBg==", "license": "MIT", "dependencies": { - "invariant": "^2.2.4", - "nullthrows": "^1.1.1" - }, - "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" + "@radix-ui/react-slot": "1.3.3" }, "peerDependencies": { - "@types/react": "^19.2.0", - "react": "*", - "react-native": "0.86.2" + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { "optional": true + }, + "@types/react-dom": { + "optional": true } } }, - "node_modules/@sinclair/typebox": { - "version": "0.27.12", - "resolved": "https://registry.npmmirror.com/@sinclair/typebox/-/typebox-0.27.12.tgz", - "integrity": "sha512-hhyNJ+nbR6ZR7pToHvllEFun9TL0sbL+tk/ON75lo+Xas054uez98qRbsuNt7MBCyZKK4+8Yli/OAGZhmfBZ/g==", - "license": "MIT" - }, - "node_modules/@types/istanbul-lib-coverage": { - "version": "2.0.6", + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.19", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.19.tgz", + "integrity": "sha512-V9jI6hDjT7l3jsCQD9bLNvDLM3tH/gdbOTp7Tefp3hbbgCGQoK7tUvrWiRlcoBHIZ809ElXwNQwVo0B98LuTXQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-collection": "1.1.15", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.1.4", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-is-hydrated": "0.1.3", + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.3.3", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.3.3.tgz", + "integrity": "sha512-qx7oqnYbxnK9kYI9m317qmFmEgo6ywqWvbTogdj7cL9p3/yx4M48p7Rnw5z3H890cL/ow/EeWJsuTykeZVXP5Q==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.5" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.4.tgz", + "integrity": "sha512-R6OUY2e2fA6Yn6s+VSx5KBV6Nx8LQEhu+cz7LCej18rQ1HLyg9PSC9jP/ZNx0o6FAIK9c0F1kHylzSxKsdlkrQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.6", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.6.tgz", + "integrity": "sha512-uEQJGT97ZA/TgP/Hydw47lHu+/vQj6z/0jA+WeTbK1o9Rx45GImjpD0tc3W5ad3D6XTSR6e1yEO0FvGq6WQfVQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-use-effect-event": "0.0.5", + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.5", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.5.tgz", + "integrity": "sha512-7cshFL8HGS/7HEiHH+9kL9HBwp2sa9yX18Knwek6KYWmXwM7pegMgta2AXMQKI+rq3JnfSj9x8wYqFMTdG1Jgg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-is-hydrated": { + "version": "0.1.3", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.3.tgz", + "integrity": "sha512-umO/aJ+82CpOnhDZUTbILCQf7kU/g0iv+oGs/Q8jw7IkhWBzaEP4sA268PhFAJTFetbwp3ICc6ktpI4TqtxcIw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.4.tgz", + "integrity": "sha512-K20DkRkUwDnxEYMBPcg3Y6voLkEy5p5QQmszZgLngKKiC7dzBR/aEuK3w1qlx2JWDUNH6FluahYdgR3BP+QbYw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@react-native-masked-view/masked-view": { + "version": "0.3.2", + "resolved": "https://registry.npmmirror.com/@react-native-masked-view/masked-view/-/masked-view-0.3.2.tgz", + "integrity": "sha512-XwuQoW7/GEgWRMovOQtX3A4PrXhyaZm0lVUiY8qJDvdngjLms9Cpdck6SmGAUNqQwcj2EadHC1HwL0bEyoa/SQ==", + "license": "MIT", + "peerDependencies": { + "react": ">=16", + "react-native": ">=0.57" + } + }, + "node_modules/@react-native/assets-registry": { + "version": "0.86.2", + "resolved": "https://registry.npmmirror.com/@react-native/assets-registry/-/assets-registry-0.86.2.tgz", + "integrity": "sha512-vcX/mBjWAVnWofu7KecotquI2unZ/tITwA7OGdq/mdY/zmGXIEvYhfEYyOQij/LRqi9WAL+iizInTBWnxDhK/Q==", + "license": "MIT", + "engines": { + "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" + } + }, + "node_modules/@react-native/babel-plugin-codegen": { + "version": "0.86.2", + "resolved": "https://registry.npmmirror.com/@react-native/babel-plugin-codegen/-/babel-plugin-codegen-0.86.2.tgz", + "integrity": "sha512-NNDZqOlNbH5SzgPks1jFDYH3234Rpa5e/nhZymxhIiBH3NcE3uD+rGj/HWXhH7nHF2ToGK6XbUpqy7nmJPeh+g==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.0", + "@react-native/codegen": "0.86.2" + }, + "engines": { + "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" + } + }, + "node_modules/@react-native/codegen": { + "version": "0.86.2", + "resolved": "https://registry.npmmirror.com/@react-native/codegen/-/codegen-0.86.2.tgz", + "integrity": "sha512-xKkudsahUJ1n//55g4fXk5BStVqqmZlz8HQveL45ZxcfDnwvhuYe2GymksQANFsSN+slvrarjrfq8kIxJzbceA==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.25.2", + "@babel/parser": "^7.29.0", + "hermes-parser": "0.36.0", + "invariant": "^2.2.4", + "nullthrows": "^1.1.1", + "tinyglobby": "^0.2.15", + "yargs": "^17.6.2" + }, + "engines": { + "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" + }, + "peerDependencies": { + "@babel/core": "*" + } + }, + "node_modules/@react-native/codegen/node_modules/hermes-estree": { + "version": "0.36.0", + "resolved": "https://registry.npmmirror.com/hermes-estree/-/hermes-estree-0.36.0.tgz", + "integrity": "sha512-A1+8zn5oss2CFP7pKsOaxorQG6FNIz1WU1VDqruLPPZl3LVgeE2C5xfFg8Ow6/Ow4mSslLLtYP1J3n38eKyW9w==", + "license": "MIT" + }, + "node_modules/@react-native/codegen/node_modules/hermes-parser": { + "version": "0.36.0", + "resolved": "https://registry.npmmirror.com/hermes-parser/-/hermes-parser-0.36.0.tgz", + "integrity": "sha512-GdpwMmH5x6IpC1cijvcvYnlPB60Mh6kTSF/NFdYV/j56gYdi+0RIakYs+eqOV+bbO0SW7mgVVGSsTJxyPQfo3w==", + "license": "MIT", + "dependencies": { + "hermes-estree": "0.36.0" + } + }, + "node_modules/@react-native/community-cli-plugin": { + "version": "0.86.2", + "resolved": "https://registry.npmmirror.com/@react-native/community-cli-plugin/-/community-cli-plugin-0.86.2.tgz", + "integrity": "sha512-YHXNKoM6Y/HjREySZ5arET2xgiHgg67r1MdwJB//MPJAJ0Xc5g0u6UHxY9VzsHO3Y07dre6s0BinYwjt1SEWvQ==", + "license": "MIT", + "dependencies": { + "@react-native/dev-middleware": "0.86.2", + "debug": "^4.4.0", + "invariant": "^2.2.4", + "metro": "^0.84.3", + "metro-config": "^0.84.3", + "metro-core": "^0.84.3", + "semver": "^7.1.3" + }, + "engines": { + "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" + }, + "peerDependencies": { + "@react-native-community/cli": "*", + "@react-native/metro-config": "0.86.2" + }, + "peerDependenciesMeta": { + "@react-native-community/cli": { + "optional": true + }, + "@react-native/metro-config": { + "optional": true + } + } + }, + "node_modules/@react-native/debugger-frontend": { + "version": "0.86.2", + "resolved": "https://registry.npmmirror.com/@react-native/debugger-frontend/-/debugger-frontend-0.86.2.tgz", + "integrity": "sha512-KGS1aV5F6cIqpnoIUhLBXyVzy1oAj8jBFGau6vX4Vy0HXRJN7p+68RU7x6NuyraHvQcR14ccMGT5TkFuNjQ4gA==", + "license": "BSD-3-Clause", + "engines": { + "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" + } + }, + "node_modules/@react-native/debugger-shell": { + "version": "0.86.2", + "resolved": "https://registry.npmmirror.com/@react-native/debugger-shell/-/debugger-shell-0.86.2.tgz", + "integrity": "sha512-/TaVJ2+gGajZPJGrFaObUQmHmlaxAlfmOPZicl6pNKDUjzSgFMpcLkdTOExvb+USYTVdGX1XwxXyvjQdUO2bvg==", + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.6", + "debug": "^4.4.0", + "fb-dotslash": "0.5.8" + }, + "engines": { + "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" + } + }, + "node_modules/@react-native/dev-middleware": { + "version": "0.86.2", + "resolved": "https://registry.npmmirror.com/@react-native/dev-middleware/-/dev-middleware-0.86.2.tgz", + "integrity": "sha512-B7L0vKvg+IcEElT7Vpqh1xj5yJAqWUegjbP+bQRaorJMAYnv11GkliTnZV2AdTDfZQJWgOEx8i8LGkHkUg7bnA==", + "license": "MIT", + "dependencies": { + "@isaacs/ttlcache": "^1.4.1", + "@react-native/debugger-frontend": "0.86.2", + "@react-native/debugger-shell": "0.86.2", + "chrome-launcher": "^0.15.2", + "chromium-edge-launcher": "^0.3.0", + "connect": "^3.6.5", + "debug": "^4.4.0", + "invariant": "^2.2.4", + "nullthrows": "^1.1.1", + "open": "^7.0.3", + "serve-static": "^1.16.2", + "ws": "^7.5.10" + }, + "engines": { + "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" + } + }, + "node_modules/@react-native/gradle-plugin": { + "version": "0.86.2", + "resolved": "https://registry.npmmirror.com/@react-native/gradle-plugin/-/gradle-plugin-0.86.2.tgz", + "integrity": "sha512-2F6x14NcHMpVmfTTFKfMkpV5dZedZrLiv6PE+c3vgnesV2bjleUBydr4U+NI8VkI7OwW71L0A5qQ76I9LCrfoQ==", + "license": "MIT", + "engines": { + "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" + } + }, + "node_modules/@react-native/js-polyfills": { + "version": "0.86.2", + "resolved": "https://registry.npmmirror.com/@react-native/js-polyfills/-/js-polyfills-0.86.2.tgz", + "integrity": "sha512-bIwNcGBaQ74shB5z1mRkxOpjikimuwsnOCEkZSzL67Z1FTyK1ObpENfyd2QvcvVW9Cjl+tHuw9ynpBnb2jPoJQ==", + "license": "MIT", + "engines": { + "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" + } + }, + "node_modules/@react-native/normalize-colors": { + "version": "0.86.2", + "resolved": "https://registry.npmmirror.com/@react-native/normalize-colors/-/normalize-colors-0.86.2.tgz", + "integrity": "sha512-EzPFc9Y6lzYOWeso2almwXI7f8+qReHxWvT+algsOczb2UhWXIWXDoSvkdwoSfiwwmGt/ijJgKJoeHlzPkLwRg==", + "license": "MIT" + }, + "node_modules/@react-native/virtualized-lists": { + "version": "0.86.2", + "resolved": "https://registry.npmmirror.com/@react-native/virtualized-lists/-/virtualized-lists-0.86.2.tgz", + "integrity": "sha512-uO0J72gh3EvE+1/GHRk18QRyBDTRHRB0AraAfojsRjbT7VMuJwKrZYaKGshavoaEud6aw00ZB9/8mTMIKjjcAw==", + "license": "MIT", + "dependencies": { + "invariant": "^2.2.4", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" + }, + "peerDependencies": { + "@types/react": "^19.2.0", + "react": "*", + "react-native": "0.86.2" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.12", + "resolved": "https://registry.npmmirror.com/@sinclair/typebox/-/typebox-0.27.12.tgz", + "integrity": "sha512-hhyNJ+nbR6ZR7pToHvllEFun9TL0sbL+tk/ON75lo+Xas054uez98qRbsuNt7MBCyZKK4+8Yli/OAGZhmfBZ/g==", + "license": "MIT" + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.10.0", + "resolved": "https://registry.npmmirror.com/@testing-library/jest-dom/-/jest-dom-6.10.0.tgz", + "integrity": "sha512-HQwu0KaB2zyT0iLzBL+8CLyZDL3KlZlZJ+2iyc9uCUnlJVskJU/UlPuVCyIPhtukjPQdT2QNoR5nCP5FqTmmDQ==", + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=22", + "npm": ">=6", + "yarn": ">=1" + }, + "peerDependencies": { + "@testing-library/dom": ">=10 <11" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmmirror.com/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "license": "MIT" + }, + "node_modules/@testing-library/user-event": { + "version": "14.6.5", + "resolved": "https://registry.npmmirror.com/@testing-library/user-event/-/user-event-14.6.5.tgz", + "integrity": "sha512-FhqjldLTpteueBaKflhNFlMT3+PM0O5fiBUivht6b9CZ1eesJyy7+g3Jr7XwJzt/Hip3ZG5hWwK1MX1FuDiE4w==", + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", "resolved": "https://registry.npmmirror.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", "license": "MIT" @@ -1706,6 +2224,16 @@ "undici-types": "~8.3.0" } }, + "node_modules/@types/react": { + "version": "19.2.18", + "resolved": "https://registry.npmmirror.com/@types/react/-/react-19.2.18.tgz", + "integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, "node_modules/@types/yargs": { "version": "17.0.35", "resolved": "https://registry.npmmirror.com/@types/yargs/-/yargs-17.0.35.tgz", @@ -1863,6 +2391,27 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "license": "Python-2.0" }, + "node_modules/aria-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmmirror.com/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmmirror.com/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, "node_modules/asap": { "version": "2.0.6", "resolved": "https://registry.npmmirror.com/asap/-/asap-2.0.6.tgz", @@ -2015,6 +2564,12 @@ "node": ">=0.6" } }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "license": "ISC" + }, "node_modules/bplist-creator": { "version": "0.1.0", "resolved": "https://registry.npmmirror.com/bplist-creator/-/bplist-creator-0.1.0.tgz", @@ -2226,6 +2781,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmmirror.com/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" + }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmmirror.com/cliui/-/cliui-8.0.1.tgz", @@ -2249,6 +2810,19 @@ "node": ">=0.8" } }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmmirror.com/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", @@ -2267,6 +2841,16 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmmirror.com/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, "node_modules/commander": { "version": "7.2.0", "resolved": "https://registry.npmmirror.com/commander/-/commander-7.2.0.tgz", @@ -2375,26 +2959,89 @@ "browserslist": "^4.28.7" }, "engines": { - "node": ">=6.4.0" + "node": ">=6.4.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmmirror.com/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-tree": { + "version": "1.1.3", + "resolved": "https://registry.npmmirror.com/css-tree/-/css-tree-1.1.3.tgz", + "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", + "license": "MIT", + "dependencies": { + "mdn-data": "2.0.14", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/css-tree/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmmirror.com/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmmirror.com/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/core-js" + "url": "https://github.com/sponsors/fb55" } }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmmirror.com/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "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", @@ -2413,6 +3060,15 @@ } } }, + "node_modules/decode-uri-component": { + "version": "0.2.2", + "resolved": "https://registry.npmmirror.com/decode-uri-component/-/decode-uri-component-0.2.2.tgz", + "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, "node_modules/deepmerge": { "version": "4.3.1", "resolved": "https://registry.npmmirror.com/deepmerge/-/deepmerge-4.3.1.tgz", @@ -2443,6 +3099,15 @@ "node": ">= 0.8" } }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/destroy": { "version": "1.2.0", "resolved": "https://registry.npmmirror.com/destroy/-/destroy-1.2.0.tgz", @@ -2462,12 +3127,73 @@ "node": ">=8" } }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, "node_modules/dnssd-advertise": { "version": "1.1.6", "resolved": "https://registry.npmmirror.com/dnssd-advertise/-/dnssd-advertise-1.1.6.tgz", "integrity": "sha512-Ndrrf6BMPalkQPd/zubL+4YghH2J9NspapQ09uDXwYbvOPkP0oaqf5CkcwJ0b50kS2O3ul6yVu+jz+RY62Cejg==", "license": "MIT" }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmmirror.com/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmmirror.com/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmmirror.com/ee-first/-/ee-first-1.1.1.tgz", @@ -2495,6 +3221,18 @@ "node": ">= 0.8" } }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmmirror.com/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/error-stack-parser": { "version": "2.1.4", "resolved": "https://registry.npmmirror.com/error-stack-parser/-/error-stack-parser-2.1.4.tgz", @@ -2620,6 +3358,69 @@ } } }, + "node_modules/expo-constants": { + "version": "57.0.12", + "resolved": "https://registry.npmmirror.com/expo-constants/-/expo-constants-57.0.12.tgz", + "integrity": "sha512-js9qxZw4G9ulSc72GGpYGwtmk/IzFcf6/tTcEG6wB8HTlkunMS4W86R+4e6oHIrIkHA7W6s/79Se+E+DC3RAqA==", + "license": "MIT", + "dependencies": { + "@expo/env": "~2.4.2" + }, + "peerDependencies": { + "expo": "*", + "react-native": "*" + } + }, + "node_modules/expo-font": { + "version": "57.0.1", + "resolved": "https://registry.npmmirror.com/expo-font/-/expo-font-57.0.1.tgz", + "integrity": "sha512-QyS9L1Kh9sKJg4gfU6rdbpxpmH+DyzBX8z6jVvXMUDoqLr1GqmkO/Wu379KCXjL///kWbhpNlbi7AgBuj4VdIQ==", + "license": "MIT", + "dependencies": { + "fontfaceobserver": "^2.1.0" + }, + "peerDependencies": { + "expo": "*", + "react": "*", + "react-native": "*" + } + }, + "node_modules/expo-glass-effect": { + "version": "57.0.1", + "resolved": "https://registry.npmmirror.com/expo-glass-effect/-/expo-glass-effect-57.0.1.tgz", + "integrity": "sha512-m/n8maxqNcHk6ZDhuqXBfD5Kt1Iz3M8xykVgdB0iSCIXvF70IqWXmQhX8Psswhrp8eZ+3r0mAD0Jh/2gFA3QaA==", + "license": "MIT", + "peerDependencies": { + "expo": "*", + "react": "*", + "react-native": "*" + } + }, + "node_modules/expo-linear-gradient": { + "version": "57.0.1", + "resolved": "https://registry.npmmirror.com/expo-linear-gradient/-/expo-linear-gradient-57.0.1.tgz", + "integrity": "sha512-CpS8eMqoIWcHVGKV66zbDvzotCw9qYp3f8CuI9N+h1LaO0tMLUzBpkhAKePUsXlpN3yolYlHFSPkfVZ/uSh+iA==", + "license": "MIT", + "peerDependencies": { + "expo": "*", + "react": "*", + "react-native": "*" + } + }, + "node_modules/expo-linking": { + "version": "57.0.6", + "resolved": "https://registry.npmmirror.com/expo-linking/-/expo-linking-57.0.6.tgz", + "integrity": "sha512-NdKjEGk+ywwGRNvgQCq9KettM1eVHfR1+uLov+0ohb6rmmMEgnstfLcc3f0DKMQ0PnROsY5G5+ZkNeiairYRXA==", + "license": "MIT", + "dependencies": { + "expo-constants": "~57.0.11", + "invariant": "^2.2.4" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, "node_modules/expo-modules-autolinking": { "version": "57.0.10", "resolved": "https://registry.npmmirror.com/expo-modules-autolinking/-/expo-modules-autolinking-57.0.10.tgz", @@ -2665,6 +3466,170 @@ "react-native": "*" } }, + "node_modules/expo-router": { + "version": "57.0.14", + "resolved": "https://registry.npmmirror.com/expo-router/-/expo-router-57.0.14.tgz", + "integrity": "sha512-5Yai7kDj6xYMGw4Q7AW+EJlr1nDFH6DWy1/sr6XrxqC7x5YPk3sxv4rAWIQ8fDO9CwSCRAVisVqmul77Px0sXw==", + "license": "MIT", + "dependencies": { + "@expo/log-box": "^57.0.3", + "@expo/metro-runtime": "^57.0.11", + "@expo/schema-utils": "^57.0.2", + "@expo/ui": "^57.0.11", + "@radix-ui/react-slot": "^1.2.0", + "@radix-ui/react-tabs": "^1.1.12", + "@react-native-masked-view/masked-view": "^0.3.2", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/user-event": "^14.6.1", + "client-only": "^0.0.1", + "color": "^4.2.3", + "debug": "^4.3.4", + "escape-string-regexp": "^4.0.0", + "expo-glass-effect": "^57.0.1", + "expo-server": "^57.0.3", + "expo-symbols": "^57.0.2", + "fast-deep-equal": "^3.1.3", + "invariant": "^2.2.4", + "nanoid": "^3.3.8", + "query-string": "^7.1.3", + "react-fast-compare": "^3.2.2", + "react-is": "^19.1.0", + "react-native-drawer-layout": "^4.2.2", + "react-native-screens": "^4.26.0", + "server-only": "^0.0.1", + "sf-symbols-typescript": "^2.1.0", + "shallowequal": "^1.1.0", + "standard-navigation": "^0.0.5", + "vaul": "^1.1.2" + }, + "peerDependencies": { + "@expo/log-box": "^57.0.3", + "@expo/metro-runtime": "^57.0.11", + "@testing-library/react-native": ">= 13.2.0", + "expo": "*", + "expo-constants": "^57.0.12", + "expo-linking": "^57.0.6", + "react": "*", + "react-dom": "*", + "react-native": "*", + "react-native-gesture-handler": "*", + "react-native-reanimated": "*", + "react-native-safe-area-context": ">= 5.4.0", + "react-native-screens": "^4.26.0", + "react-native-web": "*", + "react-server-dom-webpack": "~19.0.4 || ~19.1.5 || ~19.2.4" + }, + "peerDependenciesMeta": { + "@testing-library/react-native": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "react-native-gesture-handler": { + "optional": true + }, + "react-native-reanimated": { + "optional": true + }, + "react-native-web": { + "optional": true + }, + "react-server-dom-webpack": { + "optional": true + } + } + }, + "node_modules/expo-router/node_modules/@expo/metro-runtime": { + "version": "57.0.11", + "resolved": "https://registry.npmmirror.com/@expo/metro-runtime/-/metro-runtime-57.0.11.tgz", + "integrity": "sha512-WsEIMvp4lN1x//73mYEiEprxl3gL8pHYH3gRBs2Wd1cra5FvGKLE4VNCMwHVkNIZtaN4ao52RpSCr/5uI5LJvQ==", + "license": "MIT", + "dependencies": { + "@expo/log-box": "^57.0.3", + "anser": "^1.4.9", + "pretty-format": "^29.7.0", + "stacktrace-parser": "^0.1.10", + "whatwg-fetch": "^3.0.0" + }, + "peerDependencies": { + "@expo/log-box": "^57.0.3", + "expo": "*", + "react": "*", + "react-dom": "*", + "react-native": "*" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/expo-router/node_modules/@expo/ui": { + "version": "57.0.11", + "resolved": "https://registry.npmmirror.com/@expo/ui/-/ui-57.0.11.tgz", + "integrity": "sha512-m1BSq15sg4sCmoFwOsMxBpETgyFhGlCbVPUyjdJ6Y00Ve0Fb4sueaFLRyeYRbZ8DC1z3Hgp5LNCYzBBBdMhxMg==", + "license": "MIT", + "dependencies": { + "sf-symbols-typescript": "^2.1.0", + "vaul": "^1.1.2" + }, + "peerDependencies": { + "@babel/core": "*", + "expo": "*", + "react": "*", + "react-dom": "*", + "react-native": "*", + "react-native-worklets": "*" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "react-native-worklets": { + "optional": true + } + } + }, + "node_modules/expo-router/node_modules/@radix-ui/react-tabs": { + "version": "1.1.21", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-tabs/-/react-tabs-1.1.21.tgz", + "integrity": "sha512-UKxJlZid7FVtsk/WTxj4i4uSEgj2Au+KBbS7SQyTlzMhhn+86Cz3tISZdTa87bfEfcuvZezf2ZsxD4xuEKtkog==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-roving-focus": "1.1.19", + "@radix-ui/react-use-controllable-state": "1.2.6" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/expo-router/node_modules/react-is": { + "version": "19.2.8", + "resolved": "https://registry.npmmirror.com/react-is/-/react-is-19.2.8.tgz", + "integrity": "sha512-s5un28nYxKJw5gvUHyW5PCC28CvBqLu9r3cWgzHT4Vo/5fqqkFcdRYsGcKf50WMPpjjFZS5d76fn3YCo2njKwQ==", + "license": "MIT" + }, "node_modules/expo-server": { "version": "57.0.3", "resolved": "https://registry.npmmirror.com/expo-server/-/expo-server-57.0.3.tgz", @@ -2685,6 +3650,22 @@ "react-native": "*" } }, + "node_modules/expo-symbols": { + "version": "57.0.2", + "resolved": "https://registry.npmmirror.com/expo-symbols/-/expo-symbols-57.0.2.tgz", + "integrity": "sha512-qZ0iqOflm5lZGwRsQ5Y8sDksw3GAUKwHSX1bJBoocXf7gu14vafIXXYWte+JT9VfXAUGyKodJhLHP/GoOrcNWg==", + "license": "MIT", + "dependencies": { + "@expo-google-fonts/material-symbols": "^0.4.1", + "sf-symbols-typescript": "^2.0.0" + }, + "peerDependencies": { + "expo": "*", + "expo-font": "*", + "react": "*", + "react-native": "*" + } + }, "node_modules/expo/node_modules/@expo/cli": { "version": "57.0.16", "resolved": "https://registry.npmmirror.com/@expo/cli/-/cli-57.0.16.tgz", @@ -2797,37 +3778,9 @@ "react-dom": { "optional": true }, - "react-server-dom-webpack": { - "optional": true - } - } - }, - "node_modules/expo/node_modules/@expo/dom-webview": { - "version": "57.0.1", - "resolved": "https://registry.npmmirror.com/@expo/dom-webview/-/dom-webview-57.0.1.tgz", - "integrity": "sha512-lAKsME4SAq+8sf56oN0DX5TBYyruupoRxbWbD2xf9RnKY8y6x8eb9LCE5pxSN0qyWdqnp+0wmyWzDkKboThKAw==", - "license": "MIT", - "peerDependencies": { - "expo": "*", - "react": "*", - "react-native": "*" - } - }, - "node_modules/expo/node_modules/@expo/log-box": { - "version": "57.0.3", - "resolved": "https://registry.npmmirror.com/@expo/log-box/-/log-box-57.0.3.tgz", - "integrity": "sha512-qv/cMliNax6es07Un/4IGJIcs4PUuhTKKTrJZX/0X2YRcOT5cqm/QicibZwTIbiZWi1i8P4YBRQTfFT2B17giw==", - "license": "MIT", - "dependencies": { - "@expo/dom-webview": "^57.0.1", - "anser": "^1.4.9", - "stacktrace-parser": "^0.1.10" - }, - "peerDependencies": { - "@expo/dom-webview": "^57.0.1", - "expo": "*", - "react": "*", - "react-native": "*" + "react-server-dom-webpack": { + "optional": true + } } }, "node_modules/expo/node_modules/@expo/metro-config": { @@ -2988,19 +3941,6 @@ "react-native": "*" } }, - "node_modules/expo/node_modules/expo-constants": { - "version": "57.0.12", - "resolved": "https://registry.npmmirror.com/expo-constants/-/expo-constants-57.0.12.tgz", - "integrity": "sha512-js9qxZw4G9ulSc72GGpYGwtmk/IzFcf6/tTcEG6wB8HTlkunMS4W86R+4e6oHIrIkHA7W6s/79Se+E+DC3RAqA==", - "license": "MIT", - "dependencies": { - "@expo/env": "~2.4.2" - }, - "peerDependencies": { - "expo": "*", - "react-native": "*" - } - }, "node_modules/expo/node_modules/expo-file-system": { "version": "57.0.4", "resolved": "https://registry.npmmirror.com/expo-file-system/-/expo-file-system-57.0.4.tgz", @@ -3011,20 +3951,6 @@ "react-native": "*" } }, - "node_modules/expo/node_modules/expo-font": { - "version": "57.0.1", - "resolved": "https://registry.npmmirror.com/expo-font/-/expo-font-57.0.1.tgz", - "integrity": "sha512-QyS9L1Kh9sKJg4gfU6rdbpxpmH+DyzBX8z6jVvXMUDoqLr1GqmkO/Wu379KCXjL///kWbhpNlbi7AgBuj4VdIQ==", - "license": "MIT", - "dependencies": { - "fontfaceobserver": "^2.1.0" - }, - "peerDependencies": { - "expo": "*", - "react": "*", - "react-native": "*" - } - }, "node_modules/expo/node_modules/expo-keep-awake": { "version": "57.0.1", "resolved": "https://registry.npmmirror.com/expo-keep-awake/-/expo-keep-awake-57.0.1.tgz", @@ -3119,6 +4045,12 @@ "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", "license": "Apache-2.0" }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, "node_modules/fb-dotslash": { "version": "0.5.8", "resolved": "https://registry.npmmirror.com/fb-dotslash/-/fb-dotslash-0.5.8.tgz", @@ -3158,6 +4090,15 @@ "node": ">=8" } }, + "node_modules/filter-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/filter-obj/-/filter-obj-1.1.0.tgz", + "integrity": "sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/finalhandler": { "version": "1.1.2", "resolved": "https://registry.npmmirror.com/finalhandler/-/finalhandler-1.1.2.tgz", @@ -3239,6 +4180,15 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/getenv": { "version": "2.0.0", "resolved": "https://registry.npmmirror.com/getenv/-/getenv-2.0.0.tgz", @@ -3397,6 +4347,15 @@ "node": ">=16.x" } }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz", @@ -3412,6 +4371,12 @@ "loose-envify": "^1.0.0" } }, + "node_modules/is-arrayish": { + "version": "0.3.4", + "resolved": "https://registry.npmmirror.com/is-arrayish/-/is-arrayish-0.3.4.tgz", + "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==", + "license": "MIT" + }, "node_modules/is-core-module": { "version": "2.16.2", "resolved": "https://registry.npmmirror.com/is-core-module/-/is-core-module-2.16.2.tgz", @@ -4074,6 +5039,12 @@ "integrity": "sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ==", "license": "Apache-2.0" }, + "node_modules/mdn-data": { + "version": "2.0.14", + "resolved": "https://registry.npmmirror.com/mdn-data/-/mdn-data-2.0.14.tgz", + "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==", + "license": "CC0-1.0" + }, "node_modules/memoize-one": { "version": "5.2.1", "resolved": "https://registry.npmmirror.com/memoize-one/-/memoize-one-5.2.1.tgz", @@ -4413,6 +5384,15 @@ "node": ">=4" } }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/minimatch": { "version": "10.2.6", "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-10.2.6.tgz", @@ -4527,6 +5507,18 @@ "node": "^16.14.0 || >=18.0.0" } }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, "node_modules/nullthrows": { "version": "1.1.1", "resolved": "https://registry.npmmirror.com/nullthrows/-/nullthrows-1.1.1.tgz", @@ -4908,6 +5900,24 @@ "node": ">= 6" } }, + "node_modules/query-string": { + "version": "7.1.3", + "resolved": "https://registry.npmmirror.com/query-string/-/query-string-7.1.3.tgz", + "integrity": "sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==", + "license": "MIT", + "dependencies": { + "decode-uri-component": "^0.2.2", + "filter-obj": "^1.1.0", + "split-on-first": "^1.0.0", + "strict-uri-encode": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/queue": { "version": "6.0.2", "resolved": "https://registry.npmmirror.com/queue/-/queue-6.0.2.tgz", @@ -4945,6 +5955,24 @@ "ws": "^7" } }, + "node_modules/react-fast-compare": { + "version": "3.2.2", + "resolved": "https://registry.npmmirror.com/react-fast-compare/-/react-fast-compare-3.2.2.tgz", + "integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==", + "license": "MIT" + }, + "node_modules/react-freeze": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/react-freeze/-/react-freeze-1.0.4.tgz", + "integrity": "sha512-r4F0Sec0BLxWicc7HEyo2x3/2icUTrRmDjaaRyzzn+7aDyFZliszMDOgLVwSnQnYENOlL1o569Ze2HZefk8clA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": ">=17.0.0" + } + }, "node_modules/react-is": { "version": "18.3.1", "resolved": "https://registry.npmmirror.com/react-is/-/react-is-18.3.1.tgz", @@ -5010,6 +6038,61 @@ } } }, + "node_modules/react-native-drawer-layout": { + "version": "4.2.10", + "resolved": "https://registry.npmmirror.com/react-native-drawer-layout/-/react-native-drawer-layout-4.2.10.tgz", + "integrity": "sha512-O6TQdZ5LSm3dqnuR4rX9KPtE+9dVg7jsezEYz1l01rkQTe4fQvYZIoPu2sZFl5X2N70uhRdjnPULySVR3sBUwA==", + "license": "MIT", + "dependencies": { + "color": "^4.2.3", + "use-latest-callback": "^0.2.4" + }, + "peerDependencies": { + "react": ">= 18.2.0", + "react-native": "*", + "react-native-gesture-handler": ">= 2.0.0", + "react-native-reanimated": ">= 2.0.0" + } + }, + "node_modules/react-native-safe-area-context": { + "version": "5.7.0", + "resolved": "https://registry.npmmirror.com/react-native-safe-area-context/-/react-native-safe-area-context-5.7.0.tgz", + "integrity": "sha512-/9/MtQz8ODphjsLdZ+GZAIcC/RtoqW9EeShf7Uvnfgm/pzYrJ75y3PV/J1wuAV1T5Dye5ygq4EAW20RoBq0ABQ==", + "license": "MIT", + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, + "node_modules/react-native-screens": { + "version": "4.26.2", + "resolved": "https://registry.npmmirror.com/react-native-screens/-/react-native-screens-4.26.2.tgz", + "integrity": "sha512-2XnWsZToKj76trGtEZzx5ELD/qOICFEprEeUntImmitQFVUkea27fiWdUSITArI356Y1qynpXZINW+Unbhky/A==", + "license": "MIT", + "dependencies": { + "react-freeze": "^1.0.0", + "warn-once": "^0.1.0" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, + "node_modules/react-native-svg": { + "version": "15.15.4", + "resolved": "https://registry.npmmirror.com/react-native-svg/-/react-native-svg-15.15.4.tgz", + "integrity": "sha512-boT/vIRgj6zZKBpfTPJJiYWMbZE9duBMOwPK6kCSTgxsS947IFMOq9OgIFkpWZTB7t229H24pDRkh3W9ZK/J1A==", + "license": "MIT", + "dependencies": { + "css-select": "^5.1.0", + "css-tree": "^1.1.3", + "warn-once": "0.1.1" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, "node_modules/react-native/node_modules/commander": { "version": "12.1.0", "resolved": "https://registry.npmmirror.com/commander/-/commander-12.1.0.tgz", @@ -5028,6 +6111,88 @@ "node": ">=0.10.0" } }, + "node_modules/react-remove-scroll": { + "version": "2.7.2", + "resolved": "https://registry.npmmirror.com/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", + "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmmirror.com/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmmirror.com/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/regenerate": { "version": "1.4.2", "resolved": "https://registry.npmmirror.com/regenerate/-/regenerate-1.4.2.tgz", @@ -5306,12 +6471,33 @@ "node": ">= 0.8" } }, + "node_modules/server-only": { + "version": "0.0.1", + "resolved": "https://registry.npmmirror.com/server-only/-/server-only-0.0.1.tgz", + "integrity": "sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==", + "license": "MIT" + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmmirror.com/setprototypeof/-/setprototypeof-1.2.0.tgz", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "license": "ISC" }, + "node_modules/sf-symbols-typescript": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/sf-symbols-typescript/-/sf-symbols-typescript-2.2.0.tgz", + "integrity": "sha512-TPbeg0b7ylrswdGCji8FRGFAKuqbpQlLbL8SOle3j1iHSs5Ob5mhvMAxWN2UItOjgALAB5Zp3fmMfj8mbWvXKw==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/shallowequal": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/shallowequal/-/shallowequal-1.1.0.tgz", + "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==", + "license": "MIT" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz", @@ -5362,6 +6548,15 @@ "plist": "^3.0.5" } }, + "node_modules/simple-swizzle": { + "version": "0.2.4", + "resolved": "https://registry.npmmirror.com/simple-swizzle/-/simple-swizzle-0.2.4.tgz", + "integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, "node_modules/sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmmirror.com/sisteransi/-/sisteransi-1.0.5.tgz", @@ -5414,6 +6609,15 @@ "node": ">=0.10.0" } }, + "node_modules/split-on-first": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/split-on-first/-/split-on-first-1.1.0.tgz", + "integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/stackframe": { "version": "1.3.4", "resolved": "https://registry.npmmirror.com/stackframe/-/stackframe-1.3.4.tgz", @@ -5432,6 +6636,12 @@ "node": ">=6" } }, + "node_modules/standard-navigation": { + "version": "0.0.5", + "resolved": "https://registry.npmmirror.com/standard-navigation/-/standard-navigation-0.0.5.tgz", + "integrity": "sha512-YAmzwAiiQVocZxO/VGPFiQHcu5pKiz09QIGC0MK6aRMoa3E0QkoTQgcqJr7ZZ3OMiNhu4DkaGElFI5htjOIDbw==", + "license": "MIT" + }, "node_modules/statuses": { "version": "1.5.0", "resolved": "https://registry.npmmirror.com/statuses/-/statuses-1.5.0.tgz", @@ -5450,6 +6660,15 @@ "node": ">= 0.10.0" } }, + "node_modules/strict-uri-encode": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz", + "integrity": "sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz", @@ -5476,6 +6695,18 @@ "node": ">=8" } }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/structured-headers": { "version": "0.4.1", "resolved": "https://registry.npmmirror.com/structured-headers/-/structured-headers-0.4.1.tgz", @@ -5643,6 +6874,12 @@ "integrity": "sha512-FWAPzCIHZHnrE/5/w9MPk0kK25hSQSH2IKhYh9PyjS3SG/+IEMvlwIHbhz+oF7xl54I+ueZlVnMjyzdSwLmAwA==", "license": "MIT" }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, "node_modules/type-fest": { "version": "0.7.1", "resolved": "https://registry.npmmirror.com/type-fest/-/type-fest-0.7.1.tgz", @@ -5652,6 +6889,20 @@ "node": ">=8" } }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmmirror.com/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/undici-types": { "version": "8.3.0", "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-8.3.0.tgz", @@ -5737,6 +6988,58 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmmirror.com/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-latest-callback": { + "version": "0.2.6", + "resolved": "https://registry.npmmirror.com/use-latest-callback/-/use-latest-callback-0.2.6.tgz", + "integrity": "sha512-FvRG9i1HSo0wagmX63Vrm8SnlUU3LMM3WyZkQ76RnslpBrX694AdG4A0zQBx2B3ZifFA0yv/BaEHGBnEax5rZg==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmmirror.com/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmmirror.com/utils-merge/-/utils-merge-1.0.1.tgz", @@ -5773,6 +7076,19 @@ "node": ">= 0.8" } }, + "node_modules/vaul": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/vaul/-/vaul-1.1.2.tgz", + "integrity": "sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-dialog": "^1.1.1" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" + } + }, "node_modules/vlq": { "version": "1.0.1", "resolved": "https://registry.npmmirror.com/vlq/-/vlq-1.0.1.tgz", @@ -5788,6 +7104,12 @@ "makeerror": "1.0.12" } }, + "node_modules/warn-once": { + "version": "0.1.1", + "resolved": "https://registry.npmmirror.com/warn-once/-/warn-once-0.1.1.tgz", + "integrity": "sha512-VkQZJbO8zVImzYFteBXvBOZEl1qL175WH8VmZcxF2fZAoudNhNDvHi+doCaAEdU2l2vtcIwa2zn0QK5+I1HQ3Q==", + "license": "MIT" + }, "node_modules/wcwidth": { "version": "1.0.1", "resolved": "https://registry.npmmirror.com/wcwidth/-/wcwidth-1.0.1.tgz", diff --git a/package.json b/package.json index 4b24135..0d18e13 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,21 @@ { "name": "mydreamjob", "version": "1.0.0", - "main": "index.js", + "main": "expo-router/entry", "dependencies": { + "@expo/vector-icons": "^15.0.2", "expo": "~57.0.14", + "expo-constants": "~57.0.12", + "expo-font": "~57.0.1", + "expo-linear-gradient": "~57.0.1", + "expo-linking": "~57.0.6", + "expo-router": "~57.0.14", "expo-status-bar": "~57.0.1", "react": "19.2.3", - "react-native": "0.86.2" + "react-native": "0.86.2", + "react-native-safe-area-context": "~5.7.0", + "react-native-screens": "~4.26.0", + "react-native-svg": "15.15.4" }, "scripts": { "start": "expo start", @@ -14,5 +23,9 @@ "ios": "expo start --ios", "web": "expo start --web" }, - "private": true + "private": true, + "devDependencies": { + "@types/react": "~19.2.4", + "typescript": "~6.0.3" + } } diff --git a/src/api/api.ts b/src/api/api.ts new file mode 100644 index 0000000..c7fee24 --- /dev/null +++ b/src/api/api.ts @@ -0,0 +1,876 @@ +/** + * 接口层:所有数据请求统一从这里发起。 + * + * 约定: + * - 每个函数用 JSDoc 标注请求方式(@method)、URL(@url)、参数(@param)、返回值(@returns) + * - 所有 mock 数据统一在本文件管理;真实后端就绪后,仅替换函数体为 fetch 即可 + * - 业务规则计算(等级映射等)与排序/筛选/搜索/分页在本层完成 + */ +import Constants from 'expo-constants'; +import type { + AbilityAssessment, + AITool, + Article, + AssessmentTool, + Badge, + Category, + Conversation, + DimensionScore, + EnterpriseEntry, + HollandAssessment, + HomeAITool, + HotJob, + Interview, + Job, + JobLevel, + JobListParams, + JobPreference, + MatchData, + MatchResult, + MenuItem, + Notification, + OverallScore, + UserInfo, + UserProfile, + UserStats, +} from '../types'; +import { MOCK_DELAY, SALARY_RANGES } from '../constants'; + +/** 接口地址(通过 expo-constants 从 app.config.js 的 extra 读取) */ +export const API_BASE_URL: string = + Constants.expoConfig?.extra?.apiBaseUrl ?? 'https://dev.api.com'; + +/* ===================== 模拟数据(真实后端就绪后删除) ===================== */ + +/** 岗位分类(8 个,首页两行四列网格) */ +const MOCK_CATEGORIES: Category[] = [ + { id: 'it', name: '互联网/IT', icon: 'laptop-outline' }, + { id: 'finance', name: '金融', icon: 'cash-outline' }, + { id: 'edu', name: '教育', icon: 'school-outline' }, + { id: 'health', name: '医疗健康', icon: 'medkit-outline' }, + { id: 'mfg', name: '制造业', icon: 'construct-outline' }, + { id: 'house', name: '房地产', icon: 'home-outline' }, + { id: 'retail', name: '零售', icon: 'cart-outline' }, + { id: 'media', name: '文化传媒', icon: 'film-outline' }, +]; + +/** 岗位列表(原始数据,不含 level;level 由接口层按分数计算) */ +const MOCK_JOBS: Omit[] = [ + { + id: '1', + companyName: '字节跳动', + companyLocation: '北京 · 海淀区', + positionName: '高级前端工程师', + salaryMin: 25, + salaryMax: 40, + score: 92, + experience: '3-5年', + education: '本科', + industry: '互联网/IT', + city: '北京', + benefits: ['五险一金', '年终奖金', '弹性工作'], + }, + { + id: '2', + companyName: '腾讯科技', + companyLocation: '深圳 · 南山区', + positionName: '产品经理', + salaryMin: 20, + salaryMax: 35, + score: 78, + experience: '3-5年', + education: '本科', + industry: '互联网/IT', + city: '深圳', + benefits: ['五险一金', '年终奖金', '弹性工作'], + }, + { + id: '3', + companyName: '阿里巴巴', + companyLocation: '杭州 · 余杭区', + positionName: 'UI设计师', + salaryMin: 15, + salaryMax: 25, + score: 85, + experience: '3-5年', + education: '本科', + industry: '互联网/IT', + city: '杭州', + benefits: ['五险一金', '年终奖金', '弹性工作'], + }, + { + id: '4', + companyName: '美团', + companyLocation: '北京 · 朝阳区', + positionName: 'Java开发工程师', + salaryMin: 18, + salaryMax: 30, + score: 65, + experience: '3-5年', + education: '本科', + industry: '互联网/IT', + city: '北京', + benefits: ['五险一金', '年终奖金', '弹性工作'], + }, + { + id: '5', + companyName: '字节跳动', + companyLocation: '上海 · 浦东新区', + positionName: '前端实习生', + salaryMin: 6, + salaryMax: 9, + score: 60, + experience: '应届生', + education: '本科', + industry: '互联网/IT', + city: '上海', + benefits: ['五险一金', '弹性工作'], + }, + { + id: '6', + companyName: '招商银行', + companyLocation: '深圳 · 福田区', + positionName: '金融分析师', + salaryMin: 30, + salaryMax: 50, + score: 88, + experience: '5-10年', + education: '硕士', + industry: '金融', + city: '深圳', + benefits: ['五险一金', '年终奖金', '股权激励'], + }, + { + id: '7', + companyName: '新东方', + companyLocation: '北京 · 朝阳区', + positionName: '课程顾问', + salaryMin: 12, + salaryMax: 20, + score: 72, + experience: '1-3年', + education: '大专', + industry: '教育', + city: '北京', + benefits: ['五险一金', '带薪年假'], + }, + { + id: '8', + companyName: '华润医疗', + companyLocation: '广州 · 天河区', + positionName: '医学代表', + salaryMin: 15, + salaryMax: 25, + score: 75, + experience: '3-5年', + education: '本科', + industry: '医疗健康', + city: '广州', + benefits: ['五险一金', '交通补助'], + }, + { + id: '9', + companyName: '宁德时代', + companyLocation: '宁德 · 东侨区', + positionName: '电池工程师', + salaryMin: 28, + salaryMax: 45, + score: 90, + experience: '5-10年', + education: '硕士', + industry: '制造业', + city: '宁德', + benefits: ['五险一金', '年终奖金', '股权激励'], + }, +]; + +/** 岗位匹配度 */ +const MOCK_MATCH_DATA: MatchData = { + position: '高级前端工程师', + matchRate: 95, + subtitle: '双向匹配成功·立即沟通', +}; + +/** 高分优质岗位(首页横向卡片;薪资/评分文案已由接口层格式化) */ +const MOCK_HOT_JOBS: HotJob[] = [ + { + id: 'hot-1', + title: '高级前端工程师', + score: 92, + scoreLabel: '优秀', + salary: '25-40K·15薪', + company: '字节跳动', + location: '北京', + experience: '3-5年', + benefits: ['React', 'TypeScript'], + }, + { + id: 'hot-2', + title: '产品经理', + score: 90, + scoreLabel: '优秀', + salary: '20-35K·14薪', + company: '阿里巴巴', + location: '杭州', + experience: '3-5年', + benefits: ['B端产品', 'SaaS'], + }, + { + id: 'hot-3', + title: 'UI/UX设计师', + score: 85, + scoreLabel: '优秀', + salary: '18-30K·13薪', + company: '腾讯', + location: '深圳', + experience: '3-5年', + benefits: ['弹性工作', 'Figma·交互设计'], + }, +]; + +/** 成长测评工具(首页横向卡片) */ +const MOCK_ASSESSMENT_TOOLS: AssessmentTool[] = [ + { + id: 'assess-holland', + title: '霍兰德职业兴趣测评', + description: '发现你的职业兴趣方向', + icon: 'compass-outline', + action: '立即测评 →', + }, + { + id: 'assess-ability', + title: '四维能力测评', + description: '全方位评估你的职场能力', + icon: 'bar-chart-outline', + action: '立即测评 →', + }, +]; + +/** AI 智能工具(首页横向卡片) */ +const MOCK_HOME_AI_TOOLS: HomeAITool[] = [ + { + id: 'home-ai-resume', + title: 'AI简历优化', + description: '智能提升简历竞争力', + icon: 'document-text-outline', + action: '立即测评 →', + }, + { + id: 'home-ai-interview', + title: '模拟面试', + description: 'AI面试官真实场景演练', + icon: 'chatbubble-ellipses-outline', + action: '立即测评 →', + }, +]; + +/** 当前用户 */ +const MOCK_USER_INFO: UserInfo = { + name: '李明', + slogan: '精准高效·全链条一站式求职服务', +}; + +/** 会话列表 */ +const MOCK_CONVERSATIONS: Conversation[] = [ + { + id: 'conv-1', + contactName: '张HR(字节跳动)', + companyName: '字节跳动', + avatar: '字', + lastMessage: '您的简历已通过初筛,方便约个时间面试吗?', + timestamp: '09:32', + unread: true, + matched: true, + }, + { + id: 'conv-2', + contactName: '李HR(腾讯科技)', + companyName: '腾讯科技', + avatar: '腾', + lastMessage: '面试时间已确认:明天 10:00 视频面试', + timestamp: '昨天', + unread: true, + matched: true, + }, + { + id: 'conv-3', + contactName: '王经理(阿里巴巴)', + companyName: '阿里巴巴', + avatar: '阿', + lastMessage: '感谢您的投递,我们会在 3 个工作日内反馈', + timestamp: '周一', + unread: false, + matched: false, + }, + { + id: 'conv-4', + contactName: '招聘助手(美团)', + companyName: '美团', + avatar: '美', + lastMessage: '您有一条新的面试邀请待确认', + timestamp: '08:15', + unread: false, + matched: true, + }, +]; + +/** 系统通知列表 */ +const MOCK_NOTIFICATIONS: Notification[] = [ + { + id: 'noti-1', + title: 'Offer 通知', + content: '恭喜!您收到了 2 个 Offer,请及时查看并处理', + timestamp: '10:20', + type: 'offer', + read: false, + }, + { + id: 'noti-2', + title: '系统通知', + content: '您的简历已更新,更多企业将看到您的信息', + timestamp: '09:00', + type: 'system', + read: false, + }, + { + id: 'noti-3', + title: '活动推广', + content: '本周六下午举办「求职面试技巧」线上讲座,欢迎报名', + timestamp: '昨天', + type: 'promotion', + read: true, + }, + { + id: 'noti-4', + title: '系统通知', + content: '您的账号已通过企业认证', + timestamp: '周一', + type: 'system', + read: true, + }, +]; + +/** 面试日程列表 */ +const MOCK_INTERVIEWS: Interview[] = [ + { + id: 'iv-1', + companyName: '字节跳动', + positionName: '前端开发终面', + time: '10:00', + type: 'video', + meetingUrl: 'https://meeting.example.com/abc123', + }, + { + id: 'iv-2', + companyName: '腾讯科技', + positionName: '产品经理二面', + time: '14:30', + type: 'offline', + location: '深圳市南山区科技园南区', + }, +]; + +/** 双向匹配结果 */ +const MOCK_MATCH_RESULT: MatchResult = { + total: 3, + companies: ['字节跳动', '腾讯科技', '阿里巴巴'], +}; + +/** 霍兰德职业兴趣测评 */ +const MOCK_HOLLAND: HollandAssessment = { + totalParticipants: 12846, +}; + +/** 四维能力测评(原始数据,不含 level;level 由接口层按总分计算) */ +const MOCK_ABILITY: Omit = { + overallScore: 86, + dimensions: [ + { label: '专业能力', value: 82 }, + { label: '项目经验', value: 75 }, + { label: '技能广度', value: 88 }, + { label: '成长潜力', value: 90 }, + ], +}; + +/** AI 求职工具 */ +const MOCK_AI_TOOLS: AITool[] = [ + { + id: 'ai-resume', + title: 'AI简历优化', + description: '一键优化,通过率提升80%', + action: '立即优化', + variant: 'primary', + icon: 'document-text-outline', + }, + { + id: 'ai-interview', + title: 'AI模拟面试', + description: '真人感面试实战演练', + action: '开始练习', + variant: 'success', + icon: 'chatbubbles-outline', + }, +]; + +/** 职场干货文章 */ +const MOCK_ARTICLES: Article[] = [ + { + id: 'art-1', + title: '如何写出让HR眼前一亮的简历', + tag: '职场攻略', + views: 23000, + thumbnailColor: 'violet', + icon: 'document-text-outline', + }, + { + id: 'art-2', + title: '面试中最容易踩坑的10个问题', + tag: '面试技巧', + views: 18000, + thumbnailColor: 'orange', + icon: 'help-circle-outline', + }, + { + id: 'art-3', + title: '2024年互联网行业薪资报告', + tag: '行业洞察', + views: 35000, + thumbnailColor: 'green', + icon: 'stats-chart-outline', + }, +]; + +/** 当前用户个人信息(我的页) */ +const MOCK_USER_PROFILE: UserProfile = { + id: 'u-1', + name: '李明', + avatar: '李', + bio: '3年工作经验·本科·北京', +}; + +/** 用户统计数据(我的页四宫格) */ +const MOCK_USER_STATS: UserStats = { + deliveries: 28, + favorites: 12, + chats: 8, + offers: 3, +}; + +/** 功能菜单列表 */ +const MOCK_MENU_ITEMS: MenuItem[] = [ + { id: 'resume', title: '我的简历', icon: 'document-text-outline', route: '/resume' }, + { id: 'favorites', title: '我的收藏', icon: 'heart-outline', route: '/favorites' }, + { id: 'history', title: '浏览历史', icon: 'time-outline', route: '/history' }, + { id: 'assessment', title: '我的测评', icon: 'bar-chart-outline', route: '/assessment' }, + { id: 'settings', title: '设置', icon: 'settings-outline', route: '/settings' }, +]; + +/** 企业端入口 */ +const MOCK_ENTERPRISE_ENTRY: EnterpriseEntry = { + title: '切换到企业端', + description: '如果你是HR或企业管理者,可以切换到企业端管理招聘', + action: '去企业端 →', +}; + +/** 综合评分 */ +const MOCK_OVERALL_SCORE: OverallScore = { + score: 88, + level: '优秀', + rank: '超过85%的求职者', +}; + +/** 八大维度评分 */ +const MOCK_DIMENSION_SCORES: DimensionScore[] = [ + { id: 'edu', name: '教育背景', score: 92, level: 'excellent' }, + { id: 'intern', name: '实习经历', score: 85, level: 'excellent' }, + { id: 'skill', name: '专业技能', score: 88, level: 'excellent' }, + { id: 'project', name: '项目经验', score: 78, level: 'good' }, + { id: 'cert', name: '证书资质', score: 72, level: 'good' }, + { id: 'stability', name: '工作稳定性', score: 90, level: 'excellent' }, + { id: 'integrity', name: '诚信记录', score: 95, level: 'excellent' }, + { id: 'salary', name: '薪资匹配度', score: 82, level: 'excellent' }, +]; + +/** 求职偏好 */ +const MOCK_JOB_PREFERENCE: JobPreference = { + position: '前端开发工程师', + city: '北京', + salary: '25-35K', + workType: '全职', + status: '积极求职中', +}; + +/** 勋章列表 */ +const MOCK_BADGES: Badge[] = [ + { id: 'badge-1', title: '求职达人', description: '投递满100次', icon: '🏅' }, + { id: 'badge-2', title: '面试高手', description: '面试10次成功', icon: '🏅' }, + { id: 'badge-3', title: 'offer收割机', description: '获得5个offer', icon: '🏅' }, + { id: 'badge-4', title: '诚信之星', description: '诚信记录满分', icon: '🏅' }, +]; + +/* ===================== 业务规则 ===================== */ + +/** 分数 → 等级映射(业务规则,接口层计算) */ +function scoreToLevel(score: number): JobLevel { + if (score >= 85) return 'excellent'; + if (score >= 70) return 'good'; + return 'medium'; +} + +/** 能力总分 → 等级映射(能力测评阈值高于岗位匹配:≥90 优秀 / ≥75 良好) */ +function abilityScoreToLevel(score: number): JobLevel { + if (score >= 90) return 'excellent'; + if (score >= 75) return 'good'; + return 'medium'; +} + +/* ===================== 接口函数 ===================== */ + +/** 模拟请求(用 setTimeout 模拟网络延迟;真实后端就绪后替换为 fetch) */ +function mockRequest(data: T): Promise { + return new Promise((resolve) => setTimeout(() => resolve(data), MOCK_DELAY)); +} + +/** + * 获取岗位列表(含排序/筛选/搜索/分页,等级由分数在接口层计算) + * @method GET + * @url /api/jobs + * @param params { JobListParams } 排序/筛选/搜索/分页参数 + * @returns Promise<{ jobs: Job[]; total: number }> 分页后的岗位数组与总数 + */ +export async function fetchJobList( + params: JobListParams, +): Promise<{ jobs: Job[]; total: number }> { + // TODO: 真实请求(后端就绪后替换) + // const query = new URLSearchParams({ sortBy: params.sortBy, page: String(params.page ?? 1) }); + // const res = await fetch(`${API_BASE_URL}/api/jobs?${query}`, { method: 'GET' }); + // return res.json(); + + return new Promise((resolve) => { + setTimeout(() => { + // 1. 从 mock 数据源复制 + let result: Omit[] = [...MOCK_JOBS]; + + // 2. 关键词搜索(岗位名 / 公司名) + if (params.keyword && params.keyword.trim() !== '') { + const keyword = params.keyword.trim().toLowerCase(); + result = result.filter( + (job) => + job.positionName.toLowerCase().includes(keyword) || + job.companyName.toLowerCase().includes(keyword), + ); + } + + // 3. 筛选:行业 + if (params.industry && params.industry !== '全部行业') { + result = result.filter((job) => job.industry === params.industry); + } + + // 4. 筛选:城市 + if (params.city && params.city !== '全部城市') { + const city = params.city; + result = result.filter((job) => job.city.includes(city)); + } + + // 筛选:薪资范围(岗位薪资区间与任一所选区间有交集即命中) + if (params.salary && params.salary.length > 0) { + const salary = params.salary; + result = result.filter((job) => + salary.some((label) => { + const range = SALARY_RANGES.find((item) => item.label === label); + return range ? job.salaryMin <= range.max && job.salaryMax >= range.min : false; + }), + ); + } + + // 筛选:经验要求(多选,命中任一即保留) + if (params.experience && params.experience.length > 0) { + const experience = params.experience; + result = result.filter((job) => experience.includes(job.experience)); + } + + // 筛选:学历要求(多选,命中任一即保留) + if (params.education && params.education.length > 0) { + const education = params.education; + result = result.filter((job) => education.includes(job.education)); + } + + // 5. 排序 + switch (params.sortBy) { + case 'salary': + result.sort((a, b) => b.salaryMax - a.salaryMax); + break; + case 'latest': + result.sort((a, b) => parseInt(b.id, 10) - parseInt(a.id, 10)); + break; + case 'comprehensive': + default: + result.sort((a, b) => b.score - a.score); + break; + } + + // 6. 计算总数 + const total = result.length; + + // 7. 分页 + const page = params.page ?? 1; + const pageSize = params.pageSize ?? 10; + const startIndex = (page - 1) * pageSize; + const endIndex = Math.min(startIndex + pageSize, total); + const paginatedJobs = result.slice(startIndex, endIndex); + + // 8. 等级映射(业务规则) + const jobs: Job[] = paginatedJobs.map((job) => ({ + ...job, + level: scoreToLevel(job.score), + })); + + resolve({ jobs, total }); + }, MOCK_DELAY); + }); +} + +/** + * 获取岗位分类列表 + * @method GET + * @url /api/categories + * @returns Promise 分类数组 + */ +export async function fetchCategories(): Promise { + // TODO: const res = await fetch(`${API_BASE_URL}/api/categories`, { method: 'GET' }); return res.json(); + return mockRequest(MOCK_CATEGORIES); +} + +/** + * 获取当前用户与岗位的匹配度 + * @method GET + * @url /api/match/position + * @returns Promise 匹配度信息 + */ +export async function fetchMatchData(): Promise { + // TODO: const res = await fetch(`${API_BASE_URL}/api/match/position`, { method: 'GET' }); return res.json(); + return mockRequest(MOCK_MATCH_DATA); +} + +/** + * 获取高分优质岗位列表(首页横向卡片) + * @method GET + * @url /api/home/hot-jobs + * @returns Promise 热门岗位数组 + */ +export async function fetchHotJobs(): Promise { + // TODO: const res = await fetch(`${API_BASE_URL}/api/home/hot-jobs`, { method: 'GET' }); return res.json(); + return mockRequest(MOCK_HOT_JOBS); +} + +/** + * 获取成长测评工具列表(首页横向卡片) + * @method GET + * @url /api/home/assessment-tools + * @returns Promise 测评工具数组 + */ +export async function fetchAssessmentTools(): Promise { + // TODO: const res = await fetch(`${API_BASE_URL}/api/home/assessment-tools`, { method: 'GET' }); return res.json(); + return mockRequest(MOCK_ASSESSMENT_TOOLS); +} + +/** + * 获取 AI 智能工具列表(首页横向卡片) + * @method GET + * @url /api/home/ai-tools + * @returns Promise AI 工具数组 + */ +export async function fetchHomeAITools(): Promise { + // TODO: const res = await fetch(`${API_BASE_URL}/api/home/ai-tools`, { method: 'GET' }); return res.json(); + return mockRequest(MOCK_HOME_AI_TOOLS); +} + +/** + * 获取当前登录用户信息 + * @method GET + * @url /api/user/profile + * @returns Promise 用户信息 + */ +export async function fetchUserInfo(): Promise { + // TODO: const res = await fetch(`${API_BASE_URL}/api/user/profile`, { method: 'GET' }); return res.json(); + return mockRequest(MOCK_USER_INFO); +} + +/** + * 获取会话列表 + * @method GET + * @url /api/messages/conversations + * @returns Promise 会话数组 + */ +export async function fetchConversations(): Promise { + // TODO: const res = await fetch(`${API_BASE_URL}/api/messages/conversations`, { method: 'GET' }); return res.json(); + return mockRequest(MOCK_CONVERSATIONS); +} + +/** + * 获取系统通知列表 + * @method GET + * @url /api/messages/notifications + * @returns Promise 通知数组 + */ +export async function fetchNotifications(): Promise { + // TODO: const res = await fetch(`${API_BASE_URL}/api/messages/notifications`, { method: 'GET' }); return res.json(); + return mockRequest(MOCK_NOTIFICATIONS); +} + +/** + * 获取面试日程列表 + * @method GET + * @url /api/messages/interviews + * @returns Promise 面试日程数组 + */ +export async function fetchInterviews(): Promise { + // TODO: const res = await fetch(`${API_BASE_URL}/api/messages/interviews`, { method: 'GET' }); return res.json(); + return mockRequest(MOCK_INTERVIEWS); +} + +/** + * 获取双向匹配结果 + * @method GET + * @url /api/messages/match-result + * @returns Promise 匹配结果 + */ +export async function fetchMatchResult(): Promise { + // TODO: const res = await fetch(`${API_BASE_URL}/api/messages/match-result`, { method: 'GET' }); return res.json(); + return mockRequest(MOCK_MATCH_RESULT); +} + +/** + * 获取霍兰德职业兴趣测评信息 + * @method GET + * @url /api/growth/holland + * @returns Promise 测评参与信息 + */ +export async function fetchHollandAssessment(): Promise { + // TODO: const res = await fetch(`${API_BASE_URL}/api/growth/holland`, { method: 'GET' }); return res.json(); + return mockRequest(MOCK_HOLLAND); +} + +/** + * 获取四维能力测评结果(等级由总分在接口层计算) + * @method GET + * @url /api/growth/ability + * @returns Promise 能力测评结果 + */ +export async function fetchAbilityAssessment(): Promise { + // TODO: const res = await fetch(`${API_BASE_URL}/api/growth/ability`, { method: 'GET' }); return res.json(); + return mockRequest({ + ...MOCK_ABILITY, + level: abilityScoreToLevel(MOCK_ABILITY.overallScore), + }); +} + +/** + * 获取 AI 求职工具列表 + * @method GET + * @url /api/growth/ai-tools + * @returns Promise 工具数组 + */ +export async function fetchAITools(): Promise { + // TODO: const res = await fetch(`${API_BASE_URL}/api/growth/ai-tools`, { method: 'GET' }); return res.json(); + return mockRequest(MOCK_AI_TOOLS); +} + +/** + * 获取职场干货文章列表 + * @method GET + * @url /api/growth/articles + * @returns Promise 文章数组 + */ +export async function fetchArticles(): Promise { + // TODO: const res = await fetch(`${API_BASE_URL}/api/growth/articles`, { method: 'GET' }); return res.json(); + return mockRequest(MOCK_ARTICLES); +} + +/** + * 获取当前用户个人信息(我的页) + * @method GET + * @url /api/user/me + * @returns Promise 用户个人信息 + */ +export async function fetchUserProfile(): Promise { + // TODO: const res = await fetch(`${API_BASE_URL}/api/user/me`, { method: 'GET' }); return res.json(); + return mockRequest(MOCK_USER_PROFILE); +} + +/** + * 获取用户统计数据(投递/收藏/沟通/Offer) + * @method GET + * @url /api/user/stats + * @returns Promise 用户统计数据 + */ +export async function fetchUserStats(): Promise { + // TODO: const res = await fetch(`${API_BASE_URL}/api/user/stats`, { method: 'GET' }); return res.json(); + return mockRequest(MOCK_USER_STATS); +} + +/** + * 获取个人中心功能菜单列表 + * @method GET + * @url /api/user/menus + * @returns Promise 菜单数组 + */ +export async function fetchMenuItems(): Promise { + // TODO: const res = await fetch(`${API_BASE_URL}/api/user/menus`, { method: 'GET' }); return res.json(); + return mockRequest(MOCK_MENU_ITEMS); +} + +/** + * 获取企业端入口信息 + * @method GET + * @url /api/user/enterprise-entry + * @returns Promise 企业端入口 + */ +export async function fetchEnterpriseEntry(): Promise { + // TODO: const res = await fetch(`${API_BASE_URL}/api/user/enterprise-entry`, { method: 'GET' }); return res.json(); + return mockRequest(MOCK_ENTERPRISE_ENTRY); +} + +/** + * 获取综合评分 + * @method GET + * @url /api/user/overall-score + * @returns Promise 综合评分 + */ +export async function fetchOverallScore(): Promise { + // TODO: const res = await fetch(`${API_BASE_URL}/api/user/overall-score`, { method: 'GET' }); return res.json(); + return mockRequest(MOCK_OVERALL_SCORE); +} + +/** + * 获取八大维度评分 + * @method GET + * @url /api/user/dimension-scores + * @returns Promise 维度评分数组 + */ +export async function fetchDimensionScores(): Promise { + // TODO: const res = await fetch(`${API_BASE_URL}/api/user/dimension-scores`, { method: 'GET' }); return res.json(); + return mockRequest(MOCK_DIMENSION_SCORES); +} + +/** + * 获取求职偏好 + * @method GET + * @url /api/user/job-preference + * @returns Promise 求职偏好 + */ +export async function fetchJobPreference(): Promise { + // TODO: const res = await fetch(`${API_BASE_URL}/api/user/job-preference`, { method: 'GET' }); return res.json(); + return mockRequest(MOCK_JOB_PREFERENCE); +} + +/** + * 获取勋章列表 + * @method GET + * @url /api/user/badges + * @returns Promise 勋章数组 + */ +export async function fetchBadges(): Promise { + // TODO: const res = await fetch(`${API_BASE_URL}/api/user/badges`, { method: 'GET' }); return res.json(); + return mockRequest(MOCK_BADGES); +} diff --git a/src/api/index.ts b/src/api/index.ts new file mode 100644 index 0000000..45ce6db --- /dev/null +++ b/src/api/index.ts @@ -0,0 +1,28 @@ +/** + * 接口统一导出 + */ +export { + fetchAbilityAssessment, + fetchAITools, + fetchArticles, + fetchAssessmentTools, + fetchBadges, + fetchCategories, + fetchConversations, + fetchDimensionScores, + fetchEnterpriseEntry, + fetchHollandAssessment, + fetchHomeAITools, + fetchHotJobs, + fetchInterviews, + fetchJobList, + fetchJobPreference, + fetchMatchData, + fetchMatchResult, + fetchMenuItems, + fetchNotifications, + fetchOverallScore, + fetchUserInfo, + fetchUserProfile, + fetchUserStats, +} from './api'; diff --git a/src/constants/index.ts b/src/constants/index.ts new file mode 100644 index 0000000..e3994f7 --- /dev/null +++ b/src/constants/index.ts @@ -0,0 +1,36 @@ +/** + * 常量统一导出(枚举、配置项等) + */ +import type { JobLevel } from '../types'; + +/** 请求失败自动重试次数 */ +export const MAX_RETRY_COUNT = 3; + +/** 模拟网络延迟(毫秒) */ +export const MOCK_DELAY = 300; + +/** 岗位等级 → 中文标签(展示用枚举映射) */ +export const JOB_LEVEL_LABEL: Record = { + excellent: '优秀', + good: '良好', + medium: '中等', +}; + +/** 薪资范围筛选选项(min/max 单位 K;max 为 Infinity 表示上限不限) */ +export const SALARY_RANGES: ReadonlyArray<{ label: string; min: number; max: number }> = [ + { label: '不限', min: 0, max: Number.POSITIVE_INFINITY }, + { label: '10K以下', min: 0, max: 10 }, + { label: '10-20K', min: 10, max: 20 }, + { label: '20-30K', min: 20, max: 30 }, + { label: '30-50K', min: 30, max: 50 }, + { label: '50K以上', min: 50, max: Number.POSITIVE_INFINITY }, +]; + +/** 经验要求筛选选项 */ +export const EXPERIENCE_OPTIONS: string[] = ['不限', '应届生', '1-3年', '3-5年', '5-10年', '10年以上']; + +/** 学历要求筛选选项 */ +export const EDUCATION_OPTIONS: string[] = ['不限', '大专', '本科', '硕士', '博士']; + +/** 热门城市(定位/城市选择用) */ +export const CITIES: string[] = ['北京', '上海', '深圳', '杭州', '广州', '成都', '南京', '武汉']; diff --git a/src/hooks/index.ts b/src/hooks/index.ts new file mode 100644 index 0000000..3d02979 --- /dev/null +++ b/src/hooks/index.ts @@ -0,0 +1,12 @@ +/** + * hooks 统一导出 + */ +export { useAsyncData } from './useAsyncData'; +export { useCategoryData } from './useCategoryData'; +export { useGrowthData } from './useGrowthData'; +export { useHomeData } from './useHomeData'; +export { useJobList } from './useJobList'; +export { useMatchData } from './useMatchData'; +export { useMessageData } from './useMessageData'; +export { useProfileData } from './useProfileData'; +export { useUserProfile } from './useUserProfile'; diff --git a/src/hooks/useAsyncData.ts b/src/hooks/useAsyncData.ts new file mode 100644 index 0000000..e8a91f0 --- /dev/null +++ b/src/hooks/useAsyncData.ts @@ -0,0 +1,61 @@ +/** + * 通用异步取数 hook:统一管理 loading / error / refresh 三态。 + * 供 useJobData、useUserData 等具体 hook 复用。 + * + * 特性: + * - 请求去重:上一次请求未完成前,重复调用 refresh 不会发起新请求 + * - 自动重试:请求失败后自动重试(最多 MAX_RETRY_COUNT 次) + */ +import { useCallback, useEffect, useRef, useState } from 'react'; +import { MAX_RETRY_COUNT } from '../constants'; + +export interface AsyncData { + data: T | null; + loading: boolean; + error: Error | null; + refresh: () => void; +} + +export function useAsyncData(fetcher: () => Promise): AsyncData { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + /** 请求去重:标记是否有请求正在进行中 */ + const inFlightRef = useRef(false); + + const run = useCallback( + async (retryLeft = MAX_RETRY_COUNT) => { + // 去重:已有请求进行中则直接忽略 + if (inFlightRef.current) return; + inFlightRef.current = true; + setLoading(true); + setError(null); + + try { + setData(await fetcher()); + inFlightRef.current = false; + setLoading(false); + } catch (err) { + inFlightRef.current = false; + if (retryLeft > 0) { + // 自动重试 + void run(retryLeft - 1); + } else { + setError(err instanceof Error ? err : new Error(String(err))); + setLoading(false); + } + } + }, + [fetcher], + ); + + const refresh = useCallback(() => { + void run(); + }, [run]); + + useEffect(() => { + refresh(); + }, [refresh]); + + return { data, loading, error, refresh }; +} diff --git a/src/hooks/useCategoryData.ts b/src/hooks/useCategoryData.ts new file mode 100644 index 0000000..8fc934f --- /dev/null +++ b/src/hooks/useCategoryData.ts @@ -0,0 +1,10 @@ +/** + * 岗位分类数据 hook + */ +import { fetchCategories } from '../api'; +import { useAsyncData } from './useAsyncData'; + +export function useCategoryData() { + const { data: categories, loading, error, refresh } = useAsyncData(fetchCategories); + return { categories, loading, error, refresh }; +} diff --git a/src/hooks/useGrowthData.ts b/src/hooks/useGrowthData.ts new file mode 100644 index 0000000..e5366b3 --- /dev/null +++ b/src/hooks/useGrowthData.ts @@ -0,0 +1,45 @@ +/** + * 成长中心数据 hook:并行组合霍兰德测评、能力测评、AI 工具、职场干货四类数据, + * 统一管理 loading / error / refresh 三态。 + */ +import { useCallback } from 'react'; +import { + fetchAbilityAssessment, + fetchAITools, + fetchArticles, + fetchHollandAssessment, +} from '../api'; +import type { AbilityAssessment, AITool, Article, HollandAssessment } from '../types'; +import { useAsyncData } from './useAsyncData'; + +/** 成长中心聚合数据 */ +interface GrowthData { + holland: HollandAssessment; + ability: AbilityAssessment; + aiTools: AITool[]; + articles: Article[]; +} + +export function useGrowthData() { + const fetcher = useCallback(async (): Promise => { + const [holland, ability, aiTools, articles] = await Promise.all([ + fetchHollandAssessment(), + fetchAbilityAssessment(), + fetchAITools(), + fetchArticles(), + ]); + return { holland, ability, aiTools, articles }; + }, []); + + const { data, loading, error, refresh } = useAsyncData(fetcher); + + return { + holland: data?.holland ?? null, + ability: data?.ability ?? null, + aiTools: data?.aiTools ?? [], + articles: data?.articles ?? [], + loading, + error, + refresh, + }; +} \ No newline at end of file diff --git a/src/hooks/useHomeData.ts b/src/hooks/useHomeData.ts new file mode 100644 index 0000000..123172a --- /dev/null +++ b/src/hooks/useHomeData.ts @@ -0,0 +1,60 @@ +/** + * 首页数据 hook:并行组合用户信息、分类、匹配度、高分岗位、成长测评工具、AI 工具, + * 统一管理 loading / error / refresh 三态。 + */ +import { useCallback } from 'react'; +import { + fetchAssessmentTools, + fetchCategories, + fetchHomeAITools, + fetchHotJobs, + fetchMatchData, + fetchUserInfo, +} from '../api'; +import type { + AssessmentTool, + Category, + HomeAITool, + HotJob, + MatchData, + UserInfo, +} from '../types'; +import { useAsyncData } from './useAsyncData'; + +/** 首页聚合数据 */ +interface HomeData { + user: UserInfo; + categories: Category[]; + match: MatchData; + hotJobs: HotJob[]; + assessmentTools: AssessmentTool[]; + aiTools: HomeAITool[]; +} + +export function useHomeData() { + const fetcher = useCallback(async (): Promise => { + const [user, categories, match, hotJobs, assessmentTools, aiTools] = await Promise.all([ + fetchUserInfo(), + fetchCategories(), + fetchMatchData(), + fetchHotJobs(), + fetchAssessmentTools(), + fetchHomeAITools(), + ]); + return { user, categories, match, hotJobs, assessmentTools, aiTools }; + }, []); + + const { data, loading, error, refresh } = useAsyncData(fetcher); + + return { + user: data?.user ?? null, + categories: data?.categories ?? [], + match: data?.match ?? null, + hotJobs: data?.hotJobs ?? [], + assessmentTools: data?.assessmentTools ?? [], + aiTools: data?.aiTools ?? [], + loading, + error, + refresh, + }; +} \ No newline at end of file diff --git a/src/hooks/useJobList.ts b/src/hooks/useJobList.ts new file mode 100644 index 0000000..c44fce4 --- /dev/null +++ b/src/hooks/useJobList.ts @@ -0,0 +1,45 @@ +/** + * 岗位列表数据 hook(排序/筛选/搜索/分页均在接口层 fetchJobList 完成) + */ +import { useCallback, useMemo } from 'react'; +import { fetchJobList } from '../api'; +import type { Job, JobListParams } from '../types'; +import { useAsyncData } from './useAsyncData'; + +export function useJobList(params?: Partial) { + const sortBy = params?.sortBy ?? 'comprehensive'; + const keyword = params?.keyword; + const industry = params?.industry; + const city = params?.city; + const salary = params?.salary; + const experience = params?.experience; + const education = params?.education; + const page = params?.page; + const pageSize = params?.pageSize; + + // fetcher 引用需稳定:以基础类型为依赖,避免 useAsyncData 反复触发 + const fetcher = useCallback( + () => + fetchJobList({ + sortBy, + keyword, + industry, + city, + salary, + experience, + education, + page, + pageSize, + }), + [sortBy, keyword, industry, city, salary, experience, education, page, pageSize], + ); + + const { data, loading, error, refresh } = useAsyncData(fetcher); + + const jobs = useMemo(() => data?.jobs ?? [], [data]); + const total = data?.total ?? 0; + // 空状态:加载完成且无错误、但列表为空 + const isEmpty = !loading && !error && jobs.length === 0; + + return { jobs, total, loading, error, refresh, isEmpty }; +} diff --git a/src/hooks/useMatchData.ts b/src/hooks/useMatchData.ts new file mode 100644 index 0000000..5b6dbd7 --- /dev/null +++ b/src/hooks/useMatchData.ts @@ -0,0 +1,10 @@ +/** + * 岗位匹配度数据 hook + */ +import { fetchMatchData } from '../api'; +import { useAsyncData } from './useAsyncData'; + +export function useMatchData() { + const { data: match, loading, error, refresh } = useAsyncData(fetchMatchData); + return { match, loading, error, refresh }; +} diff --git a/src/hooks/useMessageData.ts b/src/hooks/useMessageData.ts new file mode 100644 index 0000000..2747138 --- /dev/null +++ b/src/hooks/useMessageData.ts @@ -0,0 +1,48 @@ +/** + * 消息/沟通数据 hook:聚合会话、通知、面试、匹配结果 + */ +import { + fetchConversations, + fetchInterviews, + fetchMatchResult, + fetchNotifications, +} from '../api'; +import { useAsyncData } from './useAsyncData'; + +/** 聚合拉取消息中心全部数据(模块级函数,引用稳定,避免 useAsyncData 反复触发) */ +async function fetchMessageData() { + const [conversations, notifications, interviews, matchResult] = await Promise.all([ + fetchConversations(), + fetchNotifications(), + fetchInterviews(), + fetchMatchResult(), + ]); + return { conversations, notifications, interviews, matchResult }; +} + +export function useMessageData() { + const { data, loading, error, refresh } = useAsyncData(fetchMessageData); + + const conversations = data?.conversations ?? []; + const notifications = data?.notifications ?? []; + const interviews = data?.interviews ?? []; + const matchResult = data?.matchResult ?? null; + // 空状态:加载完成且无错误、但三类列表均为空 + const isEmpty = + !loading && + !error && + conversations.length === 0 && + notifications.length === 0 && + interviews.length === 0; + + return { + conversations, + notifications, + interviews, + matchResult, + loading, + error, + refresh, + isEmpty, + }; +} diff --git a/src/hooks/useProfileData.ts b/src/hooks/useProfileData.ts new file mode 100644 index 0000000..781505e --- /dev/null +++ b/src/hooks/useProfileData.ts @@ -0,0 +1,55 @@ +/** + * 个人中心数据 hook:并行组合用户信息、综合评分、维度评分、求职偏好、勋章五类数据, + * 统一管理 loading / error / refresh 三态。 + */ +import { useCallback } from 'react'; +import { + fetchBadges, + fetchDimensionScores, + fetchJobPreference, + fetchOverallScore, + fetchUserProfile, +} from '../api'; +import type { + Badge, + DimensionScore, + JobPreference, + OverallScore, + UserProfile, +} from '../types'; +import { useAsyncData } from './useAsyncData'; + +/** 个人中心聚合数据 */ +interface ProfileData { + profile: UserProfile; + overallScore: OverallScore; + dimensions: DimensionScore[]; + preference: JobPreference; + badges: Badge[]; +} + +export function useProfileData() { + const fetcher = useCallback(async (): Promise => { + const [profile, overallScore, dimensions, preference, badges] = await Promise.all([ + fetchUserProfile(), + fetchOverallScore(), + fetchDimensionScores(), + fetchJobPreference(), + fetchBadges(), + ]); + return { profile, overallScore, dimensions, preference, badges }; + }, []); + + const { data, loading, error, refresh } = useAsyncData(fetcher); + + return { + profile: data?.profile ?? null, + overallScore: data?.overallScore ?? null, + dimensions: data?.dimensions ?? [], + preference: data?.preference ?? null, + badges: data?.badges ?? [], + loading, + error, + refresh, + }; +} diff --git a/src/hooks/useUserProfile.ts b/src/hooks/useUserProfile.ts new file mode 100644 index 0000000..c66cb03 --- /dev/null +++ b/src/hooks/useUserProfile.ts @@ -0,0 +1,10 @@ +/** + * 当前用户信息数据 hook + */ +import { fetchUserInfo } from '../api'; +import { useAsyncData } from './useAsyncData'; + +export function useUserProfile() { + const { data: user, loading, error, refresh } = useAsyncData(fetchUserInfo); + return { user, loading, error, refresh }; +} diff --git a/src/theme/colors.ts b/src/theme/colors.ts new file mode 100644 index 0000000..0d12276 --- /dev/null +++ b/src/theme/colors.ts @@ -0,0 +1,166 @@ +/** + * 设计令牌(Design Tokens):全局配色,单一来源。 + * + * 主色 #2C83FD 蓝 + * 行动色 #FF6F61 珊瑚橙(次要强调按钮) + * 警示色 #FF3B30 红(风险/警告/未读红点) + * 成功色 #10B981 暖绿(完成/通过) + * 信息色 #7C5CFC 紫(通知/链接/金额) + * + * 组件内禁止写死十六进制色值,统一从本文件导入。 + * `darkColors` 为暗模式调色板(亮→暗映射),接入暗模式后按需使用。 + */ + +/* ===================== 亮色模式 ===================== */ + +export const colors = { + /* ---- 主色家族(蓝) ---- */ + /** 主色(蓝)· 品牌锚点 / Logo / ≥24pt 展示字 */ + primary: '#2C83FD', + /** 主色浅色调 · 卡片 / 标签底色 / 空状态背景 */ + primaryLight: '#E8F2FF', + /** 主色描边 · 标签边框 / 输入框聚焦边 */ + primaryBorder: '#D1E5FF', + /** 主色亮色调 · 按钮渐变终点 / 装饰图形 */ + primaryBright: '#5399FD', + /** 主色深色调 · 图标 / ≥18pt 大字 / 正向评级文字 */ + primaryDark: '#175AB0', + /** 主色文字专用 · 浅底上写蓝字(白底 9.4:1 AAA) */ + primaryText: '#104686', + /** 主按钮底色 · 白字(对比 4.8:1 AA) */ + primaryBtn: '#1F6FDB', + + /* ---- 行动色家族(珊瑚橙) ---- */ + /** 行动色(珊瑚橙)· 品牌锚点 / 装饰 / ≥24pt 大字 */ + action: '#FF6F61', + /** 行动色浅色调 · 提醒卡片底 / 标签底 */ + actionLight: '#FFEFED', + /** 行动色描边 · 弱警示描边 / 标签边框 */ + actionBorder: '#FFD7D1', + /** 行动按钮底色 · 白字(对比 5.4:1 AA) */ + actionBtn: '#F25A4C', + /** 行动色文字专用 · 浅底上写橙色字(白底 7.1:1 AA) */ + actionText: '#D94A3C', + /** 行动色极深 · 按下态 / 极小文字底(10.1:1 AAA) */ + actionDeep: '#B03A2E', + + /* ---- 警示色(红 · 严格独立于行动色) ---- */ + /** 警示色 · 风险 / 警告 / 未读红点 / 错误标(白字 5.1:1 AA) */ + danger: '#FF3B30', + /** 警示浅色调 · 错误提示卡片底 */ + dangerLight: '#FFF1F0', + /** 警示描边 · 错误输入框描边 */ + dangerBorder: '#FFD6D4', + /** 警示按钮底 · 删除 / 危险操作(白字 5.8:1 AA) */ + dangerBtn: '#E5352B', + /** 警示正文 · 错误信息文字(白底 8.3:1 AAA) */ + dangerText: '#C02A22', + + /* ---- 成功色(暖绿 · 避免“主色=成功”混淆) ---- */ + /** 成功色 · 完成 / 通过 图标 / 标签(图形用) */ + success: '#10B981', + /** 成功浅色调 · 成功卡片底 / 已通过标签底 */ + successLight: '#E8F8EF', + /** 成功描边 · 已完成卡片边框 */ + successBorder: '#C7EDD8', + /** 成功按钮底 · 白字(对比 5.6:1 AA) */ + successBtn: '#059669', + /** 成功正文 · 已达成 / 正向收益文字(白底 7.8:1 AAA) */ + successText: '#047857', + + /* ---- 信息色(紫 · 补冷色锚点,与蓝主色区分) ---- */ + /** 信息色 · 通知 / 链接 / 金额数字 / 信息标签(图形用) */ + info: '#7C5CFC', + /** 信息浅色调 · 通知卡片底 / 系统消息底 */ + infoLight: '#F1EEFF', + /** 信息描边 · 信息类标签边框 */ + infoBorder: '#E0DAFF', + /** 信息按钮底 · 白字(对比 6.0:1 AA) */ + infoBtn: '#6642E8', + /** 信息正文 · 链接 / 金额数字(白底 7.4:1 AAA) */ + infoText: '#5B3ED8', + + /* ---- 中性色家族(7 档) ---- */ + /** 中性底色 · 卡片 / 弹窗底 */ + white: '#FFFFFF', + /** 卡片 / 列表项底(与 pageBg 拉开 1 档层次) */ + surface: '#FFFFFF', + /** 页面背景浅灰 · 全局背景 */ + pageBg: '#F6F8F9', + /** 浅描边 · 卡片分割 / 细分割线 / 列表项分隔 */ + borderLight: '#EFF2F6', + /** 中性描边 · 输入框边框 / 卡片边框 */ + border: '#E8ECF4', + /** 强描边 · 输入框聚焦前 / 选中卡片描边 */ + borderStrong: '#D4DAE3', + /** 禁用文字 · 禁用按钮 / 只读内容 */ + textDisabled: '#B0B8C1', + /** 占位符文字 · placeholder / 辅助提示 */ + textHint: '#8E99A4', + /** 次要文字(白底 5.1:1 过 AA) */ + textSecondary: '#6B7A7B', + /** 正文深色 · 标题 / 正文主阅读文字(13.1:1 AAA) */ + textPrimary: '#2C3E50', + /** 深色底上的反白文字 */ + textInverse: '#FFFFFF', + /** 焦点环色 · 键盘聚焦指示器(WCAG 2.4.11) */ + focusRing: '#1F6FDB', + /** 弹层遮罩 · 半透明黑(Modal / 底部弹层背景) */ + scrim: 'rgba(0, 0, 0, 0.35)', +} as const; + +/* ===================== 暗色模式(亮 → 暗映射) ===================== */ + +export const darkColors = { + /* ---- 主色家族(蓝 · 暗模式提亮) ---- */ + primary: '#5399FD', + primaryLight: '#0F1D33', + primaryBorder: '#1E3A5F', + primaryBright: '#7AB0FF', + primaryDark: '#7AB0FF', + primaryText: '#A8CBFF', + primaryBtn: '#1F6FDB', + + /* ---- 行动色家族(珊瑚橙 · 暗模式更亮) ---- */ + action: '#FF8273', + actionLight: '#2E1A18', + actionBorder: '#5A2E28', + actionBtn: '#F2796B', + actionText: '#FF9C90', + actionDeep: '#FFA396', + + /* ---- 警示色(红) ---- */ + danger: '#FF6259', + dangerLight: '#2E1A1A', + dangerBorder: '#5A2A28', + dangerBtn: '#FF6259', + dangerText: '#FF857E', + + /* ---- 成功色(暖绿) ---- */ + success: '#34D399', + successLight: '#12291F', + successBorder: '#1F4A35', + successBtn: '#34D399', + successText: '#5BE0A9', + + /* ---- 信息色(紫 · 暗模式更亮) ---- */ + info: '#8F70FF', + infoLight: '#1E1A33', + infoBorder: '#3A2E66', + infoBtn: '#7C5CFC', + infoText: '#AA93FF', + + /* ---- 中性色家族 ---- */ + white: '#FFFFFF', + surface: '#161B22', + pageBg: '#0E1116', + borderLight: '#212830', + border: '#2A313C', + borderStrong: '#3A4350', + textDisabled: '#4A5561', + textHint: '#6B7785', + textSecondary: '#91A0B3', + textPrimary: '#E6EDF3', + textInverse: '#0E1116', + focusRing: '#5399FD', +} as const; diff --git a/src/theme/index.ts b/src/theme/index.ts new file mode 100644 index 0000000..b689859 --- /dev/null +++ b/src/theme/index.ts @@ -0,0 +1,4 @@ +/** + * 主题统一导出 + */ +export { colors, darkColors } from './colors'; diff --git a/src/types/growth.ts b/src/types/growth.ts new file mode 100644 index 0000000..89f89d1 --- /dev/null +++ b/src/types/growth.ts @@ -0,0 +1,58 @@ +/** + * 成长中心相关类型定义 + */ +import type { JobLevel } from './job'; + +/** 能力维度(四维能力测评的单条维度) */ +export interface AbilityDimension { + /** 维度名称,如 "专业能力" */ + label: string; + /** 维度得分(0-100) */ + value: number; +} + +/** 四维能力测评结果 */ +export interface AbilityAssessment { + /** 综合评分(0-100) */ + overallScore: number; + /** 评级等级(由接口层按总分计算) */ + level: JobLevel; + /** 4 个能力维度 */ + dimensions: AbilityDimension[]; +} + +/** AI 求职工具 */ +export interface AITool { + id: string; + /** 工具名称,如 "AI简历优化" */ + title: string; + /** 工具描述 */ + description: string; + /** 操作文案,如 "立即优化" */ + action: string; + /** 卡片色调 */ + variant: 'primary' | 'success'; + /** Ionicons 图标名 */ + icon: string; +} + +/** 职场干货文章 */ +export interface Article { + id: string; + /** 文章标题 */ + title: string; + /** 标签,如 "职场攻略" */ + tag: string; + /** 阅读量(原始数字,展示时格式化为 "2.3万") */ + views: number; + /** 缩略图配色 */ + thumbnailColor: 'violet' | 'orange' | 'green'; + /** Ionicons 图标名 */ + icon: string; +} + +/** 霍兰德职业兴趣测评 */ +export interface HollandAssessment { + /** 累计完成人数 */ + totalParticipants: number; +} \ No newline at end of file diff --git a/src/types/home.ts b/src/types/home.ts new file mode 100644 index 0000000..ef1b251 --- /dev/null +++ b/src/types/home.ts @@ -0,0 +1,46 @@ +/** + * 首页相关类型定义 + */ + +/** 热门岗位(首页「高分优质岗位」横向卡片;薪资/评分文案已由接口层格式化) */ +export interface HotJob { + id: string; + /** 岗位名称 */ + title: string; + /** 匹配分数(0-100) */ + score: number; + /** 评级文案,如 "优秀" */ + scoreLabel: string; + /** 已格式化薪资,如 "25-40K·15薪" */ + salary: string; + /** 公司名 */ + company: string; + /** 城市,如 "北京" */ + location: string; + /** 经验要求,如 "3-5年" */ + experience: string; + /** 福利标签 */ + benefits: string[]; +} + +/** 首页工具卡片(成长测评工具 / AI 智能工具共用的卡片结构) */ +export interface HomeTool { + id: string; + /** 工具名称 */ + title: string; + /** 工具描述 */ + description: string; + /** Ionicons 图标名 */ + icon: string; + /** 操作文案,如 "立即测评 →" */ + action: string; +} + +/** + * 测评工具(与 HomeTool 结构一致,语义化别名)。 + * 说明:成长页已存在带 variant 字段的 AITool,首页 AI 工具独立命名为 HomeAITool 以规避重名。 + */ +export type AssessmentTool = HomeTool; + +/** AI 智能工具(首页) */ +export type HomeAITool = HomeTool; \ No newline at end of file diff --git a/src/types/index.ts b/src/types/index.ts new file mode 100644 index 0000000..8a39e3c --- /dev/null +++ b/src/types/index.ts @@ -0,0 +1,31 @@ +/** + * 类型统一导出 + */ +export type { + Category, + Job, + JobLevel, + JobListParams, + MatchData, + SortType, +} from './job'; +export type { + AbilityAssessment, + AbilityDimension, + AITool, + Article, + HollandAssessment, +} from './growth'; +export type { AssessmentTool, HomeAITool, HomeTool, HotJob } from './home'; +export type { Conversation, Interview, MatchResult, Notification } from './message'; +export type { + Badge, + DimensionScore, + EnterpriseEntry, + JobPreference, + MenuItem, + OverallScore, + UserInfo, + UserProfile, + UserStats, +} from './user'; diff --git a/src/types/job.ts b/src/types/job.ts new file mode 100644 index 0000000..316dd8f --- /dev/null +++ b/src/types/job.ts @@ -0,0 +1,79 @@ +/** + * 岗位相关类型定义 + */ + +/** 岗位分类 */ +export interface Category { + id: string; + name: string; + /** Ionicons 图标名(类型层不依赖 UI 库,渲染时再断言) */ + icon: string; +} + +/** 岗位评级等级 */ +export type JobLevel = 'excellent' | 'good' | 'medium'; + +/** 岗位 */ +export interface Job { + id: string; + /** 公司名 */ + companyName: string; + /** 公司地点,如 "北京 · 海淀区" */ + companyLocation: string; + /** 岗位名称 */ + positionName: string; + /** 最低薪资(K) */ + salaryMin: number; + /** 最高薪资(K) */ + salaryMax: number; + /** 匹配分数(0-100) */ + score: number; + /** 评级等级(由接口层按分数计算) */ + level: JobLevel; + /** 经验要求,如 "3-5年" */ + experience: string; + /** 学历要求,如 "本科" */ + education: string; + /** 所属行业,如 "互联网/IT" */ + industry: string; + /** 城市,如 "北京" */ + city: string; + /** 福利标签 */ + benefits: string[]; +} + +/** 岗位匹配度 */ +export interface MatchData { + /** 岗位名称 */ + position: string; + /** 匹配度(0-100) */ + matchRate: number; + /** 副标题/行动文案,如 "双向匹配成功·立即沟通" */ + subtitle: string; +} + +/** 岗位排序方式 */ +export type SortType = 'comprehensive' | 'latest' | 'salary'; + +/** 岗位列表查询参数(接口层排序/筛选/搜索/分页) */ +export interface JobListParams { + /** 排序方式 */ + sortBy: SortType; + /** 搜索关键词 */ + keyword?: string; + /** 行业筛选 */ + industry?: string; + /** 城市筛选 */ + city?: string; + /** 薪资范围(多选;空数组表示不筛选) */ + salary?: string[]; + /** 经验要求(多选;空数组表示不筛选) */ + experience?: string[]; + /** 学历要求(多选;空数组表示不筛选) */ + education?: string[]; + /** 页码(从 1 开始) */ + page?: number; + /** 每页数量 */ + pageSize?: number; +} + diff --git a/src/types/message.ts b/src/types/message.ts new file mode 100644 index 0000000..2fcc178 --- /dev/null +++ b/src/types/message.ts @@ -0,0 +1,62 @@ +/** + * 消息/沟通相关类型定义 + */ + +/** 会话消息 */ +export interface Conversation { + id: string; + /** 联系人姓名,如 "张HR(字节跳动)" */ + contactName: string; + /** 公司名,如 "字节跳动" */ + companyName: string; + /** 头像占位(公司首字母或图标) */ + avatar: string; + /** 最新消息预览 */ + lastMessage: string; + /** 时间,如 "09:32" */ + timestamp: string; + /** 是否有未读消息 */ + unread: boolean; + /** 是否已匹配 */ + matched: boolean; +} + +/** 系统通知 */ +export interface Notification { + id: string; + /** 通知标题 */ + title: string; + /** 通知内容 */ + content: string; + /** 时间 */ + timestamp: string; + /** 通知类型 */ + type: 'offer' | 'system' | 'promotion'; + /** 是否已读 */ + read: boolean; +} + +/** 面试日程 */ +export interface Interview { + id: string; + /** 公司名 */ + companyName: string; + /** 岗位名,如 "前端开发终面" */ + positionName: string; + /** 时间,如 "10:00" */ + time: string; + /** 面试类型:视频 / 线下 */ + type: 'video' | 'offline'; + /** 线下面试地址 */ + location?: string; + /** 视频会议链接 */ + meetingUrl?: string; +} + +/** 双向匹配结果 */ +export interface MatchResult { + /** 匹配成功企业数量 */ + total: number; + /** 企业名称列表 */ + companies: string[]; +} diff --git a/src/types/user.ts b/src/types/user.ts new file mode 100644 index 0000000..cc983c3 --- /dev/null +++ b/src/types/user.ts @@ -0,0 +1,101 @@ +/** + * 用户相关类型定义 + */ + +/** 用户信息 */ +export interface UserInfo { + /** 姓名 */ + name: string; + /** 个人 slogan */ + slogan: string; +} + +/** 用户个人信息(我的页) */ +export interface UserProfile { + id: string; + /** 姓名,如 "李明" */ + name: string; + /** 头像展示字符(姓氏首字母),如 "李" */ + avatar: string; + /** 个人简介,如 "3年工作经验·本科·北京" */ + bio: string; +} + +/** 用户统计数据(我的页四宫格) */ +export interface UserStats { + /** 投递数 */ + deliveries: number; + /** 收藏数 */ + favorites: number; + /** 沟通数 */ + chats: number; + /** Offer 数 */ + offers: number; +} + +/** 功能菜单项 */ +export interface MenuItem { + id: string; + /** 菜单标题 */ + title: string; + /** Ionicons 图标名 */ + icon: string; + /** 跳转路由(暂为占位) */ + route?: string; +} + +/** 企业端入口 */ +export interface EnterpriseEntry { + /** 标题,如 "切换到企业端" */ + title: string; + /** 描述文案 */ + description: string; + /** 操作文案,如 "去企业端 →" */ + action: string; +} + +/** 综合评分(我的页) */ +export interface OverallScore { + /** 综合分数,如 88 */ + score: number; + /** 等级文案,如 "优秀" */ + level: string; + /** 排名文案,如 "超过85%的求职者" */ + rank: string; +} + +/** 维度评分(我的页八大维度) */ +export interface DimensionScore { + id: string; + /** 维度名称,如 "教育背景" */ + name: string; + /** 分数,如 92 */ + score: number; + /** 等级(两档) */ + level: 'excellent' | 'good'; +} + +/** 求职偏好 */ +export interface JobPreference { + /** 期望职位 */ + position: string; + /** 期望城市 */ + city: string; + /** 期望薪资 */ + salary: string; + /** 工作类型 */ + workType: string; + /** 求职状态 */ + status: string; +} + +/** 勋章 */ +export interface Badge { + id: string; + /** 勋章标题 */ + title: string; + /** 勋章描述 */ + description: string; + /** 勋章图标(emoji) */ + icon: string; +} diff --git a/src/utils/index.ts b/src/utils/index.ts new file mode 100644 index 0000000..e8b6bd5 --- /dev/null +++ b/src/utils/index.ts @@ -0,0 +1,21 @@ +/** + * 纯工具函数(字符串 / 数字格式化等)。 + */ + +/** + * 千分位格式化:12846 → "12,846" + */ +export function formatThousands(n: number): string { + return n.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ','); +} + +/** + * 阅读量格式化:23000 → "2.3万";不足一万返回原数字字符串(如 "8500")。 + */ +export function formatViews(views: number): string { + if (views >= 10000) { + const wan = views / 10000; + return `${wan.toFixed(1).replace(/\.0$/, '')}万`; + } + return String(views); +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..9e9d5f3 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "expo/tsconfig.base", + "compilerOptions": { + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true + }, + "include": [ + "**/*.ts", + "**/*.tsx" + ] +}