Skip to content

Commit b2edc00

Browse files
committed
UPDATE - shop open
1 parent 6a362d1 commit b2edc00

8 files changed

Lines changed: 205 additions & 28 deletions

File tree

client/src/components/AdminStatsPage.css

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,27 @@
3636
border-bottom: 2px solid rgba(247, 207, 87, 0.3);
3737
}
3838

39+
.astat-section-featured {
40+
padding: 1.4rem;
41+
border: 3px solid rgba(247, 207, 87, 0.55);
42+
border-radius: 16px;
43+
background: rgba(27, 58, 122, 0.82);
44+
}
45+
46+
.astat-section-featured .astat-section-title {
47+
margin-bottom: 0.5rem;
48+
border-bottom: none;
49+
padding-bottom: 0;
50+
font-size: 0.95rem;
51+
}
52+
53+
.astat-section-note {
54+
margin: 0 0 1.2rem;
55+
color: rgba(232, 244, 255, 0.72);
56+
font-size: 0.92rem;
57+
line-height: 1.45;
58+
}
59+
3960
/* Stat cards */
4061
.astat-grid {
4162
display: grid;

client/src/components/AdminStatsPage.jsx

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,20 @@ function fmt(n, decimals = 1) {
77
return Number.isFinite(num) ? num.toFixed(decimals).replace(/\.0+$/, "") : "—";
88
}
99

10+
function fmtInt(n) {
11+
if (n === undefined || n === null) return "—";
12+
const num = Number(n);
13+
return Number.isFinite(num) ? num.toLocaleString() : "—";
14+
}
15+
16+
function fmtUsdRange(min, max) {
17+
const lo = Number(min);
18+
const hi = Number(max);
19+
if (!Number.isFinite(lo) || !Number.isFinite(hi)) return "—";
20+
const format = (value) => `$${value.toLocaleString(undefined, { maximumFractionDigits: 0 })}`;
21+
return lo === hi ? format(lo) : `${format(lo)}${format(hi)}`;
22+
}
23+
1024
function StatCard({ label, value, sub, accent }) {
1125
return (
1226
<div className="astat-card" style={accent ? { "--astat-accent": accent } : undefined}>
@@ -238,6 +252,39 @@ export function AdminStatsPage() {
238252

239253
{stats ? (
240254
<>
255+
<section className="astat-section astat-section-featured">
256+
<h2 className="astat-section-title">Spendable from approved projects</h2>
257+
<p className="astat-section-note">
258+
Total across all users with approved hours, regardless of whether they also have pending or draft projects.
259+
</p>
260+
<div className="astat-grid">
261+
<StatCard
262+
label="Total coins / bricks"
263+
value={fmtInt(stats.spendableFromApproved.totalCoins)}
264+
sub={`${fmt(stats.spendableFromApproved.approvedHours)} approved hrs × ${stats.spendableFromApproved.bricksPerHour}`}
265+
accent="#ffd740"
266+
/>
267+
<StatCard
268+
label="Total USD value"
269+
value={fmtUsdRange(stats.spendableFromApproved.usdMin, stats.spendableFromApproved.usdMax)}
270+
sub="$5/h – $7.5/h program rate"
271+
accent="#9cffcf"
272+
/>
273+
<StatCard
274+
label="Approved hours"
275+
value={fmt(stats.spendableFromApproved.approvedHours)}
276+
sub="all projects, all statuses"
277+
accent="#8af4ff"
278+
/>
279+
<StatCard
280+
label="Users with approved hrs"
281+
value={stats.spendableFromApproved.users}
282+
sub="could spend shop currency"
283+
accent="#c8d4ff"
284+
/>
285+
</div>
286+
</section>
287+
241288
{/* Projects */}
242289
<section className="astat-section">
243290
<h2 className="astat-section-title">Projects</h2>

client/src/components/DeadlineCountdown.jsx

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,33 @@ export function useDeadlineState(deadlineMs) {
1515
};
1616
}
1717

18-
export function DeadlineCountdown({ label, deadlineMs, className = "" }) {
18+
export function DeadlineCountdown({ label, deadlineMs, className = "", layout = "inline" }) {
1919
const { isOpen, countdown } = useDeadlineState(deadlineMs);
2020

21+
if (layout === "banner") {
22+
return (
23+
<section
24+
className={`${className}${isOpen ? " is-open" : " is-closed"}`.trim()}
25+
aria-live="polite"
26+
role="status"
27+
>
28+
<p className="shop-page__deadline-kicker">
29+
{isOpen ? "Limited time — shop is open!" : "Shop closed"}
30+
</p>
31+
{isOpen ? (
32+
<p className="shop-page__deadline-heading">
33+
<span className="shop-page__deadline-label">{label} closes in</span>
34+
<strong className="shop-page__deadline-time">{countdown}</strong>
35+
</p>
36+
) : (
37+
<p className="shop-page__deadline-heading">
38+
<span className="shop-page__deadline-label">Purchases are unavailable</span>
39+
</p>
40+
)}
41+
</section>
42+
);
43+
}
44+
2145
return (
2246
<p className={className} aria-live="polite">
2347
{isOpen ? `${label} closes in ${countdown}` : `${label} closed`}

client/src/components/ShopPage.css

Lines changed: 82 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -17,20 +17,80 @@
1717

1818
.shop-page__deadline {
1919
position: absolute;
20-
top: 2.5rem;
21-
right: clamp(1rem, 3vw, 2.5rem);
22-
z-index: 4;
23-
width: fit-content;
24-
max-width: min(22rem, 92vw);
25-
padding: 0.45rem 0.75rem;
26-
border: 2px solid #162456;
27-
border-radius: 999px;
28-
background: rgba(255, 248, 216, 0.92);
29-
color: #27376f;
30-
font-size: 0.88rem;
31-
font-weight: 700;
20+
top: 1rem;
21+
left: 50%;
22+
z-index: 5;
23+
width: min(40rem, calc(100% - 2rem));
24+
transform: translateX(-50%);
25+
padding: 0.95rem 1.35rem 1.05rem;
26+
border: 3px solid #162456;
27+
border-radius: 18px;
28+
background: linear-gradient(180deg, #fff8d8 0%, #ffe9a8 100%);
29+
color: #162456;
3230
text-align: center;
33-
box-shadow: 0 3px 0 rgba(22, 36, 86, 0.18);
31+
box-shadow:
32+
0 6px 0 rgba(22, 36, 86, 0.28),
33+
0 0 0 4px rgba(255, 255, 255, 0.35);
34+
}
35+
36+
.shop-page__deadline.is-open {
37+
animation: shop-deadline-pulse 2.4s ease-in-out infinite;
38+
}
39+
40+
.shop-page__deadline.is-closed {
41+
background: linear-gradient(180deg, #ffd6df 0%, #ffb8c6 100%);
42+
border-color: #8f1d35;
43+
box-shadow:
44+
0 6px 0 rgba(143, 29, 53, 0.24),
45+
0 0 0 4px rgba(255, 255, 255, 0.28);
46+
}
47+
48+
.shop-page__deadline-kicker {
49+
margin: 0 0 0.35rem;
50+
color: #dd3b45;
51+
font-size: clamp(0.82rem, 1.4vw, 0.98rem);
52+
font-weight: 900;
53+
letter-spacing: 0.06em;
54+
text-transform: uppercase;
55+
}
56+
57+
.shop-page__deadline-heading {
58+
margin: 0;
59+
display: grid;
60+
gap: 0.2rem;
61+
justify-items: center;
62+
}
63+
64+
.shop-page__deadline-label {
65+
color: #27376f;
66+
font-size: clamp(0.95rem, 1.8vw, 1.15rem);
67+
font-weight: 800;
68+
}
69+
70+
.shop-page__deadline-time {
71+
display: block;
72+
color: #12235c;
73+
font-size: clamp(1.8rem, 4.2vw, 2.75rem);
74+
font-weight: 900;
75+
line-height: 1.05;
76+
letter-spacing: 0.04em;
77+
text-shadow: 0 2px 0 rgba(255, 255, 255, 0.55);
78+
font-variant-numeric: tabular-nums;
79+
}
80+
81+
@keyframes shop-deadline-pulse {
82+
0%,
83+
100% {
84+
box-shadow:
85+
0 6px 0 rgba(22, 36, 86, 0.28),
86+
0 0 0 4px rgba(255, 255, 255, 0.35);
87+
}
88+
89+
50% {
90+
box-shadow:
91+
0 8px 0 rgba(22, 36, 86, 0.24),
92+
0 0 0 6px rgba(247, 207, 87, 0.55);
93+
}
3494
}
3595

3696
.shop-page__left-panel {
@@ -64,7 +124,7 @@
64124
position: relative;
65125
z-index: 3;
66126
width: min(58rem, 67vw);
67-
margin: 8.3rem clamp(5rem, 7.8vw, 8rem) 0 auto;
127+
margin: 10.5rem clamp(5rem, 7.8vw, 8rem) 0 auto;
68128
display: grid;
69129
grid-template-columns: repeat(3, minmax(13rem, 1fr));
70130
gap: 1.5rem;
@@ -503,7 +563,7 @@
503563
.shop-page__grid {
504564
width: min(44rem, 71vw);
505565
gap: 1.15rem;
506-
margin: 10rem clamp(3.6rem, 6.4vw, 5.8rem) 0 auto;
566+
margin: 11.5rem clamp(3.6rem, 6.4vw, 5.8rem) 0 auto;
507567
}
508568

509569
.shop-page__actions {
@@ -531,9 +591,15 @@
531591
transform: translate(0.55rem, 0.35rem) rotate(-6deg);
532592
}
533593

594+
.shop-page__deadline {
595+
top: 0.65rem;
596+
width: calc(100% - 1.2rem);
597+
padding: 0.75rem 0.9rem 0.85rem;
598+
}
599+
534600
.shop-page__grid {
535601
width: min(25rem, 72vw);
536-
margin: 9.4rem clamp(1.8rem, 7vw, 3.2rem) 0 auto;
602+
margin: 10.8rem clamp(1.8rem, 7vw, 3.2rem) 0 auto;
537603
grid-template-columns: repeat(2, minmax(10.5rem, 1fr));
538604
}
539605

client/src/components/ShopPage.jsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,12 @@ export function ShopPage() {
132132

133133
<PlatformStatusBar user={user} />
134134

135-
<DeadlineCountdown label="Shop" deadlineMs={SHOP_CLOSE_MS} className="shop-page__deadline" />
135+
<DeadlineCountdown
136+
label="Shop"
137+
deadlineMs={SHOP_CLOSE_MS}
138+
layout="banner"
139+
className="shop-page__deadline"
140+
/>
136141

137142
<section className="shop-page__left-panel" aria-label="Inspiration panel">
138143
<p className="shop-page__inspired">1 hour = 20 Coins!</p>

client/src/utils/eventDeadlines.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
/** Jul 1, 2026 10:00 AM Eastern (EDT, UTC-4). */
22
export const SHIP_CLOSE_MS = Date.parse("2026-07-01T14:00:00Z");
33

4-
/** Jul 2, 2026 10:00 AM Eastern (EDT, UTC-4). */
5-
export const SHOP_CLOSE_MS = Date.parse("2026-07-02T14:00:00Z");
4+
/** Jul 8, 2026 3:27 AM Eastern (EDT, UTC-4) — 24.1h shop reopen window. */
5+
export const SHOP_CLOSE_MS = Date.parse("2026-07-08T07:27:00Z");
66

77
export const SHIP_CLOSED_MESSAGE = "Shipping closed on Jul 1 at 10:00 AM ET.";
8-
export const SHOP_CLOSED_MESSAGE = "Shop closed on Jul 2 at 10:00 AM ET.";
8+
export const SHOP_CLOSED_MESSAGE = "Shop closed on Jul 8 at 3:27 AM ET.";
99

1010
export function isDeadlineOpen(deadlineMs, nowMs = Date.now()) {
1111
return nowMs < deadlineMs;

server/adminStats.js

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
import { pool } from "./db.js";
2+
import { BRICKS_PER_APPROVED_HOUR } from "./projects.js";
3+
4+
const USD_PER_APPROVED_HOUR_MIN = 5;
5+
const USD_PER_APPROVED_HOUR_MAX = 7.5;
26

37
export async function ensureAuditLogTable() {
48
if (!pool) return;
@@ -116,16 +120,15 @@ export async function getAdminStats() {
116120
),
117121
0
118122
)
119-
) FILTER (WHERE status IN ('in-review', 'pending-reship')), 0) AS pending_review_hours
123+
) FILTER (WHERE status IN ('in-review', 'pending-reship')), 0) AS pending_review_hours,
124+
COUNT(DISTINCT user_id) FILTER (WHERE COALESCE(approved_hours, 0) > 0)::int AS users_with_approved_hours
120125
FROM projects
121126
`),
122127
pool.query(`
123128
SELECT COALESCE(SUM(hours_worked), 0) AS total_journal_hours FROM journal_entries
124129
`),
125130
pool.query(`SELECT COALESCE(SUM(bricks), 0) AS wallet_bricks FROM users`),
126-
pool.query(`
127-
SELECT COALESCE(SUM(bricks_earned), 0) AS total_earned FROM projects WHERE COALESCE(approved_hours, 0) > 0
128-
`),
131+
pool.query(`SELECT COALESCE(SUM(bricks_earned), 0) AS total_earned FROM projects`),
129132
pool.query(`
130133
SELECT
131134
COUNT(*)::int AS total_users,
@@ -146,8 +149,19 @@ export async function getAdminStats() {
146149

147150
const totalEarned = Number(er.total_earned);
148151
const walletBricks = Number(wr.wallet_bricks);
152+
const approvedHours = Number(pr.approved_hours);
153+
const usersWithApprovedHours = Number(pr.users_with_approved_hours);
149154

150155
return {
156+
spendableFromApproved: {
157+
approvedHours,
158+
totalBricks: totalEarned,
159+
totalCoins: totalEarned,
160+
bricksPerHour: BRICKS_PER_APPROVED_HOUR,
161+
usdMin: approvedHours * USD_PER_APPROVED_HOUR_MIN,
162+
usdMax: approvedHours * USD_PER_APPROVED_HOUR_MAX,
163+
users: usersWithApprovedHours,
164+
},
151165
projects: {
152166
total: pr.total,
153167
draft: pr.draft,

server/eventDeadlines.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
/** Jul 1, 2026 10:00 AM Eastern (EDT, UTC-4). */
22
export const SHIP_CLOSE_UTC = new Date("2026-07-01T14:00:00Z");
33

4-
/** Jul 2, 2026 10:00 AM Eastern (EDT, UTC-4). */
5-
export const SHOP_CLOSE_UTC = new Date("2026-07-02T14:00:00Z");
4+
/** Jul 8, 2026 3:27 AM Eastern (EDT, UTC-4) — 24.1h shop reopen window. */
5+
export const SHOP_CLOSE_UTC = new Date("2026-07-08T07:27:00Z");
66

77
export const SHIP_CLOSED_MESSAGE = "Shipping closed on Jul 1 at 10:00 AM ET.";
8-
export const SHOP_CLOSED_MESSAGE = "Shop closed on Jul 2 at 10:00 AM ET.";
8+
export const SHOP_CLOSED_MESSAGE = "Shop closed on Jul 8 at 3:27 AM ET.";
99

1010
export function isBeforeDeadline(deadline, now = new Date()) {
1111
return now.getTime() < deadline.getTime();

0 commit comments

Comments
 (0)