- Habits and the account now accumulate XP and levels from check streaks, with streak/level-up celebration banners and push notifications. - The "yesterday missed" banner on /today now lets a habit be explicitly marked failed (not just checked done), backed by a new HabitLog.status column so completion-rate/streak calculations never count a failed day as done. - The habit management tabs scroll horizontally on narrow screens instead of wrapping to two lines, with a pure-CSS edge shadow indicating more content.
118 lines
4.7 KiB
HTML
118 lines
4.7 KiB
HTML
<div
|
|
data-habit-id="{{ habit.id }}"
|
|
x-data="{
|
|
editing: false,
|
|
mask: {{ habit.weekdays_mask }},
|
|
difficulty: '{{ habit.difficulty.value }}',
|
|
alarmOn: {{ 'true' if habit.reminder_time else 'false' }},
|
|
days: ['월', '화', '수', '목', '금', '토', '일'],
|
|
}"
|
|
>
|
|
<div class="habit-item" x-show="!editing">
|
|
<div class="habit-item-main">
|
|
<span class="drag-handle" aria-hidden="true">⠿</span>
|
|
<div>
|
|
<div class="habit-item-name">{{ habit.name }}</div>
|
|
{% if habit.condition_text %}<div class="habit-item-condition">{{ habit.condition_text }}</div>{% endif %}
|
|
<div class="habit-item-meta">
|
|
{{ weekday_label(habit.weekdays_mask) }}
|
|
{% if habit.reminder_time %}· 알람 {{ habit.reminder_time.strftime("%H:%M") }}{% endif %}
|
|
</div>
|
|
{% set progress = goal_progress(habit) %}
|
|
<div class="goal-progress-badge">
|
|
{% if progress %}
|
|
목표 {{ difficulty_label(habit.difficulty) }} · {{ progress.elapsed }}/{{ progress.target }}일차{% if progress.reached %} · 목표 기간 달성!{% else %} (D-{{ progress.remaining }}){% endif %}
|
|
{% else %}
|
|
{{ difficulty_label(habit.difficulty) }}
|
|
{% endif %}
|
|
</div>
|
|
{% set stats = stats_map[habit.id] %}
|
|
{% if stats.scheduled_days > 0 %}
|
|
<div class="habit-item-stats">
|
|
<span class="badge">완료율 {{ stats.completion_rate }}%</span>
|
|
{% if stats.current_streak > 0 %}
|
|
<span class="badge{{ ' badge-milestone' if is_milestone_streak(stats.current_streak) }}">
|
|
{{ '🏆' if is_milestone_streak(stats.current_streak) else '🔥' }} 연속 {{ stats.current_streak }}일
|
|
</span>
|
|
{% endif %}
|
|
<span class="badge level-badge">{{ level_tier_emoji(stats.level_info.level) }} Lv.{{ stats.level_info.level }}</span>
|
|
</div>
|
|
<div class="xp-bar-wrap">
|
|
<div class="xp-bar"><div class="xp-bar-fill" style="width: {{ stats.level_info.progress_pct }}%;"></div></div>
|
|
<span class="xp-bar-label">{{ stats.level_info.xp_into_level }}/{{ stats.level_info.xp_for_next_level }} xp</span>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
<div class="habit-item-actions">
|
|
<button type="button" class="btn btn-secondary" @click="editing = true">수정</button>
|
|
{% if habit.status.value == "active" %}
|
|
<button
|
|
type="button"
|
|
class="btn btn-secondary"
|
|
hx-post="/habits/{{ habit.id }}/complete"
|
|
hx-target="body"
|
|
hx-swap="none"
|
|
hx-confirm="'{{ habit.name }}' 습관을 완료 처리할까요?"
|
|
>완료 처리</button>
|
|
<button
|
|
type="button"
|
|
class="btn btn-secondary"
|
|
hx-post="/habits/{{ habit.id }}/abandon"
|
|
hx-target="body"
|
|
hx-swap="none"
|
|
hx-confirm="'{{ habit.name }}' 습관을 포기할까요?"
|
|
>포기</button>
|
|
{% else %}
|
|
<button
|
|
type="button"
|
|
class="btn btn-secondary"
|
|
hx-post="/habits/{{ habit.id }}/reactivate"
|
|
hx-target="body"
|
|
hx-swap="none"
|
|
>다시 진행</button>
|
|
{% endif %}
|
|
<button
|
|
type="button"
|
|
class="btn btn-danger-ghost"
|
|
hx-delete="/habits/{{ habit.id }}/delete"
|
|
hx-target="body"
|
|
hx-swap="none"
|
|
hx-confirm="'{{ habit.name }}' 습관을 삭제할까요? 기록도 함께 삭제됩니다."
|
|
>삭제</button>
|
|
</div>
|
|
</div>
|
|
|
|
<form
|
|
x-show="editing"
|
|
x-cloak
|
|
style="padding: 12px 4px; border-bottom: 1px solid var(--color-border);"
|
|
hx-post="/habits/{{ habit.id }}/edit"
|
|
hx-target="#habit-edit-error-{{ habit.id }}"
|
|
hx-swap="innerHTML"
|
|
>
|
|
<input type="hidden" name="weekdays_mask" :value="mask" />
|
|
<input type="hidden" name="difficulty" :value="difficulty" />
|
|
|
|
<div class="field">
|
|
<label for="edit-name-{{ habit.id }}">습관 이름</label>
|
|
<input type="text" id="edit-name-{{ habit.id }}" name="name" required value="{{ habit.name }}" />
|
|
</div>
|
|
|
|
<div class="field">
|
|
<label for="edit-condition-{{ habit.id }}">달성 조건 (선택)</label>
|
|
<input type="text" id="edit-condition-{{ habit.id }}" name="condition_text" value="{{ habit.condition_text or '' }}" />
|
|
</div>
|
|
|
|
{% set reminder_value = habit.reminder_time.strftime("%H:%M") if habit.reminder_time else "" %}
|
|
{% include "partials/_weekday_alarm_fields.html" %}
|
|
|
|
<div id="habit-edit-error-{{ habit.id }}" class="form-error-text"></div>
|
|
|
|
<div style="display:flex; gap:8px;">
|
|
<button type="submit" class="btn btn-primary" style="flex:1;">저장</button>
|
|
<button type="button" class="btn btn-secondary" @click="editing = false">취소</button>
|
|
</div>
|
|
</form>
|
|
</div>
|