Add full SkinsPins application
- FastAPI + SQLite + Alpine.js + HTMX PWA for Bingo Bango Bongo golf scoring - Passwordless magic link auth with email (aiosmtplib) and admin invite system - Real-time score entry via WebSocket with drag-and-drop player ordering - Course management with per-hole par and stroke index - Scorecard with golf score markers (birdie, eagle, bogey, etc.) - Two-step new game form with tee selection per player - Dashboard with stats, live games social stream, and recent game history - Game summary view (read-only scorecard with leaderboard) - User profile pages with win stats - PWA install support with generated PNG icons - Admin panel for users, courses, games, and invitations Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
function avatarUpload(formAction, redirectTo) {
|
||||
return {
|
||||
tab: 'emoji',
|
||||
showModal: false,
|
||||
previewUrl: null,
|
||||
croppedBlob: null,
|
||||
cropper: null,
|
||||
|
||||
openFile(e) {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
const reader = new FileReader();
|
||||
reader.onload = (ev) => {
|
||||
this.previewUrl = ev.target.result;
|
||||
this.showModal = true;
|
||||
this.$nextTick(() => this._initCropper());
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
// Clear the input so re-selecting the same file triggers change again
|
||||
e.target.value = '';
|
||||
},
|
||||
|
||||
_initCropper() {
|
||||
if (this.cropper) { this.cropper.destroy(); this.cropper = null; }
|
||||
const img = this.$refs.cropImg;
|
||||
this.cropper = new Cropper(img, {
|
||||
aspectRatio: 1,
|
||||
viewMode: 1,
|
||||
dragMode: 'move',
|
||||
autoCropArea: 1,
|
||||
cropBoxMovable: false,
|
||||
cropBoxResizable: false,
|
||||
toggleDragModeOnDblclick: false,
|
||||
background: false,
|
||||
});
|
||||
},
|
||||
|
||||
confirmCrop() {
|
||||
const canvas = this.cropper.getCroppedCanvas({ width: 256, height: 256 });
|
||||
canvas.toBlob((blob) => {
|
||||
this.croppedBlob = blob;
|
||||
this.previewUrl = canvas.toDataURL('image/jpeg');
|
||||
this.showModal = false;
|
||||
this.cropper.destroy();
|
||||
this.cropper = null;
|
||||
}, 'image/jpeg', 0.88);
|
||||
},
|
||||
|
||||
cancelCrop() {
|
||||
this.showModal = false;
|
||||
if (this.cropper) { this.cropper.destroy(); this.cropper = null; }
|
||||
if (!this.croppedBlob) this.previewUrl = null;
|
||||
},
|
||||
|
||||
async submitForm(e) {
|
||||
e.preventDefault();
|
||||
const form = e.target;
|
||||
const fd = new FormData(form);
|
||||
|
||||
if (this.tab === 'upload' && this.croppedBlob) {
|
||||
fd.delete('avatar_file');
|
||||
fd.set('avatar_file', this.croppedBlob, 'avatar.jpg');
|
||||
fd.set('avatar_emoji', '');
|
||||
} else if (this.tab === 'emoji') {
|
||||
fd.delete('avatar_file');
|
||||
}
|
||||
|
||||
const resp = await fetch(formAction, { method: 'POST', body: fd, redirect: 'follow' });
|
||||
window.location.href = resp.url || redirectTo;
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user