Merge branch 'mcpeakml/portfolio'
This commit is contained in:
commit
cb3bb16eab
16 changed files with 323 additions and 81 deletions
1
.env
Normal file
1
.env
Normal file
|
|
@ -0,0 +1 @@
|
|||
VITE_API_URL=http://localhost:5173/
|
||||
|
|
@ -8,14 +8,11 @@ on:
|
|||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
container: oven/bun:1
|
||||
runs-on: self-hosted
|
||||
container: git.mcpeakdev.com/mcpeakdev/bun-ci:latest
|
||||
outputs:
|
||||
artifact_name: ${{ steps.meta.outputs.name }}
|
||||
steps:
|
||||
- name: Install git and node
|
||||
run: apt-get update && apt-get install -y git nodejs
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
|
|
@ -30,7 +27,13 @@ jobs:
|
|||
echo "name=portfolio_${tag}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
run: |
|
||||
printf '@nychthemeron:registry=https://git.mcpeakdev.com/api/packages/McPeakDev/npm/\n//git.mcpeakdev.com/api/packages/McPeakDev/npm/:_authToken=%s\n' "$BUN_AUTH_TOKEN" > .npmrc
|
||||
bun install --frozen-lockfile
|
||||
rm .npmrc
|
||||
working-directory: ui
|
||||
env:
|
||||
BUN_AUTH_TOKEN: ${{ secrets.BUN_AUTH_TOKEN }}
|
||||
|
||||
- name: Audit dependencies
|
||||
run: bun audit || true
|
||||
|
|
@ -53,8 +56,8 @@ jobs:
|
|||
publish:
|
||||
needs: build
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
runs-on: ubuntu-latest
|
||||
container: catthehacker/ubuntu:act-latest
|
||||
runs-on: self-hosted
|
||||
container: git.mcpeakdev.com/mcpeakdev/docker-pub:latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
|
|
|||
25
bun.lock
25
bun.lock
|
|
@ -5,13 +5,14 @@
|
|||
"": {
|
||||
"name": "portfolio",
|
||||
"dependencies": {
|
||||
"@nychthemeron/library": "^0.0.1",
|
||||
"vue": "^3.5.32",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.59.1",
|
||||
"@tsconfig/node24": "^24.0.4",
|
||||
"@types/jsdom": "^28.0.1",
|
||||
"@types/node": "^24.12.2",
|
||||
"@types/node": "^25.9.3",
|
||||
"@vitejs/plugin-vue": "^6.0.6",
|
||||
"@vitest/eslint-plugin": "^1.6.16",
|
||||
"@vue/eslint-config-typescript": "^14.7.0",
|
||||
|
|
@ -170,6 +171,8 @@
|
|||
|
||||
"@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="],
|
||||
|
||||
"@nychthemeron/library": ["@nychthemeron/library@0.0.1", "https://git.mcpeakdev.com/api/packages/McPeakDev/npm/%40nychthemeron%2Flibrary/-/0.0.1/library-0.0.1.tgz", { "peerDependencies": { "primevue": "^4.5.0", "vue": "^3.5.0" } }, "sha512-84pcTnF8Ead8D7TSLhKTS8ACpaOCTS0mylzkWqgGrNs4fnrH9tEJg874nFHegiFy5P1fuKCk4B7Yt/Pjc4NK/Q=="],
|
||||
|
||||
"@one-ini/wasm": ["@one-ini/wasm@0.1.1", "", {}, "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw=="],
|
||||
|
||||
"@oxc-project/types": ["@oxc-project/types@0.133.0", "", {}, "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA=="],
|
||||
|
|
@ -256,6 +259,16 @@
|
|||
|
||||
"@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="],
|
||||
|
||||
"@primeuix/styled": ["@primeuix/styled@0.7.4", "", { "dependencies": { "@primeuix/utils": "^0.6.1" } }, "sha512-QSO/NpOQg8e9BONWRBx9y8VGMCMYz0J/uKfNJEya/RGEu7ARx0oYW0ugI1N3/KB1AAvyGxzKBzGImbwg0KUiOQ=="],
|
||||
|
||||
"@primeuix/styles": ["@primeuix/styles@2.0.3", "", { "dependencies": { "@primeuix/styled": "^0.7.4" } }, "sha512-2ykAB6BaHzR/6TwF8ShpJTsZrid6cVIEBVlookSdvOdmlWuevGu5vWOScgIwqWwlZcvkFYAGR/SUV3OHCTBMdw=="],
|
||||
|
||||
"@primeuix/utils": ["@primeuix/utils@0.6.4", "", {}, "sha512-pZ5f+vj7wSzRhC7KoEQRU5fvYAe+RP9+m39CTscZ3UywCD1Y2o6Fe1rRgklMPSkzUcty2jzkA0zMYkiJBD1hgg=="],
|
||||
|
||||
"@primevue/core": ["@primevue/core@4.5.5", "", { "dependencies": { "@primeuix/styled": "^0.7.4", "@primeuix/utils": "^0.6.2" }, "peerDependencies": { "vue": "^3.5.0" } }, "sha512-JpkXhq1ddc70JdsC3CC4dM+UbeeWuCW/8DpS9dNBfrOk824TLSlRlMEGFyVKqRMn5WPQvYLiy3xXfLQeNdSqhQ=="],
|
||||
|
||||
"@primevue/icons": ["@primevue/icons@4.5.5", "", { "dependencies": { "@primeuix/utils": "^0.6.2", "@primevue/core": "4.5.5" } }, "sha512-eteOhTdAOXEYE9qW1AOrBBgDxQ2szHJxSkEK1XVdV2TKxGM5FQf03Ovms0VDyZTc16XBIgvwYjXJQS0BPbhPaA=="],
|
||||
|
||||
"@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.3", "", { "os": "android", "cpu": "arm64" }, "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw=="],
|
||||
|
||||
"@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA=="],
|
||||
|
|
@ -306,7 +319,7 @@
|
|||
|
||||
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
|
||||
|
||||
"@types/node": ["@types/node@24.13.1", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-RSpUJGmvsJ1ZeBehQZFhIdpsz+bIpES0nIQXko4Ybq+N+kX6XvOq3Jo+iJ82FWLdblFq85AsMikd3m35jgezYg=="],
|
||||
"@types/node": ["@types/node@25.9.3", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg=="],
|
||||
|
||||
"@types/tough-cookie": ["@types/tough-cookie@4.0.5", "", {}, "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA=="],
|
||||
|
||||
|
|
@ -720,6 +733,8 @@
|
|||
|
||||
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
|
||||
|
||||
"primevue": ["primevue@4.5.5", "", { "dependencies": { "@primeuix/styled": "^0.7.4", "@primeuix/styles": "^2.0.3", "@primeuix/utils": "^0.6.2", "@primevue/core": "4.5.5", "@primevue/icons": "4.5.5" } }, "sha512-Kv5REIewCdP806QaoU+4nBXfmpzOGFKkZ9qH4KsL6MjiAQVc4PUzypt8erl4r3Vzh3nr3aWZIxkxYRRsLGiX2A=="],
|
||||
|
||||
"proto-list": ["proto-list@1.2.4", "", {}, "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA=="],
|
||||
|
||||
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
||||
|
|
@ -876,7 +891,9 @@
|
|||
|
||||
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
|
||||
|
||||
"@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
|
||||
"@types/jsdom/@types/node": ["@types/node@24.13.1", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-RSpUJGmvsJ1ZeBehQZFhIdpsz+bIpES0nIQXko4Ybq+N+kX6XvOq3Jo+iJ82FWLdblFq85AsMikd3m35jgezYg=="],
|
||||
|
||||
"@types/node/undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="],
|
||||
|
||||
"@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
|
||||
|
||||
|
|
@ -918,6 +935,8 @@
|
|||
|
||||
"wrap-ansi-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
|
||||
"@types/jsdom/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
|
||||
|
||||
"cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||
|
||||
"editorconfig/minimatch/brace-expansion": ["brace-expansion@2.1.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA=="],
|
||||
|
|
|
|||
1
bunfig.toml
Normal file
1
bunfig.toml
Normal file
|
|
@ -0,0 +1 @@
|
|||
env = false
|
||||
|
|
@ -0,0 +1,112 @@
|
|||
# Refactor portfolio to use `@nychthemeron/library`
|
||||
|
||||
## Context
|
||||
|
||||
The portfolio currently ships a hand-rolled "Nychthemeron" design system:
|
||||
local components in `src/components/ds/` (`NychButton`, `NychCard`,
|
||||
`NychInputText`, `NychTextarea`, `NychMessage`, `NychTag`, `LaurelSpinner`,
|
||||
`LaurelDivider`, `ImageSlot`) backed by ~1200 lines of token CSS in
|
||||
`src/assets/styles/`.
|
||||
|
||||
`@nychthemeron/library` (now installed from the private registry at
|
||||
`git.mcpeakdev.com`) is the extracted, PrimeVue-backed version of this same
|
||||
design system: unstyled PrimeVue components (`Button`, `Card`, `InputText`,
|
||||
`Textarea`, `Message`, `Tag`, `LoadingIcon`, plus others not yet used here)
|
||||
wired up with the same `nych-*` passthrough classes, and ships the same
|
||||
theme as CSS via `@nychthemeron/library/style` and `@nychthemeron/library/theme`.
|
||||
|
||||
Goal: replace the local duplicated implementation with the library, keeping
|
||||
only what the library doesn't provide (`LaurelDivider`, `ImageSlot`, and the
|
||||
580px static hero spinner).
|
||||
|
||||
## Styling / setup
|
||||
|
||||
- `src/main.ts`:
|
||||
- Add `import '@nychthemeron/library/style'` and
|
||||
`import '@nychthemeron/library/theme'`.
|
||||
- Register PrimeVue: `app.use(PrimeVue)` (the library's components are
|
||||
PrimeVue components under the hood).
|
||||
- Keep the existing `import './assets/styles/styles.css'`, but trim its
|
||||
`@import` manifest (see below) so it only covers portfolio-specific
|
||||
CSS; the library CSS imports are added separately in `main.ts`.
|
||||
- Delete `src/assets/styles/tokens/fonts.css`, `tokens/colors.css`,
|
||||
`tokens/base.css` — fully superseded by `@nychthemeron/library/style`
|
||||
(same theme CSS variables for both `hades`/`apollo` themes, font imports,
|
||||
base reset, focus-ring, and the `nych-spin` keyframes).
|
||||
- `src/assets/styles/tokens/components.css`: delete every section except
|
||||
**LaurelDivider** (currently the last section, ~line 738 onward). Rename
|
||||
the file to `tokens/laurel-divider.css` containing just that section.
|
||||
Everything else (Button, Input/Textarea, Checkbox/Radio, ToggleSwitch,
|
||||
Select, Card, Message, Tag, Dialog) is covered by
|
||||
`@nychthemeron/library/theme`.
|
||||
- Keep as-is: `tokens/spacing.css`, `tokens/typography.css`,
|
||||
`portfolio.css`, brand SVGs in `src/assets/brand/`.
|
||||
- Update `src/assets/styles/styles.css`'s `@import` manifest to drop the
|
||||
removed files and add `tokens/laurel-divider.css` in place of
|
||||
`tokens/components.css`.
|
||||
|
||||
## Component mapping
|
||||
|
||||
| Local component | Replacement | Notes |
|
||||
|---|---|---|
|
||||
| `NychTag` | `Tag` from `@nychthemeron/library` | `severity` / `rounded` props pass straight through |
|
||||
| `NychMessage` | `Message` | `severity` / `closable` pass straight through |
|
||||
| `NychInputText` | `InputText` | `v-model`, `placeholder`, `type`, `invalid` pass through; drop the local `size="normal"` prop (PrimeVue `size` only accepts `small`/`large`/default — `normal` maps to "don't pass `size`") |
|
||||
| `NychTextarea` | `Textarea` | `v-model`, `rows`, `placeholder`, `invalid` — direct match |
|
||||
| `NychButton` | `Button` | `severity`, `disabled`, `loading`, default + `icon` slots — direct match. The library's `Button` has a built-in loading spinner (laurel wreath), replacing the local `LaurelSpinner` usage inside `NychButton` |
|
||||
| `NychCard` | `Card` | slot rename: local `image` slot → PrimeVue `header` slot; local default slot → PrimeVue `content` slot; `title`/`subtitle` props match |
|
||||
| `LaurelSpinner` (small, used inside `NychButton`) | dropped — covered by `Button`'s built-in loader | |
|
||||
| `LaurelSpinner` (Hero, 580px static, `:spinning="false"`) | kept as a local component, rewritten as a thin wrapper around `NychLoadingIcon` imported from `@nychthemeron/library/components`, with scoped CSS to set `width`/`height` and override/disable the `nych-spin` animation when `spinning` is false | per approved "wrap with CSS overrides" approach |
|
||||
| `LaurelDivider`, `ImageSlot` | unchanged, no library equivalent | |
|
||||
|
||||
## Files affected
|
||||
|
||||
**Deleted:**
|
||||
- `src/components/ds/NychTag.vue`
|
||||
- `src/components/ds/NychMessage.vue`
|
||||
- `src/components/ds/NychInputText.vue`
|
||||
- `src/components/ds/NychTextarea.vue`
|
||||
- `src/components/ds/NychButton.vue`
|
||||
- `src/components/ds/NychCard.vue`
|
||||
- `src/assets/styles/tokens/fonts.css`
|
||||
- `src/assets/styles/tokens/colors.css`
|
||||
- `src/assets/styles/tokens/base.css`
|
||||
|
||||
**Rewritten:**
|
||||
- `src/components/ds/LaurelSpinner.vue` — becomes a thin wrapper around the
|
||||
library's `NychLoadingIcon`, preserving its existing `size` / `color` /
|
||||
`spinning` prop API so `HeroSection.vue` doesn't need to change its usage.
|
||||
- `src/assets/styles/tokens/components.css` → `tokens/laurel-divider.css`
|
||||
(LaurelDivider section only).
|
||||
- `src/assets/styles/styles.css` (updated `@import` manifest).
|
||||
- `src/main.ts` (add PrimeVue + library CSS imports, `app.use(PrimeVue)`).
|
||||
|
||||
**Updated imports/usages:**
|
||||
- `src/components/portfolio/SkillsSection.vue` — `NychTag` from library.
|
||||
- `src/components/portfolio/WorkSection.vue` — `NychCard`, `NychTag` from
|
||||
library; update `NychCard` slot usage (`image` → `header`, default →
|
||||
`content`).
|
||||
- `src/components/portfolio/ContactSection.vue` — `NychInputText`,
|
||||
`NychTextarea`, `NychButton`, `NychMessage` from library.
|
||||
- `src/components/portfolio/HeroSection.vue` — `NychButton` from library;
|
||||
`LaurelSpinner` continues to be imported from `./ds/LaurelSpinner.vue`
|
||||
(rewritten).
|
||||
|
||||
## Verification
|
||||
|
||||
- `bun run build` and `bun run type-check` must pass.
|
||||
- Visually verify via dev server (`bun run dev`):
|
||||
- Hero section renders the large static laurel wreath at the same size.
|
||||
- Skills/Work section tags render with correct severity colors.
|
||||
- Work section cards render header image, title, content, footer tags.
|
||||
- Contact form inputs/textarea/button/message render and behave
|
||||
(loading spinner shows in button while "sending").
|
||||
- Both `hades` and `apollo` themes (toggle `data-theme` on `<html>`) still
|
||||
render correctly.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- No changes to `LaurelDivider.vue` or `ImageSlot.vue` internals.
|
||||
- No changes to other library components not currently used
|
||||
(`Checkbox`, `Select`, `Dialog`, `ToggleSwitch`, `RadioButton`, etc.).
|
||||
- No changes to the CI workflow or registry configuration.
|
||||
|
|
@ -17,13 +17,14 @@
|
|||
"format": "oxfmt src/"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nychthemeron/library": "^0.0.1",
|
||||
"vue": "^3.5.32"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.59.1",
|
||||
"@tsconfig/node24": "^24.0.4",
|
||||
"@types/jsdom": "^28.0.1",
|
||||
"@types/node": "^24.12.2",
|
||||
"@types/node": "^25.9.3",
|
||||
"@vitejs/plugin-vue": "^6.0.6",
|
||||
"@vitest/eslint-plugin": "^1.6.16",
|
||||
"@vue/eslint-config-typescript": "^14.7.0",
|
||||
|
|
|
|||
1
sample.env
Normal file
1
sample.env
Normal file
|
|
@ -0,0 +1 @@
|
|||
VITE_API_URL=http://localhost:5173/
|
||||
52
src/__tests__/composables/useAPI.spec.ts
Normal file
52
src/__tests__/composables/useAPI.spec.ts
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
|
||||
import { useAPI } from "../../composables/useAPI";
|
||||
|
||||
describe("useAPI", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("creates properly", () => {
|
||||
const api = useAPI();
|
||||
expect(api).not.toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns data", async () => {
|
||||
const mockData = {
|
||||
something: "test",
|
||||
};
|
||||
|
||||
globalThis.fetch = vi.fn(() =>
|
||||
Promise.resolve({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(mockData),
|
||||
} as Response),
|
||||
);
|
||||
|
||||
const api = useAPI();
|
||||
|
||||
const data = await api.getPortfolioData();
|
||||
|
||||
expect(data).toStrictEqual(mockData);
|
||||
});
|
||||
|
||||
it("returns undefined on failure", async () => {
|
||||
const mockData = {
|
||||
something: "test",
|
||||
};
|
||||
|
||||
globalThis.fetch = vi.fn(() =>
|
||||
Promise.reject({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(mockData),
|
||||
} as Response),
|
||||
);
|
||||
|
||||
const api = useAPI();
|
||||
|
||||
const data = await api.getPortfolioData();
|
||||
|
||||
expect(data).toStrictEqual(undefined);
|
||||
});
|
||||
});
|
||||
20
src/composables/useAPI.ts
Normal file
20
src/composables/useAPI.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import { useUtils } from "./useUtils";
|
||||
|
||||
export const useAPI = (baseURL = API_URL) => {
|
||||
const { url } = useUtils();
|
||||
|
||||
const normalizedURL = url.normalizePath(baseURL.trim());
|
||||
|
||||
const generateURL = (path: string) => {
|
||||
return normalizedURL + path;
|
||||
};
|
||||
|
||||
const getPortfolioData = async () =>
|
||||
fetch(generateURL("/portfolio/"))
|
||||
.then((resp) => resp.json())
|
||||
.catch(() => undefined);
|
||||
|
||||
return {
|
||||
getPortfolioData,
|
||||
};
|
||||
};
|
||||
11
src/composables/useUtils.ts
Normal file
11
src/composables/useUtils.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
export const useUtils = () => {
|
||||
const normalizePath = (path: string) => {
|
||||
path.endsWith("/") ? path.slice(0, -1) : path;
|
||||
};
|
||||
|
||||
return {
|
||||
url: {
|
||||
normalizePath,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
|
||||
"include": ["vite-env.d.ts", "env.d.ts", "src/**/*", "src/**/*.vue"],
|
||||
"exclude": ["src/**/__tests__/*"],
|
||||
"compilerOptions": {
|
||||
// Extra safety for array and object lookups, but may have false positives.
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
"compilerOptions": {
|
||||
// Vitest runs in a different environment than the application code.
|
||||
// Adjust lib and types accordingly.
|
||||
"lib": [],
|
||||
"lib": ["dom", "es2015", "esnext"],
|
||||
"types": ["node", "jsdom"],
|
||||
|
||||
// `vue-tsc --build` produces a .tsbuildinfo file for incremental type-checking.
|
||||
|
|
|
|||
13
vite-env.d.ts
vendored
Normal file
13
vite-env.d.ts
vendored
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
interface ViteTypeOptions {
|
||||
strictImportMetaEnv: unknown;
|
||||
}
|
||||
|
||||
interface ImportMetaEnv {
|
||||
readonly VITE_API_URL: string;
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv;
|
||||
}
|
||||
|
||||
declare const API_URL: string;
|
||||
|
|
@ -1,18 +1,24 @@
|
|||
import { fileURLToPath, URL } from 'node:url'
|
||||
import { fileURLToPath, URL } from "node:url";
|
||||
import { defineConfig, loadEnv } from "vite";
|
||||
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import vueDevTools from 'vite-plugin-vue-devtools'
|
||||
import vue from "@vitejs/plugin-vue";
|
||||
import vueDevTools from "vite-plugin-vue-devtools";
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
vueDevTools(),
|
||||
],
|
||||
export default defineConfig(({ mode }) => {
|
||||
const env = loadEnv(mode, process.cwd(), "VITE");
|
||||
const baseURL = fileURLToPath(new URL("./src", import.meta.url));
|
||||
|
||||
return {
|
||||
define: {
|
||||
API_URL: JSON.stringify(env.VITE_API_URL),
|
||||
},
|
||||
plugins: [vue(), vueDevTools()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||
"@": baseURL,
|
||||
"~": baseURL,
|
||||
},
|
||||
},
|
||||
})
|
||||
};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,14 +1,16 @@
|
|||
import { fileURLToPath } from 'node:url'
|
||||
import { mergeConfig, defineConfig, configDefaults } from 'vitest/config'
|
||||
import viteConfig from './vite.config'
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { mergeConfig, defineConfig, configDefaults } from "vitest/config";
|
||||
import viteConfig from "./vite.config";
|
||||
|
||||
export default mergeConfig(
|
||||
viteConfig,
|
||||
export default defineConfig((configEnv) =>
|
||||
mergeConfig(
|
||||
viteConfig(configEnv),
|
||||
defineConfig({
|
||||
test: {
|
||||
environment: 'jsdom',
|
||||
exclude: [...configDefaults.exclude, 'e2e/**'],
|
||||
root: fileURLToPath(new URL('./', import.meta.url)),
|
||||
environment: "jsdom",
|
||||
exclude: [...configDefaults.exclude, "e2e/**"],
|
||||
root: fileURLToPath(new URL("./", import.meta.url)),
|
||||
},
|
||||
}),
|
||||
)
|
||||
),
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in a new issue