Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
},
"dependencies": {
"@lit-labs/router": "^0.1.4",
"@lit/context": "^1.1.6",
"lit": "^3.3.2",
"urlpattern-polyfill": "^10.1.0"
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import { css, html, LitElement } from 'lit'
import { customElement, property } from 'lit/decorators.js'
import { adminFormStyles } from '../../../admin/admin-form-styles.js'
import type { MeCertification } from '../../../admin/types.js'
import '../ui/me-admin-panel.js'
import '../ui/me-admin-section.js'
import '../ui/me-text-input.js'

@customElement('me-profile-certifications-editor')
export class MeProfileCertificationsEditor extends LitElement {
@property({ type: Array }) certifications: MeCertification[] = []

private dispatchChange(next: MeCertification[]) {
this.dispatchEvent(
new CustomEvent<MeCertification[]>('change', {
detail: next,
bubbles: true,
composed: true,
}),
)
}

private addItem = () => {
const next = [
...this.certifications,
{ name: '', issuer: '', year: new Date().getFullYear() },
]
this.dispatchChange(next)
}

private removeItem(index: number) {
const next = this.certifications.filter((_, i) => i !== index)
this.dispatchChange(next)
}

private updateItem(index: number, patch: Partial<MeCertification>) {
const next = [...this.certifications]
next[index] = { ...next[index], ...patch }
this.dispatchChange(next)
}

render() {
return html`
<me-admin-section
title="Certifications"
description="month は任意です。年だけでも掲載できます。"
>
<button
slot="header-actions"
type="button"
class="subtle"
@click=${this.addItem}
>
資格を追加
</button>

<div class="stack">
${
this.certifications.length === 0
? html`<p class="empty-text">資格がまだありません。</p>`
: this.certifications.map(
(cert, index) => html`
<me-admin-panel title="資格 ${index + 1}">
<button
slot="header-actions"
type="button"
class="subtle danger"
@click=${() => this.removeItem(index)}
>
削除
</button>

<div class="grid">
<me-text-input
label="資格名"
.value=${cert.name}
@change=${(e: CustomEvent) =>
this.updateItem(index, { name: e.detail })}
></me-text-input>

<me-text-input
label="Issuer"
.value=${cert.issuer}
@change=${(e: CustomEvent) =>
this.updateItem(index, { issuer: e.detail })}
></me-text-input>

<me-text-input
label="Year"
type="number"
.value=${String(cert.year)}
@change=${(e: CustomEvent) =>
this.updateItem(index, {
year: Number(e.detail || '0'),
})}
></me-text-input>
Comment on lines +107 to +115
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

年が空の場合に0が設定される可能性があります。

e.detail || '0' により、ユーザーが年を空にした場合に year: 0 が設定されます。これは意図した動作でしょうか?バリデーションエラーとして扱うか、undefined を許容する方が適切かもしれません。

🛡️ 修正案: 空の場合は現在年をフォールバックとして使用
                      <me-text-input
                        label="Year"
                        type="number"
                        .value=${String(cert.year)}
                        `@change`=${(e: CustomEvent) =>
                          this.updateItem(index, {
-                           year: Number(e.detail || '0'),
+                           year: e.detail ? Number(e.detail) : new Date().getFullYear(),
                          })}
                      ></me-text-input>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<me-text-input
label="Year"
type="number"
.value=${String(cert.year)}
@change=${(e: CustomEvent) =>
this.updateItem(index, {
year: Number(e.detail || '0'),
})}
></me-text-input>
<me-text-input
label="Year"
type="number"
.value=${String(cert.year)}
`@change`=${(e: CustomEvent) =>
this.updateItem(index, {
year: e.detail ? Number(e.detail) : new Date().getFullYear(),
})}
></me-text-input>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/components/admin/profile/me-profile-certifications-editor.ts`
around lines 88 - 96, The change handler for the me-text-input currently uses
Number(e.detail || '0') which will coerce an empty input into year: 0; update
the handler in the me-text-input `@change` to detect an empty detail (e.detail ===
'' or null/undefined) and instead set year to a sensible fallback (e.g., new
Date().getFullYear()) or to undefined if you want to treat it as absent, by
calling this.updateItem(index, { year: fallbackYear }) or { year: undefined }
accordingly; modify the updateItem call site that expects a numeric year to
accept undefined if you choose that route.


<me-text-input
label="Month"
type="number"
.value=${cert.month ? String(cert.month) : ''}
@change=${(e: CustomEvent) =>
this.updateItem(index, {
month: e.detail ? Number(e.detail) : undefined,
})}
></me-text-input>
</div>
</me-admin-panel>
`,
)
}
</div>
</me-admin-section>
`
}

static styles = [
adminFormStyles,
css`
:host {
display: block;
}

.stack {
display: grid;
gap: 20px;
}

.grid {
display: grid;
gap: 16px;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
}

.empty-text {
color: var(--color-text-tertiary);
font-size: 14px;
font-style: italic;
}
`,
]
}

declare global {
interface HTMLElementTagNameMap {
'me-profile-certifications-editor': MeProfileCertificationsEditor
}
}
149 changes: 149 additions & 0 deletions frontend/src/components/admin/profile/me-profile-experiences-editor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import { css, html, LitElement } from 'lit'
import { customElement, property } from 'lit/decorators.js'
import { adminFormStyles } from '../../../admin/admin-form-styles.js'
import type { MeExperience } from '../../../admin/types.js'
import '../ui/me-admin-panel.js'
import '../ui/me-admin-section.js'
import '../ui/me-text-input.js'

@customElement('me-profile-experiences-editor')
export class MeProfileExperiencesEditor extends LitElement {
@property({ type: Array }) experiences: MeExperience[] = []

private dispatchChange(next: MeExperience[]) {
this.dispatchEvent(
new CustomEvent<MeExperience[]>('change', {
detail: next,
bubbles: true,
composed: true,
}),
)
}

private addItem = () => {
const next = [
...this.experiences,
{ company: '', url: '', startYear: new Date().getFullYear() },
]
this.dispatchChange(next)
}

private removeItem(index: number) {
const next = this.experiences.filter((_, i) => i !== index)
this.dispatchChange(next)
}

private updateItem(index: number, patch: Partial<MeExperience>) {
const next = [...this.experiences]
next[index] = { ...next[index], ...patch }
this.dispatchChange(next)
}

render() {
return html`
<me-admin-section
title="Experiences"
description="endYear を空にすると、継続中の経歴として扱えます。"
>
<button
slot="header-actions"
type="button"
class="subtle"
@click=${this.addItem}
>
経歴を追加
</button>

<div class="stack">
${
this.experiences.length === 0
? html`<p class="empty-text">経歴がまだありません。</p>`
: this.experiences.map(
(exp, index) => html`
<me-admin-panel title="経歴 ${index + 1}">
<button
slot="header-actions"
type="button"
class="subtle danger"
@click=${() => this.removeItem(index)}
>
削除
</button>

<div class="grid">
<me-text-input
label="Company"
.value=${exp.company}
@change=${(e: CustomEvent) =>
this.updateItem(index, { company: e.detail })}
></me-text-input>

<me-text-input
label="URL"
type="url"
.value=${exp.url}
@change=${(e: CustomEvent) =>
this.updateItem(index, { url: e.detail })}
></me-text-input>

<me-text-input
label="Start year"
type="number"
.value=${String(exp.startYear)}
@change=${(e: CustomEvent) =>
this.updateItem(index, {
startYear: Number(e.detail || '0'),
})}
></me-text-input>
Comment on lines +108 to +116
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

startYearが空の場合に0が設定されます。

certifications-editor と同様に、e.detail || '0' により空の入力が startYear: 0 になります。これは西暦0年として扱われる可能性があり、意図しない動作かもしれません。

🛡️ 修正案
                      <me-text-input
                        label="Start year"
                        type="number"
                        .value=${String(exp.startYear)}
                        `@change`=${(e: CustomEvent) =>
                          this.updateItem(index, {
-                           startYear: Number(e.detail || '0'),
+                           startYear: e.detail ? Number(e.detail) : new Date().getFullYear(),
                          })}
                      ></me-text-input>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/components/admin/profile/me-profile-experiences-editor.ts`
around lines 89 - 97, The change handler for the me-text-input currently forces
empty input to 0 via (e.detail || '0'), causing startYear to be set to year 0;
update the handler in me-profile-experiences-editor.ts (the me-text-input that
calls updateItem) to only convert and set startYear when e.detail is non-empty
(e.g. check e.detail !== '' && e.detail != null) and otherwise pass
null/undefined (or omit the field) so an empty input does not become 0; use
Number(e.detail) or parseInt(e.detail, 10) for the numeric conversion when
present and keep updateItem call and symbol names unchanged.


<me-text-input
label="End year"
type="number"
.value=${exp.endYear ? String(exp.endYear) : ''}
@change=${(e: CustomEvent) =>
this.updateItem(index, {
endYear: e.detail ? Number(e.detail) : undefined,
})}
></me-text-input>
</div>
</me-admin-panel>
`,
)
}
</div>
</me-admin-section>
`
}

static styles = [
adminFormStyles,
css`
:host {
display: block;
}

.stack {
display: grid;
gap: 20px;
}

.grid {
display: grid;
gap: 16px;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
}

.empty-text {
color: var(--color-text-tertiary);
font-size: 14px;
font-style: italic;
}
`,
]
}

declare global {
interface HTMLElementTagNameMap {
'me-profile-experiences-editor': MeProfileExperiencesEditor
}
}
Loading