monsters
Browse files- package-lock.json +9 -0
- package.json +3 -0
- src/lib/components/MonsterGenerator/MonsterGenerator.svelte +1 -1
- src/lib/components/MonsterGenerator/MonsterResult.svelte +117 -12
- src/lib/components/MonsterGenerator/todo.txt +67 -0
- src/lib/db/index.ts +16 -0
- src/lib/db/monsters.ts +23 -0
- src/lib/db/schema.ts +9 -0
package-lock.json
CHANGED
@@ -7,6 +7,9 @@
|
|
7 |
"": {
|
8 |
"name": "svelte",
|
9 |
"version": "0.0.0",
|
|
|
|
|
|
|
10 |
"devDependencies": {
|
11 |
"@sveltejs/vite-plugin-svelte": "^5.0.3",
|
12 |
"@tsconfig/svelte": "^5.0.4",
|
@@ -953,6 +956,12 @@
|
|
953 |
"node": ">=0.10.0"
|
954 |
}
|
955 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
956 |
"node_modules/esbuild": {
|
957 |
"version": "0.25.6",
|
958 |
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.6.tgz",
|
|
|
7 |
"": {
|
8 |
"name": "svelte",
|
9 |
"version": "0.0.0",
|
10 |
+
"dependencies": {
|
11 |
+
"dexie": "^4.0.11"
|
12 |
+
},
|
13 |
"devDependencies": {
|
14 |
"@sveltejs/vite-plugin-svelte": "^5.0.3",
|
15 |
"@tsconfig/svelte": "^5.0.4",
|
|
|
956 |
"node": ">=0.10.0"
|
957 |
}
|
958 |
},
|
959 |
+
"node_modules/dexie": {
|
960 |
+
"version": "4.0.11",
|
961 |
+
"resolved": "https://registry.npmjs.org/dexie/-/dexie-4.0.11.tgz",
|
962 |
+
"integrity": "sha512-SOKO002EqlvBYYKQSew3iymBoN2EQ4BDw/3yprjh7kAfFzjBYkaMNa/pZvcA7HSWlcKSQb9XhPe3wKyQ0x4A8A==",
|
963 |
+
"license": "Apache-2.0"
|
964 |
+
},
|
965 |
"node_modules/esbuild": {
|
966 |
"version": "0.25.6",
|
967 |
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.6.tgz",
|
package.json
CHANGED
@@ -17,5 +17,8 @@
|
|
17 |
"svelte-check": "^4.1.6",
|
18 |
"typescript": "~5.8.3",
|
19 |
"vite": "^6.3.5"
|
|
|
|
|
|
|
20 |
}
|
21 |
}
|
|
|
17 |
"svelte-check": "^4.1.6",
|
18 |
"typescript": "~5.8.3",
|
19 |
"vite": "^6.3.5"
|
20 |
+
},
|
21 |
+
"dependencies": {
|
22 |
+
"dexie": "^4.0.11"
|
23 |
}
|
24 |
}
|
src/lib/components/MonsterGenerator/MonsterGenerator.svelte
CHANGED
@@ -281,7 +281,7 @@ Assistant:`;
|
|
281 |
isProcessing={state.isProcessing}
|
282 |
/>
|
283 |
{:else if state.currentStep === 'complete'}
|
284 |
-
<MonsterResult
|
285 |
{:else}
|
286 |
<div class="processing-container">
|
287 |
<div class="spinner"></div>
|
|
|
281 |
isProcessing={state.isProcessing}
|
282 |
/>
|
283 |
{:else if state.currentStep === 'complete'}
|
284 |
+
<MonsterResult workflowState={state} onReset={reset} />
|
285 |
{:else}
|
286 |
<div class="processing-container">
|
287 |
<div class="spinner"></div>
|
src/lib/components/MonsterGenerator/MonsterResult.svelte
CHANGED
@@ -1,36 +1,81 @@
|
|
1 |
<script lang="ts">
|
2 |
import type { MonsterWorkflowState } from '$lib/types';
|
|
|
3 |
|
4 |
interface Props {
|
5 |
-
|
6 |
onReset: () => void;
|
7 |
}
|
8 |
|
9 |
-
let {
|
|
|
|
|
|
|
10 |
|
11 |
function downloadImage() {
|
12 |
-
if (!
|
13 |
|
14 |
const link = document.createElement('a');
|
15 |
-
link.href =
|
16 |
link.download = `monster-${Date.now()}.png`;
|
17 |
link.click();
|
18 |
}
|
19 |
|
20 |
function copyPrompt() {
|
21 |
-
if (!
|
22 |
-
navigator.clipboard.writeText(
|
23 |
alert('Prompt copied to clipboard!');
|
24 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
25 |
</script>
|
26 |
|
27 |
<div class="result-container">
|
28 |
<h3>Your Monster Has Been Created!</h3>
|
29 |
|
30 |
-
{#if
|
31 |
<div class="monster-image-container">
|
32 |
<img
|
33 |
-
src={
|
34 |
alt="Generated Monster"
|
35 |
class="monster-image"
|
36 |
/>
|
@@ -41,22 +86,22 @@
|
|
41 |
<div class="result-section">
|
42 |
<h4>Original Description</h4>
|
43 |
<div class="result-content">
|
44 |
-
<p>{
|
45 |
</div>
|
46 |
</div>
|
47 |
|
48 |
<div class="result-section">
|
49 |
<h4>Monster Concept</h4>
|
50 |
<div class="result-content">
|
51 |
-
<p>{
|
52 |
</div>
|
53 |
</div>
|
54 |
|
55 |
<div class="result-section">
|
56 |
<h4>Generation Prompt</h4>
|
57 |
<div class="result-content">
|
58 |
-
<p>{
|
59 |
-
{#if
|
60 |
<button class="copy-button" onclick={copyPrompt}>
|
61 |
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
62 |
<path d="M4 2a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V2zm2 0v8h8V2H6zM2 6a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2v-2h-2v2H2V8h2V6H2z"/>
|
@@ -75,6 +120,26 @@
|
|
75 |
</svg>
|
76 |
Download Monster
|
77 |
</button>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
78 |
<button class="action-button reset" onclick={onReset}>
|
79 |
<svg width="20" height="20" viewBox="0 0 20 20" fill="currentColor">
|
80 |
<path d="M4 2a1 1 0 011 1v2.101a7.002 7.002 0 0111.601 2.566 1 1 0 11-1.885.666A5.002 5.002 0 005.058 7.293a1 1 0 01-1.414 1.414l-2.35-2.35A1 1 0 011 5.648V3a1 1 0 011-1zm.008 9.057a1 1 0 011.276.61A5.002 5.002 0 0014.943 13H13a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0v-2.101a7.002 7.002 0 01-11.601-2.566 1 1 0 01.61-1.276z"/>
|
@@ -82,6 +147,10 @@
|
|
82 |
Create Another Monster
|
83 |
</button>
|
84 |
</div>
|
|
|
|
|
|
|
|
|
85 |
</div>
|
86 |
|
87 |
<style>
|
@@ -197,6 +266,42 @@
|
|
197 |
background: #5a6268;
|
198 |
}
|
199 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
200 |
@media (max-width: 768px) {
|
201 |
.result-container {
|
202 |
padding: 1rem;
|
|
|
1 |
<script lang="ts">
|
2 |
import type { MonsterWorkflowState } from '$lib/types';
|
3 |
+
import { saveMonster } from '$lib/db/monsters';
|
4 |
|
5 |
interface Props {
|
6 |
+
workflowState: MonsterWorkflowState;
|
7 |
onReset: () => void;
|
8 |
}
|
9 |
|
10 |
+
let { workflowState, onReset }: Props = $props();
|
11 |
+
let isSaving = $state(false);
|
12 |
+
let isSaved = $state(false);
|
13 |
+
let saveError: string | null = $state(null);
|
14 |
|
15 |
function downloadImage() {
|
16 |
+
if (!workflowState.monsterImage?.imageUrl) return;
|
17 |
|
18 |
const link = document.createElement('a');
|
19 |
+
link.href = workflowState.monsterImage.imageUrl;
|
20 |
link.download = `monster-${Date.now()}.png`;
|
21 |
link.click();
|
22 |
}
|
23 |
|
24 |
function copyPrompt() {
|
25 |
+
if (!workflowState.imagePrompt) return;
|
26 |
+
navigator.clipboard.writeText(workflowState.imagePrompt);
|
27 |
alert('Prompt copied to clipboard!');
|
28 |
}
|
29 |
+
|
30 |
+
async function saveToCollection() {
|
31 |
+
if (!workflowState.monsterImage || !workflowState.imageCaption || !workflowState.monsterConcept || !workflowState.imagePrompt) {
|
32 |
+
saveError = 'Missing monster data';
|
33 |
+
return;
|
34 |
+
}
|
35 |
+
|
36 |
+
isSaving = true;
|
37 |
+
saveError = null;
|
38 |
+
|
39 |
+
try {
|
40 |
+
// Extract monster name from concept (usually first line or after "Monster Name:")
|
41 |
+
let monsterName = 'Unknown Monster';
|
42 |
+
const conceptLines = workflowState.monsterConcept.split('\n');
|
43 |
+
for (const line of conceptLines) {
|
44 |
+
if (line.includes('Monster Name:') || line.includes('**Monster Name:**')) {
|
45 |
+
monsterName = line.replace(/\*\*Monster Name:\*\*|Monster Name:/g, '').trim();
|
46 |
+
break;
|
47 |
+
} else if (line.trim() && !line.includes(':')) {
|
48 |
+
// First non-empty line without colon might be the name
|
49 |
+
monsterName = line.trim();
|
50 |
+
break;
|
51 |
+
}
|
52 |
+
}
|
53 |
+
|
54 |
+
await saveMonster({
|
55 |
+
name: monsterName,
|
56 |
+
imageUrl: workflowState.monsterImage.imageUrl,
|
57 |
+
imageCaption: workflowState.imageCaption,
|
58 |
+
concept: workflowState.monsterConcept,
|
59 |
+
imagePrompt: workflowState.imagePrompt
|
60 |
+
});
|
61 |
+
|
62 |
+
isSaved = true;
|
63 |
+
} catch (err) {
|
64 |
+
console.error('Failed to save monster:', err);
|
65 |
+
saveError = 'Failed to save monster to collection';
|
66 |
+
} finally {
|
67 |
+
isSaving = false;
|
68 |
+
}
|
69 |
+
}
|
70 |
</script>
|
71 |
|
72 |
<div class="result-container">
|
73 |
<h3>Your Monster Has Been Created!</h3>
|
74 |
|
75 |
+
{#if workflowState.monsterImage}
|
76 |
<div class="monster-image-container">
|
77 |
<img
|
78 |
+
src={workflowState.monsterImage.imageUrl}
|
79 |
alt="Generated Monster"
|
80 |
class="monster-image"
|
81 |
/>
|
|
|
86 |
<div class="result-section">
|
87 |
<h4>Original Description</h4>
|
88 |
<div class="result-content">
|
89 |
+
<p>{workflowState.imageCaption || 'No caption available'}</p>
|
90 |
</div>
|
91 |
</div>
|
92 |
|
93 |
<div class="result-section">
|
94 |
<h4>Monster Concept</h4>
|
95 |
<div class="result-content">
|
96 |
+
<p>{workflowState.monsterConcept || 'No concept available'}</p>
|
97 |
</div>
|
98 |
</div>
|
99 |
|
100 |
<div class="result-section">
|
101 |
<h4>Generation Prompt</h4>
|
102 |
<div class="result-content">
|
103 |
+
<p>{workflowState.imagePrompt || 'No prompt available'}</p>
|
104 |
+
{#if workflowState.imagePrompt}
|
105 |
<button class="copy-button" onclick={copyPrompt}>
|
106 |
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
107 |
<path d="M4 2a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V2zm2 0v8h8V2H6zM2 6a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2v-2h-2v2H2V8h2V6H2z"/>
|
|
|
120 |
</svg>
|
121 |
Download Monster
|
122 |
</button>
|
123 |
+
<button
|
124 |
+
class="action-button save"
|
125 |
+
onclick={saveToCollection}
|
126 |
+
disabled={isSaving || isSaved}
|
127 |
+
>
|
128 |
+
{#if isSaving}
|
129 |
+
<div class="spinner-small"></div>
|
130 |
+
Saving...
|
131 |
+
{:else if isSaved}
|
132 |
+
<svg width="20" height="20" viewBox="0 0 20 20" fill="currentColor">
|
133 |
+
<path d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"/>
|
134 |
+
</svg>
|
135 |
+
Saved!
|
136 |
+
{:else}
|
137 |
+
<svg width="20" height="20" viewBox="0 0 20 20" fill="currentColor">
|
138 |
+
<path d="M2 6a2 2 0 012-2h5l2 2h5a2 2 0 012 2v6a2 2 0 01-2 2H4a2 2 0 01-2-2V6z"/>
|
139 |
+
</svg>
|
140 |
+
Save to Collection
|
141 |
+
{/if}
|
142 |
+
</button>
|
143 |
<button class="action-button reset" onclick={onReset}>
|
144 |
<svg width="20" height="20" viewBox="0 0 20 20" fill="currentColor">
|
145 |
<path d="M4 2a1 1 0 011 1v2.101a7.002 7.002 0 0111.601 2.566 1 1 0 11-1.885.666A5.002 5.002 0 005.058 7.293a1 1 0 01-1.414 1.414l-2.35-2.35A1 1 0 011 5.648V3a1 1 0 011-1zm.008 9.057a1 1 0 011.276.61A5.002 5.002 0 0014.943 13H13a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0v-2.101a7.002 7.002 0 01-11.601-2.566 1 1 0 01.61-1.276z"/>
|
|
|
147 |
Create Another Monster
|
148 |
</button>
|
149 |
</div>
|
150 |
+
|
151 |
+
{#if saveError}
|
152 |
+
<div class="error-message">{saveError}</div>
|
153 |
+
{/if}
|
154 |
</div>
|
155 |
|
156 |
<style>
|
|
|
266 |
background: #5a6268;
|
267 |
}
|
268 |
|
269 |
+
.action-button.save {
|
270 |
+
background: #007bff;
|
271 |
+
color: white;
|
272 |
+
}
|
273 |
+
|
274 |
+
.action-button.save:hover:not(:disabled) {
|
275 |
+
background: #0056b3;
|
276 |
+
}
|
277 |
+
|
278 |
+
.action-button:disabled {
|
279 |
+
opacity: 0.6;
|
280 |
+
cursor: not-allowed;
|
281 |
+
}
|
282 |
+
|
283 |
+
.spinner-small {
|
284 |
+
width: 16px;
|
285 |
+
height: 16px;
|
286 |
+
border: 2px solid #ffffff;
|
287 |
+
border-top-color: transparent;
|
288 |
+
border-radius: 50%;
|
289 |
+
animation: spin 0.8s linear infinite;
|
290 |
+
}
|
291 |
+
|
292 |
+
.error-message {
|
293 |
+
margin-top: 1rem;
|
294 |
+
padding: 0.5rem;
|
295 |
+
background: #f8d7da;
|
296 |
+
color: #721c24;
|
297 |
+
border-radius: 4px;
|
298 |
+
text-align: center;
|
299 |
+
}
|
300 |
+
|
301 |
+
@keyframes spin {
|
302 |
+
to { transform: rotate(360deg); }
|
303 |
+
}
|
304 |
+
|
305 |
@media (max-width: 768px) {
|
306 |
.result-container {
|
307 |
padding: 1rem;
|
src/lib/components/MonsterGenerator/todo.txt
ADDED
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
Long term I want to turn this into a monster battle game. As part of this I need some kind of internal DB for the game.
|
2 |
+
When I worked on a Flutter-based version of this I used Isar DB, is there an equivalent that would work well here?
|
3 |
+
|
4 |
+
---
|
5 |
+
|
6 |
+
How could you update the image processing so that pure white is made transparent? Could you store the transparent images in local storage?
|
7 |
+
|
8 |
+
---
|
9 |
+
|
10 |
+
Note the "Sign in with Hugging Face" button that is added with my page, can I just auto
|
11 |
+
<iframe src="https://fraser-piclets.static.hf.space/index.html?embed=true&__sign=eyJhbGciOiJFZERTQSJ9.eyJyZWFkIjp0cnVlLCJwZXJtaXNzaW9ucyI6eyJyZXBvLmNvbnRlbnQucmVhZCI6dHJ1ZX0sIm9uQmVoYWxmT2YiOnsia2luZCI6InVzZXIiLCJfaWQiOiI1ZjE5NTc4NDkyNWI5ODYzZTI4YWQ2MTAiLCJ1c2VyIjoiRnJhc2VyIiwic2Vzc2lvbklkIjoiNjg3NjU0ZjZhYmI2ZWE2ZTk0OThkNjVmIn0sImlhdCI6MTc1MjY1NzYxMiwic3ViIjoiL3NwYWNlcy9GcmFzZXIvcGljbGV0cyIsImV4cCI6MTc1Mjc0NDAxMiwiaXNzIjoiaHR0cHM6Ly9odWdnaW5nZmFjZS5jbyJ9.vH_qEMDwpCpEapX36n-JPgfj6P7jxGdpwomhT6MIpY-r2OS9Wc1bFsQq0USfbQqKxif2rR9XL7sB8f0ximxCDA" aria-label="static space app" class="space-iframe outline-hidden grow bg-white p-0" allow="accelerometer; ambient-light-sensor; autoplay; battery; camera; clipboard-read; clipboard-write; display-capture; document-domain; encrypted-media; fullscreen; geolocation; gyroscope; layout-animations; legacy-image-formats; magnetometer; microphone; midi; oversized-images; payment; picture-in-picture; publickey-credentials-get; serial; sync-xhr; usb; vr ; wake-lock; xr-spatial-tracking" sandbox="allow-downloads allow-forms allow-modals allow-pointer-lock allow-popups allow-popups-to-escape-sandbox allow-same-origin allow-scripts allow-storage-access-by-user-activation" scrolling="yes" id="iFrameResizer0" style="overflow: auto;"></iframe>
|
12 |
+
|
13 |
+
---
|
14 |
+
|
15 |
+
Now I would also like the text generator to produce a JSON object based on the monster concept.
|
16 |
+
This will be created with the following schema:
|
17 |
+
- name: string
|
18 |
+
- description: string
|
19 |
+
- rarity: likert (very-low, low, medium, high, very-high)
|
20 |
+
- HP: likert
|
21 |
+
- defence: likert
|
22 |
+
- attack: likert
|
23 |
+
- speed: likert
|
24 |
+
- special ability: string (passive trait that gives the monster a unique advantage in battle)
|
25 |
+
- attack action description: string (deals damage)
|
26 |
+
- attack action description: string (buff monsters own stats/status)
|
27 |
+
- disparage action description: string (lowers enemy stats/status)
|
28 |
+
- special action description: string (powerful action with single use per battle)
|
29 |
+
|
30 |
+
---
|
31 |
+
|
32 |
+
```python
|
33 |
+
import json
|
34 |
+
|
35 |
+
from pydantic import BaseModel
|
36 |
+
|
37 |
+
STRUCTURED_OUTPUT_FORMAT_INSTRUCTIONS = """The output should be formatted as a JSON instance that conforms to the JSON schema below.
|
38 |
+
|
39 |
+
As an example, for the schema {{"properties": {{"foo": {{"title": "Foo", "description": "a list of strings", "type": "array", "items": {{"type": "string"}}}}}}, "required": ["foo"]}}
|
40 |
+
the object {{"foo": ["bar", "baz"]}} is a well-formatted instance of the schema. The object {{"properties": {{"foo": ["bar", "baz"]}}}} is not well-formatted.
|
41 |
+
|
42 |
+
Here is the output schema:
|
43 |
+
```
|
44 |
+
{schema}
|
45 |
+
```"""
|
46 |
+
|
47 |
+
|
48 |
+
class StructuredOutputParser(BaseModel):
|
49 |
+
pydantic_class: type[BaseModel]
|
50 |
+
|
51 |
+
def get_schema(self, indent: int = 2) -> str:
|
52 |
+
# Copy schema to avoid altering original Pydantic schema.
|
53 |
+
schema = self.pydantic_class.model_json_schema()
|
54 |
+
# Iterate over fields to remove from the schema
|
55 |
+
for field_name in ["title", "type"]:
|
56 |
+
if field_name in schema:
|
57 |
+
schema.pop(field_name)
|
58 |
+
return json.dumps(schema, indent=indent, ensure_ascii=False)
|
59 |
+
|
60 |
+
def get_format_instructions(self) -> str:
|
61 |
+
schema_str = self.get_schema()
|
62 |
+
# Ensure json in context is well-formed with double quotes.
|
63 |
+
return STRUCTURED_OUTPUT_FORMAT_INSTRUCTIONS.format(schema=schema_str)
|
64 |
+
|
65 |
+
def parse(self, json_string: str) -> BaseModel:
|
66 |
+
return self.pydantic_class.model_validate_json(json_string)
|
67 |
+
```
|
src/lib/db/index.ts
ADDED
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import Dexie, { type Table } from 'dexie';
|
2 |
+
import type { Monster } from './schema';
|
3 |
+
|
4 |
+
export class MonsterDatabase extends Dexie {
|
5 |
+
monsters!: Table<Monster>;
|
6 |
+
|
7 |
+
constructor() {
|
8 |
+
super('MonsterGeneratorDB');
|
9 |
+
|
10 |
+
this.version(1).stores({
|
11 |
+
monsters: '++id, name, createdAt'
|
12 |
+
});
|
13 |
+
}
|
14 |
+
}
|
15 |
+
|
16 |
+
export const db = new MonsterDatabase();
|
src/lib/db/monsters.ts
ADDED
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { db } from './index';
|
2 |
+
import type { Monster } from './schema';
|
3 |
+
|
4 |
+
export async function saveMonster(monsterData: Omit<Monster, 'id' | 'createdAt'>): Promise<number> {
|
5 |
+
const monster: Omit<Monster, 'id'> = {
|
6 |
+
...monsterData,
|
7 |
+
createdAt: new Date()
|
8 |
+
};
|
9 |
+
|
10 |
+
return await db.monsters.add(monster);
|
11 |
+
}
|
12 |
+
|
13 |
+
export async function getAllMonsters(): Promise<Monster[]> {
|
14 |
+
return await db.monsters.toArray();
|
15 |
+
}
|
16 |
+
|
17 |
+
export async function getMonster(id: number): Promise<Monster | undefined> {
|
18 |
+
return await db.monsters.get(id);
|
19 |
+
}
|
20 |
+
|
21 |
+
export async function deleteMonster(id: number): Promise<void> {
|
22 |
+
await db.monsters.delete(id);
|
23 |
+
}
|
src/lib/db/schema.ts
ADDED
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
export interface Monster {
|
2 |
+
id?: number;
|
3 |
+
name: string;
|
4 |
+
imageUrl: string;
|
5 |
+
imageCaption: string;
|
6 |
+
concept: string;
|
7 |
+
imagePrompt: string;
|
8 |
+
createdAt: Date;
|
9 |
+
}
|