From 9b95bef9c698b0a4dde838547babd5faf28d8505 Mon Sep 17 00:00:00 2001 From: kfj001 Date: Tue, 23 Jun 2026 00:30:47 +0000 Subject: [PATCH] Retires Support/donation/tiers page in favor of simple configurable link --- .claude/CLAUDE.md | 6 +- backend/__pycache__/seed.cpython-312.pyc | Bin 22664 -> 18848 bytes backend/app/__pycache__/main.cpython-312.pyc | Bin 6457 -> 6335 bytes .../app/__pycache__/models.cpython-312.pyc | Bin 7359 -> 6851 bytes .../app/__pycache__/schemas.cpython-312.pyc | Bin 9938 -> 8945 bytes .../app/api/__pycache__/admin.cpython-312.pyc | Bin 2241 -> 2046 bytes backend/app/api/admin.py | 3 +- backend/app/api/tiers.py | 73 ------- backend/app/main.py | 3 +- backend/app/models.py | 16 +- backend/app/schemas.py | 31 +-- backend/seed.py | 53 +---- retire-donation-page.md | 76 +++++++ src/app/about/about.component.html | 4 +- src/app/about/about.component.ts | 3 +- .../admin/admin-station-form.component.html | 9 + src/app/admin/admin-station-form.component.ts | 3 + src/app/admin/admin-tier-form.component.html | 50 ----- src/app/admin/admin-tier-form.component.scss | 129 ------------ src/app/admin/admin-tier-form.component.ts | 109 ---------- src/app/admin/admin.component.html | 54 +---- src/app/admin/admin.component.ts | 45 +--- src/app/app.routes.ts | 5 - src/app/contact/contact.component.html | 1 - src/app/donate/donate.component.html | 81 ------- src/app/donate/donate.component.scss | 197 ------------------ src/app/donate/donate.component.ts | 36 ---- src/app/footer/footer.component.html | 8 +- src/app/hero/hero.component.html | 4 +- src/app/interfaces/station-config.ts | 1 + src/app/interfaces/tier.ts | 8 - src/app/navbar/navbar.component.html | 4 +- src/app/services/station-config.service.ts | 1 + src/app/services/tier.service.ts | 35 ---- 34 files changed, 124 insertions(+), 924 deletions(-) delete mode 100644 backend/app/api/tiers.py create mode 100644 retire-donation-page.md delete mode 100644 src/app/admin/admin-tier-form.component.html delete mode 100644 src/app/admin/admin-tier-form.component.scss delete mode 100644 src/app/admin/admin-tier-form.component.ts delete mode 100644 src/app/donate/donate.component.html delete mode 100644 src/app/donate/donate.component.scss delete mode 100644 src/app/donate/donate.component.ts delete mode 100644 src/app/interfaces/tier.ts delete mode 100644 src/app/services/tier.service.ts diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 0cf1b6f..edb2297 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -2,12 +2,12 @@ ## Project overview -A three-tier web app for a community radio station: Angular 21 frontend, FastAPI backend, PostgreSQL database. Renders a hero page, about page, broadcast schedule, donation tiers, upcoming events, and a contact form — styled with a warm mountain/nature color palette (blue primary + orange accent on cream). +A three-tier web app for a community radio station: Angular 21 frontend, FastAPI backend, PostgreSQL database. Renders a hero page, about page, broadcast schedule, upcoming events, and a contact form — styled with a warm mountain/nature color palette (blue primary + orange accent on cream). Donations route to an externally configured URL. ## Code style - **Components:** Standalone only (no NgModules). Every component is a single file with `@Component` decorator, `imports` array, and four standard files (`.ts`, `.html`, `.scss`, component). -- **Data patterns:** Page content (programs, events, tiers) is stored in PostgreSQL and served via the FastAPI backend. The frontend uses HTTP services ([src/app/services/](src/app/services/)) to fetch data at runtime. To add or modify content, interact with the API or run `seed.py` — never hardcode content in components. +- **Data patterns:** Page content (programs, events) is stored in PostgreSQL and served via the FastAPI backend. The frontend uses HTTP services ([src/app/services/](src/app/services/)) to fetch data at runtime. To add or modify content, interact with the API or run `seed.py` — never hardcode content in components. - **SCSS:** All design tokens go in [src/styles/_variables.scss](src/styles/_variables.scss). Mixins in [_mixins.scss](src/styles/_mixins.scss). CSS custom properties exposed in [_themes.scss](src/styles/_themes.scss). Components import via `@use 'variables'` (no relative path needed). - **Routes:** Defined in [src/app/app.routes.ts](src/app/app.routes.ts) using `loadComponent()` lazy loading. Add new pages here after creating the component. - **TypeScript:** Strict mode enabled. Use `readonly` for constants (config, URLs). Keep data shapes close to their components where used only once. @@ -24,7 +24,7 @@ See [README.md](README.md) for a full architecture map. Key files: - `src/styles/_themes.scss` — SCSS → CSS custom property mapping - `docker-compose.dev.yml` — dev-sidecar compose for VS Code dev container - `backend/app/main.py` — FastAPI entry point (CORS, router registration) -- `backend/app/models.py` — SQLAlchemy ORM (Program, Event, DonationTier) +- `backend/app/models.py` — SQLAlchemy ORM (Show, Event, StationConfig) ## Common tasks diff --git a/backend/__pycache__/seed.cpython-312.pyc b/backend/__pycache__/seed.cpython-312.pyc index e62c571f5d26f12df59b3c2bd33b5e0e99e0b465..de7faa2bf5e30d4752ff5553c029c3be86f1f116 100644 GIT binary patch delta 4390 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 delta 6839 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 delta 896 zcmZ{gKWx)L6vpptx28#*M8f&ol%@$z>LelxOFKv~3;_nyyOx+ZiOwk~2#KW| zQ@MqWPM|6skTM`71c()oP$fXhzzhouGl+L_ha%#W{OP^-``*2Gep7vUQhTpy0R-pc z?^CwDttIiR?ZUIGPNtE;hy=D!W07JKr1H8%X1A1qu)=9%B}8SSkq`;f>|SJ`9Mp3( zXJ7E*!%C-Uz+T}QY$kK|>S5x=G-cQMO>Fqk1kfE{ngsgIm(BrAOMW+}fi`^UJkZCI zA;Zr6trW#|{>9h30A5`-9!!2;#;*VWYGRQ52HuMeWRGD4f9diBU*YyLLM%@ZS!%@{ zvtfdB@*J}~aga%h{|@Sx`X#f>pUUsTGoxvGhqim}XsXwwY}i%yrQk8xf-MNi@Kx2E zJ_zrtmvCwLL9MEIp8pB$;6?r3M~H{+G5<*>NOL*sm{<2ak`X0Oi@qUiOyw>o!SmK25AvCHjsn6*J!ia(4R z>O}x`v&dgWe@0RevyzaSkTQQBs~N(ui++J3=Iwjd>5$D9J;$y?4a4WY=6c*vt*^-X Uj4XaWzRPbU=J=2JzH$xlU(a}^lK=n! diff --git a/backend/app/__pycache__/models.cpython-312.pyc b/backend/app/__pycache__/models.cpython-312.pyc index 66c581547ff3d7ce95c867279c5e6c7a9b244cd0..cdce449179afd24a41d6e1fc35bb2f2dd1dc7df5 100644 GIT binary patch delta 1887 zcma)6U1%It6rQ^~JNrMo$!_*%x@NaFO**wdNNpurOUQ<#BqoSf!9ZDOGn4Lw*`4(6 ztabacwuC;2pzTFLd?`pD#8Q>K_#zb)6bgk>3o+p%8rYoI4~;mrAm5zn%Nt zbMF1_Ip?0+<3Apk?p~*V+-A!n%_X}YQT-?nqxR-({Lk*7wwjz#$2gtTkHxsmYlS0 z%g|b`I0a_x0r`1&5d0_tj&=xOP54cW&^+hax4Gi_{XmBd?1i}-){SMYV>w?EI?CP_ zXX6>P$|4N0TVnS_9IYg6$uZwGXc{Nc_EN*pJC@O=Ike;Z7Vt~if*d;=`aXuQ{=(%>S)~_8EQiT05+A9DMHTE#v@UC$HFm*-DtS9&g`tukZWw)cl`!MSCI6_~DtH>c7yB53b zNvpC;iDJMBu)Wej_9&ZLe*jbVnmp7Sz8TvtccoiaD{k@)M{`WORkd5o<~hiSH?1x4 zIb;NO-oWfn(@{V*=t`ZIdA?&%tQOAk3%lan{Fw!cv7*lcxY4tE4!a56ZgHu#4z>EDFpK9ROu%S2);}3| zlRWeO%s-NlRX!WNp85Q_Z$jVEUo(@JUiHdzmZvX~`M%emJ}+shokYPouGlp++Ltd^ ztlN5*z5wg=f0Jh5Dneagr?Y3hv>9B|PuV}XBpyYttTsMHb6-WJcOlM`HG!Vnwd4lt zrtL;o8@6NfC_Y^!v>jac zlcvytcO~AUdAHB*lm>3>3CX!Tr@|-(@18QKB`_0UE z&$)B%ch0%<@c8G?`M#DUHwRbc&weLKd{{KVznSmNd@1OkLP4gu2cPmz*L|R zoC@-sjT_};;W{VVmB8IPbmh3I5Mv#{I+f5iThCY#SeJzjGu92P$HF!+)(flzY;|kl z2%~*K`z^+ej12%Aw6IaehJdZNurbDlfo-s`amGe~ZM3ilD$!(Y12z=nk~Tw>b0wve zFDQnHxS$o4q?gX{1BrCGppg8G;T+O5Rml|%VHive;p{8t()2d}s3{C<5Cj~$9pF0m zy&#ep<7u-PTf1c|^TAelH=MF^DOXlYcX-lCdxW!|2DFMGG}0xZa>9#Nj@(Qse?=i7 zd~xYBvx;0+6`e%Uj;$NVr^FUSX|MeQ2b)OHb^GJSE_C$iv)X)i+dD&==x2_$#zQc- zfpuW|55QBBCq48J$H}TC8ZtSq&YIikT5A?Ve6n%B>I)NvSp$YFZ*ymn(tD61UZD`^oi?jGvpEa zsyoKtr@g^$y6O(K-E(X=hiKyz0ftL<%Hmxc_`xOH@44vjwu4pI7Bl5RJH?AL+nmHW z`wq@maVI^7cUUX3z%zM;7(V&$w`>Vsb;ve+W8+zX6x6q)T+Gjv6@o>`RNTNQn6lM$ z(!U=WS*C~3%L4#4TF}bH5;=p64;gVe$RXeodJX^*9&THlq@Ozd z^!14AOy=W}n&;%=@VZpLJi0Qp%G31i3pMHJVrtzJTy9?p(XQ2U&GQUc_#1A@s~2ki z?!~e7aD1g7?7~NvCe7ZMz(m;2+h|{~Yi-rtX%i|<$%FJ4Z_7(e6YSFz=>$yHk#le} zRY8JDV+iAunKZ$|By9kOYkU&7n#&g#yeU@GizOCA^jBkktxblGTU_HJv6s;Xkaf<(Egv&>ZJ$5 zu})S${~OU9x**Kci(z60G{*w^Ib35Ju{2uGjr|K{#=?qv=Q**5O!F8NfQ@^H4n?NT zsOGT}>BGq5Hdc7eV3V@eoMF!wW#uZNPZ*q9w9HckmeFARi EFPTcq5C8xG diff --git a/backend/app/__pycache__/schemas.cpython-312.pyc b/backend/app/__pycache__/schemas.cpython-312.pyc index 3c72926f22c11e32b8a590e61661fdadc0175f08..f90a25bf03313f3381f5bad4e9d65241fc94d579 100644 GIT binary patch delta 1471 zcmaizVQf=X6vunV+i2hVx~=cM_4RG-w$gTc!WOcjY^)OvlCjC&kZm9XQXafFDVDar zc7*YP0MTd+BsRyGnV1M6BN#Pt;WJF}o2VGZhoydj#4I6E6V1Mv816Y+ghYu=?yvXV z^FQa@bMLwLM*i3txFboThySkK?mN+`l>=JUGMn#MX2MLj(Q{+G>Qptm4`0hVd>>Na zcUgqdbw2p9_Cljm?FhD<;;y!0s~I*~Gt+2<;E?}8pyIdlIjfY(=hD+|7E)3QE_ws3 z2=7W$^-Do@QBWT!iOZ#*L`PUUIVr_rr>Pg(FvPPh8)4E4)f?vjUB`+ zGt7`H@>q%D?#Sn+GH*;2Y^&s$ZVxxRTM*A9UO;R`^dkBY+YtQ-jO7j>UgUuDbz#fU z+z2j25iJOcDBvuHXhS?RK^I#8FN+RyYF*t%)+3u!0rI&J+t-YbKp{ZO1h~tAVuEg7L&bR3fH83MSYf`TQ2%2%=Ac z9|Q$H4ffaXLXn0TLJUJ$*WxgM#7P*H_puR5u^^ZJ9d?H@ zB#xpQF%2IneQcD}kCg0&ijcETI7vPqfg6^OWO5Ds7{={@b3T=g!56w#`;-a3(_?k}QGj@# zkK#V~OOL^1XaMGTf6s^BQ~xt4n3@R}6(0k*rtN~eiq3DP2`)89;H7XE8-{#%i+B(t zBd)-g;Uqh_b`e*(xvckc^7P_i6d|tjqil!m@c?XWYGa4jj|0ojq}iKrMAAeHb%>iV5#7ry%JEHf zz<&%4HizlHiV>4JTtM96qbPB_<5_A@76oNVl-?d%?ZzDWA8PrTm(tt%ink(UGsTi? zQLX|;K-`15)-)@SHrn=a;w?0|9Hv{uX$etdaFp&r$8C zaf)~d-Ld`bH08M(TW4go&a?%wc7#or}m)hr~9MJp>wCBqq@#OHW3leay8?l hC<7#3*HqRyJGA2Ae=D0CSo^H8;^BWQo1i`U`D6F?TqXbj delta 2103 zcmb7EU2IcT9PjCFyY}|Gz5Uo7YuCE57T7S`$|!7r+1Q7$I>$C(0k+k9>q@$H+!kjQ zu`o27@uB4J!;D!V5ls{o4X-|Ed@z9*b<-?0F<@{pF^ibgkQg+c|LJfG5fhu7-~FHe z_t)uf`@iWm-!z$c75y!IzVqa)c+u=p2i2bhS4gK?)uoaeFQ}v@S$nk%RaC0sD#mT5 z-0%jjmvMJcZp#L)FS|1oxo%fKry-qis!D`k)qJc>=A^P#>PptiWfl14B#zz7DWyRw zs|}SG_<@A1v@7yhMlNuZV-s>gJ2sIzJ(*D|sY*c?lPBePB9ktdq(piuIhGwsDUz&2 zLpo^iR+tK!crrDXDR9X|Isa+s_fnwsh~8YsY>*aF zzsPx)y@6ckYO*|Nh_maL(^IL*w5)7Hm;c^` zE5cR-tCdx_9d0`vJ-bmIp_`uc;}mFxI>SuYe~oRCbKMeZFK51TY&&5@?p%tTW_-nC=$z>02#r7!Y9)15UtO`nc_3XKmjhq~yH;MgK1ZHrdjLb_hv&(sy9Nc!yu&{H4zI!9Hg*eXIi9 zHZ|&$Fja}Zr-hFNAN=IB@cn3ua1M@}YvF)hggvgU@TK_~&^Rn447)61%^_4`IkObm zYI~5!Hhlo8FjTv%_`^sd%)%wh5d7>A!D7w8YXW)#YY(gndXk3~YXtT>MTppvfzmRe zw`id(mPi%!Q{$;jYUH$%WXXo~^e;d(97oSAg%kL6_+xlSSOAN?l^ny7f^5I+WV^|d zVZ8D$>T!t5hFUJ{~w3~doU zf_p+YiP2WSObh+Y+008QLiiN^a&9IP^R~zJcB!Lq-{qwgjnBtX5#bBkvO2h?F~b?R zpTwDg#ac;6=Y_lOK08iM2_n=}7>FvvIDCE_IS4l)<=I8XufOkUAtVVuRy;v|fEsZ` zn?Vis9Ql(-A}qtZVm~>_g4iqDjJP?<1cj(Fh;BDeBLiWDp0tSqeYiGZkrU|4z0gzI zt!ZZeVdeLtBN+`arKgp4wr&9z|dqJc?8d zfh`PM1~yUF#Ay_;9_Elb57+!v{Hv5C^rTzhJO7bZ4RPlyiYodkS~VnCqJu@Nj@0KP gMHT%NMS;}M_Y_t1Q*5x3z&mpqt4Z-o zy zqe!V#zjwdvlu7~Ja>@?onJlH!!m@&HRx6bU^_=P&;$3JXYGYDaN_?2Xcf?+*4=)TdP#JcFIn{JtG=y pX1TaeiU*P8Ug{|wQlhXIsb{H|xA$T4P>I9BkxG=QF}Z2%{ts~jn>YXf delta 681 zcmXAm&1(}u7{=$_-OO&1&3D=~i6*9zrtKxRR8y*L5$vKZ^`xK$3H?~4C23@nV)bBJ zFDfn6Fc<#-FXE|6FOo~rB$r+SMH&J{1jTDgFKrLvJL|ynQdM`!Z!*(D=wtt~^QLj)lsOzW%I}qdc z)VlbXJO|t-*|}H+Co4?e_Fj;;SJiML{Q#@H^Xlq9YXhs;DP>A Mkq80P88K}B2m4sF1^@s6 diff --git a/backend/app/api/admin.py b/backend/app/api/admin.py index fd6aa89..12ecaa3 100644 --- a/backend/app/api/admin.py +++ b/backend/app/api/admin.py @@ -2,7 +2,7 @@ from fastapi import APIRouter from app.config import settings from app.database import get_session -from app.models import DonationTier, Event, StationConfig, Show, ShowSchedule +from app.models import Event, StationConfig, Show, ShowSchedule router = APIRouter() @@ -20,7 +20,6 @@ async def reset_database(): await session.execute(ShowSchedule.__table__.delete()) await session.execute(Show.__table__.delete()) await session.execute(Event.__table__.delete()) - await session.execute(DonationTier.__table__.delete()) await session.execute(StationConfig.__table__.delete()) await session.commit() diff --git a/backend/app/api/tiers.py b/backend/app/api/tiers.py deleted file mode 100644 index 15f2cdd..0000000 --- a/backend/app/api/tiers.py +++ /dev/null @@ -1,73 +0,0 @@ -from fastapi import APIRouter, Depends -from sqlalchemy import select -from sqlalchemy.ext.asyncio import AsyncSession - -from app.auth import get_current_admin_user -from app.database import get_session -from app.models import DonationTier -from app.schemas import TierCreate, TierUpdate, TierResponse -from app.user_models import User - -router = APIRouter() - - -@router.get("/", response_model=list[TierResponse]) -async def list_tiers(session: AsyncSession = Depends(get_session)): - result = await session.execute( - select(DonationTier).order_by(DonationTier.display_order) - ) - return list(result.scalars().all()) - - -@router.get("/{tier_id}", response_model=TierResponse) -async def get_tier(tier_id: int, session: AsyncSession = Depends(get_session)): - result = await session.execute(select(DonationTier).where(DonationTier.id == tier_id)) - tier = result.scalar_one_or_none() - if not tier: - return {"error": "Tier not found"} - return tier - - -@router.post("/", response_model=TierResponse, status_code=201) -async def create_tier( - payload: TierCreate, - session: AsyncSession = Depends(get_session), - _: User = Depends(get_current_admin_user), -): - tier = DonationTier(**payload.model_dump()) - session.add(tier) - await session.commit() - await session.refresh(tier) - return tier - - -@router.put("/{tier_id}", response_model=TierResponse) -async def update_tier( - tier_id: int, - payload: TierUpdate, - session: AsyncSession = Depends(get_session), - _: User = Depends(get_current_admin_user), -): - result = await session.execute(select(DonationTier).where(DonationTier.id == tier_id)) - tier = result.scalar_one_or_none() - if not tier: - return {"error": "Tier not found"} - for key, value in payload.model_dump(exclude_unset=True).items(): - setattr(tier, key, value) - await session.commit() - await session.refresh(tier) - return tier - - -@router.delete("/{tier_id}", status_code=204) -async def delete_tier( - tier_id: int, - session: AsyncSession = Depends(get_session), - _: User = Depends(get_current_admin_user), -): - result = await session.execute(select(DonationTier).where(DonationTier.id == tier_id)) - tier = result.scalar_one_or_none() - if not tier: - return {"error": "Tier not found"} - await session.delete(tier) - await session.commit() diff --git a/backend/app/main.py b/backend/app/main.py index b58463a..3742bb9 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -8,7 +8,7 @@ from app.config import settings from app.database import async_session, engine from app.models import Base, Show, StationConfig, HistoryEntry, TeamMember, CommunityHighlight from app.user_models import User -from app.api import events, tiers, auth, station_config, history, team, community, shows, storage +from app.api import events, auth, station_config, history, team, community, shows, storage async def _ensure_station_config(session): @@ -87,7 +87,6 @@ app.add_middleware( # Register routers app.include_router(auth.router, prefix="/api/auth", tags=["auth"]) app.include_router(events.router, prefix="/api/events", tags=["events"]) -app.include_router(tiers.router, prefix="/api/tiers", tags=["tiers"]) app.include_router(station_config.router, prefix="/api/station-config", tags=["station-config"]) app.include_router(history.router, prefix="/api/history", tags=["history"]) app.include_router(team.router, prefix="/api/team", tags=["team"]) diff --git a/backend/app/models.py b/backend/app/models.py index c683101..2209ecc 100644 --- a/backend/app/models.py +++ b/backend/app/models.py @@ -7,10 +7,8 @@ from sqlalchemy import ( LargeBinary, String, Text, - Numeric, Boolean, Date, - JSON, ForeignKey, UniqueConstraint, ) @@ -63,17 +61,6 @@ class Event(Base): active = Column(Boolean, default=True) -class DonationTier(Base): - __tablename__ = "donation_tiers" - - id = Column(Integer, primary_key=True, autoincrement=True) - name = Column(String(50), unique=True, nullable=False) - amount = Column(Numeric(10, 2), nullable=False) - icon = Column(String(10), nullable=False) - benefits = Column(JSON, nullable=False) - display_order = Column(Integer, nullable=False) - - class StationConfig(Base): __tablename__ = "station_config" @@ -104,6 +91,9 @@ class StationConfig(Base): email = Column(String(100), nullable=False, default="hello@kmountainflower.org") website = Column(String(200), nullable=False, default="kmountainflower.org") + # Donation + donation_url = Column(String(500), nullable=False, default="") + class HistoryEntry(Base): __tablename__ = "history_entries" diff --git a/backend/app/schemas.py b/backend/app/schemas.py index 234d3e5..0020c05 100644 --- a/backend/app/schemas.py +++ b/backend/app/schemas.py @@ -51,35 +51,6 @@ class EventResponse(BaseModel): model_config = {"from_attributes": True} -# ── DonationTier ───────────────────────────────────────── - -class TierCreate(BaseModel): - name: str - amount: float - icon: str - benefits: list[str] - display_order: int - - -class TierUpdate(BaseModel): - name: Optional[str] = None - amount: Optional[float] = None - icon: Optional[str] = None - benefits: Optional[list[str]] = None - display_order: Optional[int] = None - - -class TierResponse(BaseModel): - id: int - name: str - amount: float - icon: str - benefits: list[str] - display_order: int - - model_config = {"from_attributes": True} - - # ── StationConfig ───────────────────────────────────────── class StationConfigResponse(BaseModel): @@ -101,6 +72,7 @@ class StationConfigResponse(BaseModel): phone: str email: str website: str + donation_url: str model_config = {"from_attributes": True} @@ -122,6 +94,7 @@ class StationConfigUpdate(BaseModel): phone: Optional[str] = None email: Optional[str] = None website: Optional[str] = None + donation_url: Optional[str] = None # ── HistoryEntry ────────────────────────────────────────── diff --git a/backend/seed.py b/backend/seed.py index dd9dd99..9b2f095 100644 --- a/backend/seed.py +++ b/backend/seed.py @@ -14,7 +14,7 @@ from datetime import date from sqlalchemy import select from app.database import async_session -from app.models import Show, ShowSchedule, Event, DonationTier, StationConfig +from app.models import Show, ShowSchedule, Event, StationConfig from app.models import HistoryEntry, TeamMember, CommunityHighlight # ── Seed data ────────────────────────────────────────────── @@ -193,37 +193,6 @@ EVENTS: list[dict] = [ }, ] -TIERS: list[dict] = [ - { - "name": "Seed", - "amount": 5.00, - "icon": "🌱", - "benefits": ["Digital thank-you card", "Name listed on our website", "Quarterly newsletter"], - "display_order": 1, - }, - { - "name": "Stream", - "amount": 15.00, - "icon": "🎵", - "benefits": ["All Seed benefits", "KMountain Flower pin", "Priority show-request line", "Annual station update call"], - "display_order": 2, - }, - { - "name": "Ridge", - "amount": 30.00, - "icon": "🏔️", - "benefits": ["All Stream benefits", "Official member patch", "Exclusive show invitations", "Annual station tour"], - "display_order": 3, - }, - { - "name": "Summit", - "amount": 60.00, - "icon": "⭐", - "benefits": ["All Ridge benefits", "Featured on the Summit Wall", "Name on-air during anniversary special", "Lifetime patron recognition"], - "display_order": 4, - }, -] - STATION_CONFIG: dict = { "key": "default", "callsign": "KMTN", @@ -242,6 +211,7 @@ STATION_CONFIG: dict = { "phone": "(970) 555-0198", "email": "hello@kmountainflower.org", "website": "kmountainflower.org", + "donation_url": "", } HISTORY_ENTRIES: list[dict] = [ @@ -348,19 +318,6 @@ async def _upsert_event(session, data: dict) -> None: session.add(Event(**data, active=True)) -async def _upsert_tier(session, data: dict) -> None: - """Get-or-create a donation tier matched on name.""" - existing = await session.execute( - select(DonationTier).where(DonationTier.name == data["name"]) - ) - tier = existing.scalar_one_or_none() - if tier: - for key, value in data.items(): - setattr(tier, key, value) - else: - session.add(DonationTier(**data)) - - async def _upsert_station_config(session, data: dict) -> None: """Get-or-create the singleton station config (key='default').""" existing = await session.execute( @@ -452,11 +409,6 @@ async def seed() -> None: await session.commit() print(f" ✓ Seeded {len(EVENTS)} events") - for data in TIERS: - await _upsert_tier(session, data) - await session.commit() - print(f" ✓ Seeded {len(TIERS)} donation tiers") - await _upsert_station_config(session, STATION_CONFIG) await session.commit() print(" ✓ Seeded station config") @@ -491,7 +443,6 @@ async def truncate_all() -> None: await session.execute(ShowSchedule.__table__.delete()) await session.execute(Show.__table__.delete()) await session.execute(Event.__table__.delete()) - await session.execute(DonationTier.__table__.delete()) await session.execute(StationConfig.__table__.delete()) await session.execute(HistoryEntry.__table__.delete()) await session.execute(TeamMember.__table__.delete()) diff --git a/retire-donation-page.md b/retire-donation-page.md new file mode 100644 index 0000000..5764000 --- /dev/null +++ b/retire-donation-page.md @@ -0,0 +1,76 @@ +# Retire Donation Page, Add Configurable External Donation URL + +## Context + +The donation page (`/donate`) is being retired — it was a tier-based landing page with calls to action. All donation links will instead point to an external URL (e.g., a nonprofit payment processor). Admins configure this URL in the existing Station Config screen. + +## Approach + +1. Add `donation_url` field to the existing `StationConfig` singleton (backend model + schema, frontend interface + admin form) +2. Replace all donation links across the site with external `` links driven by `config().donation_url`, hidden when empty +3. Delete the donation page, tier service, tier admin UI, and all backend tier infrastructure + +## Step-by-Step + +### Phase 1 — Add `donation_url` to StationConfig (backend) + +- **`backend/app/models.py`** — Add `donation_url = Column(String(500), nullable=False, default="")` to `StationConfig` class +- **`backend/app/schemas.py`** — Add `donation_url: str` to `StationConfigResponse`; add `donation_url: Optional[str] = None` to `StationConfigUpdate` +- **`backend/seed.py`** — Add `"donation_url": ""` to `STATION_CONFIG` dict + +### Phase 2 — Add `donation_url` to StationConfig (frontend) + +- **`src/app/interfaces/station-config.ts`** — Add `donation_url: string` +- **`src/app/services/station-config.service.ts`** — Add `donation_url: ''` to `DEFAULT_CONFIG` +- **`src/app/admin/admin-station-form.component.ts`** — Add `donation_url` property, wire it in `populate()` and `onSubmit()` payload +- **`src/app/admin/admin-station-form.component.html`** — Add a "Donation" form section with a URL input field +- **`src/app/admin/admin.component.html`** — Show `donation_url` in the station config summary view + +### Phase 3 — Replace donation links with external links + +All links switch from `routerLink="/donate"` to `[href]="config().donation_url"` with `target="_blank" rel="noopener noreferrer"`, wrapped in `@if (config().donation_url)` so they hide when empty. + +- **`src/app/navbar/navbar.component.html`** — "Donate Now" button (line 37) +- **`src/app/hero/hero.component.html`** — "Support the Station" button (line 28) +- **`src/app/about/about.component.html`** — "Become a Member" CTA section (lines 76-88), wrap entire block in `@if` +- **`src/app/footer/footer.component.html`** — "Donate" and "Become a Member" links (lines 26, 33) + +### Phase 4 — Delete donation page + +- Remove `/donate` route from **`src/app/app.routes.ts`** (lines 22-25) +- Delete **`src/app/donate/`** directory (3 files: `.ts`, `.html`, `.scss`) + +### Phase 5 — Remove tier infrastructure (frontend) + +- Delete **`src/app/interfaces/tier.ts`** +- Delete **`src/app/services/tier.service.ts`** +- Delete **`src/app/admin/admin-tier-form.component.*`** (3 files) +- **`src/app/admin/admin.component.ts`** — Remove `Tier`/`TierService`/`AdminTierFormComponent` imports, `TabKey` entry, injected service, `tiers$` BehaviorSubject, `editingTier`, `showTierForm`, `reloadTiers()`, and all tier CRUD methods +- **`src/app/admin/admin.component.html`** — Remove Tiers tab button, Tiers tab content panel, tier form modal + +### Phase 6 — Remove tier infrastructure (backend) + +- Delete **`backend/app/api/tiers.py`** +- **`backend/app/models.py`** — Delete `DonationTier` class +- **`backend/app/schemas.py`** — Delete `TierCreate`, `TierUpdate`, `TierResponse` +- **`backend/app/main.py`** — Remove `tiers` import and router registration +- **`backend/app/api/admin.py`** — Remove `DonationTier` from reset truncation +- **`backend/seed.py`** — Remove `DonationTier` import, `TIERS` list, `_upsert_tier()` helper, tier seeding, tier truncation + +### Phase 7 — Contact form cleanup + +- **`src/app/contact/contact.component.html`** — Remove `` + +## Execution Order + +1 → 2 → 3 → 4 → 5 → 6 → 7 + +## Verification + +1. Grep for remaining references to `Tier`, `tier`, `DonationTier`, `donate`, `/donate` — none should remain in live code +2. `ng build` — no import errors, no unresolved references +3. Backend starts without import errors +4. Admin station form shows the new "External Donation URL" field +5. Setting a URL → all 4 link locations render external links with `target="_blank"` +6. Clearing the URL → all 4 link locations hide +7. Navigate to `/donate` → 404 or wildcard redirect to home \ No newline at end of file diff --git a/src/app/about/about.component.html b/src/app/about/about.component.html index 0c1e79a..76a1c4f 100644 --- a/src/app/about/about.component.html +++ b/src/app/about/about.component.html @@ -74,6 +74,7 @@ } + @if (config().donation_url) { diff --git a/src/app/about/about.component.ts b/src/app/about/about.component.ts index 6a0ab90..93c65bc 100644 --- a/src/app/about/about.component.ts +++ b/src/app/about/about.component.ts @@ -1,5 +1,4 @@ import { Component, inject } from '@angular/core'; -import { RouterLink } from '@angular/router'; import { AsyncPipe } from '@angular/common'; import { BehaviorSubject } from 'rxjs'; @@ -15,7 +14,7 @@ import { resolveImageUrl } from '../services/app-config.service'; @Component({ selector: 'app-about', standalone: true, - imports: [RouterLink, AsyncPipe], + imports: [AsyncPipe], templateUrl: './about.component.html', styleUrl: './about.component.scss', }) diff --git a/src/app/admin/admin-station-form.component.html b/src/app/admin/admin-station-form.component.html index ebc7c9f..f06d5c4 100644 --- a/src/app/admin/admin-station-form.component.html +++ b/src/app/admin/admin-station-form.component.html @@ -130,6 +130,15 @@ + +
+

Donation

+
+ + +
+
+
-
- - @if (error) { - - } - -
-
-
- - -
- -
- - -
-
- -
-
- - -
- -
- - -
-
- -
- - -
- -
- - -
-
- - diff --git a/src/app/admin/admin-tier-form.component.scss b/src/app/admin/admin-tier-form.component.scss deleted file mode 100644 index 854d9bc..0000000 --- a/src/app/admin/admin-tier-form.component.scss +++ /dev/null @@ -1,129 +0,0 @@ -@use '../../styles/variables' as *; -@use '../../styles/mixins' as *; - -.form-overlay { - position: fixed; - inset: 0; - background: rgba(0, 0, 0, 0.5); - @include flex-center; - z-index: $z-modal; - @include fade-in; -} - -.form-dialog { - background: $neutral-white; - border-radius: $radius-lg; - padding: $spacing-xl; - width: 90%; - max-width: 560px; - box-shadow: $shadow-xl; -} - -.form-header { - @include flex-between; - margin-bottom: $spacing-lg; - - h2 { - font-family: $font-heading; - color: $primary-blue; - margin: 0; - font-size: 1.4rem; - } -} - -.btn-close { - background: none; - border: none; - font-size: 1.5rem; - cursor: pointer; - color: $neutral-medium; - line-height: 1; - padding: $spacing-xs; - - &:hover { - color: $neutral-dark; - } -} - -.form-error { - background: rgba($danger-red, 0.1); - color: $danger-red; - padding: $spacing-sm $spacing-md; - border-radius: $radius-md; - font-size: 0.875rem; - margin-bottom: $spacing-md; - text-align: center; -} - -.admin-form { - .form-row { - display: grid; - grid-template-columns: 1fr 1fr; - gap: $spacing-md; - } - - .form-group { - margin-bottom: $spacing-md; - - label { - display: block; - font-size: 0.875rem; - font-weight: 600; - color: $neutral-dark; - margin-bottom: $spacing-xs; - } - - input, select, textarea { - width: 100%; - padding: $spacing-sm $spacing-md; - border: 1px solid $neutral-light; - border-radius: $radius-md; - font-size: 0.95rem; - transition: border-color $transition-fast; - box-sizing: border-box; - - &:focus { - outline: none; - border-color: $primary-blue; - box-shadow: 0 0 0 3px rgba($primary-blue, 0.15); - } - - &.is-invalid { - border-color: $danger-red; - box-shadow: 0 0 0 3px rgba($danger-red, 0.15); - } - } - } -} - -.form-actions { - display: flex; - justify-content: flex-end; - gap: $spacing-sm; - margin-top: $spacing-md; -} - -.btn-save { - @include button-style($primary-blue, $neutral-white, $primary-blue-light); - - &:disabled { - opacity: 0.6; - cursor: not-allowed; - } -} - -.btn-cancel { - background: $neutral-light; - color: $neutral-dark; - border: none; - border-radius: $radius-md; - padding: $spacing-sm $spacing-lg; - font-size: 0.95rem; - cursor: pointer; - transition: background $transition-fast; - - &:hover { - background: $neutral-medium; - color: $neutral-white; - } -} diff --git a/src/app/admin/admin-tier-form.component.ts b/src/app/admin/admin-tier-form.component.ts deleted file mode 100644 index d2dcb95..0000000 --- a/src/app/admin/admin-tier-form.component.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { Component, EventEmitter, inject, Input, OnChanges, Output, SimpleChanges } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { FormsModule } from '@angular/forms'; -import { firstValueFrom } from 'rxjs'; - -import { Tier } from '../interfaces/tier'; -import { TierService } from '../services/tier.service'; - -@Component({ - selector: 'app-admin-tier-form', - standalone: true, - imports: [CommonModule, FormsModule], - templateUrl: './admin-tier-form.component.html', - styleUrl: './admin-tier-form.component.scss', -}) -export class AdminTierFormComponent implements OnChanges { - private tierService = inject(TierService); - - @Input() editItem: Tier | null = null; - @Output() saved = new EventEmitter(); - @Output() closed = new EventEmitter(); - - id: number | null = null; - name = ''; - amount = 0; - icon = '⭐'; - benefits_text = ''; - display_order = 1; - - loading = false; - error = ''; - submitted = false; - - ngOnChanges(changes: SimpleChanges): void { - if (changes['editItem'] && this.editItem) { - this.edit(this.editItem); - } - } - - edit(tier: Tier): void { - this.id = tier.id; - this.name = tier.name; - this.amount = tier.amount; - this.icon = tier.icon; - this.benefits_text = tier.benefits.join(', '); - this.display_order = tier.display_order; - } - - formatError(err: any): string { - if (err?.error?.detail) return err.error.detail; - if (err?.error?.message) return err.error.message; - if (err?.message) return err.message; - return 'Failed to save. Please try again.'; - } - - reset(): void { - this.id = null; - this.name = ''; - this.amount = 0; - this.icon = '⭐'; - this.benefits_text = ''; - this.display_order = 1; - this.error = ''; - this.submitted = false; - } - - onClose(): void { - this.closed.emit(); - this.reset(); - } - - async onSubmit(): Promise { - this.submitted = true; - if (!this.name || this.amount <= 0) { - this.error = 'Please fill in all required fields.'; - return; - } - - this.loading = true; - this.error = ''; - - const benefits = this.benefits_text - .split(',') - .map((b) => b.trim()) - .filter((b) => b.length > 0); - - const payload = { - name: this.name, - amount: this.amount, - icon: this.icon, - benefits: benefits, - display_order: this.display_order, - }; - - try { - if (this.id) { - await firstValueFrom(this.tierService.updateTier(this.id, payload)); - } else { - await firstValueFrom(this.tierService.createTier(payload)); - } - this.saved.emit(); - this.reset(); - } catch (err: any) { - this.error = this.formatError(err); - } finally { - this.loading = false; - } - } -} diff --git a/src/app/admin/admin.component.html b/src/app/admin/admin.component.html index 1dfe7e3..a18172d 100644 --- a/src/app/admin/admin.component.html +++ b/src/app/admin/admin.component.html @@ -2,7 +2,7 @@

Admin Dashboard

-

Manage shows, events, tiers, history, team, and community content

+

Manage shows, events, history, team, and community content

@@ -17,11 +17,6 @@ [class.active]="activeTab() === 'events'" (click)="activeTab.set('events')" >Events - -
- - - - - - - - - - - - - @for (tier of tiers; track tier.id) { - - - - - - - - } @empty { - - } - -
OrderNameAmountBenefitsActions
{{ tier.display_order }}{{ tier.icon }} {{ tier.name }}${{ tier.amount.toFixed(2) }}{{ tier.benefits.join(', ') }} - - -
No tiers yet. Click "Add Tier" to create one.
- - } - } - @if (activeTab() === 'history') { @if (historyEntries$ | async; as entries) { @@ -336,6 +291,10 @@ Website {{ config.website }} +
+ Donation URL + {{ config.donation_url || '(not set)' }} +
} @@ -349,9 +308,6 @@ @if (showEventForm) { } - @if (showTierForm) { - - } @if (showStationForm) { } diff --git a/src/app/admin/admin.component.ts b/src/app/admin/admin.component.ts index deca079..749ae52 100644 --- a/src/app/admin/admin.component.ts +++ b/src/app/admin/admin.component.ts @@ -5,27 +5,24 @@ import { BehaviorSubject, firstValueFrom } from 'rxjs'; import { Show } from '../interfaces/show'; import { Event } from '../interfaces/event'; -import { Tier } from '../interfaces/tier'; import { StationConfig } from '../interfaces/station-config'; import { HistoryEntry } from '../interfaces/history-entry'; import { TeamMember } from '../interfaces/team-member'; import { CommunityHighlight } from '../interfaces/community-highlight'; import { ShowService } from '../services/show.service'; import { EventService } from '../services/event.service'; -import { TierService } from '../services/tier.service'; import { StationConfigService } from '../services/station-config.service'; import { HistoryEntryService } from '../services/history-entry.service'; import { TeamMemberService } from '../services/team-member.service'; import { CommunityHighlightService } from '../services/community-highlight.service'; import { AdminShowFormComponent } from './admin-show-form.component'; import { AdminEventFormComponent } from './admin-event-form.component'; -import { AdminTierFormComponent } from './admin-tier-form.component'; import { AdminStationFormComponent } from './admin-station-form.component'; import { AdminHistoryFormComponent } from './admin-history-form.component'; import { AdminTeamFormComponent } from './admin-team-form.component'; import { AdminCommunityFormComponent } from './admin-community-form.component'; -type TabKey = 'shows' | 'events' | 'tiers' | 'station' | 'history' | 'team' | 'community'; +type TabKey = 'shows' | 'events' | 'station' | 'history' | 'team' | 'community'; @Component({ selector: 'app-admin', @@ -36,7 +33,6 @@ type TabKey = 'shows' | 'events' | 'tiers' | 'station' | 'history' | 'team' | 'c FormsModule, AdminShowFormComponent, AdminEventFormComponent, - AdminTierFormComponent, AdminStationFormComponent, AdminHistoryFormComponent, AdminTeamFormComponent, @@ -48,7 +44,6 @@ type TabKey = 'shows' | 'events' | 'tiers' | 'station' | 'history' | 'team' | 'c export class AdminComponent implements OnInit { private showService = inject(ShowService); private eventService = inject(EventService); - private tierService = inject(TierService); private stationConfigService = inject(StationConfigService); private historyEntryService = inject(HistoryEntryService); private teamMemberService = inject(TeamMemberService); @@ -59,7 +54,6 @@ export class AdminComponent implements OnInit { /** Item currently being edited (set by editXxx, cleared on save/close). */ editingShow: Show | null = null; editingEvent: Event | null = null; - editingTier: Tier | null = null; editingHistory: HistoryEntry | null = null; editingTeam: TeamMember | null = null; editingCommunity: CommunityHighlight | null = null; @@ -67,7 +61,6 @@ export class AdminComponent implements OnInit { /** BehaviorSubjects guarantee change detection via the async pipe — start empty, populated on load. */ readonly shows$ = new BehaviorSubject([]); readonly events$ = new BehaviorSubject([]); - readonly tiers$ = new BehaviorSubject([]); readonly stationConfig$ = new BehaviorSubject(null); readonly historyEntries$ = new BehaviorSubject([]); readonly teamMembers$ = new BehaviorSubject([]); @@ -75,7 +68,6 @@ export class AdminComponent implements OnInit { showShowForm = false; showEventForm = false; - showTierForm = false; showStationForm = false; showHistoryForm = false; showTeamForm = false; @@ -95,11 +87,6 @@ export class AdminComponent implements OnInit { this.events$.next(data); } - async reloadTiers(): Promise { - const data = await firstValueFrom(this.tierService.getTiers()); - this.tiers$.next(data); - } - async reloadStationConfig(): Promise { await this.stationConfigService.load(); this.stationConfig$.next(this.stationConfigService.config()); @@ -124,7 +111,6 @@ export class AdminComponent implements OnInit { await Promise.all([ this.reloadShows(), this.reloadEvents(), - this.reloadTiers(), this.reloadStationConfig(), this.reloadHistoryEntries(), this.reloadTeamMembers(), @@ -190,35 +176,6 @@ export class AdminComponent implements OnInit { await this.reloadEvents(); } - // ── Tier CRUD ─────────────────────────────────────────── - - openTierForm(): void { - this.editingTier = null; - this.showTierForm = true; - } - - editTier(tier: Tier): void { - this.editingTier = tier; - this.showTierForm = true; - } - - onTierSaved(): void { - this.reloadTiers(); - this.showTierForm = false; - this.editingTier = null; - } - - onTierClosed(): void { - this.showTierForm = false; - this.editingTier = null; - } - - async deleteTier(id: number): Promise { - if (!confirm('Delete this tier?')) return; - await firstValueFrom(this.tierService.deleteTier(id)); - await this.reloadTiers(); - } - // ── Station Config ────────────────────────────────────── openStationForm(): void { diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index 97a311e..f907169 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -18,11 +18,6 @@ export const routes: Routes = [ loadComponent: () => import('./schedule/schedule.component').then((m) => m.ScheduleComponent), }, - { - path: 'donate', - loadComponent: () => - import('./donate/donate.component').then((m) => m.DonateComponent), - }, { path: 'events', loadComponent: () => diff --git a/src/app/contact/contact.component.html b/src/app/contact/contact.component.html index ae1a6ed..0ece1c3 100644 --- a/src/app/contact/contact.component.html +++ b/src/app/contact/contact.component.html @@ -27,7 +27,6 @@ - diff --git a/src/app/donate/donate.component.html b/src/app/donate/donate.component.html deleted file mode 100644 index c1ab344..0000000 --- a/src/app/donate/donate.component.html +++ /dev/null @@ -1,81 +0,0 @@ - diff --git a/src/app/donate/donate.component.scss b/src/app/donate/donate.component.scss deleted file mode 100644 index 090cb73..0000000 --- a/src/app/donate/donate.component.scss +++ /dev/null @@ -1,197 +0,0 @@ -@use '../../styles/variables' as *; -@use '../../styles/mixins' as *; - -.donate { - background: var(--color-cream); -} - -// Banner -.donate-banner { - position: relative; - margin-bottom: $spacing-3xl; -} - -.donate-banner-overlay { - background: $mixed-gradient; - padding: $spacing-3xl $spacing-lg; - text-align: center; -} - -.donate-banner-content { - position: relative; - z-index: 1; -} - -.donate-flower-hero { - width: 80px; - height: 80px; - margin: 0 auto $spacing-md; - opacity: 0.7; - animation: float 4s ease-in-out infinite; -} - -.donate-banner h2 { - color: var(--color-white); - font-size: 2.4rem; - margin-bottom: $spacing-md; -} - -.donate-subtitle { - color: rgba(255, 255, 255, 0.85); - font-size: 1.1rem; - max-width: 550px; - margin: 0 auto; - line-height: 1.7; -} - -// Tiers -.donate-tiers { - text-align: center; - margin-bottom: $spacing-3xl; - - h3 { - font-size: 1.8rem; - margin-bottom: $spacing-xl; - } -} - -.tiers-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); - gap: $spacing-lg; - margin-bottom: $spacing-xl; -} - -.tier-card { - @include card-style; - padding: $spacing-xl; - text-align: center; - background: var(--color-white); - border-top: 4px solid var(--color-accent); - - .tier-icon { - font-size: 2rem; - margin-bottom: $spacing-sm; - } - - .tier-amount { - font-size: 2.8rem; - font-weight: 700; - color: var(--color-accent); - line-height: 1; - } - - .tier-period { - font-size: 0.85rem; - color: var(--color-medium); - margin-bottom: $spacing-md; - } - - h4 { - font-size: 1.2rem; - color: var(--color-primary); - margin-bottom: $spacing-md; - } - - .tier-benefits { - list-style: none; - text-align: left; - margin-bottom: $spacing-lg; - - li { - padding: $spacing-xs 0; - font-size: 0.9rem; - color: var(--color-dark); - border-bottom: 1px solid var(--color-light); - - &::before { - content: '✓ '; - color: $success-green; - font-weight: 700; - } - } - } - - .btn-tier { - @include button-style($accent-orange, $neutral-white, $accent-orange-dark); - display: inline-block; - width: 100%; - text-align: center; - } -} - -// One-time -.donate-one-time { - text-align: center; - padding: $spacing-2xl $spacing-lg; - background: var(--color-primary); - border-radius: $radius-xl; - margin-bottom: $spacing-3xl; - - .donate-flower-divider { - width: 40px; - height: 40px; - margin: 0 auto $spacing-md; - - img { - width: 100%; - filter: brightness(1.2); - } - } - - h3 { - color: var(--color-white); - font-size: 1.6rem; - margin-bottom: $spacing-sm; - } - - .section-intro { - color: rgba(255, 255, 255, 0.8); - margin-bottom: $spacing-lg; - } - - .btn-lg { - @include button-style($accent-orange, $neutral-white, $accent-orange-dark); - font-size: 1.2rem; - padding: $spacing-md $spacing-2xl; - } -} - -// Why -.donate-why { - padding-bottom: $spacing-xl; -} - -.why-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); - gap: $spacing-lg; - - .why-item { - text-align: center; - padding: $spacing-lg; - - .why-icon { - display: block; - font-size: 2.5rem; - margin-bottom: $spacing-sm; - } - - strong { - display: block; - color: var(--color-primary); - margin-bottom: $spacing-xs; - font-size: 1.05rem; - } - - p { - font-size: 0.9rem; - color: var(--color-medium); - } - } -} - -@media (max-width: #{$bp-md - 1px}) { - .donate-banner h2 { font-size: 1.8rem; } - .tiers-grid { grid-template-columns: 1fr; max-width: 320px; margin-left: auto; margin-right: auto; } -} diff --git a/src/app/donate/donate.component.ts b/src/app/donate/donate.component.ts deleted file mode 100644 index 5a7e17e..0000000 --- a/src/app/donate/donate.component.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { Component, inject } from '@angular/core'; -import { AsyncPipe, NgFor } from '@angular/common'; -import { map, Observable, startWith } from 'rxjs'; - -import { TierService } from '../services/tier.service'; -import { Tier } from '../interfaces/tier'; -import { StationConfigService } from '../services/station-config.service'; -import { resolveImageUrl } from '../services/app-config.service'; - -interface TiersState { - loading: boolean; - tiers: Tier[]; -} - -@Component({ - selector: 'app-donate', - standalone: true, - imports: [AsyncPipe, NgFor], - templateUrl: './donate.component.html', - styleUrl: './donate.component.scss', -}) -export class DonateComponent { - private tierService = inject(TierService); - readonly config = inject(StationConfigService).config; - - resolveImageUrl(url: string): string { - return resolveImageUrl(url); - } - - /** Async pipe drives the template — guarantees change detection on every emission. */ - readonly tiers$: Observable = - this.tierService.getTiers().pipe( - map((tiers) => ({ loading: false, tiers })), - startWith({ loading: true, tiers: [] }), - ); -} diff --git a/src/app/footer/footer.component.html b/src/app/footer/footer.component.html index 7a4f87d..ec17f37 100644 --- a/src/app/footer/footer.component.html +++ b/src/app/footer/footer.component.html @@ -23,14 +23,18 @@
  • About Us
  • Schedule
  • Events
  • -
  • Donate
  • + @if (config().donation_url) { +
  • Donate
  • + }
  • Contact
  • diff --git a/src/app/services/station-config.service.ts b/src/app/services/station-config.service.ts index cc81299..3bf0030 100644 --- a/src/app/services/station-config.service.ts +++ b/src/app/services/station-config.service.ts @@ -25,6 +25,7 @@ const DEFAULT_CONFIG: StationConfig = { phone: '(970) 555-0198', email: 'hello@kmountainflower.org', website: 'kmountainflower.org', + donation_url: '', }; @Injectable({ diff --git a/src/app/services/tier.service.ts b/src/app/services/tier.service.ts deleted file mode 100644 index f3f88a7..0000000 --- a/src/app/services/tier.service.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { Injectable, inject } from '@angular/core'; -import { HttpClient } from '@angular/common/http'; -import { Observable } from 'rxjs'; - -import { Tier } from '../interfaces/tier'; -import { getAppConfig } from './app-config.service'; - -@Injectable({ - providedIn: 'root', -}) -export class TierService { - private http = inject(HttpClient); - private baseUrl = `${getAppConfig().apiBaseUrl}/api/tiers`; - - /** Fetch all tiers ordered by display_order. */ - getTiers(): Observable { - return this.http.get(this.baseUrl); - } - - getTier(id: number): Observable { - return this.http.get(`${this.baseUrl}/${id}`); - } - - createTier(payload: Omit): Observable { - return this.http.post(this.baseUrl, payload); - } - - updateTier(id: number, payload: Partial): Observable { - return this.http.put(`${this.baseUrl}/${id}`, payload); - } - - deleteTier(id: number): Observable { - return this.http.delete(`${this.baseUrl}/${id}`); - } -}