Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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