+44
-19
src/templates/app.html
+44
-19
src/templates/app.html
···
1018
1018
}
1019
1019
1020
1020
.watch-live-btn {
1021
-
position: fixed;
1022
-
top: clamp(1rem, 2vmin, 1.5rem);
1023
-
right: clamp(1rem, 2vmin, 1.5rem);
1024
1021
font-family: inherit;
1025
1022
font-size: clamp(0.65rem, 1.4vmin, 0.75rem);
1026
1023
color: var(--text-light);
···
1028
1025
background: var(--bg);
1029
1026
padding: clamp(0.4rem, 1vmin, 0.5rem) clamp(0.8rem, 2vmin, 1rem);
1030
1027
transition: all 0.2s ease;
1031
-
z-index: 100;
1032
1028
cursor: pointer;
1033
1029
border-radius: 2px;
1034
1030
display: flex;
···
1062
1058
animation: pulse 2s ease-in-out infinite;
1063
1059
}
1064
1060
1065
-
.filter-btn {
1061
+
/* Top right button container for filter and watch live */
1062
+
.top-right-buttons {
1066
1063
position: fixed;
1067
1064
top: clamp(1rem, 2vmin, 1.5rem);
1068
-
right: clamp(7rem, 14vmin, 10rem);
1065
+
right: clamp(1rem, 2vmin, 1.5rem);
1066
+
display: flex;
1067
+
flex-direction: row;
1068
+
align-items: center;
1069
+
gap: clamp(0.5rem, 1vmin, 0.75rem);
1070
+
z-index: 100;
1071
+
}
1072
+
1073
+
@media (max-width: 768px) {
1074
+
.top-right-buttons {
1075
+
flex-direction: column;
1076
+
align-items: flex-end;
1077
+
}
1078
+
}
1079
+
1080
+
.filter-btn {
1069
1081
font-family: inherit;
1070
1082
font-size: clamp(0.65rem, 1.4vmin, 0.75rem);
1071
1083
color: var(--text-light);
···
1073
1085
background: var(--bg);
1074
1086
padding: clamp(0.4rem, 1vmin, 0.5rem) clamp(0.8rem, 2vmin, 1rem);
1075
1087
transition: all 0.2s ease;
1076
-
z-index: 100;
1077
1088
cursor: pointer;
1078
1089
border-radius: 2px;
1079
1090
display: flex;
···
1122
1133
max-width: 280px;
1123
1134
display: none;
1124
1135
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
1136
+
}
1137
+
1138
+
@media (max-width: 768px) {
1139
+
.filter-panel {
1140
+
top: clamp(6rem, 12vmin, 8rem);
1141
+
}
1125
1142
}
1126
1143
1127
1144
@media (prefers-color-scheme: dark) {
···
1298
1315
pointer-events: none;
1299
1316
max-width: min(300px, calc(100vw - 2rem));
1300
1317
width: max-content;
1318
+
}
1319
+
1320
+
@media (max-width: 768px) {
1321
+
.firehose-toast {
1322
+
top: clamp(7rem, 14vmin, 9rem);
1323
+
}
1301
1324
}
1302
1325
1303
1326
.firehose-toast.visible {
···
2071
2094
<path d="M12 17h.01" />
2072
2095
</svg>
2073
2096
</div>
2074
-
<button class="filter-btn" id="filterBtn">
2075
-
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none"
2076
-
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
2077
-
<polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3" />
2078
-
</svg>
2079
-
<span class="filter-label-text">filter</span>
2080
-
<span class="filter-count" id="filterCount" style="display: none;"></span>
2081
-
</button>
2097
+
<div class="top-right-buttons">
2098
+
<button class="filter-btn" id="filterBtn">
2099
+
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none"
2100
+
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
2101
+
<polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3" />
2102
+
</svg>
2103
+
<span class="filter-label-text">filter</span>
2104
+
<span class="filter-count" id="filterCount" style="display: none;"></span>
2105
+
</button>
2106
+
<button class="watch-live-btn" id="watchLiveBtn">
2107
+
<span class="watch-indicator"></span>
2108
+
<span class="watch-label">watch live</span>
2109
+
</button>
2110
+
</div>
2082
2111
<div class="filter-panel" id="filterPanel">
2083
2112
<div class="filter-panel-header">
2084
2113
<span class="filter-panel-title">show apps</span>
···
2090
2119
</div>
2091
2120
<div class="filter-list" id="filterList"></div>
2092
2121
</div>
2093
-
<button class="watch-live-btn" id="watchLiveBtn">
2094
-
<span class="watch-indicator"></span>
2095
-
<span class="watch-label">watch live</span>
2096
-
</button>
2097
2122
<div class="pov-indicator">point of view of <a class="pov-handle" id="povHandle" href="#" target="_blank"
2098
2123
rel="noopener noreferrer"></a></div>
2099
2124
<div class="guestbook-sign">sign the guest list</div>
+37
-4
static/app.js
+37
-4
static/app.js
···
12
12
// APP FILTER FUNCTIONALITY
13
13
// ============================================================================
14
14
15
-
// Load hidden apps from localStorage
15
+
// Parse hidden apps from URL param
16
+
function getHiddenAppsFromUrl() {
17
+
const params = new URLSearchParams(window.location.search);
18
+
const hideParam = params.get('hide');
19
+
if (hideParam) {
20
+
return new Set(hideParam.split(',').filter(Boolean));
21
+
}
22
+
return null;
23
+
}
24
+
25
+
// Update URL with current hidden apps (without page reload)
26
+
function updateUrlWithFilters() {
27
+
const params = new URLSearchParams(window.location.search);
28
+
if (hiddenApps.size > 0) {
29
+
params.set('hide', [...hiddenApps].join(','));
30
+
} else {
31
+
params.delete('hide');
32
+
}
33
+
const newUrl = params.toString()
34
+
? `${window.location.pathname}?${params.toString()}`
35
+
: window.location.pathname;
36
+
history.replaceState(null, '', newUrl);
37
+
}
38
+
39
+
// Load hidden apps from URL param first, then localStorage
16
40
function loadHiddenApps() {
41
+
// URL takes precedence over localStorage
42
+
const urlHidden = getHiddenAppsFromUrl();
43
+
if (urlHidden) {
44
+
hiddenApps = urlHidden;
45
+
return;
46
+
}
47
+
17
48
try {
18
49
const stored = localStorage.getItem(`atme_hidden_apps_${did}`);
19
50
if (stored) {
···
24
55
}
25
56
}
26
57
27
-
// Save hidden apps to localStorage
58
+
// Save hidden apps to localStorage and update URL
28
59
function saveHiddenApps() {
29
60
try {
30
61
localStorage.setItem(`atme_hidden_apps_${did}`, JSON.stringify([...hiddenApps]));
31
62
} catch (e) {
32
63
// Silently fail
33
64
}
65
+
updateUrlWithFilters();
34
66
}
35
67
36
68
// Update filter button state
···
845
877
846
878
// After all URL validations complete, apply default "valid" filter (hide unresolved)
847
879
Promise.all(validationPromises).then(() => {
848
-
// Only apply default if user hasn't set any filters yet
849
-
if (hiddenApps.size === 0) {
880
+
// Only apply default if user hasn't set any filters yet AND no URL param was provided
881
+
const hasUrlFilters = getHiddenAppsFromUrl() !== null;
882
+
if (hiddenApps.size === 0 && !hasUrlFilters) {
850
883
// Hide apps with invalid-link class by default
851
884
const appViews = document.querySelectorAll('.app-view');
852
885
appViews.forEach(view => {