aboutsummaryrefslogtreecommitdiffstats
path: root/src/public
diff options
context:
space:
mode:
Diffstat (limited to 'src/public')
-rw-r--r--src/public/assets/overlays/chest.pngbin0 -> 115538 bytes
-rw-r--r--src/public/assets/overlays/cloud.pngbin0 -> 79115 bytes
-rw-r--r--src/public/assets/overlays/eagle.pngbin0 -> 63116 bytes
-rw-r--r--src/public/assets/overlays/explosion.pngbin0 -> 117861 bytes
-rw-r--r--src/public/assets/overlays/fire.pngbin0 -> 97785 bytes
-rw-r--r--src/public/assets/overlays/hat.pngbin0 -> 59733 bytes
-rw-r--r--src/public/assets/overlays/heart.pngbin0 -> 64053 bytes
-rw-r--r--src/public/assets/overlays/rainbow.pngbin0 -> 77826 bytes
-rw-r--r--src/public/assets/overlays/water.pngbin0 -> 108751 bytes
-rw-r--r--src/public/css/style.css203
-rw-r--r--src/public/js/app.js200
11 files changed, 402 insertions, 1 deletions
diff --git a/src/public/assets/overlays/chest.png b/src/public/assets/overlays/chest.png
new file mode 100644
index 0000000..a4db4c0
--- /dev/null
+++ b/src/public/assets/overlays/chest.png
Binary files differ
diff --git a/src/public/assets/overlays/cloud.png b/src/public/assets/overlays/cloud.png
new file mode 100644
index 0000000..b06eff4
--- /dev/null
+++ b/src/public/assets/overlays/cloud.png
Binary files differ
diff --git a/src/public/assets/overlays/eagle.png b/src/public/assets/overlays/eagle.png
new file mode 100644
index 0000000..d837908
--- /dev/null
+++ b/src/public/assets/overlays/eagle.png
Binary files differ
diff --git a/src/public/assets/overlays/explosion.png b/src/public/assets/overlays/explosion.png
new file mode 100644
index 0000000..008495b
--- /dev/null
+++ b/src/public/assets/overlays/explosion.png
Binary files differ
diff --git a/src/public/assets/overlays/fire.png b/src/public/assets/overlays/fire.png
new file mode 100644
index 0000000..e9438fd
--- /dev/null
+++ b/src/public/assets/overlays/fire.png
Binary files differ
diff --git a/src/public/assets/overlays/hat.png b/src/public/assets/overlays/hat.png
new file mode 100644
index 0000000..36f2760
--- /dev/null
+++ b/src/public/assets/overlays/hat.png
Binary files differ
diff --git a/src/public/assets/overlays/heart.png b/src/public/assets/overlays/heart.png
new file mode 100644
index 0000000..b54e462
--- /dev/null
+++ b/src/public/assets/overlays/heart.png
Binary files differ
diff --git a/src/public/assets/overlays/rainbow.png b/src/public/assets/overlays/rainbow.png
new file mode 100644
index 0000000..3e9719e
--- /dev/null
+++ b/src/public/assets/overlays/rainbow.png
Binary files differ
diff --git a/src/public/assets/overlays/water.png b/src/public/assets/overlays/water.png
new file mode 100644
index 0000000..7558079
--- /dev/null
+++ b/src/public/assets/overlays/water.png
Binary files differ
diff --git a/src/public/css/style.css b/src/public/css/style.css
index 897f54b..e152519 100644
--- a/src/public/css/style.css
+++ b/src/public/css/style.css
@@ -207,9 +207,212 @@ footer {
height: 1rem;
}
+/* Gallery page */
+.gallery-page {
+ max-width: 600px;
+ margin: 0 auto;
+}
+
+.gallery-page h1 {
+ margin-bottom: 1.5rem;
+}
+
+.gallery-feed {
+ display: flex;
+ flex-direction: column;
+ gap: 1.5rem;
+}
+
+.gallery-post {
+ background: #fff;
+ border: 1px solid #dbdbdb;
+ border-radius: 8px;
+ overflow: hidden;
+}
+
+.post-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 0.75rem 1rem;
+}
+
+.post-header time {
+ color: #999;
+ font-size: 0.85rem;
+}
+
+.gallery-post img {
+ width: 100%;
+ display: block;
+}
+
+.pagination {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ gap: 0.5rem;
+ margin-top: 2rem;
+}
+
+.pagination a {
+ padding: 0.4rem 0.75rem;
+ border: 1px solid #dbdbdb;
+ border-radius: 4px;
+ text-decoration: none;
+ color: #0095f6;
+}
+
+.pagination a:hover {
+ background: #f0f0f0;
+}
+
+.pagination .current-page {
+ padding: 0.4rem 0.75rem;
+ background: #0095f6;
+ color: #fff;
+ border-radius: 4px;
+}
+
+/* Editor page */
+.editor-page {
+ max-width: 960px;
+ margin: 0 auto;
+}
+
+.editor-page h1 {
+ margin-bottom: 1.5rem;
+}
+
+.editor-layout {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 1.5rem;
+ align-items: start;
+}
+
+.editor-source,
+.editor-preview {
+ background: #fff;
+ border: 1px solid #dbdbdb;
+ border-radius: 8px;
+ padding: 1rem;
+}
+
+.source-tabs {
+ display: flex;
+ gap: 0.5rem;
+ margin-bottom: 1rem;
+}
+
+.tab {
+ flex: 1;
+ padding: 0.5rem;
+ background: #fafafa;
+ border: 1px solid #dbdbdb;
+ border-radius: 4px;
+ cursor: pointer;
+ font-size: 0.95rem;
+}
+
+.tab.active {
+ background: #0095f6;
+ color: #fff;
+ border-color: #0095f6;
+}
+
+#webcam-video {
+ width: 100%;
+ aspect-ratio: 1;
+ object-fit: cover;
+ border-radius: 4px;
+ background: #000;
+}
+
+#preview-canvas {
+ width: 100%;
+ height: auto;
+ border-radius: 4px;
+ border: 1px solid #dbdbdb;
+}
+
+.overlay-controls {
+ margin: 0.75rem 0;
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+}
+
+.overlay-controls input[type="range"] {
+ flex: 1;
+}
+
+.editor-preview h2 {
+ font-size: 1rem;
+ margin-bottom: 0.5rem;
+}
+
+.overlay-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(80px, 1fr));
+ gap: 0.5rem;
+ margin-bottom: 1rem;
+}
+
+.overlay-thumb {
+ width: 100%;
+ aspect-ratio: 1;
+ object-fit: contain;
+ border: 2px solid #dbdbdb;
+ border-radius: 4px;
+ cursor: pointer;
+ opacity: 0.6;
+ transition: opacity 0.15s, border-color 0.15s;
+ background: #fafafa;
+ padding: 4px;
+}
+
+.overlay-thumb:hover {
+ opacity: 0.8;
+}
+
+.overlay-thumb.selected {
+ opacity: 1;
+ border-color: #0095f6;
+}
+
+.btn {
+ width: 100%;
+ padding: 0.6rem;
+ background: #0095f6;
+ color: #fff;
+ border: none;
+ border-radius: 4px;
+ font-size: 1rem;
+ cursor: pointer;
+ margin-top: 0.5rem;
+}
+
+.btn:hover {
+ background: #007ad9;
+}
+
+.btn:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
+#file-input {
+ width: 100%;
+ margin-top: 0.5rem;
+}
+
@media (max-width: 600px) {
header nav {
flex-direction: column;
gap: 0.5rem;
}
+ .editor-layout {
+ grid-template-columns: 1fr;
+ }
}
diff --git a/src/public/js/app.js b/src/public/js/app.js
index 86b3c33..a6bb9c5 100644
--- a/src/public/js/app.js
+++ b/src/public/js/app.js
@@ -1,3 +1,201 @@
document.addEventListener('DOMContentLoaded', function () {
- // App initialization
+ var canvas = document.getElementById('preview-canvas');
+ if (!canvas) return;
+
+ var ctx = canvas.getContext('2d');
+ var video = document.getElementById('webcam-video');
+ var btnCapture = document.getElementById('btn-capture');
+ var btnSave = document.getElementById('btn-save');
+ var fileInput = document.getElementById('file-input');
+ var scaleSlider = document.getElementById('overlay-scale');
+ var tabWebcam = document.getElementById('tab-webcam');
+ var tabUpload = document.getElementById('tab-upload');
+ var webcamPanel = document.getElementById('webcam-panel');
+ var uploadPanel = document.getElementById('upload-panel');
+ var csrfToken = document.getElementById('csrf-token').value;
+ var overlayThumbs = document.querySelectorAll('.overlay-thumb');
+
+ // State
+ var baseImageData = null; // base64 data URL of the captured/uploaded image
+ var selectedOverlay = null; // filename of the selected overlay
+ var overlayImg = null; // loaded Image object for the selected overlay
+ var webcamStream = null;
+
+ // -- Tabs --
+
+ tabWebcam.addEventListener('click', function () {
+ tabWebcam.classList.add('active');
+ tabUpload.classList.remove('active');
+ webcamPanel.hidden = false;
+ uploadPanel.hidden = true;
+ startWebcam();
+ });
+
+ tabUpload.addEventListener('click', function () {
+ tabUpload.classList.add('active');
+ tabWebcam.classList.remove('active');
+ uploadPanel.hidden = false;
+ webcamPanel.hidden = true;
+ stopWebcam();
+ });
+
+ // -- Webcam --
+
+ function startWebcam() {
+ if (webcamStream) return;
+ navigator.mediaDevices.getUserMedia({ video: { width: 640, height: 640, facingMode: 'user' } })
+ .then(function (stream) {
+ webcamStream = stream;
+ video.srcObject = stream;
+ })
+ .catch(function () {
+ // Camera not available — switch to upload tab
+ tabUpload.click();
+ });
+ }
+
+ function stopWebcam() {
+ if (webcamStream) {
+ webcamStream.getTracks().forEach(function (t) { t.stop(); });
+ webcamStream = null;
+ video.srcObject = null;
+ }
+ }
+
+ btnCapture.addEventListener('click', function () {
+ // Draw current video frame onto an offscreen canvas to get a data URL
+ var offscreen = document.createElement('canvas');
+ offscreen.width = 640;
+ offscreen.height = 640;
+ var offCtx = offscreen.getContext('2d');
+ // Center-crop the video frame into the square canvas
+ var vw = video.videoWidth;
+ var vh = video.videoHeight;
+ var crop = Math.min(vw, vh);
+ var sx = (vw - crop) / 2;
+ var sy = (vh - crop) / 2;
+ offCtx.drawImage(video, sx, sy, crop, crop, 0, 0, 640, 640);
+ baseImageData = offscreen.toDataURL('image/jpeg', 0.9);
+ renderPreview();
+ });
+
+ // -- Upload --
+
+ fileInput.addEventListener('change', function () {
+ var file = fileInput.files[0];
+ if (!file) return;
+ var reader = new FileReader();
+ reader.onload = function (e) {
+ baseImageData = e.target.result;
+ renderPreview();
+ };
+ reader.readAsDataURL(file);
+ });
+
+ // -- Overlay selection --
+
+ overlayThumbs.forEach(function (thumb) {
+ thumb.addEventListener('click', function () {
+ // Deselect previous
+ overlayThumbs.forEach(function (t) { t.classList.remove('selected'); });
+ thumb.classList.add('selected');
+ selectedOverlay = thumb.dataset.filename;
+
+ // Preload the full overlay image for canvas rendering
+ overlayImg = new Image();
+ overlayImg.onload = renderPreview;
+ overlayImg.src = thumb.src;
+ });
+ });
+
+ // -- Scale slider --
+
+ scaleSlider.addEventListener('input', renderPreview);
+
+ // -- Preview --
+
+ function renderPreview() {
+ ctx.clearRect(0, 0, 640, 640);
+ // Fill with light gray so the canvas isn't transparent
+ ctx.fillStyle = '#eee';
+ ctx.fillRect(0, 0, 640, 640);
+
+ if (!baseImageData) {
+ ctx.fillStyle = '#999';
+ ctx.font = '18px sans-serif';
+ ctx.textAlign = 'center';
+ ctx.fillText('Take a photo or upload an image', 320, 320);
+ updateSaveButton();
+ return;
+ }
+
+ var img = new Image();
+ img.onload = function () {
+ // Center-crop into the 640x640 canvas
+ var sw = img.width;
+ var sh = img.height;
+ var crop = Math.min(sw, sh);
+ var sx = (sw - crop) / 2;
+ var sy = (sh - crop) / 2;
+ ctx.drawImage(img, sx, sy, crop, crop, 0, 0, 640, 640);
+
+ // Draw overlay on top if one is selected
+ if (overlayImg && overlayImg.complete) {
+ var scale = scaleSlider.value / 100;
+ var ow = overlayImg.width * scale;
+ var oh = overlayImg.height * scale;
+ var ox = (640 - ow) / 2;
+ var oy = (640 - oh) / 2;
+ ctx.drawImage(overlayImg, ox, oy, ow, oh);
+ }
+
+ updateSaveButton();
+ };
+ img.src = baseImageData;
+ }
+
+ function updateSaveButton() {
+ // Both a base image and an overlay must be selected
+ btnSave.disabled = !(baseImageData && selectedOverlay);
+ }
+
+ // -- Save --
+
+ btnSave.addEventListener('click', function () {
+ if (btnSave.disabled) return;
+ btnSave.disabled = true;
+ btnSave.textContent = 'Saving...';
+
+ var scale = scaleSlider.value / 100;
+
+ fetch('/editor', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ image_data: baseImageData,
+ overlay: selectedOverlay,
+ overlay_scale: scale,
+ csrf_token: csrfToken
+ })
+ })
+ .then(function (res) { return res.json(); })
+ .then(function (data) {
+ if (data.success) {
+ window.location.href = data.redirect;
+ } else {
+ alert(data.error || 'Something went wrong.');
+ btnSave.disabled = false;
+ btnSave.textContent = 'Save post';
+ }
+ })
+ .catch(function () {
+ alert('Network error. Please try again.');
+ btnSave.disabled = false;
+ btnSave.textContent = 'Save post';
+ });
+ });
+
+ // Start webcam by default
+ startWebcam();
+ renderPreview();
});