diff --git a/__tests__/components.test.ts b/__tests__/components.test.ts index b385c873..d29c979a 100644 --- a/__tests__/components.test.ts +++ b/__tests__/components.test.ts @@ -1,7 +1,8 @@ -import { describe, test, expect } from 'vitest'; +import { describe, test, expect, afterEach } from 'vitest'; import { mount } from '@vue/test-utils'; import { createRouter, createMemoryHistory } from 'vue-router'; import AppHeader from '../src/components/AppHeader.vue'; +import { i18n, setLocale } from '../src/plugins/i18n'; function createTestRouter() { return createRouter({ @@ -13,6 +14,14 @@ function createTestRouter() { }); } +const stubs = { + 'v-btn': { template: '' } +}; + +afterEach(() => { + setLocale('ja'); +}); + describe('AppHeader', () => { test('renders navigation links', async () => { const router = createTestRouter(); @@ -20,11 +29,31 @@ describe('AppHeader', () => { await router.isReady(); const wrapper = mount(AppHeader, { - global: { plugins: [router] } + global: { plugins: [router, i18n], stubs } }); expect(wrapper.findAll('li').length).toBeGreaterThan(0); + expect(wrapper.text()).toContain('ホーム'); + expect(wrapper.text()).toContain('プロフィール'); + }); + + test('toggles language between ja and en', async () => { + const router = createTestRouter(); + router.push('/'); + await router.isReady(); + + const wrapper = mount(AppHeader, { + global: { plugins: [router, i18n], stubs } + }); + + expect(wrapper.text()).toContain('ホーム'); + expect(wrapper.text()).not.toContain('Home'); + + await wrapper.find('.lang-toggle').trigger('click'); + expect(wrapper.text()).toContain('Home'); expect(wrapper.text()).toContain('Profile'); + expect(wrapper.text()).toContain('日本語'); + expect(wrapper.text()).not.toContain('ホーム'); }); }); diff --git a/__tests__/pages.test.ts b/__tests__/pages.test.ts index 59b94abe..4cf43050 100644 --- a/__tests__/pages.test.ts +++ b/__tests__/pages.test.ts @@ -7,6 +7,7 @@ import ProjectsPage from '../src/pages/ProjectsPage.vue'; import UserCareer from '../src/pages/UserCareer.vue'; import UserContact from '../src/pages/UserContact.vue'; import UserProfile from '../src/pages/UserProfile.vue'; +import { i18n } from '../src/plugins/i18n'; function createTestRouter() { return createRouter({ @@ -48,13 +49,13 @@ describe('HomePage', () => { await router.isReady(); const wrapper = mount(HomePage, { - global: { plugins: [router] } + global: { plugins: [router, i18n] } }); expect(wrapper.find('.hero').exists()).toBe(true); - expect(wrapper.find('.hero-badge').text()).toBe('Portfolio'); + expect(wrapper.find('.hero-badge').text()).toBe('ポートフォリオ'); expect(wrapper.find('.accent').text()).toBe('こめっと'); - expect(wrapper.find('.hero-sub').text()).toBe('Software Engineer'); + expect(wrapper.find('.hero-sub').text()).toBe('ソフトウェアエンジニア'); }); test('renders CTA buttons', async () => { @@ -63,7 +64,7 @@ describe('HomePage', () => { await router.isReady(); const wrapper = mount(HomePage, { - global: { plugins: [router] } + global: { plugins: [router, i18n] } }); const buttons = wrapper.findAll('button'); @@ -78,7 +79,7 @@ describe('HomePage', () => { await router.isReady(); const wrapper = mount(HomePage, { - global: { plugins: [router] } + global: { plugins: [router, i18n] } }); const pushSpy = vi.spyOn(router, 'push'); @@ -92,7 +93,7 @@ describe('HomePage', () => { await router.isReady(); const wrapper = mount(HomePage, { - global: { plugins: [router] } + global: { plugins: [router, i18n] } }); const pushSpy = vi.spyOn(router, 'push'); @@ -108,7 +109,7 @@ describe('NotFoundPage', () => { await router.isReady(); const wrapper = mount(NotFoundPage, { - global: { plugins: [router] } + global: { plugins: [router, i18n] } }); expect(wrapper.find('#not-found').exists()).toBe(true); @@ -122,7 +123,7 @@ describe('NotFoundPage', () => { await router.isReady(); const wrapper = mount(NotFoundPage, { - global: { plugins: [router] } + global: { plugins: [router, i18n] } }); const button = wrapper.find('.btn-primary'); @@ -138,11 +139,11 @@ describe('UserCareer', () => { await router.isReady(); const wrapper = mount(UserCareer, { - global: { plugins: [router], stubs: vuetifyStubs } + global: { plugins: [router, i18n], stubs: vuetifyStubs } }); expect(wrapper.find('#contact').exists()).toBe(true); - expect(wrapper.text()).toContain('Career'); + expect(wrapper.text()).toContain('経歴'); expect(wrapper.text()).toContain('職務経歴について'); }); @@ -152,7 +153,7 @@ describe('UserCareer', () => { await router.isReady(); const wrapper = mount(UserCareer, { - global: { plugins: [router], stubs: vuetifyStubs } + global: { plugins: [router, i18n], stubs: vuetifyStubs } }); const items = wrapper.findAll('.v-timeline-item'); @@ -170,7 +171,7 @@ describe('UserCareer', () => { await router.isReady(); const wrapper = mount(UserCareer, { - global: { plugins: [router], stubs: vuetifyStubs } + global: { plugins: [router, i18n], stubs: vuetifyStubs } }); expect(wrapper.text()).toContain('情報工学科'); @@ -185,11 +186,11 @@ describe('UserContact', () => { await router.isReady(); const wrapper = mount(UserContact, { - global: { plugins: [router], stubs: vuetifyStubs } + global: { plugins: [router, i18n], stubs: vuetifyStubs } }); expect(wrapper.find('#contact').exists()).toBe(true); - expect(wrapper.text()).toContain('Contact'); + expect(wrapper.text()).toContain('お問い合わせ'); }); test('renders email information', async () => { @@ -198,7 +199,7 @@ describe('UserContact', () => { await router.isReady(); const wrapper = mount(UserContact, { - global: { plugins: [router], stubs: vuetifyStubs } + global: { plugins: [router, i18n], stubs: vuetifyStubs } }); expect(wrapper.text()).toContain('Email'); @@ -214,13 +215,13 @@ describe('UserProfile', () => { await router.isReady(); const wrapper = mount(UserProfile, { - global: { plugins: [router], stubs: vuetifyStubs } + global: { plugins: [router, i18n], stubs: vuetifyStubs } }); expect(wrapper.find('#profile').exists()).toBe(true); - expect(wrapper.text()).toContain('Profile'); - expect(wrapper.text()).toContain('Comet / こめっと'); - expect(wrapper.text()).toContain('Software Engineer'); + expect(wrapper.text()).toContain('プロフィール'); + expect(wrapper.text()).toContain('こめっと'); + expect(wrapper.text()).toContain('ソフトウェアエンジニア'); }); test('renders external links', async () => { @@ -229,7 +230,7 @@ describe('UserProfile', () => { await router.isReady(); const wrapper = mount(UserProfile, { - global: { plugins: [router], stubs: vuetifyStubs } + global: { plugins: [router, i18n], stubs: vuetifyStubs } }); const links = wrapper.findAll('a'); @@ -245,7 +246,7 @@ describe('UserProfile', () => { await router.isReady(); const wrapper = mount(UserProfile, { - global: { plugins: [router], stubs: vuetifyStubs } + global: { plugins: [router, i18n], stubs: vuetifyStubs } }); expect(wrapper.text()).toContain('技術選定'); @@ -261,7 +262,7 @@ describe('UserProfile', () => { await router.isReady(); const wrapper = mount(UserProfile, { - global: { plugins: [router], stubs: vuetifyStubs } + global: { plugins: [router, i18n], stubs: vuetifyStubs } }); expect(wrapper.text()).toContain('基本情報技術者試験'); @@ -278,7 +279,7 @@ describe('UserProfile', () => { await router.isReady(); const wrapper = mount(UserProfile, { - global: { plugins: [router], stubs: vuetifyStubs } + global: { plugins: [router, i18n], stubs: vuetifyStubs } }); const links = wrapper.findAll('a'); @@ -296,11 +297,11 @@ describe('ProjectsPage', () => { await router.isReady(); const wrapper = mount(ProjectsPage, { - global: { plugins: [router], stubs: vuetifyStubs } + global: { plugins: [router, i18n], stubs: vuetifyStubs } }); expect(wrapper.find('#projects').exists()).toBe(true); - expect(wrapper.text()).toContain('Projects'); + expect(wrapper.text()).toContain('プロジェクト'); }); test('renders backoff-util project card', async () => { @@ -309,7 +310,7 @@ describe('ProjectsPage', () => { await router.isReady(); const wrapper = mount(ProjectsPage, { - global: { plugins: [router], stubs: vuetifyStubs } + global: { plugins: [router, i18n], stubs: vuetifyStubs } }); expect(wrapper.text()).toContain('backoff-util'); @@ -322,7 +323,7 @@ describe('ProjectsPage', () => { await router.isReady(); const wrapper = mount(ProjectsPage, { - global: { plugins: [router], stubs: vuetifyStubs } + global: { plugins: [router, i18n], stubs: vuetifyStubs } }); const hrefs = wrapper.findAll('a').map((l) => l.attributes('href')); @@ -336,7 +337,7 @@ describe('ProjectsPage', () => { await router.isReady(); const wrapper = mount(ProjectsPage, { - global: { plugins: [router], stubs: vuetifyStubs } + global: { plugins: [router, i18n], stubs: vuetifyStubs } }); const links = wrapper.findAll('a'); diff --git a/e2e/smoke.spec.ts b/e2e/smoke.spec.ts index ddb0d1df..2da0b567 100644 --- a/e2e/smoke.spec.ts +++ b/e2e/smoke.spec.ts @@ -1,5 +1,13 @@ import { test, expect } from '@playwright/test'; +// i18n 導入後、デフォルト言語は日本語。テストは英語 UI を前提としているため、 +// ページ読み込み前に localStorage へ locale を設定して英語モードで開始する。 +test.beforeEach(async ({ page }) => { + await page.addInitScript(() => { + localStorage.setItem('locale', 'en'); + }); +}); + for (const { path, name } of [ { path: '/', name: 'Home' }, { path: '/profile', name: 'Profile' }, @@ -31,5 +39,5 @@ test('navigates via header links', async ({ page }) => { test('renders 404 for unknown route', async ({ page }) => { await page.goto('/#/unknown-page'); - await expect(page.getByText('ページが見つかりません')).toBeVisible(); + await expect(page.getByText('Page Not Found')).toBeVisible(); }); diff --git a/package.json b/package.json index 67b26142..877cca02 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "aecomet.github.io", - "version": "8.13.0", + "version": "9.0.0", "description": "My portfolio.", "scripts": { "dev": "vite", @@ -44,6 +44,7 @@ "dependencies": { "@mdi/js": "^7.4.47", "vue": "^3.5.41", + "vue-i18n": "^11.4.8", "vue-router": "^5.2.0", "vuetify": "^4.1.8" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 54172a4f..44f5fdb1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,6 +14,9 @@ importers: vue: specifier: ^3.5.41 version: 3.5.41(typescript@7.0.2) + vue-i18n: + specifier: ^11.4.8 + version: 11.4.8(vue@3.5.41(typescript@7.0.2)) vue-router: specifier: ^5.2.0 version: 5.2.0(@vue/compiler-sfc@3.5.41)(rolldown@1.2.3)(vite@8.2.1(@types/node@26.2.0)(sass@1.102.0)(terser@5.49.2)(yaml@2.9.0))(vue@3.5.41(typescript@7.0.2)) @@ -187,6 +190,22 @@ packages: '@noble/hashes': optional: true + '@intlify/core-base@11.4.8': + resolution: {integrity: sha512-A+Q7SKm5oEcy1E/cghqd7n/St4XjTqLhiiyDuieNcMrJcrHlkY5n0jp7Q9dD3txvVHzvsmBVV5M9wD5/s1zfzw==} + engines: {node: '>= 22'} + + '@intlify/devtools-types@11.4.8': + resolution: {integrity: sha512-MGpID+rlfzGUbNcnC20bm5NMSBHPrvx0atLTfv9dftn3kjXw1hGKDcIcwrO99tSrZEc2i+hczRL7ks8qXsHPkQ==} + engines: {node: '>= 22'} + + '@intlify/message-compiler@11.4.8': + resolution: {integrity: sha512-vbzk17dYwduYiv52EK61+FDCyhfVg1uPUtPmiD/d45W99uJIcXywrweOBcHv7n9/iEqmXiMGT52bgJbZDQqK3w==} + engines: {node: '>= 22'} + + '@intlify/shared@11.4.8': + resolution: {integrity: sha512-XbRgrv+XEuvDr7UCY55oibVrh+o4u+A0VB6nSL0F5Z8LcZxE/8j573LYG6bCrOigIcHdGpSNI7Rh5UpC5/B/eg==} + engines: {node: '>= 22'} + '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -868,6 +887,9 @@ packages: '@vue/compiler-ssr@3.5.41': resolution: {integrity: sha512-U3v5OejKEGqOI0Wy0+Sz7hGuIFZHA4LSXzrNM3IMIeDyJEBBfTpX26n3SDgToRpP2bLc9FfI2j/kSgcJ8Emq5A==} + '@vue/devtools-api@6.6.4': + resolution: {integrity: sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==} + '@vue/devtools-api@8.2.1': resolution: {integrity: sha512-6u4vXBlIBAC1wMplIZgpyPn7uh/s4Bf6F5bMzvLv+EdJ0aHs/+4B7Ygv864EStQSjRbsRzTko/kUG1A1IejQ3A==} @@ -1993,6 +2015,12 @@ packages: vue-component-type-helpers@3.3.9: resolution: {integrity: sha512-3c/UfMe0SqyEfcGTyH7mfshHagJ9QTCbppCb0/uGpHZpFug7+If3GeGZN7I0YheKEExemx3xldQPoO7PQSOLQg==} + vue-i18n@11.4.8: + resolution: {integrity: sha512-0ULeHP6Z9CGvAm67S77ZEp41cfGXIREGL8qfhos2BMgcQQewtQcDKuojt6jjasAD/S8GwfTp2ySPmDSpwvrCMQ==} + engines: {node: '>= 22'} + peerDependencies: + vue: ^3.0.0 + vue-router@5.2.0: resolution: {integrity: sha512-QAC5i0LEb1GLG0LXDQmHu8L7FX12j0KwU/JTKmLQUJMrn04gQdKP6Du+p0QwpHb3iy71vBlqnHQ8WAfOSAWhqw==} peerDependencies: @@ -2191,6 +2219,24 @@ snapshots: '@exodus/bytes@1.15.1': {} + '@intlify/core-base@11.4.8': + dependencies: + '@intlify/devtools-types': 11.4.8 + '@intlify/message-compiler': 11.4.8 + '@intlify/shared': 11.4.8 + + '@intlify/devtools-types@11.4.8': + dependencies: + '@intlify/core-base': 11.4.8 + '@intlify/shared': 11.4.8 + + '@intlify/message-compiler@11.4.8': + dependencies: + '@intlify/shared': 11.4.8 + source-map-js: 1.2.1 + + '@intlify/shared@11.4.8': {} + '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 @@ -2643,6 +2689,8 @@ snapshots: '@vue/compiler-dom': 3.5.41 '@vue/shared': 3.5.41 + '@vue/devtools-api@6.6.4': {} + '@vue/devtools-api@8.2.1': dependencies: '@vue/devtools-kit': 8.2.1 @@ -3743,6 +3791,14 @@ snapshots: vue-component-type-helpers@3.3.9: {} + vue-i18n@11.4.8(vue@3.5.41(typescript@7.0.2)): + dependencies: + '@intlify/core-base': 11.4.8 + '@intlify/devtools-types': 11.4.8 + '@intlify/shared': 11.4.8 + '@vue/devtools-api': 6.6.4 + vue: 3.5.41(typescript@7.0.2) + vue-router@5.2.0(@vue/compiler-sfc@3.5.41)(rolldown@1.2.3)(vite@8.2.1(@types/node@26.2.0)(sass@1.102.0)(terser@5.49.2)(yaml@2.9.0))(vue@3.5.41(typescript@7.0.2)): dependencies: '@babel/generator': 8.0.0 diff --git a/src/app.ts b/src/app.ts index 23a9385b..14a76274 100755 --- a/src/app.ts +++ b/src/app.ts @@ -2,6 +2,7 @@ // Libraries import App from '@src/App.vue'; import '@src/assets/style.scss'; +import { i18n } from '@src/plugins/i18n'; import { router } from '@src/plugins/router'; import MyVuetify from '@src/plugins/vuetify'; import { ViteSSG } from 'vite-ssg/single-page'; @@ -9,4 +10,5 @@ import { ViteSSG } from 'vite-ssg/single-page'; export const createApp = ViteSSG(App, ({ app }) => { app.use(router); app.use(MyVuetify); + app.use(i18n); }); diff --git a/src/assets/404.html b/src/assets/404.html index 33694bb7..6099d7e9 100644 --- a/src/assets/404.html +++ b/src/assets/404.html @@ -34,6 +34,10 @@ box-sizing: border-box; } + [hidden] { + display: none !important; + } + html, body { margin: 0; @@ -278,14 +282,55 @@ -

+

お探しのページは存在しないか、移動された可能性があります。 トップページへ戻って再度アクセスしてください。

+
- トップページへ移動 + トップページへ移動 +
+ diff --git a/src/assets/style.scss b/src/assets/style.scss index 20a8d293..1214d822 100644 --- a/src/assets/style.scss +++ b/src/assets/style.scss @@ -70,6 +70,16 @@ nav { backdrop-filter: blur(0px); } +.brand { + font-family: 'DM Mono', monospace; + font-size: 0.9rem; + letter-spacing: 0.12em; + text-transform: uppercase; + color: var(--text); + text-decoration: none; + margin-right: auto; +} + .nav-links { display: flex; gap: 2.5rem; @@ -83,6 +93,7 @@ nav { text-transform: uppercase; color: var(--muted); text-decoration: none; + white-space: nowrap; transition: color 0.3s; cursor: pointer; } @@ -552,10 +563,22 @@ nav { /* ── RESPONSIVE ── */ @media (max-width: 768px) { nav { - padding: 1.2rem 1.5rem; + padding: 10px 24px; + flex-wrap: wrap; + row-gap: 6px; } .nav-links { - gap: 1.5rem; + order: 3; + flex: 1 1 100%; + justify-content: flex-end; + gap: 12px; + } + .page { + padding-top: 10rem; + } + #home { + align-items: flex-start; + padding-top: 10rem; } .container { padding: 0 1.5rem; @@ -569,6 +592,20 @@ nav { } } +@media (max-width: 480px) { + nav { + padding: 10px 16px; + } + .nav-links { + gap: 8px; + flex-wrap: nowrap; + } + .nav-links a { + font-size: 0.55rem; + letter-spacing: 0.1em; + } +} + /* ── CURSOR GLOW ── */ .cursor-glow { position: fixed; diff --git a/src/components/AppHeader.vue b/src/components/AppHeader.vue index 52aaa764..fc0d2bda 100644 --- a/src/components/AppHeader.vue +++ b/src/components/AppHeader.vue @@ -1,29 +1,252 @@ + + diff --git a/src/locales/en.json b/src/locales/en.json new file mode 100644 index 00000000..6b71b349 --- /dev/null +++ b/src/locales/en.json @@ -0,0 +1,123 @@ +{ + "brand": { + "name": "Comet" + }, + "nav": { + "home": "Home", + "profile": "Profile", + "career": "Career", + "projects": "Projects", + "contact": "Contact", + "languageTitle": "Switch language", + "toEnglish": "EN", + "toJapanese": "日本語" + }, + "home": { + "badge": "Portfolio", + "subtitle": "Software Engineer", + "ctaProfile": "Profile", + "ctaCareer": "View Career" + }, + "profile": { + "label": "— Profile", + "role": "Software Engineer", + "links": "External Links", + "experience": "Areas of Experience", + "certifications": "Certifications", + "experiences": { + "techSelection": "Technology Selection", + "systemArchitect": "System Architecture", + "teamLead": "Team Leadership", + "incidentResponse": "Incident Response", + "development": "Implementation & Maintenance" + }, + "certifications": { + "fe": "Fundamental Information Technology Engineer Examination", + "ap": "Applied Information Technology Engineer Examination", + "security": "Information Security Specialist Examination", + "database": "Database Specialist Examination", + "architect": "Systems Architect Examination", + "eiken": "EIKEN Grade 2" + } + }, + "career": { + "label": "— Career", + "title": "Career History", + "description": "For a detailed career history, feel free to email me and I'll send you my résumé.", + "items": { + "highSchool": { + "date": "March 2013", + "header": "Graduated from High School", + "note": "I wasn't directly exposed to programming in high school, but editing videos for the school festival sparked my interest in computers." + }, + "university": { + "date": "April 2013", + "header": "Entered University", + "note": "Majored in information engineering. During my studies I worked part-time as a programmer at a startup, where I first got hands-on with PHP, Ruby, and jQuery. In class I also learned the basics of Java, C, and Perl." + }, + "gradUniversity": { + "date": "March 2017", + "header": "Graduated from University", + "note": "" + }, + "gradSchool": { + "date": "April 2017", + "header": "Entered Graduate School", + "note": "Pursued a master's degree in information engineering while continuing my part-time programming work and joining company internships in summer and winter. My research involved building applications with Django (Python) and Vuex." + }, + "gradGradSchool": { + "date": "March 2019", + "header": "Completed Graduate School", + "note": "" + }, + "company": { + "date": "April 2019", + "header": "Joined a Company as an Engineer", + "note": "Joined the development team of a shared infrastructure platform supporting social game development as an application engineer. For the first few years I built systems with Ruby, Java, Python, and Vue, and later took charge of detailed system design with UML as the lead on client projects. Since then I've served as the team lead driving feature development for microservices that manage authentication and game users, and I continue to lead a team of five." + }, + "securityExam": { + "date": "June 2023", + "header": "Passed the Information Security Specialist Examination", + "note": "" + }, + "databaseExam": { + "date": "December 2023", + "header": "Passed the Database Specialist Examination", + "note": "" + }, + "architectExam": { + "date": "July 2024", + "header": "Passed the Systems Architect Examination", + "note": "" + }, + "eiken": { + "date": "August 2024", + "header": "Passed EIKEN Grade 2", + "note": "" + }, + "newRole": { + "date": "March 2026", + "header": "Changed Jobs and Started a New Challenge", + "note": "To be continued..." + } + } + }, + "projects": { + "label": "— Projects", + "title": "Projects", + "desc": "A showcase of the libraries and sites I've published as personal projects.", + "backoffDesc": "A TypeScript utility library that adds exponential backoff retry logic to any async function. Zero dependencies, with support for both ESM and CJS.", + "moreHint": "※ More projects are coming soon" + }, + "contact": { + "label": "— Contact", + "email": "Email", + "emailNote": "※ Replace *** with gmail.com" + }, + "notFound": { + "label": "— Not Found", + "title": "Page Not Found", + "description": "The page you're looking for doesn't exist or may have been moved.", + "back": "Back to Home" + } +} diff --git a/src/locales/ja.json b/src/locales/ja.json new file mode 100644 index 00000000..1c33d6d8 --- /dev/null +++ b/src/locales/ja.json @@ -0,0 +1,123 @@ +{ + "brand": { + "name": "こめっと" + }, + "nav": { + "home": "ホーム", + "profile": "プロフィール", + "career": "経歴", + "projects": "プロジェクト", + "contact": "お問い合わせ", + "languageTitle": "言語を切り替える", + "toEnglish": "EN", + "toJapanese": "日本語" + }, + "home": { + "badge": "ポートフォリオ", + "subtitle": "ソフトウェアエンジニア", + "ctaProfile": "プロフィール", + "ctaCareer": "経歴を見る" + }, + "profile": { + "label": "— プロフィール", + "role": "ソフトウェアエンジニア", + "links": "外部リンク", + "experience": "経験領域", + "certifications": "試験合格実績", + "experiences": { + "techSelection": "技術選定", + "systemArchitect": "システムアーキテクト", + "teamLead": "チームリーダー", + "incidentResponse": "障害対応", + "development": "実装・保守" + }, + "certifications": { + "fe": "基本情報技術者試験", + "ap": "応用情報技術者試験", + "security": "情報処理安全確保支援士試験", + "database": "データベーススペシャリスト試験", + "architect": "システムアーキテクト試験", + "eiken": "英検2級" + } + }, + "career": { + "label": "— 経歴", + "title": "職務経歴について", + "description": "詳しい職務経歴についてはメールしていただければ職務経歴書を送付します", + "items": { + "highSchool": { + "date": "2013年3月", + "header": "高等学校卒業", + "note": "高校ではプログラミングに直接関わる機会はなかったが文化祭で動画編集を担当したことがきっかけでコンピュータ関連に興味を持つ。" + }, + "university": { + "date": "2013年4月", + "header": "大学入学", + "note": "情報工学科。在学中にベンチャー企業にてプログラミングのアルバイトを行う。このとき初めてPHP, Ruby, jQueryを触る。授業では簡単なJava, C, Perlを学ぶ" + }, + "gradUniversity": { + "date": "2017年3月", + "header": "大学卒業", + "note": "" + }, + "gradSchool": { + "date": "2017年4月", + "header": "大学院入学", + "note": "情報工学専攻。プログラミングのアルバイトは継続。その他夏冬に企業インターンシップに参加。研究でPythonのDjango, Vuexを用いたアプリケーション開発を行う。" + }, + "gradGradSchool": { + "date": "2019年3月", + "header": "大学院卒業", + "note": "" + }, + "company": { + "date": "2019年4月", + "header": "企業に入社", + "note": "ソーシャルゲーム開発を支援する共通基盤システムの開発チームにアプリケーションエンジニアとして入社。入社後数年はRuby、Java、 Python、Vueを用いたシステム開発に従事。その後は案件の主担当としてUMLを用いたシステムの詳細設計も担当。その後、認証やゲームユーザーを管理するマイクロサービスの機能開発を主導するチームリーダーに就任し、5人のメンバーをリードしながらシステム開発を続けている。" + }, + "securityExam": { + "date": "2023年6月", + "header": "情報処理安全確保支援士試験 合格", + "note": "" + }, + "databaseExam": { + "date": "2023年12月", + "header": "データベーススペシャリスト試験 合格", + "note": "" + }, + "architectExam": { + "date": "2024年7月", + "header": "システムアーキテクト試験 合格", + "note": "" + }, + "eiken": { + "date": "2024年8月", + "header": "英検2級 合格", + "note": "" + }, + "newRole": { + "date": "2026年3月", + "header": "転職して新しい環境での挑戦を開始", + "note": "To be continued..." + } + } + }, + "projects": { + "label": "— プロジェクト", + "title": "プロジェクト", + "desc": "個人開発で公開しているライブラリ・サイトの紹介です。", + "backoffDesc": "任意の async 関数に指数バックオフ付きのリトライ処理を組み込める TypeScript ユーティリティライブラリ。ゼロ依存で ESM / CJS 両対応。", + "moreHint": "※ 他のプロジェクトも随時追加予定" + }, + "contact": { + "label": "— お問い合わせ", + "email": "Email", + "emailNote": "※ ***にはgmail.comを入力してください" + }, + "notFound": { + "label": "— 見つかりません", + "title": "ページが見つかりません", + "description": "指定されたURLは存在しないか、移動された可能性があります。", + "back": "トップページへ戻る" + } +} diff --git a/src/pages/HomePage.vue b/src/pages/HomePage.vue index f52d5e23..91883ad0 100644 --- a/src/pages/HomePage.vue +++ b/src/pages/HomePage.vue @@ -1,22 +1,24 @@ diff --git a/src/pages/NotFoundPage.vue b/src/pages/NotFoundPage.vue index 694fe72f..60fe514c 100644 --- a/src/pages/NotFoundPage.vue +++ b/src/pages/NotFoundPage.vue @@ -1,16 +1,19 @@ diff --git a/src/pages/ProjectsPage.vue b/src/pages/ProjectsPage.vue index 99b066e5..17b40333 100644 --- a/src/pages/ProjectsPage.vue +++ b/src/pages/ProjectsPage.vue @@ -1,10 +1,10 @@ - +