diff --git a/backend/main.py b/backend/main.py
index 027e1c4..2ce442c 100644
--- a/backend/main.py
+++ b/backend/main.py
@@ -175,6 +175,12 @@ def _migrate():
conn.commit()
except Exception:
pass
+ try:
+ conn.execute(text("ALTER TABLE groups ADD COLUMN icon VARCHAR(16)"))
+ conn.commit()
+ logging.info("Migration: added icon to groups")
+ except Exception:
+ pass
_migrate()
diff --git a/backend/models.py b/backend/models.py
index 2ace8c2..d515637 100644
--- a/backend/models.py
+++ b/backend/models.py
@@ -268,6 +268,7 @@ class Group(Base):
id = Column(Integer, primary_key=True, index=True)
name = Column(String(100), nullable=False)
+ icon = Column(String(16), nullable=True) # emoji shown for the group
created_by = Column(Integer, ForeignKey("users.id"), nullable=False)
created_at = Column(String(50), nullable=True) # ISO 8601
diff --git a/backend/routers/groups_router.py b/backend/routers/groups_router.py
index bc72b65..73fc163 100644
--- a/backend/routers/groups_router.py
+++ b/backend/routers/groups_router.py
@@ -34,6 +34,12 @@ def _now_iso() -> str:
class GroupCreate(BaseModel):
name: str
member_ids: List[int] = []
+ icon: Optional[str] = None
+
+
+class GroupUpdate(BaseModel):
+ name: Optional[str] = None
+ icon: Optional[str] = None
class MemberAdd(BaseModel):
@@ -86,7 +92,8 @@ def create_group(
db: Session = Depends(get_db),
current_user: models.User = Depends(get_current_user),
):
- group = models.Group(name=data.name, created_by=current_user.id, created_at=_now_iso())
+ group = models.Group(name=data.name, icon=(data.icon or None),
+ created_by=current_user.id, created_at=_now_iso())
db.add(group)
db.flush()
@@ -135,6 +142,7 @@ def list_groups(
out.append({
"id": group.id,
"name": group.name,
+ "icon": group.icon,
"role": m.role,
"member_count": member_count,
"group_calendar_id": _group_calendar_id(db, group.id),
@@ -155,12 +163,30 @@ def _group_detail(db: Session, group: models.Group, current_user: models.User) -
return {
"id": group.id,
"name": group.name,
+ "icon": group.icon,
"created_by": group.created_by,
"members": member_dicts,
"group_calendar_id": _group_calendar_id(db, group.id),
}
+@router.put("/{group_id}")
+def update_group(
+ group_id: int,
+ data: GroupUpdate,
+ db: Session = Depends(get_db),
+ current_user: models.User = Depends(get_current_user),
+):
+ group = _get_group_or_404(db, group_id)
+ _require_owner(db, group, current_user)
+ if data.name is not None and data.name.strip():
+ group.name = data.name.strip()
+ if data.icon is not None:
+ group.icon = data.icon or None
+ db.commit()
+ return _group_detail(db, group, current_user)
+
+
@router.get("/{group_id}")
def get_group(
group_id: int,
diff --git a/frontend/css/app.css b/frontend/css/app.css
index 5b62aaf..80e40d1 100644
--- a/frontend/css/app.css
+++ b/frontend/css/app.css
@@ -1923,3 +1923,19 @@ a { color: var(--primary); text-decoration: none; }
user-select: none;
}
.cal-item.cal-dragging { opacity: .5; }
+
+/* Group emoji + icon picker */
+.group-emoji { flex: 0 0 auto; font-size: 16px; cursor: pointer; line-height: 1; }
+.cal-shared-flag { flex: 0 0 auto; font-size: 12px; opacity: .8; }
+.group-icon-picker { display: flex; flex-wrap: wrap; gap: 6px; }
+.group-icon-opt {
+ width: 38px; height: 38px;
+ display: inline-flex; align-items: center; justify-content: center;
+ font-size: 18px; line-height: 1;
+ background: var(--bg-app);
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ cursor: pointer;
+}
+.group-icon-opt:hover { background: var(--bg-surface); }
+.group-icon-opt.on { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent) inset; }
diff --git a/frontend/index.html b/frontend/index.html
index f3125d1..97ecf10 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -376,6 +376,10 @@
+
`
).join('');
@@ -2267,9 +2278,10 @@ function renderGroupList() {
}
el.innerHTML = state.groups.map(g =>
`
+
${escHtml(g.icon || 'π₯')}
${escHtml(g.name)}
`
).join('');
@@ -2316,7 +2328,11 @@ async function openGroupModal(groupId) {
const existingMemberIds = new Set((detail ? detail.members : []).map(m => m.id));
document.getElementById('group-name').value = detail ? detail.name : '';
- document.getElementById('group-name').disabled = isEdit; // rename not supported by API
+ document.getElementById('group-name').disabled = false; // rename supported via PUT
+
+ // Icon picker
+ modal.__icon = (detail && detail.icon) || 'π₯';
+ renderGroupIconPicker();
// Member picker: current members are checked; the owner (me) is excluded.
modal.__directory = directory;
@@ -2326,6 +2342,23 @@ async function openGroupModal(groupId) {
openModal('modal-group');
}
+const GROUP_ICONS = ['π₯', 'π¨βπ©βπ§', 'π ', 'β€οΈ', 'π§βπ€βπ§', 'β½', 'π', 'πΌ', 'π', 'πΆ', 'βοΈ', 'π΅', 'π', 'π', 'π³', 'β'];
+function renderGroupIconPicker() {
+ const modal = document.getElementById('modal-group');
+ const sel = modal.__icon || 'π₯';
+ const picker = document.getElementById('group-icon-picker');
+ if (!picker) return;
+ picker.innerHTML = GROUP_ICONS.map(ic =>
+ ``
+ ).join('');
+ picker.querySelectorAll('.group-icon-opt').forEach(b => {
+ b.addEventListener('click', () => {
+ modal.__icon = b.dataset.icon;
+ renderGroupIconPicker();
+ });
+ });
+}
+
function renderGroupMemberPicker() {
const modal = document.getElementById('modal-group');
const dir = modal.__directory || [];
@@ -2360,8 +2393,12 @@ function bindGroupUI() {
const groupId = modal.dataset.groupId;
const memberIds = [...(modal.__memberIds || new Set())];
try {
+ const name = document.getElementById('group-name').value.trim();
+ const icon = modal.__icon || 'π₯';
+ if (!name) { showToast(t('error_enter_title'), true); return; }
if (groupId) {
- // Manage mode: sync member additions/removals.
+ // Manage mode: update name/icon, then sync member additions/removals.
+ await api.put(`/groups/${groupId}`, { name, icon });
const detail = await api.get(`/groups/${groupId}`);
const me = JSON.parse(localStorage.getItem('user') || '{}');
const current = new Set(detail.members.map(m => m.id).filter(id => id !== me.id));
@@ -2369,9 +2406,7 @@ function bindGroupUI() {
for (const id of current) if (!memberIds.includes(id)) await api.delete(`/groups/${groupId}/members/${id}`);
showToast(t('group_saved'));
} else {
- const name = document.getElementById('group-name').value.trim();
- if (!name) { showToast(t('error_enter_title'), true); return; }
- await api.post('/groups/', { name, member_ids: memberIds });
+ await api.post('/groups/', { name, member_ids: memberIds, icon });
showToast(t('group_created'));
}
closeModal('modal-group');
diff --git a/frontend/js/i18n.js b/frontend/js/i18n.js
index b26bd6c..1adb819 100644
--- a/frontend/js/i18n.js
+++ b/frontend/js/i18n.js
@@ -121,6 +121,8 @@ const translations = {
group_created: 'Gruppe erstellt',
group_view_label: 'Gruppenansicht: {name}',
group_exit: 'Gruppenansicht verlassen',
+ group_icon: 'Icon',
+ group_visible_flag: 'FΓΌr deine Gruppen sichtbar',
upload_too_large: 'Datei zu groΓ (Server-Limit). Bitte Upload-Limit erhΓΆhen.',
shared_with_me: 'Mit dir geteilt',
settings_calendars: 'Kalender',
@@ -385,6 +387,8 @@ const translations = {
group_created: 'Group created',
group_view_label: 'Group view: {name}',
group_exit: 'Exit group view',
+ group_icon: 'Icon',
+ group_visible_flag: 'Visible to your groups',
upload_too_large: 'File too large (server limit). Please raise the upload limit.',
shared_with_me: 'Shared with me',
settings_calendars: 'Calendars',
diff --git a/frontend/js/version.js b/frontend/js/version.js
index a1a28e2..09e33fd 100644
--- a/frontend/js/version.js
+++ b/frontend/js/version.js
@@ -1,2 +1,2 @@
// Increment APP_VERSION with every code change
-export const APP_VERSION = 'v33';
+export const APP_VERSION = 'v34';