Skip to main content

max / makenotwork

7.7 KB · 179 lines History Blame Raw
1 {% include "wizards/partials/step_nav.html" %}
2
3 <div class="wizard-step">
4 <h2 class="subtitle-h2">Appearance</h2>
5 <p class="step-description">Upload a project image. This appears on your project page and in search results.</p>
6
7 <form hx-post="/dashboard/new-project/{{ slug }}/step/appearance"
8 hx-target="#wizard-step" hx-swap="innerHTML"
9 hx-push-url="/dashboard/new-project/{{ slug }}/step/monetization">
10
11 <div class="form-group">
12 <label>Project Image</label>
13
14 <div class="project-image-upload" id="image-upload-area">
15 <div class="project-image-dropzone" id="image-dropzone">
16 {% if let Some(url) = cover_image_url %}
17 <div class="project-image-current" id="image-current">
18 <img src="{{ url }}" alt="Project image" class="wizard-cover-img">
19 </div>
20 {% else %}
21 <div class="project-image-placeholder" id="image-placeholder">
22 <p>Square, at least 400x400px</p>
23 <p class="hint">JPG, PNG, or WebP. Max 10 MB.</p>
24 </div>
25 {% endif %}
26 <input type="file" id="image-file-input" accept="image/jpeg,image/png,image/webp" class="sr-only">
27 </div>
28
29 <button type="button" class="btn-secondary" id="choose-image-btn"
30 onclick="document.getElementById('image-file-input').click()">Choose File</button>
31
32 <div class="upload-status" id="image-upload-status">
33 <div class="upload-progress-inline hidden" id="image-upload-progress">
34 <span id="image-upload-filename"></span>
35 <span id="image-upload-percent">0%</span>
36 <div class="progress-bar-container">
37 <div class="progress-bar" id="image-progress-bar"></div>
38 </div>
39 </div>
40 <div class="upload-error-inline hidden" id="image-upload-error">
41 <span class="error-message" id="image-error-message"></span>
42 </div>
43 </div>
44 </div>
45
46 <input type="hidden" name="cover_image_url" id="cover-image-url"
47 value="{% if let Some(url) = cover_image_url %}{{ url }}{% endif %}">
48 </div>
49
50 <div class="form-group">
51 <label>Preview</label>
52 <div class="project-card-preview">
53 <div class="preview-cover" id="preview-cover">
54 {% if let Some(url) = cover_image_url %}
55 <img src="{{ url }}" alt="Preview" class="wizard-cover-img">
56 {% else %}
57 <div class="preview-cover-empty">No image</div>
58 {% endif %}
59 </div>
60 <div class="preview-info">
61 <strong>{{ project_title }}</strong>
62 </div>
63 </div>
64 </div>
65
66 <div class="wizard-actions">
67 <button type="button" class="btn-secondary"
68 hx-get="/dashboard/new-project/{{ slug }}/step/basics"
69 hx-target="#wizard-step" hx-swap="innerHTML"
70 hx-push-url="/dashboard/new-project/{{ slug }}/step/basics">Back</button>
71 <button type="button" class="btn-secondary"
72 hx-get="/dashboard/new-project/{{ slug }}/step/monetization"
73 hx-target="#wizard-step" hx-swap="innerHTML"
74 hx-push-url="/dashboard/new-project/{{ slug }}/step/monetization">Skip</button>
75 <button type="submit" class="btn-primary">Continue</button>
76 </div>
77 </form>
78 </div>
79
80 <script>
81 (function() {
82 var projectId = '{{ project_id }}';
83 var dropzone = document.getElementById('image-dropzone');
84 var fileInput = document.getElementById('image-file-input');
85 var hiddenUrl = document.getElementById('cover-image-url');
86 var progressEl = document.getElementById('image-upload-progress');
87 var errorEl = document.getElementById('image-upload-error');
88 var placeholder = document.getElementById('image-placeholder');
89 var currentImg = document.getElementById('image-current');
90 var chooseBtn = document.getElementById('choose-image-btn');
91
92 var uploader = new S3Uploader({
93 filenameEl: document.getElementById('image-upload-filename'),
94 percentEl: document.getElementById('image-upload-percent'),
95 progressBar: document.getElementById('image-progress-bar'),
96 });
97
98 initDropzone(dropzone, fileInput, function(file) {
99 uploadProjectImage(file);
100 });
101
102 function uploadProjectImage(file) {
103 errorEl.classList.add('hidden');
104 progressEl.classList.remove('hidden');
105 chooseBtn.disabled = true;
106
107 fetch('/api/projects/image/presign', {
108 method: 'POST',
109 headers: { 'Content-Type': 'application/json', ...csrfHeaders() },
110 body: JSON.stringify({
111 project_id: projectId,
112 file_name: file.name,
113 content_type: file.type || 'image/jpeg'
114 })
115 })
116 .then(function(res) {
117 if (!res.ok) return res.text().then(function(body) {
118 try { var d = JSON.parse(body); throw new Error(d.error || 'Failed to get upload URL'); }
119 catch(e) { if (e.message && e.message !== body) throw e; throw new Error('Failed to get upload URL'); }
120 });
121 return res.json();
122 })
123 .then(function(data) {
124 return uploader.upload(data.upload_url, file, data.s3_key, file.type || 'image/jpeg', data.cache_control);
125 })
126 .then(function(s3Key) {
127 return fetch('/api/projects/image/confirm', {
128 method: 'POST',
129 headers: { 'Content-Type': 'application/json', ...csrfHeaders() },
130 body: JSON.stringify({
131 project_id: projectId,
132 s3_key: s3Key
133 })
134 });
135 })
136 .then(function(res) {
137 if (!res.ok) return res.text().then(function(body) {
138 try { var d = JSON.parse(body); throw new Error(d.error || 'Failed to confirm upload'); }
139 catch(e) { if (e.message && e.message !== body) throw e; throw new Error('Failed to confirm upload'); }
140 });
141 return res.json();
142 })
143 .then(function(data) {
144 hiddenUrl.value = data.image_url;
145 progressEl.classList.add('hidden');
146 chooseBtn.disabled = false;
147 showUploadedImage(data.image_url);
148 })
149 .catch(function(err) {
150 progressEl.classList.add('hidden');
151 chooseBtn.disabled = false;
152 showError(err.message || 'Upload failed. Please try again.');
153 });
154 }
155
156 function showUploadedImage(url) {
157 if (placeholder) placeholder.classList.add('hidden');
158 if (currentImg) {
159 currentImg.querySelector('img').src = url;
160 currentImg.classList.remove('hidden');
161 } else {
162 var div = document.createElement('div');
163 div.className = 'project-image-current';
164 div.id = 'image-current';
165 div.innerHTML = '<img src="' + url + '" alt="Project image" class="wizard-cover-img">';
166 dropzone.insertBefore(div, fileInput);
167 currentImg = div;
168 }
169 var preview = document.getElementById('preview-cover');
170 preview.innerHTML = '<img src="' + url + '" alt="Preview" class="wizard-cover-img">';
171 }
172
173 function showError(message) {
174 errorEl.classList.remove('hidden');
175 document.getElementById('image-error-message').textContent = message;
176 }
177 })();
178 </script>
179