diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml new file mode 100644 index 0000000..70644f3 --- /dev/null +++ b/.github/workflows/pages.yml @@ -0,0 +1,44 @@ +name: Build and deploy React portfolio +on: + push: + branches: [main] + pull_request: + branches: [main] + workflow_dispatch: +permissions: + contents: read +concurrency: + group: pages-${{ github.ref }} + cancel-in-progress: true +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + with: + version: 10.15.1 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: pnpm + - run: pnpm install --frozen-lockfile + - run: pnpm test + - run: pnpm build + - uses: actions/upload-pages-artifact@v3 + with: + path: dist + deploy: + if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request' + needs: build + runs-on: ubuntu-latest + permissions: + pages: write + id-token: write + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - uses: actions/configure-pages@v5 + - uses: actions/deploy-pages@v4 + id: deployment diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..46f4486 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +dist/ +.env* +!.env.example diff --git a/README.md b/README.md new file mode 100644 index 0000000..9e8ea4c --- /dev/null +++ b/README.md @@ -0,0 +1,80 @@ +# Bo Li · React Portfolio + +基于 React + Vite 的中英文个人主页,继续使用 GitHub Pages。默认中文,右上角切换英文。无需后端或付费托管服务。 + +## 本地开发 + +安装 Node.js 22.12+(或 Node.js 24)和 pnpm 10.15.1,然后在本目录运行: + +```sh +pnpm install --frozen-lockfile +pnpm dev +``` + +发布前执行 `pnpm test` 和 `pnpm build`。`pnpm preview` 可以预览 `dist/` 中的生产构建。不要直接双击 index.html。 + +## 手动修改的位置 + +| 内容 | 文件 | +| ------------------------------------------------ | ----------------------------- | +| 个人简介、联系方式、工作、教育、技能、中英文文案 | `src/data/profile.js` | +| 展示项目、AI 视频、研究、更多项目 | `src/data/projects.js` | +| 主题颜色、间距、移动端布局 | `src/styles.css` | +| 页面区块顺序 | `src/App.jsx` | +| 可复用项目卡片、导航、经历区块、标题 | `src/components/` | +| 头像 | `public/images/bo-li.jpg` | +| 最新 PDF 简历 | `public/resumes/` | +| 视频、封面、字幕 | `public/media/` | +| 自动构建和部署 | `.github/workflows/pages.yml` | + +文案用 `text('中文', 'English')` 管理,不需要在组件里查找简历内容。新增项目只需在 `projects` 数组添加记录,使用唯一 `id`。 + +## 添加 AI 视频 + +把视频和封面放入 `public/media/`,在 `src/data/projects.js` 中修改 AI 影像记录: + +```js +{ + id: 'video', + category: 'AI / MOTION', + title: text('我的作品标题', 'My film title'), + description: text('作品说明', 'Film description'), + tags: ['Generative AI', 'Video'], + visual: 'motion', + status: 'published', + media: { + src: '/media/my-film.mp4', + poster: '/media/my-film.jpg', + // 可选 WebVTT 字幕;仅在文件真实存在时填写。 + // captions: '/media/my-film.zh.vtt', + // captionsLang: 'zh', + }, + url: null, +} +``` + +也可将 `media.src` 替换为 HTTPS 直链视频。YouTube/Bilibili 等播放页面链接放入 `url` 并保留 `media: null`,卡片会显示外链。视频使用浏览器原生控件、不自动播放、移动端内联播放、按需加载,播放失败提供原视频入口。包含对白时可添加字幕。 + +当前 AI 卡片是预留位置,没有虚构作品或播放按钮。大型视频建议使用视频平台或对象存储,避免把大文件提交到 Git 仓库。 + +## GitHub Pages 发布 + +GitHub Pages 支持 React 的静态构建。此用户主页仓库部署在域名根目录,所以 Vite `base` 是 `/`。页面采用锚点导航,不依赖服务端路由,刷新不会出现 SPA 子路由 404。 + +1. 在仓库 **Settings → Pages → Build and deployment → Source** 选择 **GitHub Actions**。 +2. 将重构分支合并到 `main`。PR 阶段仅构建检查;`main` 更新后自动发布 `dist/`。 +3. 在 Actions 中确认 `Build and deploy React portfolio` 成功,然后访问 https://li-bo-github.github.io/ 。 + +如需撤回,revert 本次提交并恢复原来的 Pages 发布来源设置。旧 MDB 的 css/js/scss 和 License.pdf 保留在仓库中用于历史兼容及版权说明,但不会进入新的 Vite 构建。 + +## 内容来源 + +- 工作与教育信息以用户提供的《简历中文 - 李博.pdf》为主要来源,《resume bo 中英.pdf》补充英文表达。 +- Amazon 2025 的工作终止日期为 2025.12,没有标为在职;跨境事件采用中文版的 40,000+ 汇总口径。 +- Tharzen、ClaimTrust、其他研究和早期项目保留自原主页,并保留已有代码链接。 +- 下载入口提供用户提交的原始 PDF;两份 PDF 中电话号码和内容细节存在差异,文件未被改写。页面联系方式采用两份简历一致的邮箱。 +- 公共网站没有后端上传功能。手动修改数据、添加媒体、提交到 main 即可更新。 + +## 验证范围 + +`pnpm test` 检查双语字段、项目 ID、链接协议和静态资源路径;`pnpm build` 检查 React/Vite 生产构建。未提供真实视频,因此发布后的真实媒体还需自行确认浏览器解码、外链访问和字幕同步。 diff --git a/index.html b/index.html index 9923fe6..95bbf87 100644 --- a/index.html +++ b/index.html @@ -1,588 +1,20 @@ - - + + - - - Bo Li Profolio - - - - - - + + + + + + + + + 李博 Bo Li — 软件工程师 · AI 探索者 - - - -
- - -
- -
-
-
-
-
- -
- personal photo -

Bo Li

-
- - LIBO.TOM.MAIL@GMAIL.COM - -
-
-
-
-
- -
-
ABOUT
-

- Hi, I'm interested in the field of supervised learning and autonomous driving. - I am planing to work as software engineer after graduate. -

- Resume -
-
-
-
-
- -
- - - -
- -
-
-
-
-
-
-
-
-
- EDUCATION -
-
-
- - -
    -
  • -
    -
    -
    Master of Computer science
    -

    University of Illinois Urbana-Champaign, Jan 2024 - May 2025

    -
    -
    -
  • -
  • -
    -
    -
    B.S. Computer Science, Data Science
    -

    University of Wisconsin-Madison, Sep 2019 - May 2023

    -
    -
    -
  • -
- -
-
-
-
- EXPERIENCE -
-
-
- - -
    -
  • -
    -
    -
    Tharzen - Software Engineer Internship
    -

    May 2024 - Aug 2024

    -
      -
    • Designed and developed a reversible PHP interpreter based on bidirectional evaluation, leading Test-Driven Development (TDD) to ensure robust feature implementation and comprehensive testing.
    • -
    • Implemented interpreter functionality using Rust classes and traits to enhance the core logic and maintainability of the reversible PHP interpreter.
    • -
    -
    -
    -
  • -
  • -
    -
    -
    Spark Education Group - Software Engineer Internship
    -

    Aug 2023 - Dec 2023

    -
      -
    • Designed and developed a Python application utilizing longchain embedding, chat completion, and vector store, allowing users to interact with the project codebase effectively.
    • -
    • Led the deployment of a large language model platform using React, Node.js, Python, FastAPI, and PostgreSQL, integrating new features such as user statistics, top prompts display, and custom masks.
    • -
    • Drove database and API enhancements, contributing to a 30% increase in weekly active users and achieving a peak of 185 daily active users.
    • -
    -
    -
    -
  • -
  • -
    -
    -
    Amazon - Software Engineer Internship
    -

    May 2022 - Aug 2022

    -
      -
    • Designed and implemented the backend API for the changelog function using Node.js and GraphQL, enhancing user interaction with changelog data based on permissions.
    • -
    • Created GraphQL schema, field resolver, and record resolver for seamless data mapping and CRUD operations, ensuring high performance and scalability.
    • -
    • Managed the end-to-end development cycle, from design documents and UI wireframes to code implementation, code review, and deployment.
    • -
    -
    -
    -
  • -
    -
-
-
-
-
- RESEARCH -
-
-
- - -
    -
  • -
    -
    -
    ClaimTrust – A Propagation-Based Trust Scoring Framework for Retrieval-Augmented Generation (RAG) Systems
    -

    Professor: Heng Ji, Aug 2024 - Dec 2024

    -
      -
    • Developed a graph-based trust scoring algorithm to evaluate document reliability, improving RAG system response quality by 11.2%.
    • -
    • Processed 814 articles to extract claims and classify relationships for trust propagation.
    • -
    • Optimized claim extraction and relationship classification, reducing computational complexity from O(n²) to O(n).
    • -
    -
    -
    -
  • -
  • -
    -
    -
    Simulation of the Collaborative Automated Driving Systems
    -

    Professor: Bin Ran, Feb 2022 - Dec 2022

    -
      -
    • working with the prediction group focusing on trajectory prediction of autonomous driving cars and investigating trajectory prediction methods for the project.
    • -
    • Generating status info of vehicles from Carla simulator in the format that can be used for future tasks as the data for fitting trajectory prediction models.
    • -
    • Contributed to the unification of simulator export data format and model input data format.
    • -
    -
    -
    -
  • -
    -
  • -
    -
    -
    Offloading pushdown db onto SmartNIC based JBOF device
    -

    Professor: Ming Liu , Oct 2021 - Dec 2021

    -
      -
    • Customize FlexPushdownDB developed based on C++ hybrid with caching and pushdown capabilities and weighted cache policy from AWS cloud to JBOF device.
    • -
    -
    -
    -
  • -
-
-
-
-
- PROJECTS -
-
-
- - -
    -
  • -
    -
    -
    Auto parking with Gem e4 Vehicle
    -

    algorithm developer

    -
      -
    • Designed and implemented image recognition and calibration transposition algorithms using lidar and camera sensors to enhance parking system accuracy.
    • -
    • Developed and deployed an MPC trajectory tracker and hybrid A* route planning for autonomous vehicle navigation.
    • -
    -
    -
    -
  • -
  • -
    -
    -
    Cloud-Based IoT Infrastructure for Vehicle Monitoring
    -

    AWS developer

    -
      -
    • Designed and deployed scalable IoT infrastructure using AWS IoT Core, AWS GreenGrass, and AWS IoT Analytics to monitor data from a fleet of 120,000 simulated vehicles.
    • -
    • Implemented AWS Lambda functions to analyze CO2 emissions and return diagnostics, utilizing the publish-subscribe MQTT model for seamless device communication.
    • -
    • Built data visualization pipelines with AWS Firehose, AWS Glue, and AWS Athena, leveraging AWS Sagemaker Jupyter Notebooks for insights.
    • -
    -
    -
    -
  • -
  • -
    -
    -
    Farm data management system
    -

    Java programmer

    -
      -
    • Designed the system and user interface using JavaFX and CSS.
    • -
    • Utilizing java packages processing data from csv documents for computing and visualizing.
    • -
    • Allows users to search data from the database on different categories, displaying the data by farm report, annual report, monthly report, and date Range report with percent of total weight.
    • -
    -
    -
    -
  • -
    -
  • -
    -
    -
    Account holders household clustering with associate bank
    -

    Python programmer

    -
      -
    • Custom distance functions based on address and joint account information using scikit-learn hierarchical clustering, and access data from Snowflake flatform.
    • -
    • The cost of the model using i7-9700 CPUis exponential 1.21 seconds for 1000 records and 43.09 seconds for 10,000 records, and the highest accuracy of the model is 96.6% with different distance threshold.
    • -
    • Write a distance clustering function to weight address clustering method, utilize shared account relationship columns in clustering function, and test functionality of the project.
    • -
    -
    -
    -
  • -
    -
  • -
    -
    -
    Data research on undergraduate enrollment
    -

    Data analysis

    -
      -
    • Utilized Python to develop web crawlers for collecting data from online databases.
    • -
    • Preprocessed data by slicing, reformatting, and implementing a pipeline with StandardScaler transformer.
    • -
    • Analyzed data using PCA and linear regression methods from the PyTorch library.
    • -
    -
    -
    -
  • -
    -
  • -
    -
    -
    Website for Chinese Students and Scholars Association at UW-MADISON
    -

    Web develope

    -
      -
    • Maintained and updated the website for the Chinese Students and Scholars Association at UW-Madison.
    • -
    • Implemented new features and functionality based on user requirements.
    • -
    • Collected and analyzed data from blog and tweet sources. Extracted relevant insights and trends from the data for further analysis.
    • -
    -
    -
    -
  • -
-
-
-
-
- SKILLS -
-
-
- - -
-
-
    -
  • -
    -
    Python
    -
    -
  • -
  • -
    -
    Java
    -
    -
  • -
  • -
    -
    Javascript
    -
    -
  • -
  • -
    -
    SQLite3/SQL
    -
    -
  • -
  • -
    -
    HTML + CSS
    -
    -
  • -
  • -
    -
    R
    -
    -
  • -
  • -
    -
    C++
    -
    -
  • -
  • -
    -
    Typescript
    -
    -
  • -
-
-
-
    -
  • -
    -
    Scikit-Learn
    -
    -
  • -
  • -
    -
    PyTorch
    -
    -
  • -
  • -
    -
    Tensorflow
    -
    -
  • -
  • -
    -
    Numpy
    -
    -
  • -
  • -
    -
    Spark
    -
    -
  • -
-
-
-
    -
  • -
    -
    React.js
    -
    -
  • -
  • -
    -
    Node.js
    -
    -
  • -
  • -
    -
    Amazon Web Services
    -
    -
  • -
  • -
    -
    MongoDB
    -
    -
  • -
  • -
    -
    ElasticSearch
    -
    -
  • -
  • -
    -
    GraphQL
    -
    -
  • -
  • -
    -
    Apollo Federation
    -
    -
  • -
-
-
-
    -
  • -
    -
    Git
    -
    -
  • -
  • -
    -
    AWS
    -
    -
  • -
  • -
    -
    Spring Boot
    -
    -
  • -
  • -
    -
    DynamoDB
    -
    -
  • -
  • -
    -
    GraphQL
    -
    -
  • -
  • -
    -
    Apollo Federation
    -
    -
  • -
-
-
-
-
-
-
-
-
-
-
- - - - + +
+ + diff --git a/package.json b/package.json index b00d481..ca47c01 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,21 @@ { - "name": "mdb-standard-app", - "version": "6.2.0", - "homepage": "https://mdbootstrap.com/docs/standard/", - "license": "MIT" + "name": "bo-li-portfolio", + "version": "1.0.0", + "private": true, + "type": "module", + "packageManager": "pnpm@10.15.1", + "scripts": { + "dev": "vite --host 127.0.0.1", + "build": "vite build", + "preview": "vite preview --host 127.0.0.1", + "test": "node --test tests/*.test.js" + }, + "dependencies": { + "react": "^19.1.1", + "react-dom": "^19.1.1" + }, + "devDependencies": { + "vite": "^7.1.5", + "@vitejs/plugin-react": "^5.0.2" + } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..fa61ad4 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,1108 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + react: + specifier: ^19.1.1 + version: 19.2.8 + react-dom: + specifier: ^19.1.1 + version: 19.2.8(react@19.2.8) + devDependencies: + '@vitejs/plugin-react': + specifier: ^5.0.2 + version: 5.2.0(vite@7.3.6) + vite: + specifier: ^7.1.5 + version: 7.3.6 + +packages: + + '@babel/code-frame@7.29.7': + resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.7': + resolution: {integrity: sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.7': + resolution: {integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.8': + resolution: {integrity: sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.29.7': + resolution: {integrity: sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.29.7': + resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.29.7': + resolution: {integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.29.7': + resolution: {integrity: sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-plugin-utils@7.29.7': + resolution: {integrity: sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.29.7': + resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.29.7': + resolution: {integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.29.7': + resolution: {integrity: sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.8': + resolution: {integrity: sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-transform-react-jsx-self@7.29.7': + resolution: {integrity: sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-source@7.29.7': + resolution: {integrity: sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/template@7.29.7': + resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.8': + resolution: {integrity: sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.8': + resolution: {integrity: sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==} + engines: {node: '>=6.9.0'} + + '@esbuild/aix-ppc64@0.28.2': + resolution: {integrity: sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.28.2': + resolution: {integrity: sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.28.2': + resolution: {integrity: sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.28.2': + resolution: {integrity: sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.28.2': + resolution: {integrity: sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.28.2': + resolution: {integrity: sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.28.2': + resolution: {integrity: sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.28.2': + resolution: {integrity: sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.28.2': + resolution: {integrity: sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.28.2': + resolution: {integrity: sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.28.2': + resolution: {integrity: sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.28.2': + resolution: {integrity: sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.28.2': + resolution: {integrity: sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.28.2': + resolution: {integrity: sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.28.2': + resolution: {integrity: sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.28.2': + resolution: {integrity: sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.28.2': + resolution: {integrity: sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.28.2': + resolution: {integrity: sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.28.2': + resolution: {integrity: sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.28.2': + resolution: {integrity: sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.28.2': + resolution: {integrity: sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.28.2': + resolution: {integrity: sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.28.2': + resolution: {integrity: sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.28.2': + resolution: {integrity: sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.28.2': + resolution: {integrity: sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.28.2': + resolution: {integrity: sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.6.0': + resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@napi-rs/lzma-linux-x64-gnu@1.5.1': + resolution: {integrity: sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==} + engines: {node: ^22.20 || ^24.12 || >=25} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rolldown/pluginutils@1.0.0-rc.3': + resolution: {integrity: sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==} + + '@rollup/rollup-android-arm-eabi@4.63.1': + resolution: {integrity: sha512-UZ8sUxPTiHWYX9QNdJedb1kDZSpS1t/VPWBWGSgqHNi9w3Cu6IXvu2mzbhiTiPvtrqgTQJ+zqiAq2iPIPilpaQ==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.63.1': + resolution: {integrity: sha512-cQ4nFQABN5cDvDpbvJ7bMStCpnaVxynZrRMfUJYgxcIk9Sh54FIO1vtfkg0B69REjER77ioZ/ov+eAApx/KmLQ==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.63.1': + resolution: {integrity: sha512-FQNqd1lRy/0QhDk3xeRIkSBiCpXCiDnZO3YLVdcDKN1UBiKToNftCzcXYNLshmPDUMlu2TdeS8tGcsU6f3YF1Q==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.63.1': + resolution: {integrity: sha512-pvD16V939D3CloK0+qikpGaxiPrDUXTe7Y5cWOMkMSy7m1cawa8EGy/kXYi/G/cKAC4HDAbSnzCIk1WmsoOKXg==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.63.1': + resolution: {integrity: sha512-pcFGeL2345VwdTnJhA6zLbew+YgWB0qBG2+dMtXjCicf6+rm6kO6cOoh5VnTe0ZMrMRgRyuHmCJxZWrIdzYuOw==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.63.1': + resolution: {integrity: sha512-mRJlqSRulVzcKq/LKA6ICSIc3K/l4fzlVn/gePn2nXIHy8seRi5z/eeRE0d/XMBxcMldiXtQTSpRj0tkkC3g8Q==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.63.1': + resolution: {integrity: sha512-YDUNvVM85TI3g/1OpnqKP1h4NeW/j64DfWMf+G3M809xNk1bJSnpFp4sh83NpmVE5DXnkh8ULor4LTVZKoYLHw==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.63.1': + resolution: {integrity: sha512-7Mcn71p9ZuQFAj+h+dhQXy/yeLePRS2yKRnmW1DijA9thKO5qap0GNOIQK4yQ6iP3SU0Mrb/yWo8h8vgRba8lw==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.63.1': + resolution: {integrity: sha512-4YiLQTX6U4CSl0L9cluep9A9W6UmTfqBDc2/CH6wlu54pl4E7Jn3cOD8oxzvBDEGk/JMKgJ47C8g+radF7mwvg==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.63.1': + resolution: {integrity: sha512-2ra8F7w8OquwZN9z2/fKFnli69wa8PLwaVzRMIPGb13ByMJwC28Fbp8YcVGoUhlYMTt7j5j9bNgpysrN2UM+vw==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.63.1': + resolution: {integrity: sha512-Sy20ncyhjmBP0Ml+UvQbimjlk6VFgjW5uNP+qqwHB00mTE8Bl2C1TuHTlRwK2YoXeZbee5lP2XevBWVkAQAtSQ==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-loong64-musl@4.63.1': + resolution: {integrity: sha512-noITLp8oNjYliPnGWmLyelIHwULGqbHloQHGw1rtxbWhTuWooRpnZarZQJ1y9EUC4szuCusCc+HEpUtxpIwYvA==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.63.1': + resolution: {integrity: sha512-hlxxXd+F1mWiAcaFR7Sv9ZQT6m6UfI8+Vy/kFJzztq2pDMU/0wZ9sish0iszNZvsQDo8Gc0i5yuFEOz5dDf6fA==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-musl@4.63.1': + resolution: {integrity: sha512-EF7OpqQTQ/BvGqLzUi4rEHuagCV9MugAUXSHemwPW5vxZ75RR+jxO/2j95Ph2dalMpFHSVECjRoioHZgA9zOYA==} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.63.1': + resolution: {integrity: sha512-wQO3JesW9PRkwlabQ27y7sPfVOOTLRG73I4F2UYHG5PXun3J9U3y+b7ezVKSYbsvSKGQ1k1cq8Qlun4C9kLt3w==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.63.1': + resolution: {integrity: sha512-ouAGwhO6wHRXdnOVCOsB0tRFkA7nhNB2Nwax6oECXN0YiN8EYUTBAOudADOB1PI+yDL61TeNx/u7MVCzksNbkQ==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.63.1': + resolution: {integrity: sha512-q2R38Sn+1J8RxhfJ+T54wSWmyKXWec+9jgDfqO2AtArEqHO5R2aeayp5H5OYLr5UYDVGsVaZPEFUooMhYCdz5A==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.63.1': + resolution: {integrity: sha512-gfI5T24WLLuFfSKw7Go/zDXjAAV0fny0swTaDv+WjK7vqcw4cRhFfdsyKL1n+ukI+ooBxn3bVQnyrn06WpI50w==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.63.1': + resolution: {integrity: sha512-4h6XqthmB4Hspji84wvgk+ElodTsGj+dbZqHJHHtKxj4mYq0ANSEEPX9ys3moJueqsRjwpaJYH7874Itwnj2ow==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-openbsd-x64@4.63.1': + resolution: {integrity: sha512-dlfCOa87o1VAYegLQ9EKilx2JCeRofiyPGhTCmqnuXZ6bMPiycO1rq1+sKoulAp7pGLIsTIw+1x5R+zgh5LhhA==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.63.1': + resolution: {integrity: sha512-cjkLbOlfcm3QGhMM1J5zaZjsw1GggbN6rw9UTSSRrPrR1KkcXnN7Uq9rPw34xImQ9VOY9GN+6u2Zj80B9ptkcw==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.63.1': + resolution: {integrity: sha512-Li1KdUnWGE4N3e1F/B4RTB1ms+nG4WBgjByO46pkeBVX/2UBsY53xf5vK9WygVmnH3RwncIST7lkSdLSY6P9lg==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.63.1': + resolution: {integrity: sha512-t4ZYOSoLTgwhuFMrmTMLx/+i1DQVK7HYqMc6kY46EApwi8X0nIVphzdNoThU3xt6n+N5urG1/gxBdCaKDLavfg==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.63.1': + resolution: {integrity: sha512-RgroPfMmKlD1RzSDxvwgcPiy2HNQKoYV7OmwIXDsk73uKW5t6B/V8KIy27SMv/FNXFo/oSBtWc9J0X7t91ezZg==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.63.1': + resolution: {integrity: sha512-at8QVep6S3h5Y6gSbdGU06bRY5WJkf6WUduM9YtvYMbYhB1MOFfUgc6kehitQXzOtMSaT70q7f9ydPhpqu821w==} + cpu: [x64] + os: [win32] + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@vitejs/plugin-react@5.2.0': + resolution: {integrity: sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + + baseline-browser-mapping@2.11.21: + resolution: {integrity: sha512-uh8vpY/1/YyFkunIDFH/12p7/7VdPKA1hejMVEbdkEaWnUz0Hesvx5EbiU6XxjyHZIOju+ZMbQJkRh+es3/spQ==} + engines: {node: '>=6.0.0'} + hasBin: true + + browserslist@4.28.9: + resolution: {integrity: sha512-EWazOblFYUvlGZcfGhPUPmYh3nikUxBVb+y9MJun5f3hBi812X+8MSQTujLBtgK3cf51fJWbWfOjyeO954d+Eg==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + caniuse-lite@1.0.30001810: + resolution: {integrity: sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + electron-to-chromium@1.5.423: + resolution: {integrity: sha512-rRZfTSY8ptHYMQxa+uIycJMFKmY1T0GIApNMXJYGehguTZa56TEEl19pKPCoBqk5Gpf7QizZn/jt7xur+DYxag==} + + esbuild@0.28.2: + resolution: {integrity: sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.18: + resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + node-releases@2.0.54: + resolution: {integrity: sha512-YHs7BmmcsdAI5Ozuf8JZo6PT0mv2GIWC9vMfvUC3dp65M8hn7Ux8CPL+2oBI7juNuj9d0ndhTcznq2ODBps9cQ==} + engines: {node: '>=18'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.7: + resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==} + engines: {node: '>=12'} + + postcss@8.5.28: + resolution: {integrity: sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==} + engines: {node: ^10 || ^12 || >=14} + + react-dom@19.2.8: + resolution: {integrity: sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==} + peerDependencies: + react: ^19.2.8 + + react-refresh@0.18.0: + resolution: {integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==} + engines: {node: '>=0.10.0'} + + react@19.2.8: + resolution: {integrity: sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==} + engines: {node: '>=0.10.0'} + + rollup@4.63.1: + resolution: {integrity: sha512-3Df9jsstwhccuEfmAMi9l8XUh/GOkVObmFTU7CCVBysEbcOZLl84jCtaAZMcPiMz2EGKsATzQcU+Xr3n/wU6cg==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + update-browserslist-db@1.3.2: + resolution: {integrity: sha512-UQ+MSxlhRm1bzjhU+DcuXfjFO1FzNtqhK5+9Yvlp90ItDLk5vT932A0rFu619nf7RVS+Y/VeaUW1jaRDqZ8VJw==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + vite@7.3.6: + resolution: {integrity: sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + +snapshots: + + '@babel/code-frame@7.29.7': + dependencies: + '@babel/helper-validator-identifier': 7.29.7 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.7': {} + + '@babel/core@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.8 + '@babel/helper-compilation-targets': 7.29.7 + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) + '@babel/helpers': 7.29.7 + '@babel/parser': 7.29.8 + '@babel/template': 7.29.7 + '@babel/traverse': 7.29.8 + '@babel/types': 7.29.8 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.8': + dependencies: + '@babel/parser': 7.29.8 + '@babel/types': 7.29.8 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.29.7': + dependencies: + '@babel/compat-data': 7.29.7 + '@babel/helper-validator-option': 7.29.7 + browserslist: 4.28.9 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.29.7': {} + + '@babel/helper-module-imports@7.29.7': + dependencies: + '@babel/traverse': 7.29.8 + '@babel/types': 7.29.8 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-module-imports': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + '@babel/traverse': 7.29.8 + transitivePeerDependencies: + - supports-color + + '@babel/helper-plugin-utils@7.29.7': {} + + '@babel/helper-string-parser@7.29.7': {} + + '@babel/helper-validator-identifier@7.29.7': {} + + '@babel/helper-validator-option@7.29.7': {} + + '@babel/helpers@7.29.7': + dependencies: + '@babel/template': 7.29.7 + '@babel/types': 7.29.8 + + '@babel/parser@7.29.8': + dependencies: + '@babel/types': 7.29.8 + + '@babel/plugin-transform-react-jsx-self@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-react-jsx-source@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/template@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/parser': 7.29.8 + '@babel/types': 7.29.8 + + '@babel/traverse@7.29.8': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.8 + '@babel/helper-globals': 7.29.7 + '@babel/parser': 7.29.8 + '@babel/template': 7.29.7 + '@babel/types': 7.29.8 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.8': + dependencies: + '@babel/helper-string-parser': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + + '@esbuild/aix-ppc64@0.28.2': + optional: true + + '@esbuild/android-arm64@0.28.2': + optional: true + + '@esbuild/android-arm@0.28.2': + optional: true + + '@esbuild/android-x64@0.28.2': + optional: true + + '@esbuild/darwin-arm64@0.28.2': + optional: true + + '@esbuild/darwin-x64@0.28.2': + optional: true + + '@esbuild/freebsd-arm64@0.28.2': + optional: true + + '@esbuild/freebsd-x64@0.28.2': + optional: true + + '@esbuild/linux-arm64@0.28.2': + optional: true + + '@esbuild/linux-arm@0.28.2': + optional: true + + '@esbuild/linux-ia32@0.28.2': + optional: true + + '@esbuild/linux-loong64@0.28.2': + optional: true + + '@esbuild/linux-mips64el@0.28.2': + optional: true + + '@esbuild/linux-ppc64@0.28.2': + optional: true + + '@esbuild/linux-riscv64@0.28.2': + optional: true + + '@esbuild/linux-s390x@0.28.2': + optional: true + + '@esbuild/linux-x64@0.28.2': + optional: true + + '@esbuild/netbsd-arm64@0.28.2': + optional: true + + '@esbuild/netbsd-x64@0.28.2': + optional: true + + '@esbuild/openbsd-arm64@0.28.2': + optional: true + + '@esbuild/openbsd-x64@0.28.2': + optional: true + + '@esbuild/openharmony-arm64@0.28.2': + optional: true + + '@esbuild/sunos-x64@0.28.2': + optional: true + + '@esbuild/win32-arm64@0.28.2': + optional: true + + '@esbuild/win32-ia32@0.28.2': + optional: true + + '@esbuild/win32-x64@0.28.2': + optional: true + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.6.0': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.6.0 + + '@napi-rs/lzma-linux-x64-gnu@1.5.1': + optional: true + + '@rolldown/pluginutils@1.0.0-rc.3': {} + + '@rollup/rollup-android-arm-eabi@4.63.1': + optional: true + + '@rollup/rollup-android-arm64@4.63.1': + optional: true + + '@rollup/rollup-darwin-arm64@4.63.1': + optional: true + + '@rollup/rollup-darwin-x64@4.63.1': + optional: true + + '@rollup/rollup-freebsd-arm64@4.63.1': + optional: true + + '@rollup/rollup-freebsd-x64@4.63.1': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.63.1': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.63.1': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.63.1': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.63.1': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.63.1': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.63.1': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.63.1': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.63.1': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.63.1': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.63.1': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.63.1': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.63.1': + optional: true + + '@rollup/rollup-linux-x64-musl@4.63.1': + optional: true + + '@rollup/rollup-openbsd-x64@4.63.1': + optional: true + + '@rollup/rollup-openharmony-arm64@4.63.1': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.63.1': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.63.1': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.63.1': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.63.1': + optional: true + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.29.8 + '@babel/types': 7.29.8 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.28.0 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.29.8 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.29.8 + '@babel/types': 7.29.8 + + '@types/babel__traverse@7.28.0': + dependencies: + '@babel/types': 7.29.8 + + '@types/estree@1.0.9': {} + + '@vitejs/plugin-react@5.2.0(vite@7.3.6)': + dependencies: + '@babel/core': 7.29.7 + '@babel/plugin-transform-react-jsx-self': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-react-jsx-source': 7.29.7(@babel/core@7.29.7) + '@rolldown/pluginutils': 1.0.0-rc.3 + '@types/babel__core': 7.20.5 + react-refresh: 0.18.0 + vite: 7.3.6 + transitivePeerDependencies: + - supports-color + + baseline-browser-mapping@2.11.21: {} + + browserslist@4.28.9: + dependencies: + baseline-browser-mapping: 2.11.21 + caniuse-lite: 1.0.30001810 + electron-to-chromium: 1.5.423 + node-releases: 2.0.54 + update-browserslist-db: 1.3.2(browserslist@4.28.9) + + caniuse-lite@1.0.30001810: {} + + convert-source-map@2.0.0: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + electron-to-chromium@1.5.423: {} + + esbuild@0.28.2: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.2 + '@esbuild/android-arm': 0.28.2 + '@esbuild/android-arm64': 0.28.2 + '@esbuild/android-x64': 0.28.2 + '@esbuild/darwin-arm64': 0.28.2 + '@esbuild/darwin-x64': 0.28.2 + '@esbuild/freebsd-arm64': 0.28.2 + '@esbuild/freebsd-x64': 0.28.2 + '@esbuild/linux-arm': 0.28.2 + '@esbuild/linux-arm64': 0.28.2 + '@esbuild/linux-ia32': 0.28.2 + '@esbuild/linux-loong64': 0.28.2 + '@esbuild/linux-mips64el': 0.28.2 + '@esbuild/linux-ppc64': 0.28.2 + '@esbuild/linux-riscv64': 0.28.2 + '@esbuild/linux-s390x': 0.28.2 + '@esbuild/linux-x64': 0.28.2 + '@esbuild/netbsd-arm64': 0.28.2 + '@esbuild/netbsd-x64': 0.28.2 + '@esbuild/openbsd-arm64': 0.28.2 + '@esbuild/openbsd-x64': 0.28.2 + '@esbuild/openharmony-arm64': 0.28.2 + '@esbuild/sunos-x64': 0.28.2 + '@esbuild/win32-arm64': 0.28.2 + '@esbuild/win32-ia32': 0.28.2 + '@esbuild/win32-x64': 0.28.2 + + escalade@3.2.0: {} + + fdir@6.5.0(picomatch@4.0.7): + optionalDependencies: + picomatch: 4.0.7 + + fsevents@2.3.3: + optional: true + + gensync@1.0.0-beta.2: {} + + js-tokens@4.0.0: {} + + jsesc@3.1.0: {} + + json5@2.2.3: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + ms@2.1.3: {} + + nanoid@3.3.18: {} + + node-releases@2.0.54: {} + + picocolors@1.1.1: {} + + picomatch@4.0.7: {} + + postcss@8.5.28: + dependencies: + nanoid: 3.3.18 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + react-dom@19.2.8(react@19.2.8): + dependencies: + react: 19.2.8 + scheduler: 0.27.0 + + react-refresh@0.18.0: {} + + react@19.2.8: {} + + rollup@4.63.1: + dependencies: + '@types/estree': 1.0.9 + optionalDependencies: + '@napi-rs/lzma-linux-x64-gnu': 1.5.1 + '@rollup/rollup-android-arm-eabi': 4.63.1 + '@rollup/rollup-android-arm64': 4.63.1 + '@rollup/rollup-darwin-arm64': 4.63.1 + '@rollup/rollup-darwin-x64': 4.63.1 + '@rollup/rollup-freebsd-arm64': 4.63.1 + '@rollup/rollup-freebsd-x64': 4.63.1 + '@rollup/rollup-linux-arm-gnueabihf': 4.63.1 + '@rollup/rollup-linux-arm-musleabihf': 4.63.1 + '@rollup/rollup-linux-arm64-gnu': 4.63.1 + '@rollup/rollup-linux-arm64-musl': 4.63.1 + '@rollup/rollup-linux-loong64-gnu': 4.63.1 + '@rollup/rollup-linux-loong64-musl': 4.63.1 + '@rollup/rollup-linux-ppc64-gnu': 4.63.1 + '@rollup/rollup-linux-ppc64-musl': 4.63.1 + '@rollup/rollup-linux-riscv64-gnu': 4.63.1 + '@rollup/rollup-linux-riscv64-musl': 4.63.1 + '@rollup/rollup-linux-s390x-gnu': 4.63.1 + '@rollup/rollup-linux-x64-gnu': 4.63.1 + '@rollup/rollup-linux-x64-musl': 4.63.1 + '@rollup/rollup-openbsd-x64': 4.63.1 + '@rollup/rollup-openharmony-arm64': 4.63.1 + '@rollup/rollup-win32-arm64-msvc': 4.63.1 + '@rollup/rollup-win32-ia32-msvc': 4.63.1 + '@rollup/rollup-win32-x64-gnu': 4.63.1 + '@rollup/rollup-win32-x64-msvc': 4.63.1 + fsevents: 2.3.3 + + scheduler@0.27.0: {} + + semver@6.3.1: {} + + source-map-js@1.2.1: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.7) + picomatch: 4.0.7 + + update-browserslist-db@1.3.2(browserslist@4.28.9): + dependencies: + browserslist: 4.28.9 + escalade: 3.2.0 + picocolors: 1.1.1 + + vite@7.3.6: + dependencies: + esbuild: 0.28.2 + fdir: 6.5.0(picomatch@4.0.7) + picomatch: 4.0.7 + postcss: 8.5.28 + rollup: 4.63.1 + tinyglobby: 0.2.17 + optionalDependencies: + fsevents: 2.3.3 + + yallist@3.1.1: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..efc037a --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +onlyBuiltDependencies: + - esbuild diff --git a/public/favicon.svg b/public/favicon.svg new file mode 100644 index 0000000..971b5e6 --- /dev/null +++ b/public/favicon.svg @@ -0,0 +1 @@ +b. diff --git a/public/images/bo-li.jpg b/public/images/bo-li.jpg new file mode 100644 index 0000000..24c303a Binary files /dev/null and b/public/images/bo-li.jpg differ diff --git a/public/media/.gitkeep b/public/media/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/public/resumes/bo-li-bilingual.pdf b/public/resumes/bo-li-bilingual.pdf new file mode 100644 index 0000000..5217a30 Binary files /dev/null and b/public/resumes/bo-li-bilingual.pdf differ diff --git a/public/resumes/bo-li-zh.pdf b/public/resumes/bo-li-zh.pdf new file mode 100644 index 0000000..4d89cfb Binary files /dev/null and b/public/resumes/bo-li-zh.pdf differ diff --git a/src/App.jsx b/src/App.jsx new file mode 100644 index 0000000..f429def --- /dev/null +++ b/src/App.jsx @@ -0,0 +1,142 @@ +import { useEffect, useState } from "react"; +import Header from "./components/Header"; +import ProjectCard from "./components/ProjectCard"; +import ResumeSections from "./components/ResumeSections"; +import Section from "./components/Section"; +import { copy, profile } from "./data/profile"; +import { projects, research, archive } from "./data/projects"; + +export default function App() { + const [lang, setLang] = useState("zh"); + useEffect(() => { + document.documentElement.lang = lang === "zh" ? "zh-CN" : "en"; + document.title = + lang === "zh" + ? "李博 Bo Li — 软件工程师 · AI 探索者" + : "Bo Li — Software Engineer & AI Explorer"; + }, [lang]); + return ( + <> + + {lang === "zh" ? "跳转到正文" : "Skip to content"} + +
+
+
+
+

+ {profile.title[lang]} +

+

+ {lang === "zh" ? "你好,我是李博。" : "Hi, I’m Bo Li."} +

+

{profile.headline[lang]}

+

{profile.intro[lang]}

+
+ + {copy.viewWork[lang]} + + + {copy.resume[lang]} ↓ + +
+
+ FULL STACK / CLOUD / GENERATIVE AI +
+
+
+ {lang +
+ BO LI / 李博 + ENGINEER & EXPLORER +
+
+
+
+ Amazon + + UIUC MCS ’25 + + + UW–Madison BS ’23 + + + {lang === "zh" ? "工程 × 创造力" : "ENGINEERING × CREATIVITY"} + +
+
+
+ {projects.map((p) => ( + + ))} +
+
+ {copy.archive[lang]} + {archive.map((p) => ( + + {p.title[lang]} + {p.tags} ↗ + + ))} +
+
+ +
+
+ {research.map((p) => ( + + ))} +
+
+ +
+ + ); +} diff --git a/src/components/Header.jsx b/src/components/Header.jsx new file mode 100644 index 0000000..183c801 --- /dev/null +++ b/src/components/Header.jsx @@ -0,0 +1,29 @@ +import { copy, profile } from "../data/profile"; +export default function Header({ lang, setLang }) { + return ( +
+ + b. + + +
+ + + {copy.contact[lang]} + +
+
+ ); +} diff --git a/src/components/ProjectCard.jsx b/src/components/ProjectCard.jsx new file mode 100644 index 0000000..c23be0e --- /dev/null +++ b/src/components/ProjectCard.jsx @@ -0,0 +1,90 @@ +import { useState } from "react"; +import { copy } from "../data/profile"; + +export default function ProjectCard({ project, lang }) { + const [failed, setFailed] = useState(false); + const p = project; + const coming = p.status === "coming-soon" && !p.media?.src && !p.url; + return ( +
+ {p.media?.src ? ( +
+ + {failed &&

{copy.videoError[lang]}

} + + {copy.openVideo[lang]} ↗ + +
+ ) : ( +
+ ); +} diff --git a/src/components/ResumeSections.jsx b/src/components/ResumeSections.jsx new file mode 100644 index 0000000..063a2ff --- /dev/null +++ b/src/components/ResumeSections.jsx @@ -0,0 +1,56 @@ +import Section from "./Section"; +import { copy, education, experience, skills } from "../data/profile"; +export default function ResumeSections({ lang }) { + return ( + <> +
+
+ {experience.map((item) => ( +
+
+ {item.date} + {item.location[lang]} +
+
+

+ {item.company[lang]} + {item.role[lang]} +

+
    + {item.points.map((point, i) => ( +
  • {point[lang]}
  • + ))} +
+
+
+ ))} +
+
+
+
+ {education.map((item) => ( +
+ {item.date} +

{item.school[lang]}

+

{item.degree[lang]}

+
+ ))} +
+
+
+
+ {skills.map((group) => ( +
+

{group.title[lang]}

+
+ {group.items.map((item) => ( + {item} + ))} +
+
+ ))} +
+
+ + ); +} diff --git a/src/components/Section.jsx b/src/components/Section.jsx new file mode 100644 index 0000000..a1d1874 --- /dev/null +++ b/src/components/Section.jsx @@ -0,0 +1,14 @@ +export default function Section({ id, number, title, children, intro }) { + return ( +
+
+
+ {number} / +

{title}

+
+ {intro &&

{intro}

} +
+ {children} +
+ ); +} diff --git a/src/data/profile.js b/src/data/profile.js new file mode 100644 index 0000000..b284313 --- /dev/null +++ b/src/data/profile.js @@ -0,0 +1,154 @@ +// All bilingual copy lives here. zh is the default language. +export const text = (zh, en) => ({ zh, en }); +export const profile = { + name: text("李博", "Bo Li"), + email: "libo.tom.mail@gmail.com", + github: "https://github.com/Li-Bo-github", + linkedin: "https://www.linkedin.com/in/bo-li-7b6a10105/", + instagram: "https://www.instagram.com/limbolavida/", + title: text("软件工程师 · AI 探索者", "Software engineer · AI explorer"), + headline: text("把想法,\n写成现实。", "Ideas into code.\nCode into impact."), + intro: text( + "专注于全栈开发、云基础设施与生成式 AI。从可靠的后端系统,到 AI 驱动的产品与影像,探索技术的更多可能。", + "I build across full-stack development, cloud infrastructure, and generative AI — from reliable backend systems to AI-powered products and visual experiments.", + ), +}; +export const experience = [ + { + id: "amazon-2025", + company: text("Amazon 亚马逊", "Amazon"), + role: text("软件工程师 I", "Software Development Engineer I"), + date: "2025.06 — 2025.12", + location: text("北京", "Beijing"), + points: [ + text( + "通过并行 S3 copy 优化 CloudWatch 日志归档,将 backfill 时间从 2.5 小时缩短至 20 分钟(约 86%)。", + "Optimized CloudWatch log archiving with parallel S3 copies, reducing backfill from 2.5 hours to 20 minutes (approximately 86%).", + ), + text( + "基于 Step Functions、Lambda 和 DynamoDB TTL 构建事件驱动工作流,分析超过 40,000 条跨境传输事件,标记逾百个需进一步处理的实例。", + "Built event-driven workflows with Step Functions, Lambda, and DynamoDB TTL to analyze over 40,000 cross-border transfer events and flag over 100 instances for further review.", + ), + text( + "开发 Bedrock AI 助手及每日知识库同步管道,扩展 Python Lambda 以支持 .msg 邮件解析。", + "Developed a Bedrock AI assistant and daily knowledge-base sync pipeline, extending Python Lambda processing to support .msg email files.", + ), + ], + }, + { + id: "tharzen", + company: text("Tharzen", "Tharzen"), + role: text("软件工程师实习生", "Software Engineering Intern"), + date: "2024.05 — 2024.08", + location: text("", ""), + points: [ + text( + "使用 Rust 和双向求值开发可逆 PHP 解释器,通过测试驱动开发完善核心逻辑。", + "Developed a reversible PHP interpreter using Rust and bidirectional evaluation, with test-driven development for core functionality.", + ), + ], + }, + { + id: "spark", + company: text("火花思维", "Spark Education Group"), + role: text("全栈开发实习生", "Full-stack Engineering Intern"), + date: "2023.08 — 2023.12", + location: text("北京", "Beijing"), + points: [ + text( + "开发基于向量存储的内部 GenAI 代码库问答应用,使用 React、Node.js、FastAPI 和 PostgreSQL 部署大语言模型平台。", + "Developed an internal GenAI codebase assistant with vector storage, deploying an LLM platform with React, Node.js, FastAPI, and PostgreSQL.", + ), + text( + "优化数据库与 API,使每周活跃用户增长 30%,日活跃用户峰值达到 185。", + "Improved databases and APIs, contributing to 30% growth in weekly active users and a peak of 185 daily active users.", + ), + ], + }, + { + id: "amazon-2022", + company: text("Amazon 亚马逊", "Amazon"), + role: text("软件工程师实习生", "Software Engineering Intern"), + date: "2022.05 — 2022.08", + location: text("美国 · 贝尔维尤", "Bellevue, WA"), + points: [ + text( + "使用 Node.js、GraphQL 和 PostgreSQL 开发变更日志 API 与 CRUD 操作,参与从设计、评审到部署的完整开发流程。", + "Built changelog APIs and CRUD operations with Node.js, GraphQL, and PostgreSQL, owning the lifecycle from design and review through deployment.", + ), + ], + }, +]; +export const education = [ + { + school: text( + "伊利诺伊大学香槟分校", + "University of Illinois Urbana-Champaign", + ), + degree: text("计算机科学硕士", "Master of Computer Science"), + date: "2024.01 — 2025.05", + }, + { + school: text("威斯康星大学麦迪逊分校", "University of Wisconsin–Madison"), + degree: text( + "理学学士 · 计算机科学、数据科学", + "B.S. · Computer Science & Data Science", + ), + date: "2019.09 — 2023.05", + }, +]; +export const skills = [ + { + title: text("语言与前端", "Languages & frontend"), + items: ["Java", "Python", "SQL", "TypeScript", "React"], + }, + { + title: text("云与基础设施", "Cloud & infrastructure"), + items: [ + "AWS CDK", + "Lambda", + "DynamoDB", + "Step Functions", + "EventBridge", + "S3", + "SQS", + "CloudWatch", + ], + }, + { + title: text("AI 与后端", "AI & backend"), + items: [ + "Amazon Bedrock", + "FastAPI", + "PostgreSQL", + "GraphQL", + "Scikit-learn", + "Google Guice", + "Dagger", + ], + }, +]; +export const copy = { + projects: text("项目展示", "Selected work"), + experience: text("工作经历", "Experience"), + research: text("研究探索", "Research"), + education: text("教育背景", "Education"), + skills: text("技术栈", "Toolkit"), + contact: text("联系我", "Get in touch"), + viewWork: text("看看我的项目", "Explore my work"), + resume: text("简历", "Résumé"), + projectIntro: text( + "从云端系统到 AI 创作,让技术落到实处。", + "Systems, experiments, and the things I build along the way.", + ), + source: text("查看代码", "View source"), + visit: text("查看项目", "View project"), + coming: text("作品待发布", "Coming soon"), + archive: text("更多项目", "More projects"), + footer: text("有想法?一起聊聊。", "Have something in mind? Let’s talk."), + videoError: text( + "视频暂时无法播放,可尝试打开原视频。", + "This video could not be played. Try opening the original video.", + ), + openVideo: text("打开原视频", "Open video"), +}; diff --git a/src/data/projects.js b/src/data/projects.js new file mode 100644 index 0000000..4e8f8ec --- /dev/null +++ b/src/data/projects.js @@ -0,0 +1,124 @@ +import { text } from "./profile.js"; +// Add a record to add a card. Optional media: { src, poster, captions, captionsLang }. +// Use public/media paths (e.g. /media/film.mp4) or an HTTPS video file URL. +// Platform watch URLs belong in url, not media.src. Never invent project links. +export const projects = [ + { + id: "iot", + category: "CLOUD / IoT", + title: text("让 12 万台车,连接云端。", "120,000 vehicles. One cloud."), + description: text( + "面向模拟车队的可扩展物联网基础设施。通过 MQTT 连接设备,使用 Lambda 分析 CO₂ 排放,并构建数据洞察管道。", + "Scalable IoT infrastructure for a simulated fleet, connecting devices over MQTT, analyzing CO₂ emissions with Lambda, and delivering data insights.", + ), + tags: ["AWS IoT Core", "Python", "MQTT"], + visual: "cloud", + metric: "120K", + metricLabel: text("模拟车辆", "simulated vehicles"), + }, + { + id: "video", + category: "AI / MOTION", + title: text("AI 影像实验", "AI motion experiments"), + description: text( + "用生成式 AI 探索影像叙事。新的作品将在这里呈现。", + "Exploring visual storytelling with generative AI. New work will appear here.", + ), + tags: ["Generative AI", "Video"], + visual: "motion", + status: "coming-soon", + media: null, + url: null, + }, + { + id: "parking", + category: "AUTONOMOUS SYSTEMS", + title: text("GEM e4 自动泊车", "Autonomous parking · GEM e4"), + description: text( + "结合激光雷达与摄像头标定、Hybrid A* 路径规划和 MPC 轨迹跟踪,实现自动泊车导航。", + "Autonomous parking with LiDAR and camera calibration, Hybrid A* path planning, and MPC trajectory tracking.", + ), + tags: ["MPC", "Hybrid A*", "Robotics"], + visual: "path", + url: "https://github.com/Li-Bo-github/GEMstack", + }, + { + id: "clustering", + category: "DATA SCIENCE", + title: text( + "从账户数据,发现家庭关联。", + "Finding connections in account data.", + ), + description: text( + "基于地址和联名账户构建定制距离函数,使用层次聚类分析账户持有人,模型准确率达到 96.6%。", + "Hierarchical clustering with custom distances based on addresses and joint accounts, reaching 96.6% model accuracy.", + ), + tags: ["Scikit-learn", "Snowflake", "Python"], + visual: "data", + metric: "96.6%", + metricLabel: text("模型准确率", "model accuracy"), + url: "https://github.com/CodySond/cs639-assocbank-team2", + }, +]; +export const research = [ + { + id: "claimtrust", + title: text( + "ClaimTrust · RAG 信任评分", + "ClaimTrust · Trust scoring for RAG", + ), + date: "2024.08 — 2024.12", + description: text( + "基于图的文档可信度传播,处理 814 篇文章,将 RAG 系统回答质量提升 11.2%。指导教授:Heng Ji。", + "Graph-based trust propagation across 814 articles, improving RAG response quality by 11.2%. Advised by Heng Ji.", + ), + url: "https://github.com/Li-Bo-github/ClaimTrust-A-Propagation-Based-Trust-Scoring-Framework-for-Retrieval-Augmented-Generation-Systems", + }, + { + id: "carla", + title: text( + "协同自动驾驶系统仿真", + "Collaborative automated driving simulation", + ), + date: "2022.02 — 2022.12", + description: text( + "研究车辆轨迹预测,统一 CARLA 仿真输出与预测模型输入的数据格式。指导教授:Bin Ran。", + "Investigated trajectory prediction and unified CARLA simulator exports with prediction model inputs. Advised by Bin Ran.", + ), + }, + { + id: "smartnic", + title: text( + "SmartNIC / JBOF 数据库下推", + "Database pushdown on SmartNIC / JBOF", + ), + date: "2021.10 — 2021.12", + description: text( + "定制基于 C++ 的 FlexPushdownDB,探索缓存与数据库下推能力。指导教授:Ming Liu。", + "Customized C++ FlexPushdownDB to explore caching and database pushdown capabilities. Advised by Ming Liu.", + ), + }, +]; +export const archive = [ + { + id: "farm", + title: text("农场数据管理系统", "Farm data management"), + tags: "JavaFX / Java", + url: "https://github.com/Li-Bo-github/milk-farm-management-system", + }, + { + id: "enrollment", + title: text( + "本科招生数据研究与可视化", + "Undergraduate enrollment analysis", + ), + tags: "Python / Data", + url: "https://github.com/Li-Bo-github/school_undergrad-enrollment-study-and-visulization", + }, + { + id: "cssa", + title: text("UW–Madison CSSA 网站", "UW–Madison CSSA website"), + tags: "Web development", + url: "https://github.com/cssaatuwmadisonIT", + }, +]; diff --git a/src/main.jsx b/src/main.jsx new file mode 100644 index 0000000..0beeb11 --- /dev/null +++ b/src/main.jsx @@ -0,0 +1,9 @@ +import React from "react"; +import { createRoot } from "react-dom/client"; +import App from "./App"; +import "./styles.css"; +createRoot(document.getElementById("root")).render( + + + , +); diff --git a/src/styles.css b/src/styles.css new file mode 100644 index 0000000..0fbfcfb --- /dev/null +++ b/src/styles.css @@ -0,0 +1,732 @@ +@import url("https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&family=Noto+Sans+SC:wght@400;500;600;700&display=swap"); + +/* Theme: change these tokens to restyle every section. */ +:root { + font-family: "DM Sans", "Noto Sans SC", sans-serif; + color: #202720; + background: #f6f7f2; + font-synthesis: none; + --green: #335d45; + --line: #dce1d6; + --muted: #60695f; + --radius: 16px; +} +* { + box-sizing: border-box; +} +html { + scroll-behavior: smooth; + scroll-padding-top: 110px; +} +body { + margin: 0; +} +a { + color: inherit; + text-decoration: none; +} +button { + font: inherit; +} +a, +button, +summary { + -webkit-tap-highlight-color: transparent; + touch-action: manipulation; +} +a:focus-visible, +button:focus-visible, +summary:focus-visible { + outline: 3px solid #507b40; + outline-offset: 6px; +} +a:hover { + color: var(--green); +} +button, +summary { + cursor: pointer; +} +p { + line-height: 1.8; + color: var(--muted); +} +.header, +main { + width: min(1160px, calc(100% - 80px)); + margin: auto; +} +.header { + min-height: 96px; + display: flex; + align-items: center; + justify-content: space-between; + border-bottom: 1px solid var(--line); + gap: 24px; +} +.brand { + font-size: 44px; + font-weight: 700; + letter-spacing: -5px; + line-height: 1; +} +.brand span { + color: #52784c; +} +.header nav, +.header-actions { + display: flex; + gap: 30px; + align-items: center; + font-size: 14px; +} +.header nav a { + padding: 12px 0; +} +.language { + border: 0; + border-right: 1px solid var(--line); + background: none; + padding: 10px 24px 10px 0; +} +.contact-link span { + margin-left: 12px; +} +.hero { + display: grid; + grid-template-columns: 1.3fr 1fr; + gap: 80px; + padding: 78px 0 70px; + align-items: center; +} +.eyebrow { + font-size: 13px; + letter-spacing: 0.11em; + font-weight: 600; + color: var(--green); + line-height: 1.6; +} +.dot { + display: inline-block; + width: 8px; + height: 8px; + background: #4c7454; + border-radius: 50%; + margin-right: 8px; +} +.hello { + font-size: 20px; + color: #354031; + margin: 26px 0 15px; +} +h1 { + white-space: pre-line; + font-size: clamp(42px, 5vw, 67px); + line-height: 1.22; + letter-spacing: -0.055em; + font-weight: 600; + margin: 0 0 26px; +} +.intro { + max-width: 490px; + font-size: 16px; +} +.hero-actions { + display: flex; + align-items: center; + gap: 30px; + margin-top: 30px; +} +.button { + background: var(--green); + color: white; + padding: 16px 22px; + border-radius: 7px; + font-size: 15px; +} +.button:hover { + background: #244532; + color: white; +} +.button span { + margin-left: 25px; +} +.resume-link { + font-size: 15px; + border-bottom: 1px solid #82917f; + padding: 9px 0; +} +.hero-foot { + font-size: 12px; + letter-spacing: 0.09em; + color: #727a6e; + margin-top: 44px; +} +.hero-foot span { + margin: 0 12px; + color: #b0b8a9; +} +.portrait { + margin: 0; + position: relative; + padding: 12px; + background: #e7eadd; + border-radius: 120px 16px 16px 16px; + transform: rotate(2deg); +} +.portrait img { + display: block; + width: 100%; + height: 415px; + object-fit: cover; + object-position: center 40%; + border-radius: 110px 9px 9px 9px; + filter: saturate(0.75); +} +.portrait figcaption { + display: flex; + justify-content: space-between; + gap: 8px; + padding: 18px 7px 7px; + font-size: 12px; + letter-spacing: 0.03em; +} +.credential-strip { + display: flex; + justify-content: space-between; + align-items: center; + gap: 25px; + border-top: 1px solid var(--line); + border-bottom: 1px solid var(--line); + padding: 27px 0; + color: #4a5346; + font-size: 22px; + font-weight: 600; +} +.credential-strip small { + font-size: 12px; + font-weight: 400; + margin-left: 8px; +} +.credential-strip .strip-note { + font-size: 12px; + letter-spacing: 0.08em; + font-weight: 400; +} +.section { + padding-top: 85px; +} +.section-heading { + display: flex; + justify-content: space-between; + gap: 30px; + align-items: end; + margin-bottom: 30px; +} +.section-heading > div { + display: flex; + align-items: baseline; + gap: 18px; +} +h2 { + font-size: 32px; + letter-spacing: -0.04em; + font-weight: 500; + margin: 0; +} +.section-heading > p { + font-size: 14px; + max-width: 400px; + margin: 0; +} +.project-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 28px; +} +.project-card { + border: 1px solid var(--line); + border-radius: var(--radius); + overflow: hidden; + background: #fdfefb; + transition: transform 0.2s; +} +.project-card:hover { + transform: translateY(-3px); +} +.project-art { + height: 245px; + position: relative; + overflow: hidden; + display: flex; + align-items: center; + justify-content: center; +} +.cloud { + background-color: #e2e9dd; + background-image: radial-gradient(#b2c3ad 1px, transparent 1px); + background-size: 18px 18px; +} +.metric { + font-size: 76px; + letter-spacing: -0.07em; + color: #345540; + text-align: center; +} +.metric span { + display: block; + font-size: 13px; + letter-spacing: 0.13em; +} +.motion { + background: #222b26; +} +.film-orbit { + width: 175px; + height: 175px; + border: 1px solid #9db199; + border-radius: 50%; + box-shadow: + 22px 0 0 -1px #222b26, + 23px 0 #6c8667, + -22px 0 0 -1px #222b26, + -23px 0 #6c8667; + transform: rotate(-30deg); +} +.film-orbit:after { + content: ""; + position: absolute; + inset: 28px; + border: 1px solid #c0cbb1; + border-radius: 50%; +} +.art-label { + position: absolute; + bottom: 20px; + font-size: 12px; + letter-spacing: 0.06em; + color: #bac8b2; +} +.path { + background: #e9e7de; + background-image: + linear-gradient(#d6d5ca 1px, transparent 1px), + linear-gradient(90deg, #d6d5ca 1px, transparent 1px); + background-size: 32px 32px; +} +.parking-path { + width: 210px; + height: 130px; + border: 3px dashed #6d8066; + border-bottom: 0; + border-radius: 70px 70px 0 0; + transform: rotate(-15deg); +} +.parking-path:after { + content: ""; + display: block; + width: 33px; + height: 55px; + background: #385a43; + border: 5px solid #768f73; + border-radius: 8px; + position: absolute; + right: -18px; + bottom: 0; +} +.path .art-label { + color: #59644d; +} +.data { + background: #e9ecdf; +} +.data:before, +.data:after { + content: ""; + position: absolute; + width: 180px; + height: 180px; + border: 1px solid #c4ccb7; + border-radius: 50%; + left: -55px; + top: -50px; +} +.data:after { + left: auto; + top: auto; + right: -50px; + bottom: -70px; + width: 260px; + height: 260px; +} +.project-content { + padding: 27px; +} +.card-meta { + font-size: 12px; + letter-spacing: 0.05em; + color: #67715f; + display: flex; + justify-content: space-between; + gap: 12px; + align-items: center; +} +.status { + font-size: 12px; + letter-spacing: 0; + background: #edf0e6; + padding: 4px 8px; + border-radius: 4px; +} +h3 { + font-size: 21px; + font-weight: 500; + letter-spacing: -0.025em; + line-height: 1.5; + margin: 16px 0 10px; +} +.project-content p { + font-size: 16px; + margin: 0 0 20px; + min-height: 86px; +} +.tags { + display: flex; + gap: 8px; + flex-wrap: wrap; +} +.tags span { + font-size: 14px; + padding: 5px 9px; + background: #edf0e8; + border-radius: 4px; + color: #55604e; + line-height: 1.6; +} +.text-link { + display: flex; + justify-content: space-between; + border-top: 1px solid var(--line); + margin-top: 24px; + padding-top: 17px; + font-size: 14px; +} +.video-wrap { + background: #202720; + color: white; +} +.video-wrap video { + width: 100%; + aspect-ratio: 16/9; + display: block; +} +.video-wrap p { + color: white; + padding: 10px; +} +.video-link { + display: block; + color: #fff; + padding: 12px; + font-size: 14px; +} +.video-link:hover { + color: #c6dcc3; +} +.archive { + border-bottom: 1px solid var(--line); + margin-top: 28px; +} +.archive summary { + font-size: 15px; + padding: 20px 0; +} +.archive > a { + display: flex; + justify-content: space-between; + gap: 20px; + padding: 18px 0; + border-top: 1px solid var(--line); +} +.archive small { + color: var(--muted); +} +.job { + display: grid; + grid-template-columns: 240px 1fr; + gap: 28px; + padding: 28px 0; + border-top: 1px solid var(--line); +} +.job-date { + font-size: 14px; + color: #627059; + padding-top: 8px; +} +.job-date span { + display: block; + font-size: 13px; + margin-top: 10px; +} +.job h3 { + margin: 0 0 15px; + font-size: 22px; +} +.role { + font-size: 15px; + color: var(--muted); + display: block; + margin-top: 4px; +} +.job ul { + padding-left: 18px; + margin: 0; + color: var(--muted); + line-height: 1.85; + font-size: 16px; +} +.job li { + padding-left: 5px; + margin: 8px 0; +} +.education-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 28px; +} +.education-grid article { + border-top: 1px solid var(--line); + padding: 26px 0; +} +.education-grid p { + font-size: 16px; +} +.skills-grid { + display: grid; + grid-template-columns: 1fr 1.4fr 1.2fr; + gap: 30px; + border-top: 1px solid var(--line); + padding-top: 18px; +} +.skills-grid h3 { + font-size: 16px; + margin-bottom: 20px; +} +.research-list { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + gap: 28px; +} +.research-list article { + border-top: 1px solid var(--line); + padding-top: 25px; +} +.research-list h3 { + font-size: 18px; +} +.research-list p { + font-size: 16px; +} +footer { + margin-top: 90px; + padding: 50px 0 0; + border-top: 1px solid var(--line); +} +footer h2 { + font-size: 40px; + margin: 18px 0; +} +.email { + font-size: clamp(17px, 3vw, 28px); + border-bottom: 1px solid #9aab91; + padding-bottom: 7px; + display: inline-block; +} +.footer-bottom { + display: flex; + justify-content: space-between; + gap: 20px; + padding: 50px 0 26px; + font-size: 14px; + color: #66715e; +} +.footer-bottom > div { + display: flex; + gap: 22px; +} +.skip-link { + position: absolute; + top: -100px; + left: 20px; + background: white; + padding: 15px; + z-index: 10; +} +.skip-link:focus { + top: 10px; +} +@media (min-width: 1450px) { + .hero { + padding-top: 90px; + padding-bottom: 85px; + } +} +@media (max-width: 900px) { + .header, + main { + width: calc(100% - 44px); + } + .hero { + gap: 35px; + padding: 55px 0; + } + .portrait img { + height: 350px; + } + .portrait figcaption { + flex-direction: column; + } + .header nav { + gap: 18px; + } + .credential-strip .strip-note { + display: none; + } + .job { + grid-template-columns: 180px 1fr; + } + .research-list { + grid-template-columns: 1fr; + } + .skills-grid { + grid-template-columns: 1fr 1fr; + } + .section-heading { + align-items: start; + flex-direction: column; + gap: 12px; + } +} +@media (max-width: 600px) { + .header, + main { + width: calc(100% - 36px); + } + .header { + flex-wrap: wrap; + padding: 18px 0; + gap: 15px; + } + .header nav { + order: 3; + width: 100%; + justify-content: space-between; + } + .header-actions { + gap: 14px; + } + .language { + padding-right: 15px; + } + .brand { + font-size: 36px; + } + .hero { + grid-template-columns: 1fr; + gap: 36px; + padding: 42px 0; + } + .hero-copy .eyebrow { + font-size: 12px; + } + h1 { + font-size: 48px; + } + .hero-foot { + font-size: 12px; + line-height: 1.8; + } + .hero-foot span { + margin: 0 4px; + } + .portrait { + max-width: 380px; + width: 90%; + margin: 0 auto; + } + .portrait img { + height: 340px; + } + .credential-strip { + gap: 14px; + flex-wrap: wrap; + font-size: 17px; + } + .credential-strip small { + display: block; + margin-left: 0; + margin-top: 5px; + } + .project-grid, + .education-grid, + .skills-grid { + grid-template-columns: 1fr; + } + .section { + padding-top: 58px; + } + .project-art { + height: 215px; + } + .project-content { + padding: 23px; + } + .project-content p { + min-height: 0; + } + .section-heading h2 { + font-size: 28px; + } + .job { + grid-template-columns: 1fr; + gap: 14px; + } + .job-date span { + display: inline; + margin-left: 14px; + } + .archive > a { + flex-direction: column; + gap: 6px; + } + .footer-bottom { + flex-wrap: wrap; + line-height: 2; + } + .footer-bottom > div { + gap: 15px; + } + footer h2 { + font-size: 32px; + } + .email { + overflow-wrap: anywhere; + } + .hero-actions { + gap: 25px; + } + .skills-grid { + gap: 12px; + } +} +@media (prefers-reduced-motion: reduce) { + html { + scroll-behavior: auto; + } + .project-card { + transition: none; + } + .project-card:hover { + transform: none; + } +} diff --git a/tests/content.test.js b/tests/content.test.js new file mode 100644 index 0000000..b22e944 --- /dev/null +++ b/tests/content.test.js @@ -0,0 +1,61 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { existsSync } from "node:fs"; +import { projects, research, archive } from "../src/data/projects.js"; +import { + profile, + experience, + education, + skills, + copy, +} from "../src/data/profile.js"; + +function checkTranslations(value) { + if (!value || typeof value !== "object") return; + if ("zh" in value || "en" in value) { + assert.equal(typeof value.zh, "string", "Missing Chinese translation"); + assert.equal(typeof value.en, "string", "Missing English translation"); + } else Object.values(value).forEach(checkTranslations); +} +test("all portfolio content provides both languages", () => { + [ + projects, + research, + archive, + profile, + experience, + education, + skills, + copy, + ].forEach(checkTranslations); +}); +test("project IDs are unique and linked resources are usable URLs", () => { + const records = [...projects, ...research, ...archive]; + assert.equal(new Set(records.map((p) => p.id)).size, records.length); + for (const p of records) { + assert.ok(p.title.zh && p.title.en); + if (p.url) assert.equal(new URL(p.url).protocol, "https:"); + if (p.media?.src) { + assert.ok( + !/youtube\.com\/watch|youtu\.be|bilibili\.com\/video/.test(p.media.src), + "Platform watch URLs must use url, not media.src", + ); + for (const path of [p.media.src, p.media.poster, p.media.captions].filter( + Boolean, + )) { + if (path.startsWith("/")) + assert.ok(existsSync(`public${path}`), `Missing asset: ${path}`); + else assert.equal(new URL(path).protocol, "https:"); + } + } + } +}); +test("required public assets exist", () => { + for (const path of [ + "public/images/bo-li.jpg", + "public/resumes/bo-li-zh.pdf", + "public/resumes/bo-li-bilingual.pdf", + "public/favicon.svg", + ]) + assert.ok(existsSync(path), `Missing ${path}`); +}); diff --git a/vite.config.js b/vite.config.js new file mode 100644 index 0000000..582ec4f --- /dev/null +++ b/vite.config.js @@ -0,0 +1,3 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; +export default defineConfig({ plugins: [react()], base: "/" });