Files
habit-tracker/app/templates/partials/habit_item.html
T
shinalok 800f42eab1 add habit leveling/XP system, explicit fail marking for missed habits, and scrollable habit tabs
- 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.
2026-07-20 18:18:56 +09:00

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>