From 2d2b2d040b4243d99e03bba0c5192846bd67a2fd Mon Sep 17 00:00:00 2001 From: kfj001 Date: Fri, 19 Jun 2026 17:37:19 +0000 Subject: [PATCH] additional files --- .../-workspaces-web-app/memory/MEMORY.md | 1 + .../memory/angular-zoneless-fix.md | 12 + .devcontainer/Dockerfile | 6 +- backend/__pycache__/seed.cpython-312.pyc | Bin 8567 -> 0 bytes backend/app/__pycache__/auth.cpython-312.pyc | Bin 0 -> 5685 bytes .../app/__pycache__/config.cpython-312.pyc | Bin 747 -> 1070 bytes .../app/__pycache__/database.cpython-312.pyc | Bin 813 -> 841 bytes backend/app/__pycache__/main.cpython-312.pyc | Bin 2942 -> 4361 bytes .../app/__pycache__/schemas.cpython-312.pyc | Bin 3722 -> 4240 bytes .../__pycache__/user_models.cpython-312.pyc | Bin 0 -> 1142 bytes .../app/api/__pycache__/auth.cpython-312.pyc | Bin 0 -> 5637 bytes .../api/__pycache__/events.cpython-312.pyc | Bin 4531 -> 4817 bytes .../api/__pycache__/programs.cpython-312.pyc | Bin 4830 -> 5116 bytes .../app/api/__pycache__/tiers.cpython-312.pyc | Bin 4268 -> 4514 bytes src/app/donate/donate.component.html | 34 +- src/app/donate/donate.component.ts | 35 +- src/app/events/events.component.html | 54 +-- src/app/events/events.component.ts | 35 +- src/app/navbar/navbar.component.html | 13 + src/app/navbar/navbar.component.scss | 56 +++ src/app/navbar/navbar.component.ts | 37 +- src/app/schedule/schedule.component.html | 93 ++-- src/app/schedule/schedule.component.scss | 79 +--- src/app/schedule/schedule.component.ts | 59 ++- studio.svg | 434 ++++++++++++++++++ 25 files changed, 709 insertions(+), 239 deletions(-) create mode 100644 .claude/projects/-workspaces-web-app/memory/MEMORY.md create mode 100644 .claude/projects/-workspaces-web-app/memory/angular-zoneless-fix.md delete mode 100644 backend/__pycache__/seed.cpython-312.pyc create mode 100644 backend/app/__pycache__/auth.cpython-312.pyc create mode 100644 backend/app/__pycache__/user_models.cpython-312.pyc create mode 100644 backend/app/api/__pycache__/auth.cpython-312.pyc create mode 100644 studio.svg diff --git a/.claude/projects/-workspaces-web-app/memory/MEMORY.md b/.claude/projects/-workspaces-web-app/memory/MEMORY.md new file mode 100644 index 0000000..11d542c --- /dev/null +++ b/.claude/projects/-workspaces-web-app/memory/MEMORY.md @@ -0,0 +1 @@ +- [Angular Zoneless Fix](angular-zoneless-fix.md) — Added zone.js to fix async HTTP change detection diff --git a/.claude/projects/-workspaces-web-app/memory/angular-zoneless-fix.md b/.claude/projects/-workspaces-web-app/memory/angular-zoneless-fix.md new file mode 100644 index 0000000..4c4f457 --- /dev/null +++ b/.claude/projects/-workspaces-web-app/memory/angular-zoneless-fix.md @@ -0,0 +1,12 @@ +--- +name: angular-zoneless-fix +description: Fixed Angular 21 zoneless default causing async HTTP callbacks to not trigger change detection +metadata: + type: project +--- + +Angular 21 defaults to zoneless execution. The project was missing zone.js entirely, so HTTP responses arrived but change detection never ran — components stayed stuck on loading states. + +**Fix:** Added `import 'zone.js'` to [src/main.ts](src/main.ts) and installed zone.js as a dependency. This is the simpler fix over configuring provideZoneChangeDetection() manually. + +Related: [[auto-seed-admin-bootstrap]] diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 8fbf7cf..1f3fc4f 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,6 +1,8 @@ FROM python:3.12-alpine -RUN apk update && apk add git nodejs npm +RUN apk update && apk add git nodejs npm bash # Install Angular.js via NPM -RUN npm install -g @angular/cli@21.2.14 \ No newline at end of file +RUN npm install -g @angular/cli@21.2.14 + +RUN /bin/bash \ No newline at end of file diff --git a/backend/__pycache__/seed.cpython-312.pyc b/backend/__pycache__/seed.cpython-312.pyc deleted file mode 100644 index af1e52f09d1280d1f53090c39e573246eedf8701..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8567 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 diff --git a/backend/app/__pycache__/auth.cpython-312.pyc b/backend/app/__pycache__/auth.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d1adf3b8946c75bebde0417fe08da52861031a68 GIT binary patch literal 5685 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 literal 0 HcmV?d00001 diff --git a/backend/app/__pycache__/config.cpython-312.pyc b/backend/app/__pycache__/config.cpython-312.pyc index 64826b349bdb1d7f15901b7c4845830df0b134a5..7a00907c0eb2562f0ab894246e1a3c6cb0ec72fc 100644 GIT binary patch delta 492 zcmaFOx{ibIG%qg~0}z<`nPi1AP2{U(oHwz@H7<%Xl_ixmh0_KomBJCl1?F)e>W}a?AQGQBka!F=>-Yq__@R0amSLYztka%y`$jk+-3=Fq~K!UCj z0g6Ba;(^wOhPVdb;!4dci!UfjP0Osf#p>-F;ur6yDL8osqrY+yH!uK-ct8X%h~NVe z{2)RANZex1%qyAvoY9}Lbh0^I<|qOd^yg0NPjPm#>z0w!Y*X^>SiAVL;o zABRmY(0e(lc0~$6E+Y^ZgTy~DGcq#XWzhP<#>=SPQS*fXNPSdb;1TJzYUBWG2LR36 BgpdFL delta 134 zcmZ3-@tT$IG%qg~0}x#AG0qBSoXA(p=rys&l`DlalQD|3l1r0&vOeRA$w^Eij9in; znfxceWb$V;nQY0d%kw})VS)MuVXa1=B9Y0PnT@4{fyNYxfCy0_af`zyH$SB`C)KV< dViJp(1S_Lc9LzweMVVr{qp>x?BasNn%reba2OdW(Bhh zTRa5O9fwoeNpgimCCh?s&x^UzVEgkdTf*vw;FNw6e@V1%!)OCrDn`d&8m`|k+{l)_ zQgOqE(dBH#YIIy`9z8i7bZzjAmB(5D;O*!Wnb0Sy8mRi;cT}}uAv0U`-!@NMPFtMT zU&T^Rm@DxM-Pj2Q%n=8p9PcEAZ?SS4;UA41&Efu$kwLhwB{{-42Ebw+`ORZ&8SaT$ z3f`ctfY#Ot$XnJaYSugjgcO>XTF~ItLJqgZ?FD{vwQA5=VAj^yzpCJ-HMW4()!U$> z%YY@WV8`kJSeK=~5UVW!09aBKO=3b9QJu(?#0!W2Tr}n=s9vSii#06CQd6?R0KHF* zVZp{i_O*44npCd74+zFEVFK!eTSR$t%v8{@@)y^+46b0SuT+>N8kq^~g0Tfmzze@g z_L0y@+H7e1`zib#a1u)7FOt13+2fLBMOh`1jjogRw15jgp?eJ7eMnHPes{%t6F&)^ z3L!-9JaUG5Z8$RMAI_1|@}`r>kzB|h7|yBKKy+l-f7%!Jhq#o{;qwy3;W)#{{FWP^mX%UQAD0R%JCd;5TlGDp^c(G{k$&{=6n&}7e*@@ z1D4HEgON}u5Od0Stzf1qEMiMJIX@cqMLCYm(Na6UQ8YqP8FE>Ky;Qrh0ihyvM}L4q zf=b4pXn~x`%FJ^z^NrSQb^Ba(`$CB&>AvW`HZ)&hyXyYjy}%eQ#spQCv1J(Bg3g%q zCcHP^&epmzwXSqc=j}sjTX#m+jXYVMJ)^UK>B#6B7fHNWyNHvOMNa`~DEeeiUIiFK zR#TVJ)Xix&r8V0YOx6Wcyf$lU%9xtcmCautN|(D9tj?^pHDhg^w>jrc)+Y+mtev1y zY_Tz^OeimTA2N=4gDDwKgy#(QEMreI_60_tHnw~x&$b@Ov>r&e^rj7OJY@QwT7j-Q z>Hf0@kaWHa0>->FFK=GT<02YAJAU^3+2wp(4w$N?`QklGc^arqOL?`pACHg6&&QYZ zI+S54kD>mb;bgsyJxapBV{*t3J0 zRudvFRw8=3685wa(Y(D)A8+Yj~TzS4SMv{o$XBs&p4pRNX^s{BCZ#M zXIh}Af|zM-Lilby^fXa-H;ch{w|3QfI`MmY=xL$u8M^FfbKeC$ZPfiv3&OJ|F?zNR zdOC<%r-XMv)Y5Dl6nPir(a5+I20f%Ih0D+;MJtdah4Pv4lu@!3qBLcuJdLtc4fHrv zsalzc>m?;>tP@YQNwz~$c2ZuA>;VqFl#iM#gDRJBhE*y*U8j+zTCKf2t*EDBnMuUh-f7 delta 1076 zcmZuuO=uHA6rS1L>~1&N&F0rMOdz-&We(}vyFHboj0 z4;CV!%t1lHb7_0>AcB`Pxe0=|5)d!q!Beydp@=w>DAY6q@6GqV`M&pNch(1gdzJUH zECI4kJ&kAY@EeK`DjUNK#hfeY%DIzn2y}>gbas|O(qnFmD#=QE7@)Jrifl;MJ=@9_ z_m}tT-fXOm4`i=BgXcUSu{jy<08sDU1OUbf1dxBukqaaPt*`8M%|L6;a7Pc+ zhXVf(Dg+C`NI!nUJb)~Auzq}+^)oBX>lyX{;!Q!3N?#M8^oGPHwu*zk(%TjG7FdRa z*dZ}T;sS{%Jyc2;+lM>sGK_GzAWkz86$g7VqLW-C!W%07*3;+mk)kddOY^r)T}LKQ zN*tCZtsw$r66Obhv(mHTPZe0CMlVqNn|tEfW`m`uyjEUYUpr`sKpklAIj%Mv5|D$< zhD_^nv0Pj){xaW@E{}S(N%#V44F8fP=+&quvK1TDIKCot6g#Yht^}aQbCsZz;uzF| zd}W-iDJVMRY1k{GO>UoN593)L^Jfv9UHNQ|( zpysgE>;lDJk@5qOcx$0n9%?DWyp|AZQK*gCY9j*0ae*jT{nux3TD}iM_=|k)iX2Pj zGqJp}kTy~|)1W^o!qoY^q0eSk>fB-~ZLV5d8vUL=zi1i?QLIWWW-+51reVhqm9Q{M z=o^tB-cfMBkjm%BNZVGomcg)-V}rf|d8mT@a+u0D!9K~rI8IVsoXRbrr8#}txI{`w t!nEK|_TbQG5d8?E4IVI_2BSdfx$py!ZJ+%|d>_Oi^(HQ<1#XJi{s3Hr*7pDa diff --git a/backend/app/__pycache__/schemas.cpython-312.pyc b/backend/app/__pycache__/schemas.cpython-312.pyc index 4578cd31ca4b284983c5772821b6b89fea9b7161..8272a8b14f57d11a5acc023ccc2acc6bf4bd5b14 100644 GIT binary patch literal 4240 zcmc&%&2!tv6>@`S=08=V>13zdWnacDQ9OglbIemz9|uN$tmwG2nwRzGEJ zt7|ptp_BYAHJ!lK*6!4p0F^5Y?yqz0mf_bJddjUD_A98*ATZ=v3=IlN3m+306<{o|TpcU$6?HxBJJU^mOR$Zk8Bm&sqEJEb~?9p zBlI5KV5h=9gPHv^G)I{=r4n#U6O({bC14gX6)>%f3623vMliuPV5tZucn2&U!9)ZA z%S13$L`8~3<#rV_waAX)-0`XGIGQOmC(ulTDP-9U`m<=}(1_bHrx;Sh7SM>^i|7p) zf}O+$!O+X-ErkZh_kIHg_?C;cAPk&x- zZ6~t(F)8up?rYx?xE#cZl0!4LFT>w` zg}n6gKm0F8fBwh!f^5SF9LcsY9tUgC@`7ypAH~&##gsyh34xH!pr92f==A0L)bRt6 zW%xi*!yvcRD>F;1LR_=mvQS1YTR>2eYs|Y})oaWyXnj=kgpR@xRJI`dO&k+*sGrm~ z5JPhoP7hxmNKNIpZU*`oh-PkhYc-I{nT5u+?K@3xr?Mw61cYJd!G!%4TB3x3p8a1m z6P}v|9nA^a6;tc#XtGOtsG>dI*{@;Z3<+T?XplUv7-iqOPYd}zu7hw1YTv?}g_;Tl z7Q_!2ioJv5$?L9b_ebL#j)3L@oSq2=qH$tQi0A7&%Po1XPdtx`?qPIV@#FOJxqKB1UJ}Kpcm{a&Z` zLYxg!}%x^oSUV$cT`g$AN!^<|LZKB@P3?(A2wkMfHPTF7s_%V5AV>^TCK>qRan*ryCvw{ZW#jb)WaSMj6)O)qg=zq z6~A}_NkU2CieIl<&ZbynVOLW(4ac|2ECFo{cRCC|+SoE0@pU4$(74=+#aC#)kQ-4& zBKBg&eTBxgw}TI;ci4x}SAx^~8<_pLBuQV9g|Eoef5`c#` z`%PZU#u~}axFY47*E<9*oe@Qv3;emxNLD)Cv^oSXos){R*eu{Zi=7cwT0kEzov{?$ j1AXDrnMp_!&B+ddOJ^t{Ed~BkXGnv4R=y)}5y|!gza)9> literal 3722 zcmb7HOK;p%6t*)SkLR6N6H42p$+R?vK6q59pq2%tue6yWNC6E<#))rIgJ(QkJE4Pw z6qJIJ-E>98iY?-&umMdLqbpTHVu4_V=&)hMIp>Zi&UDh2NIpLE-G}Y(e&@S4Kjrfo z1^(Xu_=QjYG!*4G{1N>nyUD}9A^BEu6i4-xs#;N1%!j<8YNC>$JmG28WF=YEE4r!- zDOVInyQ?@!mi#dx=cuFz)d5Y#P=ind&~yw<6Pf`u8$&aM<^avd&@7<^K#MUnN9Zu1 zBQZ2j=qR9LF|mCb?2y6TxLFTfzh-;pkVwC22W-K2m?w%W zF7q5~-S%AQ=<{Xy%`$cgP#(_1?{5{RR8&W)4Bb*#f@zL=9#EEqY^awddYRVClD$mF ztdTMek$;=}tK6<$W1CWaBWfMsJCu?$!)hNXykHOBm{ zItruA@iRHr5zk`CVaQ`BKrr*Xi1{#v5e(E_!)z487zP?Wj@f`I_yKGnh9)t?52S$O zgZCf+zca@-t~Z~#U%Gdsr5$fimo~0#-MOFG{<5W&+RsjHytOsgd}aGoOPgvRn%=my zb*_1F`$9{b-nVT>o0gBnrwv||kDw*KC^1Q+xc`@=sR)Q-$8gfFTG1OBGb@T+k-x)& zItY)6A=i04-%yU+uUd8(a(8JhWWi$DO!Y6pF7e2@$bs~kXD$1+6?e6t?L*LmW56qT z5dyGnWH+xoR??gEzm6aOKKyy?T z=uvFIV8RtV4*^KaV98V6OEV~pY%WL(n3~J+^o zK_8#&d4UdCs*6QlV8pP<3(y~;evucTKSWsM1?UeECWR{fA>Vz5(e;OPm-NUTdaQGF zLOzS3gaOGDy1g8_>&z?+WSXcYy2(_C^8vH)6F8N&25(*y|&^3)9kO`@CXr+mV@#%Uf5Qi`(TLZALC3K6daDde9PI!kBaHDjpWl zk((iAax=tCZiblA(tz&uxb7J&vO|mEq@4^nNxG3Wb0SZb5cvo{3~#Vo5QCP6NaaVM zA2>*+e|dHtgw~frwk1b1t8k0zg7!L#==-)HMZi0fyg^< zQ1|c{!X3uTkGco3gmMi7b6eVE zdu(Dyn~*40we#6G3YXiA;M9B!TQ4D^AqaO+GZ)yd|;jsmyNa8jL<`CMn%fIhGN Jqri>C^B+(T>GA*o diff --git a/backend/app/__pycache__/user_models.cpython-312.pyc b/backend/app/__pycache__/user_models.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7746fc5206d75589836927957e4ba17d1d1151af GIT binary patch literal 1142 zcmY*Zy>HYo6nAo$kIUWl)b;=YLP8V}0nreJ)0xcPEsdnx$O`P zj71D>)#XM|J9I1)e*o(tah4bmLeLF~vao=ilmM3e{CocW{GQ*-$J=Vv0%E+IIsfp9 z3cx4n{2AIF`0fa>0uVq70Tehukys@PRUMTUoB}O6MXEU()g3+M3dEqMW2U%BO4M>J z1*qT#K-yD)bZl)Dve-_!ZP@0HbZmv22*x<}D7H1OUkyk?eQwvv=3`7Lft z1OdU&=jNnP&q}qzYm>N1AcL{Dh$mnqw~BIYW@$2;*Yj2QD*yvWLBLUw;uKVXi&#S{ zQa20v-E_Gtrh$qZN)|#Wv`sx%n7P8p722k`Av!6CQ_A^L&a40MR?b^Fuc7j0CC^Zn z3S1Fct9jIFx;KC{TzhylP4u7n=k#EEz^?J)y-4&=ON5PPZIzcp=26Jn?mTYuDooUknAy4*NtV8%w z5ne%F6cX4@Lvjl)!Wc3)VI-~Nfg7@5(LxVnjA4|1mqdU7@@0G4}Ci<13=VPlQFD`ZCE!?YI?p*5+A6z@x9bP}PrS*o# zI(Pbm2iEk}XubRr^#;#%ruy|mYh&GG>le4Q?MJ=(&CYbcwr{!BO?tJnotr|vYQDU% zedp^>~UM|8tWWyONb@MGFLy<^xRIf7_Y1GHp%T5I*o%G#W6hHwD|1AmG|dSqGB0OO@(QzsD?g6tm@wZ8ilTe~N4|jZcVPSjIQymrjAOti*pP&i+SPKqHh0`tcGrMi zBRfD_P>`^tDo`brs1m6`xNRy`YJVmqflBRT+AFaGe|c@Eir4*N^ve~iZDrPF~&)C=grHqnn2dBJWnx{>g9$N9jGjKk*o%}8gyzblCySljjw5{ z&p)9Z=as~SCMz%S(ZqN--pP+A65~;sKZtXkycn1G(1fDMagCo)WrYvN#}Zxt!iW1f zz0CjW;0Rr=TLW@Zj!UZUdgbupSNl(gTxK_0aL^$L+mkO3!VkKl1|60;Mi6k`#a zG{K&o2hdI<@|zQ~s_8aVCN9Qg-If$p^;ANUhOe7-n;;-j5OljB#1hg(6mh2@yg4C8 z3o|BFQ}l8{5aaO#*a&v73c_{r2-W+3V>gI8p(aHzLiZ{8m>?#T-N(cb7(?ntTug?$ zML43aW2l1DtE=)H%&e}8#0JguAWZZiIGI_$% zt7)FyH)p!bHRpGYOsntc4VYPdN3t0ljJg%Y=)PpL#7%*{V8%>c4|yyUvjApAOtML3 zVJ2vuP{)FCZpmcb;V)bE}j%MQ4uDTsP0175t2&c zWLN?#*}|$IO0jTUFCSA9F+tQcC43CRf_m6zD>0gG3dgm)(a?N4cPx>BXMvN;Q6ZFw zkA=re4T;C9AV4dtTL6F|ZH{vTIfwh)zJf_Tm2=X(J;OERDr+)aP0sB-H~gTg{hF4l z?0oA$-p(jv%F<0E6};VUrEvK8ipf#kniMCa2~i3a!YFVPVuSxV_&@r}M96-^z;S3u zr!7(l+r}7s#ZxrT70={xwNXQtrcFcXv_)u=G)DPeVis5Y92=C4p0!iZzwM{i0ojAepB{3Kkt{z2JHEqS zG5y?ZCTG}bHiEBGNz?G9qD{k>iZ&x5MVbuCO25Jt!RS!Zigvn+jou}a_1El)4nm5g z@FgN(DA|z5I1fXcbEL2%Q(;G@X;Y;2+0}*(&T!M*Y*NF>M%(|R+Yj^L3%y{!Q8+`8 z&qmq}=_~y-XK2h2;8?#hV_=4cr8}bhK7(teUy=Ci3dw>$!&c|mr%#WD% z@Rfd=i*&7w49t*s#;jowpl#Z`;4pSJO16_Z=2GW&a_MvU{{;R6@ZZr&wh*cV*ytU4 zPTPr@sLUyrAxGhjX2_+%)25^36!SaNDaPlV+PNo@&>+2vNuGwad~L&bKs1ElC7;%K zC^96dX~JSuK~AE$t85(Q65LDMQ|)9Z}V0-dj3 zaY3iM{ehvuVd2PV|H!a^sNWdB>K`3_{oqJIG2=Q5a<0J=OxU*hc>#D|8+^H6I5O;i z%|Cd+zvn=|f{9G2M80au*Ry#G&L27a%E6JrV1EEIX)@U*L9RFk1)I`~oL2d?9KtdY z#|ARoD^amqc6^5{eLqUN?!7q+3FXSS+mRRz)6zh<#kaP^q8? z({0ie3%PkK67o+gRBXBh*e9Zz&e4+3Omn%4TjXg|)&Pn&gn~?Y1#t8#bsD1oKgp9> zvScC7s;r~sj-zFvBfG(WXM_KqyK%vi?bx60*q=JA-E~i-Y!eTx?yR*oZLNK;FWa~^ z-MICRb?Zmt+1|bB-o1bC$nF!<`@}oFVqs|@-4RF)9lPrerEH;ZEyTR>%knjsXD-cL zTmPW8`O3`v%sr>~mpyME$U2+T&gOfbsw?(+`v-N|b$#h|eJOs&Uusg#yVIWCFq8GP zq&+Ry#?zinkIW{Q`;p09XMar0Ui$^hqLX-PvgIA=@{UaTh6`NIQGQ|Py~^5q_3N_r zo7451Q*~QDs!7%K-D_&kHua>NdUDO}x%#H1GIN#tLI9S#sxI%pxc@45*V&Zw)?ewG z@5*>vvd)&2vjxZ9^WE3lGT!xB=lYa${o{7xS$lbAse`!qYusW2w0GXu4jTOJPaDhI5% zwU!yE=Wg*^0KZ+y0R46~g|!scGe~c=3~aLAUdIfyId8XE5%*bvK4tCB-wc!hXcH<< z8<@OlLBIpGP!KE^43KJzd4Uz15uhMYG9~GciQs!z(pD=F3>gte2{R0f%7|4;glSe{ zF`v@(RnXLs_KZfDzVQ7lX*UXEn!W_B%$PQ0LvO-oK+j=+no%LR!kyhLSr)7}3VVdr zOayNmC2b{@K(b*X7+gx4@%&PKDL+=_C2d9Gvn#aaf&Pb49H0tW`KPF|6<2_>%b`(8iXHB7^%&e4mbH5m@U6eN_dOJR}W;^=Q9etUK9a+bY*}#L!hAS`4zw|-x zLfiX0v#s0Gt=s-2{WTY^ z7r1My&$+8F$1lb+?&hqmIc00capGd)s+4iJW^Ju0TkBWOwLcj3+KKapCl6esPZsM) z+1e*LTkH2K8s}_%z|FkN1T5TpejA{lv@ro2_es0I76vzWG66exb5|YUpH?#gC--U1 z7QjEVF$nDxIuTBpy5S3gv-ktYQ+F5)Ni<~PH6S0drpYLw4$4f`k5JcIm5Da5TN161@79n5OrMTMfRIEkMn#|0wkJqh&ASFWZ^};$PLfMlH zD<+s#kfo^Puq7uY5$dUsg5g!St8YfdXy~{co21UE*Q^k_p#JKj^-wsWU?9?MSa9Ht zH1D7oAgPDLl@%rmcS@{*6#Vw3pu&1NPT`xCzx%2AqQsK_n3ew`LO(;S!xeaM)VX8= zN_Y6O9Z+l8_TSn8$|lr zCA*E;G&i|Kpj&JptnC+#Z#7Y8r|bdOAg-9`zEd*Nc1XFPiC z3^H!};?NtZqf+LO{E$>Jn^NuD7qS1i^Be4NW$z*j*kTthYuf`YdH#zQ dUuwe(nU)_t1a<1J_I!gKuIPEl0!DA%{{``r)NB9% literal 0 HcmV?d00001 diff --git a/backend/app/api/__pycache__/events.cpython-312.pyc b/backend/app/api/__pycache__/events.cpython-312.pyc index 41c6ceb5f832f9df837c0824325286ea81d22fc0..6ecf4ed51288cf532eb74bece20cfc673a7bfe64 100644 GIT binary patch delta 1808 zcmaJ>UuauZ7(eI!%gx_5O_Qcc({*dR8rC$!4W>9&TxB3->TpBoOSsKFwONxUo_n)W zXiX=AL-#Vy%O1rCX=xQ?55D>?g0(MtF*uQ_ph#+ms1M@z-J7gDcAbS7Hqndzy-W@0Yq zFZWjBGjU2J!sN~N;+X_X+|2JQ9rk8`C0VQ=h^^qJh;L>)=_i2gG06>oh7>ef_FK3q@Jn_q{@8(em{H z>iRYDMR;IPewkb#cTd2le;_Z>tnvcKVy|zItjdEz=%=qJd>j}rdJacV031~A#SRrY zGi=rle;y``;-mt=kL33?@;D^^(BGhnSn+Sj%8DZHg)_3cqKc2gLt;K~E~c&e>?q83 z+lsoPt@y-;fg4m6<62Uo1f~uJ6WR#)`KY)UOg4^g6?#r1j3Jx_aMY5`DwgB3m~GfL z=TD;N96(n0cJxSLxVLU~j=aE@9rcz`t}{yq7S9-4yV`oq96T%+pAt%29|O4Mwj$!E z;A8C!*s$-9LK|WZ6JOopC7$Jg_yU>SEs?c81g{;P_btuM>;g0eYGRkbVT869N zfU#U!wB0-FD7C6(bG$!}T&vq0x0YW*z*0E=Yxm$I-Z|(OAx6E-F7cOu!yl=&4%m_@ zrA?Clf#kMHu1PXYl7B?bG|5Pl417Z-x5;Fa41MeGc_+Q53Eq9bv8$2H$h*@!182XA z#6M4r??iIDK0l3q8BVrj=pTmrTPpM|jp(6Y(X%HL=~>#6kW3Oue=Bu6b?54OZY{M# zpW4;`E$5GbTqN3(p??UnD)b;1Nws{D5)5@z#!0CBbW1|_DHP7!&fHmDA70Ds(1~O7 b4A8VI485Bt^zA%hglhR+2}b-y*q^@vj1HH% delta 1546 zcmah}O>7%g5PtjX_4;=mJ8>MlY3j7WiJF%5P@=X;A%X*eqC&Y?0<-nLKw3LCZ`VbU z+&YbrCt$)=(c_7Vu6zfH&W>thkll9)8I9`g_gvfgM=Y3nTodxX6DM&+eR& zJ`)6;-;qZIgWr?$c>|DH0)(r)-k@yNRT;G#6~~srcM#6+27g%@RStj|-C0*Y7iNd> zN&p3J4;uvGip!Rpu*+t}WTn~)E!9|Q1@xxOMllBquq-zN$y5&0gRfGjM6b{lhdlyz zhQAZImW^Q~WILwQuuJ8dMcqK%T&>hht1NOuPx4dRf*|siHU#ox?L05b!(#$ziZ76p zJyrQP#$wi zAnuai*2pg!e=oQ$NI+tf7orRN5BlLy*hzd^ePC{mTs5IED|Sma-5pO&&1!oFPT zGF^3*D`urZZTv*gBObrcM&0uIjN;^HA&!;oFZg`yJpU$qTb%^cANGflkNB=S&F|_d z{7s zV;5RsvtFMDy5>dGrYs04u8yuMEJnrdt)3e~hh2V+R!y6QAccidL@-%pwFaA?z_6+#& z*JYRam@%x6e^0VMkojFQ&!09vY5h1e`{h)7=Bcg}5R7jl=}u4QkL{t}mx%bd&=KLa zlO&?{R_1!<#L={GE4Rla#H1|;9y<=Sw-q<4W{|lBXO6C9n diff --git a/backend/app/api/__pycache__/programs.cpython-312.pyc b/backend/app/api/__pycache__/programs.cpython-312.pyc index 595095d07a0436c4945a692ca8e786b69adae40b..a1baa4a1eec26d876ef4b44bc92e61d7c8158c77 100644 GIT binary patch delta 1898 zcmaJ>U2Gdg5Z?Q<&wm%ke{tft`E7$~Q#bq-XiEhlQlp9rXe)tVP8aXGO=HK_-kA`| z-~g3C0#z!jzE|P_Py$G_63;yF0xzv1B)BTH6+$5O#i5lzhzDlQapI!FN;k7RJM+!A zJ3IH&?!PA{zKcYH1jhP{kDa>>v!8L%8Qu;o?m8DIicN`7LiKWQC02-4;)Qr6QAl{U zKsi}S6;cwB2~$?Lf(v~#$kHX5hR&;ne%5#H@eb;areT(0iGDDwMONju9WXR%(4Mtu zmqlg9D&+SDKn9t_(ild2S+Jzi*m-R`?cjf;inGLhNf5GwDA5EPpXk*JpZb-Z=ztopH!yL0%=bSA% zrbR3Bi)P(sTwIoq>p96)Pvhc_{LbMpBy~g7S~jaCbBVEqoWy&~lFR?k(s@3vyR&jwX(!98!jA%*li(a{^+>pjREWOGpPd|#V_|`vqD5vs0 zxEeq)4#M?!!cp)_s0~#d4P_Orp$Ri| zGVXiB@8HdOqoFl?4Zrv%bXwBH1tSgJ)zCbWZv)8@_<|l$?F7$30LNUXpzsm)da+!m z%&afk%*lm#*A1iJUKIOKOrm%KgsaUvtYW)<2(TQ7^CxlVDG)imT@5(?E8XYE!g!b9 zDhq7c)n2p8b!O{e#RB1I7s;sCX!oUHYaN8o{tAS5DJ?^&k$dVCti9^Qj7#EhXj=4# zv%=r=T=OB=02cpy2-inhnZfHr-cYoi2jmrNo2;$DemA>z*mlXVO=sb=S3tDc2_F=pT86vX=k= delta 1657 zcmah}O>7%Q6rTO{`ak~3pA$D}lP0))Fn+UJ7Z)X>zl>si3w&un+YH!vUuRA(tR;^@PD(Hq`@i-d8rJ`l$tGsfR zn`MiSfG)B9vHqD9OoPaMNP^)2>HLZbmrx7LLk6X^{eFrp@+?!PJS~)Gq5I> z{oP_Ncvobj3nCdhnb6+RZS1SN*3>m^O&7V)E8_j=U_*8Dc^C3I3OnHDq@CyE;L8#G z<&5Nn+1rIujk0`g)naxg$UUt@zYLNSNG6cX0CBXU&B~VJg8 zT|_lDpJTm_a-G#3?JcuZV-{W&)Q+ojwUJttdh34?98SWvz7V;ti{h8?mOcfaZugU7 zD{@!t`X)s++9UjdLGfucn;!#n#`y2_9i6fggSm4vej4!)k3*zmKe&bkaCr&cZi)xS zvS@a#O2Z-*d$kcVtJO)!*t}v|jN^aN381PBvr@8n=b@m&D!jqUro}_x!ox@+=&bWf z4f1f5;;PN@#W)H~B0qu-IM?nhI%>6I+3qYjO0{Nl>=V4Nsn$U?F>$}ePM|0O0*QH;f?ZTj@KEdkgF79ck*sH`#MvP3&YZw}~zdY+Py~`+RN}8OBYwWsuyu{sxaP BY03Zq diff --git a/backend/app/api/__pycache__/tiers.cpython-312.pyc b/backend/app/api/__pycache__/tiers.cpython-312.pyc index 546c029e5a8b2126e4ae8f8dca5b34f347813be1..ee7913ab757efd649675c4206b8327de5707ec88 100644 GIT binary patch delta 1680 zcmaJ>O>7%Q6rR~XuYcBY?EKns>imden#4sxO$%vDKy9fiQ6cEX7qj(DXi_^dv%3nF zj9jWpMW`1v2YLf>q9pV{PMo;(0!WA|9AHJ%N(c!~ID`Z0iT8G!)Ii8c^WMC-?|tu^ zH#567^6}W*HN3T8gFCx>wcE`%hYdW$uzxSehqUyu}Tc z;pusU^>h@-LV=l1=y1*UA^RVAq@LzoNI$4xvfhpgLr~#4Kz;pRRhY&4*82Bu?4Bid z;0-p=0Y7|Yu$cV~vE{Tf-QV{HcPmv-2)^W2Y*t@bs(20;vZ|ahi`3UHfU*9mQlwOu z->XNTPC=q?2<~__*TF+B6dq2LR+hzOr(svQQ(EB{D|Vw%x@cD~^Cec2BU+wj*H3CU zsgZ_wq04*v!O@~7a%dr*7y#jG?gAH%2Fn5rF(|W!q+RlkF{t~rQdQ+w#(7uvCy`h6rY^rt6y){jbjEl);&wxHFq(SPXh!7M(F@R+LM+$PUnMi- z<~5ijFk_#oE3~M;EQTRkV4>Hbn-+hoTJspMcuNjY@gf?^cKArQX4 z;PSfTMNTdI@dsi)Domen(;I8p|w1Ez>mz^j%zm-1U_bg7sih{Z`SNu z^%AdNf%|nUIg~WeZNf#IvOV{bTu+{Eb)kiqyHa0vMfo!WZPw9Q~ z#3spalF~2a&?Xt*BxCo<>^(BONeZ&rb>rlgLGt4to!b~X{A05ByS|Bye{r z{cRQW18cCYgKitdO#FdgwpF4$PumJQ2jpZb+j^WCO=4!Mt%8042s&uUOlI2=T$zZ0 zF^DnKR?s&}6!S*@o&3AaPcm2Y8+39{MlqTTa6t!*KtEy>qp2LiO>HUQ6feQ{{{}Wa BV%Y!y delta 1496 zcmaKsO>7%Q6vt=wJ9aj)vEw9;4Ng+199oAeDhVRcgwhldM2Ub)6e+B{lj3R}o7uIf zC8MMWsXbH`XjCXS!~vyBIM7HH+wUS7b!(wa-_1n_}=XN znnakqxu3fjXYq3n?=Lzg+h7S68-&PK*QOM1z&V9QHUW;dLhT4MRwO(6j(R{#-iOxB zVh5Zxau3L4eF03ddpck#fQ^SA>r^N$X7_HzHsvsS4=v7m_Gu4v&<21ufhX2FVT9P7 zwf%Q#an>6EH9OG(OapN8-z;3&-F9-T@4k0tvavh8raEAmdVhZ4HlEK@-#AnCN|ja1 z5hZCt+?1Z(mgGF8nmD3NP4q&JuW{zq9M8p<$>sizS=H*eRV|HsCC1%CweClplItx8 zOFp>0tz4#B9CEoPPHBfnt}8r^D`fG05MK#W6>xK4d_Y{$M5YIKqONCoHFvpKv6-(| zt-4dO?4l%Q^fOcv@9O=a8~Ot1*Xn>mNkbATg$XPN;q2!|to%wptKNW=i~}XT%lrnWYS*=Z*ib*w<|4Fpq^r zj^J1BzA3EyXt*C<{1Xs)5_8tYC)$E|zo+ti*4MmxwX}Mc<9qq~iUrSKEpeo9Y|)Rn zZ#b4ye4Ul+T*E->ZlJ)nPU2|JFZ7l8Lgy#xv2|PXr4gCX_CqyX# diff --git a/src/app/donate/donate.component.html b/src/app/donate/donate.component.html index 6a1e874..5248a92 100644 --- a/src/app/donate/donate.component.html +++ b/src/app/donate/donate.component.html @@ -17,23 +17,25 @@ - @if (loading) { -
Loading events…
- } @else if (upcomingEvents.length === 0) { -
No upcoming events — check back later!
- } @else { -
-
-
- {{ event.date | date:'MMM' }} - {{ event.date | date:'d' }} - {{ event.date | date:'yyyy' }} -
-
-

{{ event.title }}

-

{{ event.description }}

-
- - {{ event.location }} - - @if (event.rsvp_url) { - RSVP → - } @else { - Learn More → - } + @if (events$ | async; as state) { + @if (state.loading) { +
Loading events…
+ } @else if (state.upcomingEvents.length === 0) { +
No upcoming events — check back later!
+ } @else { +
+
+
+ {{ event.date | date:'MMM' }} + {{ event.date | date:'d' }} + {{ event.date | date:'yyyy' }}
+
+

{{ event.title }}

+

{{ event.description }}

+
+ + {{ event.location }} + + @if (event.rsvp_url) { + RSVP → + } @else { + Learn More → + } +
+
+
-
-
+ } }
diff --git a/src/app/events/events.component.ts b/src/app/events/events.component.ts index e7ea1fc..b5cf1c5 100644 --- a/src/app/events/events.component.ts +++ b/src/app/events/events.component.ts @@ -1,32 +1,29 @@ -import { Component, inject, OnInit } from '@angular/core'; -import { DatePipe, NgFor } from '@angular/common'; +import { Component, inject } from '@angular/core'; +import { AsyncPipe, DatePipe, NgFor } from '@angular/common'; +import { map, Observable, startWith } from 'rxjs'; import { EventService } from '../services/event.service'; import { Event } from '../interfaces/event'; +interface EventsState { + loading: boolean; + upcomingEvents: Event[]; +} + @Component({ selector: 'app-events', standalone: true, - imports: [DatePipe, NgFor], + imports: [AsyncPipe, DatePipe, NgFor], templateUrl: './events.component.html', styleUrl: './events.component.scss', }) -export class EventsComponent implements OnInit { +export class EventsComponent { private eventService = inject(EventService); - upcomingEvents: Event[] = []; - loading = true; - - ngOnInit(): void { - this.eventService.getEvents(true).subscribe({ - next: (data) => { - this.upcomingEvents = data; - this.loading = false; - }, - error: () => { - this.loading = false; - this.upcomingEvents = []; - }, - }); - } + /** Async pipe drives the template — guarantees change detection on every emission. */ + readonly events$: Observable = + this.eventService.getEvents(true).pipe( + map((upcomingEvents) => ({ loading: false, upcomingEvents })), + startWith({ loading: true, upcomingEvents: [] }), + ); } diff --git a/src/app/navbar/navbar.component.html b/src/app/navbar/navbar.component.html index 3b9f6c1..c2e8d7c 100644 --- a/src/app/navbar/navbar.component.html +++ b/src/app/navbar/navbar.component.html @@ -21,7 +21,20 @@
  • Schedule
  • Events
  • Contact
  • + @if (isAdmin) { +
  • Admin
  • + } + Donate Now
    diff --git a/src/app/navbar/navbar.component.scss b/src/app/navbar/navbar.component.scss index 59c9a4a..391e0e4 100644 --- a/src/app/navbar/navbar.component.scss +++ b/src/app/navbar/navbar.component.scss @@ -99,6 +99,62 @@ background: rgba(232, 122, 46, 0.1); } } + + .nav-admin { + color: var(--color-accent-light); + font-weight: 600; + } +} + +// Auth section in navbar +.nav-auth { + display: flex; + align-items: center; + gap: $spacing-sm; + margin-left: $spacing-sm; +} + +.user-menu { + display: flex; + align-items: center; + gap: $spacing-sm; + + .user-name { + color: var(--color-light); + font-size: 0.875rem; + font-weight: 500; + } +} + +.btn-logout { + background: none; + border: 1px solid rgba(255, 255, 255, 0.3); + color: var(--color-light); + padding: $spacing-xs $spacing-sm; + border-radius: $radius-sm; + font-size: 0.8rem; + cursor: pointer; + transition: border-color $transition-fast, color $transition-fast; + + &:hover { + border-color: var(--color-accent); + color: var(--color-accent-light); + } +} + +.btn-sign-in { + color: var(--color-accent-light); + font-size: 0.9rem; + font-weight: 500; + text-decoration: none; + padding: $spacing-xs $spacing-sm; + border-radius: $radius-sm; + transition: color $transition-fast, background $transition-fast; + + &:hover { + color: var(--color-accent); + background: rgba(232, 122, 46, 0.1); + } } .btn-donate-nav { diff --git a/src/app/navbar/navbar.component.ts b/src/app/navbar/navbar.component.ts index c9edad5..a405008 100644 --- a/src/app/navbar/navbar.component.ts +++ b/src/app/navbar/navbar.component.ts @@ -1,16 +1,40 @@ -import { Component } from '@angular/core'; -import { RouterLink, RouterLinkActive } from '@angular/router'; +import { Component, DestroyRef, inject, OnInit } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterLink, RouterLinkActive, Router } from '@angular/router'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; + +import { AuthService } from '../services/auth.service'; @Component({ selector: 'app-navbar', standalone: true, - imports: [RouterLink, RouterLinkActive], + imports: [CommonModule, RouterLink, RouterLinkActive], templateUrl: './navbar.component.html', styleUrl: './navbar.component.scss', }) -export class NavbarComponent { +export class NavbarComponent implements OnInit { + private auth = inject(AuthService); + private router = inject(Router); + private destroyRef = inject(DestroyRef); + menuOpen = false; + isLoggedIn = false; + isAdmin = false; + displayName = ''; + + ngOnInit(): void { + // Subscribe to auth state — drives all three properties from a single source. + // isLoggedIn and isAdmin are derived from the user object, not from separate + // checks (localStorage vs BehaviorSubject), eliminating a race condition where + // the token exists but the /me call hasn't returned yet. + this.auth.authState$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((user) => { + this.isLoggedIn = !!user; + this.isAdmin = user?.is_admin ?? false; + this.displayName = user?.display_name ?? ''; + }); + } + toggleMenu(): void { this.menuOpen = !this.menuOpen; } @@ -18,4 +42,9 @@ export class NavbarComponent { closeMenu(): void { this.menuOpen = false; } + + onLogout(): void { + this.auth.logout(); + this.router.navigate(['/']); + } } diff --git a/src/app/schedule/schedule.component.html b/src/app/schedule/schedule.component.html index b0cdb8e..b1d80c2 100644 --- a/src/app/schedule/schedule.component.html +++ b/src/app/schedule/schedule.component.html @@ -2,79 +2,42 @@
    -

    Today's Program Schedule

    +

    Program Schedule

    All times are local (Mountain Time). Every show is free to listen to live at 98.7 FM or via our online stream.

    - @if (loading) { -
    Loading schedule…
    - } @else if (programs.length === 0) { -
    No programs scheduled. Check back soon!
    - } @else { -
    -
    Monday – Friday
    -
    -
    -
    {{ program.time }}
    -
    -
    -

    {{ program.title }}

    -
    - - {{ program.host }} - - {{ program.genre }} -
    + @if (schedule$ | async; as state) { + @if (state.loading) { +
    Loading schedule…
    + } @else if (state.dayGroups.length === 0) { +
    No programs scheduled. Check back soon!
    + } @else { + @for (group of state.dayGroups; track group.day_label) { +
    +
    {{ group.day_label }}
    +
    + @for (program of group.programs; track program.id) { +
    +
    {{ program.time }}
    +
    +
    +

    {{ program.title }}

    +
    + + {{ program.host }} + + {{ program.genre }} +
    +
    +
    + }
    -
    -
    + } + } } - -
    -
    - -

    Weekend Programming

    -
    -
    -
    -
    Saturday
    -
    -
    - 9:00 AM — Country Roads Hour -
    -
    - 11:00 AM — Community Hour (call-ins welcome!) -
    -
    - 2:00 PM — Afternoon Bluegrass -
    -
    - 4:00 PM — Record Request Hour -
    -
    -
    -
    -
    Sunday
    -
    -
    - 10:00 AM — Sunday Morning Hymns & Folk -
    -
    - 12:00 PM — Gospel & Roots -
    -
    - 3:00 PM — The Long Wave (ambient / nature sounds) -
    -
    - 6:00 PM — Sunday Sunset Jazz -
    -
    -
    -
    -
    diff --git a/src/app/schedule/schedule.component.scss b/src/app/schedule/schedule.component.scss index 0fa4cce..f65cb88 100644 --- a/src/app/schedule/schedule.component.scss +++ b/src/app/schedule/schedule.component.scss @@ -18,9 +18,17 @@ } } -.schedule-list { +.schedule-loading, +.schedule-empty { + text-align: center; + padding: $spacing-3xl $spacing-md; + color: var(--color-medium); + font-size: 1.1rem; +} + +.schedule-day-block { max-width: 800px; - margin: 0 auto $spacing-3xl; + margin: 0 auto $spacing-xl; } .schedule-day-label { @@ -101,73 +109,6 @@ } } -// Weekend -.schedule-weekend { - margin-top: $spacing-xl; -} - -.weekend-heading { - text-align: center; - margin-bottom: $spacing-lg; - - .schedule-flower-sm { - width: 35px; - height: 35px; - margin: 0 auto $spacing-sm; - opacity: 0.5; - } - - h3 { - font-size: 1.6rem; - color: var(--color-primary); - } -} - -.weekend-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); - gap: $spacing-lg; -} - -.weekend-card { - @include card-style; - padding: $spacing-xl; - background: var(--color-white); - - .card-day { - font-family: var(--font-heading); - font-size: 1.4rem; - font-weight: 700; - color: var(--color-accent); - margin-bottom: $spacing-md; - padding-bottom: $spacing-sm; - border-bottom: 2px solid var(--color-light); - } - - .card-programs { - display: flex; - flex-direction: column; - gap: $spacing-sm; - } - - .card-item { - font-size: 0.95rem; - color: var(--color-dark); - padding: $spacing-sm; - border-radius: $radius-sm; - transition: background $transition-fast; - - strong { - color: var(--color-primary); - } - - &:hover { - background: var(--color-cream); - } - } -} - @media (max-width: #{$bp-md - 1px}) { .schedule-time { width: 80px; font-size: 0.85rem; } - .weekend-grid { grid-template-columns: 1fr; } } diff --git a/src/app/schedule/schedule.component.ts b/src/app/schedule/schedule.component.ts index 28837f9..de61105 100644 --- a/src/app/schedule/schedule.component.ts +++ b/src/app/schedule/schedule.component.ts @@ -1,32 +1,53 @@ -import { Component, inject, OnInit } from '@angular/core'; -import { NgFor } from '@angular/common'; +import { Component, inject } from '@angular/core'; +import { AsyncPipe, NgFor } from '@angular/common'; +import { map, Observable, startWith } from 'rxjs'; import { ProgramService } from '../services/program.service'; import { Program } from '../interfaces/program'; +interface DayGroup { + day_label: string; + day_of_week: number; + programs: Program[]; +} + +interface ScheduleState { + loading: boolean; + dayGroups: DayGroup[]; +} + +function groupByDay(programs: Program[]): DayGroup[] { + const map = new Map(); + + for (const p of programs) { + if (!map.has(p.day_label)) { + map.set(p.day_label, { + day_label: p.day_label, + day_of_week: p.day_of_week, + programs: [], + }); + } + map.get(p.day_label)!.programs.push(p); + } + + return Array.from(map.values()).sort((a, b) => a.day_of_week - b.day_of_week); +} + @Component({ selector: 'app-schedule', standalone: true, - imports: [NgFor], + imports: [AsyncPipe, NgFor], templateUrl: './schedule.component.html', styleUrl: './schedule.component.scss', }) -export class ScheduleComponent implements OnInit { +export class ScheduleComponent { private programService = inject(ProgramService); - programs: Program[] = []; - loading = true; - - ngOnInit(): void { - this.programService.getPrograms().subscribe({ - next: (data) => { - this.programs = data; - this.loading = false; - }, - error: () => { - this.loading = false; - this.programs = []; - }, - }); - } + /** Async pipe drives the template — guarantees change detection on every emission. */ + readonly schedule$: Observable = + this.programService.getPrograms().pipe( + map((programs) => groupByDay(programs)), + map((dayGroups) => ({ loading: false, dayGroups })), + startWith({ loading: true, dayGroups: [] }), + ); } diff --git a/studio.svg b/studio.svg new file mode 100644 index 0000000..4332377 --- /dev/null +++ b/studio.svg @@ -0,0 +1,434 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ON AIR + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +