-
Notifications
You must be signed in to change notification settings - Fork 2
Avatars + Spell results #56
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
bf553db
02b75b4
778e631
da007a8
8f9fb6e
f9eb5f9
3df9f61
75e0624
58a4ce1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| -- migrate:up | ||
| CREATE TABLE character_avatars ( | ||
| id TEXT PRIMARY KEY, | ||
| character_id TEXT NOT NULL REFERENCES characters(id) ON DELETE CASCADE, | ||
| upload_id TEXT NOT NULL REFERENCES uploads(id) ON DELETE CASCADE, | ||
| is_primary BOOLEAN NOT NULL DEFAULT FALSE, | ||
| crop_x_percent REAL CHECK (crop_x_percent IS NULL OR (crop_x_percent >= 0 AND crop_x_percent <= 1)), | ||
| crop_y_percent REAL CHECK (crop_y_percent IS NULL OR (crop_y_percent >= 0 AND crop_y_percent <= 1)), | ||
| crop_width_percent REAL CHECK (crop_width_percent IS NULL OR (crop_width_percent >= 0 AND crop_width_percent <= 1)), | ||
| crop_height_percent REAL CHECK (crop_height_percent IS NULL OR (crop_height_percent >= 0 AND crop_height_percent <= 1)), | ||
| created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, | ||
| updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, | ||
| UNIQUE(character_id, upload_id) | ||
| ); | ||
|
|
||
| CREATE INDEX idx_character_avatars_character_id ON character_avatars(character_id); | ||
| CREATE INDEX idx_character_avatars_is_primary ON character_avatars(character_id, is_primary) WHERE is_primary = TRUE; | ||
|
|
||
| -- migrate:down | ||
| DROP INDEX IF EXISTS idx_character_avatars_is_primary; | ||
| DROP INDEX IF EXISTS idx_character_avatars_character_id; | ||
| DROP TABLE IF EXISTS character_avatars; | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,20 @@ | ||||||||||||||||||||||||||
| -- migrate:up | ||||||||||||||||||||||||||
| -- Migrate existing avatar_id from characters table to character_avatars table | ||||||||||||||||||||||||||
| -- Using md5 hash of character_id + timestamp to generate unique IDs (ULID-like) | ||||||||||||||||||||||||||
| INSERT INTO character_avatars (id, character_id, upload_id, is_primary, created_at, updated_at) | ||||||||||||||||||||||||||
| SELECT | ||||||||||||||||||||||||||
| SUBSTRING(MD5(id || EXTRACT(EPOCH FROM CURRENT_TIMESTAMP)::TEXT) FROM 1 FOR 26) as id, | ||||||||||||||||||||||||||
|
Comment on lines
+2
to
+6
|
||||||||||||||||||||||||||
| -- Migrate existing avatar_id from characters table to character_avatars table | |
| -- Using md5 hash of character_id + timestamp to generate unique IDs (ULID-like) | |
| INSERT INTO character_avatars (id, character_id, upload_id, is_primary, created_at, updated_at) | |
| SELECT | |
| SUBSTRING(MD5(id || EXTRACT(EPOCH FROM CURRENT_TIMESTAMP)::TEXT) FROM 1 FOR 26) as id, | |
| -- Ensure pgcrypto extension is enabled for gen_random_uuid() | |
| CREATE EXTENSION IF NOT EXISTS "pgcrypto"; | |
| -- Migrate existing avatar_id from characters table to character_avatars table | |
| -- Using md5 hash of character_id + timestamp to generate unique IDs (ULID-like) | |
| INSERT INTO character_avatars (id, character_id, upload_id, is_primary, created_at, updated_at) | |
| SELECT | |
| gen_random_uuid() as id, |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| -- migrate:up | ||
| ALTER TABLE characters DROP COLUMN avatar_id; | ||
|
|
||
| -- migrate:down | ||
| ALTER TABLE characters ADD COLUMN avatar_id TEXT REFERENCES uploads(id); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,101 @@ | ||
| import type { ComputedCharacter } from "@src/services/computeCharacter" | ||
| import { ModalContent } from "./ui/ModalContent" | ||
|
|
||
| export interface AvatarCropperProps { | ||
| character: ComputedCharacter | ||
| avatarIndex: number | ||
| } | ||
|
|
||
| export const AvatarCropper = ({ character, avatarIndex }: AvatarCropperProps) => { | ||
| const avatar = character.avatars[avatarIndex] | ||
| if (!avatar) { | ||
| throw new Error(`Avatar at index ${avatarIndex} not found`) | ||
| } | ||
|
|
||
| const uploadUrl = avatar.uploadUrl | ||
|
|
||
| const existingCrop = { | ||
| x: avatar.crop_x_percent, | ||
| y: avatar.crop_y_percent!, | ||
| width: avatar.crop_width_percent!, | ||
| height: avatar.crop_height_percent!, | ||
| } | ||
|
|
||
| const action = `/characters/${character.id}/avatars/${avatar.id}/crop` | ||
|
|
||
| return ( | ||
| <ModalContent title="Adjust Crop"> | ||
| <div class="modal-body"> | ||
| {/* Cropper.js v2 web components */} | ||
| <div class="mb-3" style="max-width: 100%;"> | ||
| <cropper-canvas style="height: 500px; width: 100%;"> | ||
| <cropper-image src={uploadUrl} alt="Avatar to crop" scalable translatable /> | ||
|
|
||
| <cropper-shade hidden></cropper-shade> | ||
| <cropper-handle action="move" plain></cropper-handle> | ||
| <cropper-selection | ||
| aspectRatio="1" | ||
| movable | ||
| resizable | ||
| zoomable | ||
| data-existingx={existingCrop.x || undefined} | ||
| data-existingy={existingCrop.y || undefined} | ||
| data-existingw={existingCrop.width || undefined} | ||
| data-existingh={existingCrop.height || undefined} | ||
| > | ||
| <cropper-grid role="grid" covered></cropper-grid> | ||
| <cropper-crosshair centered></cropper-crosshair> | ||
| <cropper-handle | ||
| action="move" | ||
| theme-color="rgba(255, 255, 255, 0.35)" | ||
| ></cropper-handle> | ||
| <cropper-handle action="n-resize"></cropper-handle> | ||
| <cropper-handle action="e-resize"></cropper-handle> | ||
| <cropper-handle action="s-resize"></cropper-handle> | ||
| <cropper-handle action="w-resize"></cropper-handle> | ||
| <cropper-handle action="ne-resize"></cropper-handle> | ||
| <cropper-handle action="nw-resize"></cropper-handle> | ||
| <cropper-handle action="se-resize"></cropper-handle> | ||
| <cropper-handle action="sw-resize"></cropper-handle> | ||
| </cropper-selection> | ||
| </cropper-canvas> | ||
| </div> | ||
|
|
||
| {/* Form for submitting crop data */} | ||
| <form id="cropForm" hx-post={action} hx-target="#character-info" hx-swap="outerHTML"> | ||
| {/* Hidden fields for crop percentages */} | ||
| <input type="hidden" id="crop_x_percent" name="crop_x_percent" /> | ||
| <input type="hidden" id="crop_y_percent" name="crop_y_percent" /> | ||
| <input type="hidden" id="crop_width_percent" name="crop_width_percent" /> | ||
| <input type="hidden" id="crop_height_percent" name="crop_height_percent" /> | ||
|
|
||
| <div id="cropError" class="alert alert-danger d-none" role="alert"></div> | ||
| </form> | ||
| </div> | ||
|
|
||
| <div class="modal-footer"> | ||
| <button | ||
| type="button" | ||
| class="btn btn-secondary" | ||
| hx-get={`/characters/${character.id}/avatars`} | ||
| hx-target="#editModalContent" | ||
| hx-swap="innerHTML" | ||
| > | ||
| <i class="bi bi-grid-3x3"></i> Back to Gallery | ||
| </button> | ||
| <button | ||
| type="button" | ||
| class="btn btn-primary" | ||
| hx-post={action} | ||
| hx-include="#cropForm" | ||
| hx-target="#editModalContent" | ||
| hx-swap="innerHTML" | ||
| > | ||
| Save Crop | ||
| </button> | ||
| </div> | ||
|
|
||
| <script src="/static/avatar-cropper.js"></script> | ||
| </ModalContent> | ||
| ) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] The UNIQUE constraint on
(character_id, upload_id)prevents the same upload from being used multiple times for the same character, even with different crop settings. This might be intentional, but it limits flexibility. Consider whether users should be able to create multiple avatars from the same upload with different crops.