Skip to content

Commit 30066b7

Browse files
committed
feat: extract reusable components and refactor forms/modals
- Added `BaseModal` for consistent modal handling. - Introduced form components to streamline form structure. - Created `InfoCard` and `UserDisplay` for reusable UI elements. - Implemented `QueryStateHandler` to simplify async state handling.
1 parent 6ea87eb commit 30066b7

37 files changed

+1006
-797
lines changed
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
# Component Extraction Opportunities
2+
3+
Analysis conducted: 2025-10-26
4+
5+
## High Priority Extractions
6+
7+
### 1. BaseModal.svelte
8+
9+
**Impact:** HIGH | **Files:** 4 | **Lines Saved:** ~60/modal
10+
11+
**Current State:** All modals duplicate dialog structure, positioning classes, and lifecycle logic.
12+
13+
**Affected Files:**
14+
15+
- `src/lib/modals/LoginModal.svelte:49-92`
16+
- `src/lib/modals/CompatibilityModal.svelte:33-47`
17+
- `src/lib/modals/EditCompatibilityModal.svelte:33-53`
18+
- `src/lib/modals/BasicModal.svelte:37-53`
19+
20+
**Proposal:** Extract base modal wrapper handling dialog lifecycle, click-outside, and positioning. Expose content slot.
21+
22+
---
23+
24+
### 2. Form Field Components
25+
26+
**Impact:** VERY HIGH | **Files:** 10+ | **Reduction:** ~60% form boilerplate
27+
28+
**Current State:** Every form repeats label + input + ValidationMessage pattern 5+ times.
29+
30+
**Affected Files:**
31+
32+
- `src/lib/components/mods/ModForm.svelte:95-259`
33+
- `src/lib/components/guides/GuideForm.svelte:40-104`
34+
- `src/lib/components/mods/VersionForm.svelte:55-166`
35+
- `src/routes/settings/+page.svelte:76-108`
36+
37+
**Proposal:** Create:
38+
39+
- `FormField.svelte` - Label + input + validation wrapper
40+
- `FormTextarea.svelte` - Textarea with optional character count
41+
- `FormSplitPreview.svelte` - Split layout with markdown preview
42+
43+
---
44+
45+
### 3. InfoCard.svelte
46+
47+
**Impact:** MEDIUM-HIGH | **Files:** 4 | **Consolidation:** 4→1 component
48+
49+
**Current State:** Mod/Guide/Version/User info cards share identical structure, differ only in content.
50+
51+
**Affected Files:**
52+
53+
- `src/lib/components/mods/ModInfo.svelte:17-36`
54+
- `src/lib/components/guides/GuideInfo.svelte:15-22`
55+
- `src/lib/components/mods/VersionInfo.svelte:15-26`
56+
- `src/lib/components/users/UserInfo.svelte:15-22`
57+
58+
**Proposal:** Generic info card with title prop and content slot. Standard card/section styling.
59+
60+
---
61+
62+
### 4. UserDisplayCard.svelte
63+
64+
**Impact:** MEDIUM | **Files:** 3 | **Consolidation:** 3→1 component
65+
66+
**Current State:** Author/user displays duplicate avatar + username grid layout.
67+
68+
**Affected Files:**
69+
70+
- `src/lib/components/mods/ModAuthors.svelte:19-40`
71+
- `src/lib/components/guides/GuideAuthor.svelte:15-31`
72+
- `src/lib/components/mods/ModAuthor.svelte:31-43`
73+
74+
**Proposal:** Reusable user card accepting user object, optional role, configurable avatar size.
75+
76+
---
77+
78+
### 5. QueryStateHandler.svelte
79+
80+
**Impact:** MEDIUM | **Files:** 19 | **Pattern:** Universal
81+
82+
**Current State:** Every data-fetching component repeats `{#if fetching}/{:else if error}` pattern.
83+
84+
**Pattern:**
85+
86+
```svelte
87+
{#if $data.fetching}
88+
<p>Loading...</p>
89+
{:else if $data.error}
90+
<p>Oh no... {$data.error.message}</p>
91+
{:else}
92+
<!-- Content -->
93+
{/if}
94+
```
95+
96+
**Proposal:** Wrapper component accepting query store, rendering loading/error/content states via slots.
97+
98+
---
99+
100+
## Medium Priority Extractions
101+
102+
### 6. DataTable.svelte
103+
104+
**Impact:** MEDIUM-HIGH | **Files:** 5+
105+
106+
**Affected Files:**
107+
108+
- `src/lib/components/mods/VersionDependenciesGrid.svelte:17-59` (contains 2 identical tables)
109+
- `src/lib/components/mods/VersionTargetSupportGrid.svelte:53-99`
110+
- `src/lib/components/mods/CompatibilityGrid.svelte:28-95`
111+
- Admin approval pages
112+
113+
**Proposal:** Generic table wrapper with slot-based column definitions. Standard styling/overflow handling.
114+
115+
---
116+
117+
### 7. DescriptionCard.svelte
118+
119+
**Impact:** MEDIUM | **Files:** 2
120+
121+
**Affected Files:**
122+
123+
- `src/lib/components/mods/ModDescription.svelte:15-33`
124+
- `src/lib/components/mods/VersionDescription.svelte:14-32`
125+
126+
**Proposal:** Card component handling markdown rendering + optional announcement display.
127+
128+
---
129+
130+
### 8. AdminApprovalPage.svelte
131+
132+
**Impact:** LOW-MEDIUM | **Files:** 2
133+
134+
**Affected Files:**
135+
136+
- `src/routes/admin/unapproved-mods/+page.svelte:1-110`
137+
- `src/routes/admin/unapproved-versions/+page.svelte:1-80`
138+
139+
**Proposal:** Reusable approval page template with approve/deny mutations, pagination, and table rendering.
140+
141+
---
142+
143+
### 9. PaginatedListContainer.svelte
144+
145+
**Impact:** MEDIUM | **Files:** 5+
146+
147+
**Affected Files:**
148+
149+
- `src/lib/components/mods/ModGrid.svelte:131-198`
150+
- `src/lib/components/guides/GuideGrid.svelte:42-75`
151+
- Multiple admin list pages
152+
153+
**Proposal:** Container managing top/bottom pagination controls, grid/list content slot, consistent spacing.
154+
155+
---
156+
157+
### 10. TabSwitcher.svelte
158+
159+
**Impact:** LOW-MEDIUM | **Files:** 2-3
160+
161+
**Affected Files:**
162+
163+
- `src/routes/guide/[guideId]/+page.svelte`
164+
- `src/routes/user/[userId]/+page.svelte`
165+
166+
**Proposal:** Tab management component handling active state, tab buttons, and content switching.
167+
168+
---
169+
170+
## Implementation Phases
171+
172+
### Phase 1: High ROI (Recommended Start)
173+
174+
1. BaseModal.svelte
175+
2. FormField.svelte + FormSplitPreview.svelte
176+
3. QueryStateHandler.svelte
177+
178+
### Phase 2: Consolidation
179+
180+
4. InfoCard.svelte
181+
5. UserDisplayCard.svelte
182+
6. DescriptionCard.svelte
183+
184+
### Phase 3: Polish
185+
186+
7. DataTable.svelte
187+
8. AdminApprovalPage.svelte
188+
9. PaginatedListContainer.svelte
189+
10. TabSwitcher.svelte
190+
191+
---
192+
193+
## Impact Summary
194+
195+
| Priority | Components | Files Affected | Est. Lines Saved |
196+
| --------- | ---------- | -------------- | ---------------- |
197+
| High | 5 | 40+ | 1000+ |
198+
| Medium | 5 | 15+ | 300+ |
199+
| **Total** | **10** | **55+** | **1300+** |
200+
201+
**Benefits:**
202+
203+
- Reduced code duplication
204+
- Improved consistency across UI
205+
- Easier maintenance and updates
206+
- Better accessibility standardization
207+
- Simplified testing surface

bun.lockb

28.8 KB
Binary file not shown.

mise.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[tools]
2-
bun = "1.3.1"
2+
bun = "1.3.2"
33

44
[env]
55
PUBLIC_TOLGEE_API_URL = 'https://translate.ficsit.app'

package.json

Lines changed: 31 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,14 @@
3636
"custom-protocol-check": "^1.4.0",
3737
"dompurify": "^3.3.0",
3838
"felte": "^1.3.0",
39-
"globals": "^16.4.0",
40-
"graphql": "^16.11.0",
39+
"globals": "^16.5.0",
40+
"graphql": "^16.12.0",
4141
"js-cookie": "^3.0.5",
42-
"jsdom": "^27.0.1",
42+
"jsdom": "^27.2.0",
4343
"jszip": "^3.10.1",
44-
"marked": "^16.4.1",
45-
"marked-gfm-heading-id": "^4.1.2",
46-
"marked-mangle": "^1.1.11",
44+
"marked": "^17.0.0",
45+
"marked-gfm-heading-id": "^4.1.3",
46+
"marked-mangle": "^1.1.12",
4747
"prismjs": "^1.30.0",
4848
"semver": "^7.7.3",
4949
"socket.io-client": "^4.8.1",
@@ -56,63 +56,63 @@
5656
"@cfworker/json-schema": "^4.1.1",
5757
"@commitlint/cli": "^20.1.0",
5858
"@commitlint/config-conventional": "^20.0.0",
59-
"@eslint/compat": "^1.4.0",
59+
"@eslint/compat": "^2.0.0",
6060
"@eslint/eslintrc": "^3.3.1",
61-
"@eslint/js": "^9.38.0",
61+
"@eslint/js": "^9.39.1",
6262
"@graphql-codegen/add": "^6.0.0",
63-
"@graphql-codegen/cli": "^6.0.1",
63+
"@graphql-codegen/cli": "^6.0.2",
6464
"@graphql-codegen/introspection": "^5.0.0",
65-
"@graphql-codegen/typed-document-node": "^6.0.2",
66-
"@graphql-codegen/typescript": "^5.0.2",
67-
"@graphql-codegen/typescript-document-nodes": "^5.0.2",
65+
"@graphql-codegen/typed-document-node": "^6.1.2",
66+
"@graphql-codegen/typescript": "^5.0.4",
67+
"@graphql-codegen/typescript-document-nodes": "^5.0.4",
6868
"@graphql-codegen/typescript-graphql-files-modules": "^3.0.1",
69-
"@graphql-codegen/typescript-operations": "^5.0.2",
69+
"@graphql-codegen/typescript-operations": "^5.0.4",
7070
"@graphql-codegen/typescript-urql": "^4.0.1",
7171
"@graphql-codegen/urql-introspection": "^3.0.1",
7272
"@graphql-typed-document-node/core": "^3.2.0",
7373
"@parcel/watcher": "^2.5.1",
74-
"@skeletonlabs/skeleton": "^4.1.4",
75-
"@skeletonlabs/skeleton-svelte": "^4.1.4",
74+
"@skeletonlabs/skeleton": "^4.3.4",
75+
"@skeletonlabs/skeleton-svelte": "^4.3.4",
7676
"@sveltejs/adapter-node": "^5.4.0",
7777
"@sveltejs/adapter-static": "^3.0.10",
78-
"@sveltejs/kit": "^2.47.3",
78+
"@sveltejs/kit": "^2.48.5",
7979
"@sveltejs/vite-plugin-svelte": "^6.2.1",
80-
"@tailwindcss/vite": "^4.1.16",
80+
"@tailwindcss/vite": "^4.1.17",
8181
"@types/js-cookie": "^3.0.6",
8282
"@types/marked": "^6.0.0",
83-
"@types/node": "^24.9.1",
83+
"@types/node": "^24.10.1",
8484
"@types/prismjs": "^1.26.5",
8585
"@types/semver": "^7.7.1",
86-
"@typescript-eslint/eslint-plugin": "^8.46.2",
87-
"@typescript-eslint/parser": "^8.46.2",
88-
"autoprefixer": "^10.4.21",
89-
"caniuse-lite": "^1.0.30001751",
86+
"@typescript-eslint/eslint-plugin": "^8.46.4",
87+
"@typescript-eslint/parser": "^8.46.4",
88+
"autoprefixer": "^10.4.22",
89+
"caniuse-lite": "^1.0.30001754",
9090
"chokidar-cli": "^3.0.0",
9191
"concurrently": "^9.2.1",
9292
"cookieconsent": "^3.1.1",
9393
"cross-env": "^10.1.0",
94-
"cssnano": "^7.1.1",
94+
"cssnano": "^7.1.2",
9595
"cz-conventional-changelog": "^3.3.0",
9696
"dotenv-flow": "^4.1.0",
97-
"eslint": "^9.38.0",
97+
"eslint": "^9.39.1",
9898
"eslint-config-prettier": "^10.1.8",
99-
"eslint-plugin-svelte": "^3.12.5",
99+
"eslint-plugin-svelte": "^3.13.0",
100100
"graphql-tag": "^2.12.6",
101101
"husky": "^9.1.7",
102102
"prettier": "^3.6.2",
103103
"prettier-plugin-svelte": "^3.4.0",
104104
"prettier-plugin-tailwindcss": "^0.7.1",
105-
"rimraf": "^6.0.1",
105+
"rimraf": "^6.1.0",
106106
"schema-dts": "^1.1.5",
107-
"svelte": "^5.42.2",
107+
"svelte": "^5.43.6",
108108
"svelte-adapter-bun": "^1.0.1",
109-
"svelte-check": "^4.3.3",
109+
"svelte-check": "^4.3.4",
110110
"svelte-preprocess": "^6.0.3",
111-
"tailwindcss": "^4.1.16",
111+
"tailwindcss": "^4.1.17",
112112
"typescript": "^5.9.3",
113-
"typescript-eslint": "^8.46.2",
113+
"typescript-eslint": "^8.46.4",
114114
"urql": "^5.0.1",
115-
"vite": "^7.1.12"
115+
"vite": "^7.2.2"
116116
},
117117
"type": "module",
118118
"packageManager": "bun@1.3.1",
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<script lang="ts">
2+
import { ValidationMessage } from '@felte/reporter-svelte';
3+
4+
interface Props {
5+
label: string;
6+
name: string;
7+
value: string;
8+
type?: string;
9+
required?: boolean;
10+
disabled?: boolean;
11+
helperText?: string;
12+
}
13+
14+
let {
15+
label,
16+
name,
17+
value = $bindable(''),
18+
type = 'text',
19+
required = false,
20+
disabled = false,
21+
helperText
22+
}: Props = $props();
23+
</script>
24+
25+
<div class="grid grid-flow-row gap-2">
26+
<label class="label">
27+
<span
28+
>{label}{#if required}
29+
*{/if}</span>
30+
<input {type} bind:value {required} {disabled} class="input p-2" />
31+
{#if helperText}
32+
<span>{helperText}</span>
33+
{/if}
34+
</label>
35+
<ValidationMessage for={name}>
36+
{#snippet children({ messages: message })}
37+
<span class="validation-message">{message || ''}</span>
38+
{/snippet}
39+
</ValidationMessage>
40+
</div>
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<script lang="ts">
2+
import { ValidationMessage } from '@felte/reporter-svelte';
3+
4+
interface Props {
5+
label: string;
6+
name: string;
7+
id: string;
8+
accept?: string;
9+
placeholder?: string;
10+
}
11+
12+
let { label, name, id, accept, placeholder }: Props = $props();
13+
</script>
14+
15+
<div class="grid grid-flow-row gap-2">
16+
<label for={id}>{label}</label>
17+
<input {id} class="base-input" {name} type="file" {accept} {placeholder} />
18+
<ValidationMessage for={name}>
19+
{#snippet children({ messages: message })}
20+
<span class="validation-message">{message || ''}</span>
21+
{/snippet}
22+
</ValidationMessage>
23+
</div>

0 commit comments

Comments
 (0)