b4um_generators 0.4.2 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,101 @@
1
+ <header class="b4um-todo__header">
2
+ <h1>Meine Aufgaben</h1>
3
+ <p>Behalte deine Aufgaben im Blick.</p>
4
+ </header>
5
+ <div class="b4um-grid">
6
+ <div class="b4um-card b4um-card--full">
7
+ <div class="b4um-todo">
8
+ <%%= render "progress" %>
9
+
10
+ <section class="b4um-todo__form">
11
+ <h2>Neue Aufgabe</h2>
12
+
13
+ <%%= form_with model: @task,
14
+ data: {
15
+ controller: "b4um-todo",
16
+ action: "turbo:submit-end->b4um-todo#resetAfterSuccess"
17
+ } do |form| %>
18
+
19
+
20
+ <div class="b4um-todo__form-fields">
21
+
22
+ <div class="form-field">
23
+ <%%= form.text_field :name,
24
+ class: "form-input",
25
+ placeholder: " ",
26
+ required: true,
27
+ autofocus: true,
28
+ data: {
29
+ b4um_todo_target: "input",
30
+ action: "input->b4um-todo#updateSubmitState"
31
+ } %>
32
+
33
+ <%%= form.label :name,
34
+ "Was möchtest du erledigen?",
35
+ class: "form-label" %>
36
+ </div>
37
+
38
+ <%%= form.submit "Hinzufügen",
39
+ class: "b4um-todo__button",
40
+ data: {
41
+ b4um_todo_target: "submit"
42
+ } %>
43
+
44
+ </div>
45
+
46
+ <%% if @task.errors.any? %>
47
+ <div class="b4um-todo__errors">
48
+ <%%= @task.errors.full_messages.to_sentence %>
49
+ </div>
50
+ <%% end %>
51
+ <%% end %>
52
+ </section>
53
+
54
+ <section class="b4um-todo__section">
55
+ <h2>
56
+ Offene Aufgaben
57
+ <span id="incomplete_tasks_count">
58
+ (<%%= @incomplete_tasks.size %>)
59
+ </span>
60
+ </h2>
61
+
62
+ <div id="incomplete_tasks" class="b4um-todo__list">
63
+ <%%= render partial: "task",
64
+ collection: @incomplete_tasks,
65
+ as: :task %>
66
+ </div>
67
+
68
+ <div id="incomplete_tasks_empty">
69
+ <%% unless @incomplete_tasks.any? %>
70
+ <p class="b4um-todo__empty">
71
+ Keine offenen Aufgaben. Sehr gut!
72
+ </p>
73
+ <%% end %>
74
+ </div>
75
+ </section>
76
+
77
+ <section class="b4um-todo__section">
78
+ <h2>
79
+ Erledigte Aufgaben
80
+ <span id="complete_tasks_count">
81
+ (<%%= @complete_tasks.size %>)
82
+ </span>
83
+ </h2>
84
+
85
+ <div id="complete_tasks" class="b4um-todo__list">
86
+ <%%= render partial: "task",
87
+ collection: @complete_tasks,
88
+ as: :task %>
89
+ </div>
90
+
91
+ <div id="complete_tasks_empty">
92
+ <%% unless @complete_tasks.any? %>
93
+ <p class="b4um-todo__empty">
94
+ Noch keine erledigten Aufgaben.
95
+ </p>
96
+ <%% end %>
97
+ </div>
98
+ </section>
99
+ </div>
100
+ </div>
101
+ </div>
@@ -0,0 +1,7 @@
1
+ class <%= class_name %> < ApplicationRecord
2
+ <% if owner? %>
3
+ belongs_to :<%= owner_reference_name %>
4
+ <% end %>
5
+
6
+ validates :name, presence: true
7
+ end
@@ -0,0 +1,191 @@
1
+ class <%= class_name.pluralize %>Controller < ApplicationController
2
+ <% if owner? %>
3
+ before_action :require_todo_owner
4
+ <% end %>
5
+
6
+ before_action :set_task, only: %i[update destroy]
7
+
8
+ def index
9
+ @task = <%= class_name %>.new
10
+ @incomplete_tasks = tasks_scope.where(complete: false).order(created_at: :desc)
11
+ @complete_tasks = tasks_scope.where(complete: true).order(updated_at: :desc)
12
+
13
+ @total_tasks = @incomplete_tasks.size + @complete_tasks.size
14
+ @completed_tasks = @complete_tasks.size
15
+ end
16
+
17
+ def create
18
+ @task = tasks_scope.new(task_params)
19
+
20
+ if @task.save
21
+ respond_to do |format|
22
+ format.turbo_stream do
23
+ load_tasks
24
+
25
+ render turbo_stream: [
26
+ turbo_stream.prepend(
27
+ "incomplete_tasks",
28
+ partial: "<%= plural_table_name %>/task",
29
+ locals: { task: @task }
30
+ ),
31
+ turbo_stream.replace(
32
+ "todo_progress",
33
+ partial: "<%= plural_table_name %>/progress"
34
+ ),
35
+ turbo_stream.update(
36
+ "incomplete_tasks_count",
37
+ "(#{@incomplete_tasks.size})"
38
+ ),
39
+ turbo_stream.update(
40
+ "incomplete_tasks_empty",
41
+ ""
42
+ )
43
+ ]
44
+ end
45
+
46
+ format.html do
47
+ redirect_to <%= plural_table_name %>_path,
48
+ notice: "Aufgabe wurde erstellt."
49
+ end
50
+ end
51
+ else
52
+ load_tasks
53
+
54
+ respond_to do |format|
55
+ format.turbo_stream do
56
+ render :index,
57
+ formats: :html,
58
+ status: :unprocessable_entity
59
+ end
60
+
61
+ format.html do
62
+ render :index, status: :unprocessable_entity
63
+ end
64
+ end
65
+ end
66
+ end
67
+
68
+ def update
69
+ if @task.update(complete: !@task.complete)
70
+ respond_to do |format|
71
+ format.turbo_stream do
72
+ load_tasks
73
+
74
+ render turbo_stream: [
75
+ turbo_stream.remove(
76
+ helpers.dom_id(@task)
77
+ ),
78
+ turbo_stream.prepend(
79
+ @task.complete? ? "complete_tasks" : "incomplete_tasks",
80
+ partial: "<%= plural_table_name %>/task",
81
+ locals: { task: @task }
82
+ ),
83
+ turbo_stream.replace(
84
+ "todo_progress",
85
+ partial: "<%= plural_table_name %>/progress"
86
+ ),
87
+ turbo_stream.update(
88
+ "incomplete_tasks_count",
89
+ "(#{@incomplete_tasks.size})"
90
+ ),
91
+ turbo_stream.update(
92
+ "complete_tasks_count",
93
+ "(#{@complete_tasks.size})"
94
+ ),
95
+ turbo_stream.update(
96
+ "incomplete_tasks_empty",
97
+ @incomplete_tasks.empty? ? '<p class="b4um-todo__empty">Keine offenen Aufgaben. Sehr gut!</p>'.html_safe : ""
98
+ ),
99
+ turbo_stream.update(
100
+ "complete_tasks_empty",
101
+ @complete_tasks.empty? ? '<p class="b4um-todo__empty">Noch keine erledigten Aufgaben.</p>'.html_safe : ""
102
+ )
103
+ ]
104
+ end
105
+
106
+ format.html do
107
+ redirect_to <%= plural_table_name %>_path
108
+ end
109
+ end
110
+ else
111
+ redirect_to <%= plural_table_name %>_path,
112
+ alert: "Aufgabe konnte nicht aktualisiert werden."
113
+ end
114
+ end
115
+
116
+ def destroy
117
+ @task.destroy!
118
+
119
+ respond_to do |format|
120
+ format.turbo_stream do
121
+ load_tasks
122
+
123
+ render turbo_stream: [
124
+ turbo_stream.remove(
125
+ helpers.dom_id(@task)
126
+ ),
127
+ turbo_stream.replace(
128
+ "todo_progress",
129
+ partial: "<%= plural_table_name %>/progress"
130
+ ),
131
+ turbo_stream.update(
132
+ "incomplete_tasks_count",
133
+ "(#{@incomplete_tasks.size})"
134
+ ),
135
+ turbo_stream.update(
136
+ "complete_tasks_count",
137
+ "(#{@complete_tasks.size})"
138
+ ),
139
+ turbo_stream.update(
140
+ "incomplete_tasks_empty",
141
+ @incomplete_tasks.empty? ? '<p class="b4um-todo__empty">Keine offenen Aufgaben. Sehr gut!</p>'.html_safe : ""
142
+ ),
143
+ turbo_stream.update(
144
+ "complete_tasks_empty",
145
+ @complete_tasks.empty? ? '<p class="b4um-todo__empty">Noch keine erledigten Aufgaben.</p>'.html_safe : ""
146
+ )
147
+ ]
148
+ end
149
+
150
+ format.html do
151
+ redirect_to <%= plural_table_name %>_path,
152
+ notice: "Aufgabe wurde gelöscht."
153
+ end
154
+ end
155
+ end
156
+
157
+ private
158
+
159
+ def tasks_scope
160
+ <% if owner? %>
161
+ <%= current_owner_method %>.tasks
162
+ <% else %>
163
+ <%= class_name %>.all
164
+ <% end %>
165
+ end
166
+
167
+ def set_task
168
+ @task = tasks_scope.find(params[:id])
169
+ end
170
+
171
+ def task_params
172
+ params.require(:<%= singular_table_name %>).permit(:name)
173
+ end
174
+
175
+ def load_tasks
176
+ @incomplete_tasks = tasks_scope.where(complete: false).order(created_at: :desc)
177
+ @complete_tasks = tasks_scope.where(complete: true).order(updated_at: :desc)
178
+
179
+ @total_tasks = @incomplete_tasks.size + @complete_tasks.size
180
+ @completed_tasks = @complete_tasks.size
181
+ end
182
+
183
+ <% if owner? %>
184
+ def require_todo_owner
185
+ return if <%= current_owner_method %>
186
+
187
+ redirect_to <%= options[:login_path] %>,
188
+ alert: "Bitte melde dich an, um deine Aufgaben zu verwalten."
189
+ end
190
+ <% end %>
191
+ end
@@ -0,0 +1,347 @@
1
+ /* -------------------------------------------------------
2
+ b4um Todo
3
+ ------------------------------------------------------- */
4
+
5
+ .b4um-todo {
6
+ max-width: 820px;
7
+ margin: 3rem auto;
8
+ padding: 0;
9
+
10
+ color: var(--todo-text);
11
+ }
12
+
13
+ /* -------------------------------------------------------
14
+ Header
15
+ ------------------------------------------------------- */
16
+
17
+ .b4um-todo__header {
18
+ margin-bottom: 2.5rem;
19
+ }
20
+
21
+ .b4um-todo__header h1 {
22
+ margin: 0 0 0.5rem;
23
+ font-size: clamp(2rem, 4vw, 2.8rem);
24
+ font-weight: 700;
25
+ letter-spacing: -0.04em;
26
+ }
27
+
28
+ .b4um-todo__header p {
29
+ margin: 0;
30
+ color: var(--todo-muted);
31
+ font-size: 1rem;
32
+ }
33
+
34
+ /* -------------------------------------------------------
35
+ Progress
36
+ ------------------------------------------------------- */
37
+
38
+ .b4um-todo__progress {
39
+ padding: 1.75rem;
40
+ margin-bottom: 2rem;
41
+
42
+ background: var(--todo-surface);
43
+ border: 1px solid var(--todo-border);
44
+ border-radius: 18px;
45
+ }
46
+
47
+ .b4um-todo__progress-info {
48
+ display: flex;
49
+ align-items: center;
50
+ justify-content: space-between;
51
+ gap: 1rem;
52
+
53
+ margin-bottom: 1rem;
54
+ }
55
+
56
+ .b4um-todo__progress-info h2 {
57
+ margin: 0;
58
+ font-size: 1.1rem;
59
+ font-weight: 600;
60
+ }
61
+
62
+ .b4um-todo__progress-info span {
63
+ color: var(--todo-muted);
64
+ font-size: 0.9rem;
65
+ }
66
+
67
+ .b4um-todo__progress-bar {
68
+ height: 9px;
69
+ overflow: hidden;
70
+
71
+ background: var(--todo-border);
72
+ border-radius: 999px;
73
+ }
74
+
75
+ .b4um-todo__progress-fill {
76
+ height: 100%;
77
+ background: var(--todo-primary);
78
+ border-radius: inherit;
79
+
80
+ transition: width 0.3s ease;
81
+ }
82
+
83
+ .b4um-todo__progress-percentage {
84
+ margin: 0.75rem 0 0;
85
+ text-align: right;
86
+
87
+ color: var(--todo-muted);
88
+ font-size: 0.85rem;
89
+ }
90
+
91
+ /* -------------------------------------------------------
92
+ Form
93
+ ------------------------------------------------------- */
94
+
95
+ .b4um-todo__form {
96
+ margin-bottom: 2.5rem;
97
+ }
98
+
99
+ .b4um-todo__form h2 {
100
+ margin: 0 0 1rem;
101
+ font-size: 1.15rem;
102
+ font-weight: 600;
103
+ }
104
+
105
+ .b4um-todo__form-fields {
106
+ display: flex;
107
+ gap: 0.75rem;
108
+ }
109
+
110
+ .b4um-todo__form-fields input[type='text'] {
111
+ flex: 1;
112
+ min-width: 0;
113
+ padding: 0.9rem 1rem;
114
+
115
+ background: var(--todo-background);
116
+ color: var(--todo-text);
117
+
118
+ border: 1px solid var(--todo-border);
119
+ border-radius: 12px;
120
+
121
+ font: inherit;
122
+ outline: none;
123
+
124
+ transition: border-color 0.2s ease;
125
+ }
126
+
127
+ .b4um-todo__form-fields input[type='text']:focus {
128
+ border-color: var(--todo-primary);
129
+ }
130
+
131
+ .b4um-todo__button {
132
+ padding: 0.9rem 1.5rem;
133
+
134
+ background: var(--todo-primary);
135
+ color: var(--b4um-text-on-primary);
136
+
137
+ border: 0;
138
+ border-radius: 12px;
139
+
140
+ font: inherit;
141
+ font-weight: 600;
142
+ cursor: pointer;
143
+
144
+ transition: background 0.2s ease;
145
+ }
146
+
147
+ .b4um-todo__button:hover {
148
+ background: var(--todo-primary-hover);
149
+ }
150
+
151
+ .b4um-todo__errors {
152
+ margin-top: 0.75rem;
153
+ color: var(--todo-danger);
154
+ font-size: 0.9rem;
155
+ }
156
+
157
+ /* -------------------------------------------------------
158
+ Sections
159
+ ------------------------------------------------------- */
160
+
161
+ .b4um-todo__section {
162
+ margin-bottom: 2.5rem;
163
+ }
164
+
165
+ .b4um-todo__section h2 {
166
+ display: flex;
167
+ align-items: center;
168
+ gap: 0.5rem;
169
+
170
+ margin: 0 0 1rem;
171
+
172
+ font-size: 1.15rem;
173
+ font-weight: 600;
174
+ }
175
+
176
+ .b4um-todo__section h2 span {
177
+ color: var(--todo-muted);
178
+ font-size: 0.9rem;
179
+ font-weight: 400;
180
+ }
181
+
182
+ .b4um-todo__list {
183
+ display: flex;
184
+ flex-direction: column;
185
+ gap: 0.65rem;
186
+ }
187
+
188
+ .b4um-todo__empty {
189
+ padding: 1.5rem;
190
+
191
+ background: var(--todo-surface);
192
+ border: 1px dashed var(--todo-border);
193
+ border-radius: 12px;
194
+
195
+ color: var(--todo-muted);
196
+ text-align: center;
197
+ }
198
+
199
+ /* -------------------------------------------------------
200
+ Task Item
201
+ ------------------------------------------------------- */
202
+
203
+ .b4um-todo__item {
204
+ display: flex;
205
+ align-items: center;
206
+ justify-content: space-between;
207
+ gap: 1rem;
208
+
209
+ padding: 1rem 1.15rem;
210
+
211
+ background: var(--todo-background);
212
+ border: 1px solid var(--todo-border);
213
+ border-radius: 12px;
214
+
215
+ transition:
216
+ border-color 0.2s ease,
217
+ background 0.2s ease;
218
+ }
219
+
220
+ .b4um-todo__item:hover {
221
+ border-color: var(--todo-primary);
222
+ }
223
+
224
+ .b4um-todo__item-content {
225
+ display: flex;
226
+ align-items: center;
227
+ gap: 0.9rem;
228
+
229
+ min-width: 0;
230
+ }
231
+
232
+ .b4um-todo__item-content form,
233
+ .b4um-todo__item > form {
234
+ margin: 0;
235
+ }
236
+
237
+ .b4um-todo__item-name {
238
+ overflow-wrap: anywhere;
239
+ line-height: 1.5;
240
+ }
241
+
242
+ .b4um-todo__item--complete .b4um-todo__item-name {
243
+ color: var(--todo-muted);
244
+ text-decoration: line-through;
245
+ }
246
+
247
+ /* -------------------------------------------------------
248
+ Checkbox
249
+ ------------------------------------------------------- */
250
+
251
+ .b4um-todo__checkbox {
252
+ display: flex;
253
+ align-items: center;
254
+ justify-content: center;
255
+
256
+ width: 25px;
257
+ height: 25px;
258
+ padding: 0;
259
+
260
+ background: transparent;
261
+ border: 2px solid var(--todo-border);
262
+ border-radius: 8px;
263
+
264
+ cursor: pointer;
265
+ }
266
+
267
+ .b4um-todo__checkbox:hover {
268
+ border-color: var(--todo-primary);
269
+ }
270
+
271
+ .b4um-todo__item--complete .b4um-todo__checkbox {
272
+ background: var(--todo-success);
273
+ border-color: var(--todo-success);
274
+ color: #ffffff;
275
+ }
276
+
277
+ .b4um-todo__checkbox-icon {
278
+ font-size: 0.9rem;
279
+ font-weight: 700;
280
+ }
281
+
282
+ /* -------------------------------------------------------
283
+ Delete Button
284
+ ------------------------------------------------------- */
285
+
286
+ .b4um-todo__delete {
287
+ display: flex;
288
+ align-items: center;
289
+ justify-content: center;
290
+
291
+ width: 30px;
292
+ height: 30px;
293
+ padding: 0;
294
+
295
+ background: transparent;
296
+ color: var(--todo-muted);
297
+
298
+ border: 0;
299
+ border-radius: 8px;
300
+
301
+ font-size: 1.5rem;
302
+ cursor: pointer;
303
+
304
+ transition:
305
+ color 0.2s ease,
306
+ background 0.2s ease;
307
+ }
308
+
309
+ .b4um-todo__delete:hover {
310
+ background: #fff0f0;
311
+ color: var(--todo-danger);
312
+ }
313
+
314
+ /* -------------------------------------------------------
315
+ Responsive
316
+ ------------------------------------------------------- */
317
+
318
+ @media (max-width: 600px) {
319
+ .b4um-todo {
320
+ margin: 2rem auto;
321
+ padding: 0 1rem;
322
+ }
323
+
324
+ .b4um-todo__progress {
325
+ padding: 1.25rem;
326
+ }
327
+
328
+ .b4um-todo__form-fields {
329
+ flex-direction: column;
330
+ }
331
+
332
+ .b4um-todo__button {
333
+ width: 100%;
334
+ }
335
+
336
+ .b4um-todo__item {
337
+ padding: 0.85rem;
338
+ }
339
+ }
340
+
341
+ /* -------------------------------------------------------
342
+ Todo form button alignment
343
+ ------------------------------------------------------- */
344
+
345
+ .b4um-todo__form-fields .b4um-todo__button {
346
+ align-self: center;
347
+ }
@@ -0,0 +1,35 @@
1
+ import { Controller } from '@hotwired/stimulus'
2
+
3
+ // Connects to data-controller="b4um-todo"
4
+ export default class extends Controller {
5
+ static targets = ['input', 'submit']
6
+
7
+ connect() {
8
+ this.updateSubmitState()
9
+ }
10
+
11
+ resetAfterSuccess(event) {
12
+ if (!event.detail.success) return
13
+
14
+ const contentType = event.detail.fetchResponse?.contentType || ''
15
+
16
+ if (!contentType.includes('turbo-stream')) return
17
+
18
+ this.reset()
19
+ }
20
+
21
+ updateSubmitState() {
22
+ if (!this.hasInputTarget || !this.hasSubmitTarget) return
23
+
24
+ const value = this.inputTarget.value.trim()
25
+
26
+ this.submitTarget.disabled = value.length === 0
27
+ }
28
+
29
+ reset() {
30
+ if (!this.hasInputTarget) return
31
+
32
+ this.inputTarget.value = ''
33
+ this.updateSubmitState()
34
+ }
35
+ }