From d867933823b473f11dfc9e36b3dd44846784dd84 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Sun, 12 Apr 2026 12:41:59 +0000 Subject: [PATCH] Autosave: 20260412-124200 --- config/__pycache__/settings.cpython-311.pyc | Bin 5552 -> 5925 bytes config/settings.py | 14 +- core/__pycache__/admin.cpython-311.pyc | Bin 212 -> 1253 bytes .../context_processors.cpython-311.pyc | Bin 763 -> 763 bytes core/__pycache__/forms.cpython-311.pyc | Bin 0 -> 8099 bytes core/__pycache__/models.cpython-311.pyc | Bin 209 -> 6706 bytes core/__pycache__/tests.cpython-311.pyc | Bin 0 -> 7005 bytes core/__pycache__/urls.cpython-311.pyc | Bin 347 -> 1590 bytes core/__pycache__/views.cpython-311.pyc | Bin 1364 -> 15900 bytes core/admin.py | 17 +- core/forms.py | 114 ++++ core/migrations/0001_initial.py | 54 ++ core/migrations/0002_seed_demo_events.py | 104 ++++ .../__pycache__/0001_initial.cpython-311.pyc | Bin 0 -> 3199 bytes .../0002_seed_demo_events.cpython-311.pyc | Bin 0 -> 4281 bytes core/models.py | 87 ++- core/templates/base.html | 85 ++- core/templates/core/event_detail.html | 94 ++++ core/templates/core/index.html | 303 ++++++----- core/templates/core/login.html | 35 ++ core/templates/core/organizer_dashboard.html | 115 ++++ core/templates/core/organizer_event_form.html | 113 ++++ core/templates/core/registration_success.html | 26 + core/tests.py | 118 +++- core/urls.py | 13 +- core/views.py | 254 ++++++++- static/css/custom.css | 503 ++++++++++++++++- staticfiles/css/custom.css | 506 +++++++++++++++++- 28 files changed, 2368 insertions(+), 187 deletions(-) create mode 100644 core/__pycache__/forms.cpython-311.pyc create mode 100644 core/__pycache__/tests.cpython-311.pyc create mode 100644 core/forms.py create mode 100644 core/migrations/0001_initial.py create mode 100644 core/migrations/0002_seed_demo_events.py create mode 100644 core/migrations/__pycache__/0001_initial.cpython-311.pyc create mode 100644 core/migrations/__pycache__/0002_seed_demo_events.cpython-311.pyc create mode 100644 core/templates/core/event_detail.html create mode 100644 core/templates/core/login.html create mode 100644 core/templates/core/organizer_dashboard.html create mode 100644 core/templates/core/organizer_event_form.html create mode 100644 core/templates/core/registration_success.html diff --git a/config/__pycache__/settings.cpython-311.pyc b/config/__pycache__/settings.cpython-311.pyc index d79d6a79890a58443ee5f65d8d558823a19e9aed..94a4110fc612dc407cb8a521d8f9ae2b5fe51fb4 100644 GIT binary patch delta 731 zcmdm>y;P5PIWI340}ynC-Ol_pkyn!O%SQF9Oin2VEexrgDTYH7PCB~*<>$fRc7-Pi^)aIx{Q{S7ceU-Tcz+W zV*=_1VhD)RK+|nK@tdu%CYqp4itWT7)|2yD1k~+P?9oinPH|{qK-1tjc^Qj3qtoPb zEUJ>uQJN_(QF%nn^@~Pl#&@?P65&^K+FuppRItzbcS?> z8irVj$?MpqCZFfy(_SxD!#I(tM>3e9hzBUI$#{z`wIZ{)q*#-wh!-eui!&v&C^fkx zzo>HZKRz+989=dO8z9lZ@PUCtgb6`B;1K-4#lRtWon8DAyZA+R$t&!VANYYvZm{rv zU`A0|BnUKMb3N}=M#goMMfn?4Zn5U%r)TEf63#D5Pt41#N-c^{Ni5Dt%1lM*tjHQffE-q23nJ`+#4Xn1f}G5fBIn8d zg5r_4IDP!xJ^kWCgM4lYL6||VE}lWI&LLniVW60QXh=M=AUI6Gt}WsR8u*LDCO1E& zG$+-rC?CiLg>o?m%jVO96PavS;67wy;1rx-agjs%28RHgw!6ro_<@~8jO_y}P~k^V p(0u_B7Z?OSh;6PHQDc;Oz$4tic|%m|0)x;EF_{Yt!Vmm1_mH)2&IicbUJgC5)(rz za~4<=NMnj|ipgX#7Gr=~+ZW$8;!)hRgfGF)0`xXW?RSuJPv8XdTPJYCq%Ig%Rnc^I!m%<#( zpy{$%fwi7va|z#6M#fc>xda-RH1#*H6FAK1V+=H{$OK5-V$RGfDKZ6d%|V0(h_D0^ zRv^L#NZevAF38C&DRKa@g9fL zTj9j%{{&|TQADwi0&HW9s77^P3v^o#4BNm+Lkw@4c_7nwF+zLr4NODN5jD0DHDOhr z747P<2EKvqLbI25kX`&Iqj5>b0%R=ycg7Mey+u~}P}Q)oGzks7@KEj1(xmz9BMXJB zzG3TLA~+FV6uPIo0SCCrtbYTV9mJ50DY7++ZJlbiL3L_)*fyzY7ia;kR}vNVZbFZaXrzLRWEVkbk+Bg4Mr`E*Pswr;z%R$({&5;GIT6G zYh*D=gMjcRAA-RZC|7cvkOa(eWW{lUh^D^stB&&_CH~1w6_N{wdP(e)rX0$8Yo0F{ zm*X*KaTKRM5sW%(9`k7;YY8LV-8kufM}4S;{@iJ7L;-80t7$0G2K&t1R7AY-DdO)F zI16jI5oe8z-g?~RC5XvD37;$hv`(Lc@#WU*?pVF`syny%^-+7a-ZrMY6L(w7>ftka z=s79!Yt^5H>C2=584QrXl%r?m%#|pwrM{nWB~2C(&xa-xzW~~ZG?5jTvy25IK!$5- z4+{Xm{8i=81IVHq1u+SmD^`JzsL@dcV+zI*OVhj?~1l0+nUq;`)sF Ezq-CJTL1t6 delta 168 zcmaFLd4(}$IWI340}#~ttjM$n(vLwL7+``jJ_`XE(-~42QW$d>av7r-85vTTf*CZK zUxE~9GTvfMOv%m6^V4Ly#g~$mn3tZfmzKBJiZhlH>PO4oI c2T+U=h>K-`#0O?ZM#dWq3Ky`UA~v830AU#=2><{9 diff --git a/core/__pycache__/context_processors.cpython-311.pyc b/core/__pycache__/context_processors.cpython-311.pyc index 75bf2234fb21a6b62efc5cec11af9512dd0c9616..2f0013b4a7cb0e3edfe0bb0a46ed6dcd125cccc4 100644 GIT binary patch delta 20 acmey(`kR$|IWI340}#~ttk}r?fe8RVqXqW> delta 20 acmey(`kR$|IWI340}xbw%iqZTfe8RYW(H&c diff --git a/core/__pycache__/forms.cpython-311.pyc b/core/__pycache__/forms.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f01cf58da73b6f6772a7928f9428654b2e6c07f7 GIT binary patch literal 8099 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 VLRirb)=u3&{S5KXUqYnf{4daY&)fh2 delta 143 zcmdmFa*;7%IWI340}#~ttjM$k(vLwL7+``jJ_`XE(-~42QW$d>av7r-85vTTf*CZK zUxE~9GTvg#%}+_qDfZK3y2Y82m6(^Fua}Zk#0-?2Y|3IT`isK`sGu|_)vkyGD8>lH U#j=yDB&E17FvtK=5gSkh0Ns!s%m4rY diff --git a/core/__pycache__/tests.cpython-311.pyc b/core/__pycache__/tests.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1587e3faa61ee9116c095b08c46bd7be88b2a2cf GIT binary patch literal 7005 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 C5J+4A literal 0 HcmV?d00001 diff --git a/core/__pycache__/urls.cpython-311.pyc b/core/__pycache__/urls.cpython-311.pyc index f705988a223abc7b5af333902925bc196186bbf5..72f9f985c41067f903f4c85a0f66c63760644470 100644 GIT binary patch literal 1590 zcmcgr&2QX96d!M9KfE7JsogJ{HmDca3f77Mfubl?6Vylrfkaw}EF|N2o_1aQvCNE< zu%{k-tT+HSdJ00#fg^tc$8lw;kvVeeEmb-7#LVn&OUlBLS{CF$NsZJG8v7N59>_T!#5`#3&q+ki@z887 z$#!Cj{0m$AptCQvKag`g$cdR;^H3-4nU<4KC9ll1oPrjyW?F1$kkd0QXTT(9XIjod zlboMvc@=8p!poMv>CTgdP1L>kSS8jq%IZ!CyG(BI(C354D&uawY{L#D+#|sKFe+`> z8>W=X*u$}splxJRmW1@h9&!Mzm#({*qv?=sM z#;MQ4H1Sv#1i;v+7Nz|#aYpk&O2N^G-hju^&2FXaaEOPIWb_hWYPdip4~0M~%=kbG z4+Ci z5}rA*2SLVD>g=ZUD>n24aC8SK8NJ~*&l)RnO0o!6=_MJg`j!1C*P}VZFb>h%;`noj z7R2%A2qWWG6pI z@h%yeWaRmKHwwG^$X>sjyDUuIrcks_vQ~ZffsYU zSl~sC3G>qZ_AjdfujP2Hz-trms34kb#dcMf&Co&1@Q(Ocx>=tWTW@>pcSm7;hpfJ#slGLKS;*|`a zfzk}Wbo4<+>6a#z=9QG{r&gpUmzLxg>6hmhWfvDDCa3Br=NAE`f#&HIRQ}?y$<0qG z%}KQ@;sa_1Ii}bPNPJ*sWMsU-Aael~Jzx;OfQmk_F|cwrxO510gv?;Oz#@N*W8V3NU&sfd? diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc index 2f0989c7137f7930a63ea76fcd63516c4974e834..efabeee89624da46093d4249ef8b87560f8755df 100644 GIT binary patch literal 15900 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 delta 860 zcmZ`%%}*0S6rb6zZmU2^wcu*$@{!1LKnWTXLO`t5VE9VGgT0X29q2Z@J2E?^Kq~a$ zU*H~$u@^6X#FIB~`X{tW51UPzka+UegqtU4TY5m_n>TOX`^|eVzj>Kjr5QQ-*w+^U zOpaGR8S^{XOZ1;p&$YRdqEIup10OL*3lCo0A7tD|)XJzJ# zuRu1e!%d(`zk+sa8uTyiBD%egh+_!Y*5GUIio16=4j;Ky?wwnV^}Er@xcgS>FIM;p zqys@&SBi4a(?wnS!1#UXaRmR#3;ms^%Qg9>Kx4g5091e;cE66qmLf+B8D#5-sI-7o zGiU3Dy{kHTf{?A24O&o5oC~hmjf(7jgJ9c2HdS>*bB5_myLEozPIwKK)lGV6B0si~ zS$~pFZEZhHZ>BQK8(YikYd-J?d+_q=(&O~@)69x{EF7O44dgLFw+zPMV7fppQ;Ya= zFscG}s4tqhj0h1KZ=bhune+#q{KCx99ZW0|jLGM)<4dfBks|^FG|>+cv{yn7C4(#` zeSwJwsi8c!DLSA|&{1XLVsR!e6tIOzf start_at: + self.add_error('registration_opens', 'Registration should open before the event begins.') + if registration_closes and start_at and registration_closes > start_at: + self.add_error('registration_closes', 'Registration should close before the event begins.') + if registration_opens and registration_closes and registration_closes <= registration_opens: + self.add_error('registration_closes', 'Registration close must be after registration open.') + if capacity is not None and capacity < 1: + self.add_error('capacity', 'Capacity must be at least 1 when provided.') + + return cleaned_data diff --git a/core/migrations/0001_initial.py b/core/migrations/0001_initial.py new file mode 100644 index 0000000..4b5cff6 --- /dev/null +++ b/core/migrations/0001_initial.py @@ -0,0 +1,54 @@ +# Generated by Django 5.2.7 on 2026-04-12 07:26 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Event', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=180)), + ('slug', models.SlugField(blank=True, max_length=200, unique=True)), + ('summary', models.CharField(max_length=260)), + ('description', models.TextField()), + ('venue', models.CharField(max_length=180)), + ('start_at', models.DateTimeField()), + ('end_at', models.DateTimeField()), + ('capacity', models.PositiveIntegerField(blank=True, null=True)), + ('is_published', models.BooleanField(default=True)), + ('registration_opens', models.DateTimeField(blank=True, null=True)), + ('registration_closes', models.DateTimeField(blank=True, null=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ], + options={ + 'ordering': ['start_at'], + }, + ), + migrations.CreateModel( + name='Registration', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('full_name', models.CharField(max_length=160)), + ('email', models.EmailField(max_length=254)), + ('company', models.CharField(blank=True, max_length=160)), + ('notes', models.TextField(blank=True)), + ('status', models.CharField(choices=[('confirmed', 'Confirmed'), ('waitlist', 'Waitlist')], default='confirmed', max_length=24)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='registrations', to='core.event')), + ], + options={ + 'ordering': ['created_at'], + 'constraints': [models.UniqueConstraint(fields=('event', 'email'), name='unique_registration_per_event_email')], + }, + ), + ] diff --git a/core/migrations/0002_seed_demo_events.py b/core/migrations/0002_seed_demo_events.py new file mode 100644 index 0000000..5861a27 --- /dev/null +++ b/core/migrations/0002_seed_demo_events.py @@ -0,0 +1,104 @@ +from datetime import timedelta + +from django.db import migrations +from django.utils import timezone + + +def seed_demo_data(apps, schema_editor): + Event = apps.get_model("core", "Event") + Registration = apps.get_model("core", "Registration") + + if Event.objects.exists(): + return + + now = timezone.now() + events = [ + { + "title": "Product Launch Breakfast", + "slug": "product-launch-breakfast", + "summary": "A polished morning showcase for partners and customers.", + "description": "Join our organizer team for a concise launch briefing, networking breakfast, and a live product walkthrough.", + "venue": "Harbor Room, San Francisco", + "start_at": now + timedelta(days=10, hours=9), + "end_at": now + timedelta(days=10, hours=12), + "capacity": 40, + "registration_opens": now - timedelta(days=2), + "registration_closes": now + timedelta(days=9), + }, + { + "title": "Community Design Workshop", + "slug": "community-design-workshop", + "summary": "A collaborative session on event experience and community building.", + "description": "This hands-on workshop covers attendee journeys, facilitation prompts, and lightweight planning rituals.", + "venue": "Northstar Studio, Oakland", + "start_at": now + timedelta(days=16, hours=14), + "end_at": now + timedelta(days=16, hours=17), + "capacity": 24, + "registration_opens": now - timedelta(days=1), + "registration_closes": now + timedelta(days=15), + }, + { + "title": "Founder Evening Meetup", + "slug": "founder-evening-meetup", + "summary": "An informal evening gathering with short talks and open networking.", + "description": "Meet local founders, hear two fast talks, and stay for the community mixer.", + "venue": "Lumen Loft, San Jose", + "start_at": now + timedelta(days=25, hours=18), + "end_at": now + timedelta(days=25, hours=21), + "capacity": 60, + "registration_opens": now, + "registration_closes": now + timedelta(days=24), + }, + ] + + created = {} + for payload in events: + event = Event.objects.create(**payload) + created[event.slug] = event + + Registration.objects.create( + event=created["product-launch-breakfast"], + full_name="Maya Chen", + email="maya@example.com", + company="Juniper Labs", + notes="Interested in partnership opportunities.", + status="confirmed", + ) + Registration.objects.create( + event=created["product-launch-breakfast"], + full_name="Noah Patel", + email="noah@example.com", + company="Signal House", + notes="Needs wheelchair-accessible seating.", + status="confirmed", + ) + Registration.objects.create( + event=created["community-design-workshop"], + full_name="Sofia Reyes", + email="sofia@example.com", + company="Craft Bureau", + notes="Bringing one notebook and many questions.", + status="confirmed", + ) + + +def clear_demo_data(apps, schema_editor): + Event = apps.get_model("core", "Event") + Event.objects.filter( + slug__in=[ + "product-launch-breakfast", + "community-design-workshop", + "founder-evening-meetup", + ] + ).delete() + + +class Migration(migrations.Migration): + + dependencies = [ + ("core", "0001_initial"), + ] + + operations = [ + migrations.RunPython(seed_demo_data, clear_demo_data), + ] diff --git a/core/migrations/__pycache__/0001_initial.cpython-311.pyc b/core/migrations/__pycache__/0001_initial.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9092545a7d41c67d2886a38fc8caa4c4e71312b8 GIT binary patch literal 3199 zcmb7GO;8)j6`mRW3xP+NA4^C`tg%7hATX@FWmvlmHZ~jE*g)cxE>nZL5raoF+L;ly zHx;iBIpnyv)gcF;NK&hV4|^atjy|@k=Ai0awkjv~B$s{KLn^OlBn*NiPD}IVb@%J{ z^S#&I^Ox@KFbD0+m47II4{_W-S*P86E$01gV7})Nhj^9C^Y9i_q2$SXO5VJe=LGI9 zhdh7ekQaM)+H1Hy56ArgkK6J-#IJK1-y!Qh%m_4GQi_IbDY|A-e@RDJH480S=hfHh zy#E<=e9vLWZeBoq-c#iAUV+2D9e;ZP5|9T*0z1K@5gzf#i+nZzo)`Rp$8B2z3OJGw ztJ_ep7TV)M1|GM8OxTfekYQt>s|)e9$jO*MuDe55&l$STIWk=><=h@HKeRg>=ny+} zoaIl{k%_dIozl_!KjzOn^WE+B5D#DL+Y`VOc-#h_^gB{L?Gca}IEBpMNlX7h(}hox zigilW;-~Blosvm($`pH05=D!=!q5IWp&t@Va=0 z%xH%m9*xv4okISyBXv|Gfc#izlxVy|QwB{G1vH7S)P8j`YRKfLAUoyg6WiY7=uU5& z-w7OX=a0Ddj^t1lP1j~RZ5D$lR^%O>{p~gADw?g$os71~qtE{=bY`k3g9-ob)gmJ5pYA|n_KKI+vf zs!Ah@DU~ams$y;|sVr;lz+9+&nr>k;!@IHi0S>ET!U-QL=-QTIlpsx^r8W!W!Dlk0 z0|KVOCoRsH2JMFzfPj~Q!nUpyuxTWr-!g%nGUz@Iiup~rpc{C~nSx~@U6bGz!4_oS zz^a3V6R%+Dkcb>pHVK4YbWI@`|ulP#`jPEz+(fr-&c0yU$=3 z6pbt@#amF`%Pc_v+O}-8*mWq=7Q2C;TQ1vu8-m`jdYuGw|v?FUpoeF4^1|1Yxg$I^ytuNiWeTRy_adrb_u>}AP zLz%X&@4s>H!FleNo7ru>gtL{+ie^=^_&F{>C`R^~Zag(%Kj17&SN7wzpPinbo{?Gr zO_g65>^gS{JAKS<0O{4y@28&LS9hCUzR&;aely60Qhy5D!Qn>q!cWnDGC1+wu01%h z?|nN;@)FSuGHBKZO?%KJq6IP}K2g1F$0yztNc?&|e%+2=C*ph~I!I!f>VO@~yzw5K zf4f0qYxUTg9a|&fdLtSqi7VA*J8@;-LlO)1#DbkzAmW#e=rBo7zxLbd>3aIAoxV!M z+5ci zi*}0HX(rG0^)x@@qC?eu^?uVM^!GF`^@qamI3OPwX> ze4{>m!ydjt#G4L^lPpn_Z%h)OugB-@_&gC8j^*Ob+`$q_+^Z+<*@=5ZTse+>ABpGc z@thsc5%HRX@+3ofl6<~O9xG&Shgjb>Iqre5D!?o8NOiv5iB_WsEDY0TZQQ)qL_{dk^g z?^@$Nh|$o|ymQz%=Zm|+)|eJY)dq0C1Q*<+>xS_Jy6hX++yv6}@I2r2bNmS5((b>( djgzzfO@Eg6H@VZ~yRa>ezGD*~&X{$4_&1W=XW{?= literal 0 HcmV?d00001 diff --git a/core/migrations/__pycache__/0002_seed_demo_events.cpython-311.pyc b/core/migrations/__pycache__/0002_seed_demo_events.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cfe9de2f7ffe7835e518d5c8b1d8cdb8734007ee GIT binary patch literal 4281 zcmbUk>u(dueb#Rq=Rsa1B!qzg#U*hPxZaU)hYGplfP)-LTD6L3-O0|_yI~(@c9sy! zkqKbh`Wlm9KJ7k2+@LV`(hmD9C zor^*{au3bLXcS;Pgg!+yb`Q}w3oj(R1TTBuzct z*Q`kirgveb_rjtrJMdbcwynV$o;21`0?+TjN$tV`d9?ixDY~C_&`#P#yLV9Z&;xr( z--Xq?7Z&Z?fp>71{M|bC@4~6;I6x24L3)_t9bAvlqkBo;g*CJn7Clyu?7-}Mn+ApO za#Tmlv2yfd^s~Wb$bju}vk-~uk%hVE#LV$@;|?Yk#j5KF+k{@WWL&j%3)?Qo zHZKrMFENe>-;#0~S_}uw2{!aQ3~#B57m0C8 zXxw%STDElXGU4-}&5UiE!+4fh_!1{lf@*KdlDwG&kO}z_N|qcys)1h4W?D|1lt9*? zjKW*wC*?J9K$X6amO`H!k7GglsPKbi?V|HQ82H|{DdQ5`8gu|7_+GSCR9=+ zsxFrN4!+IyIlIU#$M0+!Qw`f;PN{FoHci(88vHSH^n!(d0tNsh7hm_O^{Ek%4pSr2 zHgc*obpdQ>7z9=gMIGz{tQ=4c!=Fj#V|KU5z^sKfW0iX5El(%1);)q z;SvMFp>NveLX+mgv%;miJ&Zpmw*ZZ9wo4DdZizPcOVBHUEUTMxH!Ag9vR%-DV;Rvh zDy}mo+~Vt=d~JM0E*8c|%wQbsVqx8a$TW!&7(&t%h|s_m&|1`mCIgrYEFgetVJn@p zE%36X&!iA+*edYYk^`&M7-$dC2}W?X$ZJc7Wi1794N|~%b#I#bUBbW0!$RVjI$ED=1VF{{*WLi300E-{M+%L30*`YOZ-coedC-YK2D zVhP5XBOos!&+1vI=|yZ8i?BQyP&#w6eiBkGuX8vEWMY0a2n|1(cMT(F5fdr}Gl_2a zaS%}?){-B!Y{48q25SjY237%m~5c~Tb*=goE#YtY^i!Rh4x0Jak-6lN;XB8}U zo44&-LBN@yF23!84dj_7`xlf+_{jnjIn#zSj~}<^7nmwwDeNxPK!?i%m>;(6MJ|sx zeoO@cV8c%6WRiY_6pN1E;;0%JB*&;OY|iC5i_6l%&jcia^&Bu(KuZ01kt`WDp*#cA zjzq-u=k?`L4NBptJMUV;9c6c!>H^ErAjhCYuu)m4N8cREM#sj+PUjpjVU9A>&eddP zi%Wj%n?Xt-gU*2*xx+bMK;2qFUb_8v)4!Si>iyLZHqxgm=~FA8c%26xo&Wv$)f11i z8=VuC&WV+4FXHKY%g^Ke&*J?X@k5pPp(;|MZC;}Fe(ayAjt9fP&OXSlOjTp3XUyx$ zdIxS)LulY?$m<^Wdi%fDAJ2SydwpHGT1K(W# z1zrMyV|9UieQ*Szji?<%dfoVRg5 zbG$XSa;ZA4ptk)hpL(5rRTPRI3&8!j*M96_^vjk?`?2-*QTVSe*1q?n-r&&vILxIs z68-Cm{uhz;RiKIpZc_1X?rTTm3gMDb|sI|-@* zXh+fgdr<;Sjq<(!opQX=dfMv%`9}ho6K|tzNPh(}iJI;c@{}IjCmurM9q$wW^^Q9X{7lazAEMIde&g%&3!`Nvf#87o#rx)f-KL3xjZNE0SSfx$UBle0OWGIm5J~% zIX(`+kL7g(%7!0~PJ_oRO z@1lLiQ*Xx~c|lP(AFuQZCc{3`!T?Vh$0{J7jjRCpUt{hIXZ zEDSwD49zJN&4s8^Q0BrR#3BpzYbjze7H_^D0ZxL3z))d7dA)vl%!C4?`DrLu<6s-u zWr#5i1vHThHk4e>Pvv9@b`1vYbS`%rP5^SNkv+;_tEF%zfvt&4<^1G~Yu#89nr#Jj zjZ6FZDcF&>?y7tg27-kJ2IW(-E5R?|b)@N68b7FB$s^Tps4KQ|t(ro~L-$ja#K2cS zf1KM$d{jw%1XCTcm898F`I?UDt-dRCqqdK{(Y2;f zvy!h9AJz self.registration_closes: + return False + return True + + +class Registration(models.Model): + class Status(models.TextChoices): + CONFIRMED = 'confirmed', 'Confirmed' + WAITLIST = 'waitlist', 'Waitlist' + + event = models.ForeignKey(Event, on_delete=models.CASCADE, related_name='registrations') + full_name = models.CharField(max_length=160) + email = models.EmailField() + company = models.CharField(max_length=160, blank=True) + notes = models.TextField(blank=True) + status = models.CharField(max_length=24, choices=Status.choices, default=Status.CONFIRMED) + created_at = models.DateTimeField(auto_now_add=True) + + class Meta: + ordering = ['created_at'] + constraints = [ + models.UniqueConstraint(fields=['event', 'email'], name='unique_registration_per_event_email'), + ] + + def __str__(self): + return f'{self.full_name} · {self.event.title}' diff --git a/core/templates/base.html b/core/templates/base.html index 1e7e5fb..e345ca1 100644 --- a/core/templates/base.html +++ b/core/templates/base.html @@ -1,25 +1,90 @@ +{% load static %} - - {% block title %}Knowledge Base{% endblock %} - {% if project_description %} - - - - {% endif %} + + {% block title %}{{ page_title|default:project_name }}{% endblock %} + + {% if project_image_url %} {% endif %} - {% load static %} + + + + {% block head %}{% endblock %} + + + + {% if messages %} +
+ {% for message in messages %} + + {% endfor %} +
+ {% endif %} - {% block content %}{% endblock %} - +
+
+
+ + +
+ +
+
+ + + diff --git a/core/templates/core/event_detail.html b/core/templates/core/event_detail.html new file mode 100644 index 0000000..afb01b5 --- /dev/null +++ b/core/templates/core/event_detail.html @@ -0,0 +1,94 @@ +{% extends "base.html" %} + +{% block title %}{{ page_title }}{% endblock %} +{% block meta_description %}{{ meta_description }}{% endblock %} + +{% block content %} +
+
+ ← Back to events +
+
+
+ {{ event.start_at|date:"l, F j" }} +

{{ event.title }}

+

{{ event.summary }}

+
+
Venue{{ event.venue }}
+
Time{{ event.start_at|date:"g:i A" }} – {{ event.end_at|date:"g:i A" }}
+
Status{% if event.registration_is_open %}Registration open{% else %}Registration closed{% endif %}
+
Capacity{% if event.capacity %}{{ event.confirmed_registrations }}/{{ event.capacity }} booked{% else %}Flexible{% endif %}
+
+
+

About this event

+

{{ event.description|linebreaksbr }}

+
+
+
+ +
+
+
+ + {% if related_events %} +
+
+
+

More events

+

Keep exploring the calendar.

+
+
+
+ {% for related in related_events %} +
+
+
+ {{ related.start_at|date:"M d" }} + Upcoming +
+

{{ related.title }}

+

{{ related.summary }}

+ View event +
+
+ {% endfor %} +
+
+ {% endif %} +
+
+{% endblock %} diff --git a/core/templates/core/index.html b/core/templates/core/index.html index faec813..c2411c9 100644 --- a/core/templates/core/index.html +++ b/core/templates/core/index.html @@ -1,145 +1,172 @@ {% extends "base.html" %} -{% block title %}{{ project_name }}{% endblock %} - -{% block head %} - - - - -{% endblock %} +{% block title %}{{ page_title }}{% endblock %} +{% block meta_description %}{{ meta_description }}{% endblock %} {% block content %}
-
-

Analyzing your requirements and generating your app…

-
- Loading… +
+
+
+
+ Single-team event operations +

Publish beautiful event pages and collect registrations in minutes.

+

Northstar gives one organizer team a modern public event catalog, quick attendee sign-up flow, and a dashboard to monitor registrations without wrestling with spreadsheets.

+ +
+
+
+
{{ stats.upcoming_events }}
+
Upcoming events
+
+
+
+
+
{{ stats.confirmed_attendees }}
+
Confirmed attendees
+
+
+
+
+
{{ stats.waitlist_total }}
+
Waitlist entries
+
+
+
+
+
+
+
+
+
+
+
+
Featured next up
+ {% if featured_event %} +

{{ featured_event.title }}

+ {% else %} +

Ready for your first event

+ {% endif %} +
+ Live +
+ {% if featured_event %} +
    +
  • Date{{ featured_event.start_at|date:"M d, Y" }} · {{ featured_event.start_at|date:"g:i A" }}
  • +
  • Venue{{ featured_event.venue }}
  • +
  • Capacity{% if featured_event.capacity %}{{ featured_event.confirmed_registrations }}/{{ featured_event.capacity }} booked{% else %}Open attendance{% endif %}
  • +
+ Register for featured event + {% else %} +

Create events in Django Admin and the landing page will automatically showcase them here.

+ Go to admin + {% endif %} +
+
+
+
-

AppWizzy AI is collecting your requirements and applying the first changes.

-

This page will refresh automatically as the plan is implemented.

-

- Runtime: Django {{ django_version }} · Python {{ python_version }} - — UTC {{ current_time|date:"Y-m-d H:i:s" }} -

-
+ + + + +
+
+
+
+

Upcoming events

+

Public listing with instant registration.

+
+ See organizer dashboard → +
+ {% if events %} +
+ {% for event in events %} +
+
+
+ {{ event.start_at|date:"M d" }} + {% if event.capacity and event.confirmed_registrations >= event.capacity %} + Waitlist only + {% else %} + Open + {% endif %} +
+

{{ event.title }}

+

{{ event.summary }}

+
+
Venue
{{ event.venue }}
+
Starts
{{ event.start_at|date:"M d, Y · g:i A" }}
+
Availability
{% if event.capacity %}{{ event.confirmed_registrations }}/{{ event.capacity }} reserved{% else %}Open attendance{% endif %}
+
+
+ {{ event.waitlist_registrations }} on waitlist + View details +
+
+
+ {% endfor %} +
+ {% else %} +
+

No matching events yet

+

{% if query %}Try a different search term or browse all events from the dashboard.{% else %}Create your first event in admin to populate the public catalog.{% endif %}

+ +
+ {% endif %} +
+
+ +
+
+
+
+
+
01
+

Publish events

+

Create or edit event details in admin, then let the polished landing page surface them instantly.

+
+
+
+
+
02
+

Collect registrations

+

Attendees browse events, submit a secure form, and receive confirmation or waitlist handling with capacity checks.

+
+
+
+
+
03
+

Manage attendees

+

Monitor counts in the dashboard, review recent signups, and export attendee CSVs for operations.

+
+
+
+
+
-
- Page updated: {{ current_time|date:"Y-m-d H:i:s" }} (UTC) -
-{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/core/templates/core/login.html b/core/templates/core/login.html new file mode 100644 index 0000000..3c8b700 --- /dev/null +++ b/core/templates/core/login.html @@ -0,0 +1,35 @@ +{% extends "base.html" %} + +{% block title %}Organizer login | Northstar Events{% endblock %} +{% block meta_description %}Sign in to the Northstar Events organizer dashboard and admin workspace.{% endblock %} + +{% block content %} +
+
+
+ Organizer access +

Sign in to manage events.

+

Use your organizer account to open the protected dashboard, export attendee lists, and jump into Django Admin for event editing.

+
+ {% csrf_token %} + {% if form.non_field_errors %} +
{{ form.non_field_errors }}
+ {% endif %} +
+ + +
+
+ + +
+ {% if next %}{% endif %} +
+ + Open Django Admin +
+
+
+
+
+{% endblock %} diff --git a/core/templates/core/organizer_dashboard.html b/core/templates/core/organizer_dashboard.html new file mode 100644 index 0000000..02291b3 --- /dev/null +++ b/core/templates/core/organizer_dashboard.html @@ -0,0 +1,115 @@ +{% extends "base.html" %} + +{% block title %}{{ page_title }}{% endblock %} +{% block meta_description %}{{ meta_description }}{% endblock %} + +{% block content %} +
+
+
+
+
+ Organizer dashboard +

Keep every event, signup, and export in one place.

+

Create new events inside the dashboard, review registration volume, and keep a quick path into attendee records without leaving this workspace.

+
+ +
+
+
{{ dashboard_stats.upcoming_events }}
Upcoming events
+
{{ dashboard_stats.confirmed_attendees }}
Confirmed attendees
+
{{ dashboard_stats.waitlist_total }}
Waitlist total
+
+
+ +
+
+
+

Event roster

+

See registration volume at a glance.

+
+
+ {% if events %} +
+ {% for event in events %} +
+
+
+
+

{{ event.title }}

+

{{ event.start_at|date:"M d, Y · g:i A" }} · {{ event.venue }}

+
+ +
+
+
Confirmed{{ event.confirmed_registrations }}
+
Waitlist{{ event.waitlist_registrations }}
+
Total{{ event.total_registrations }}
+
+
{% if event.capacity %}Capacity {{ event.capacity }} seats.{% else %}Flexible attendance limit.{% endif %}
+
+
+ {% endfor %} +
+ {% else %} +
+

No events created yet

+

Start by creating your first event from the dashboard, then share the public event page for registrations.

+ Create first event +
+ {% endif %} +
+ +
+
+
+

Recent attendees

+

Latest registrations across events.

+
+
+ {% if recent_registrations %} +
+
+ + + + + + + + + + + + {% for registration in recent_registrations %} + + + + + + + + {% endfor %} + +
NameEventStatusEmailRegistered
{{ registration.full_name }}{{ registration.event.title }}{{ registration.get_status_display }}{{ registration.email }}{{ registration.created_at|date:"M d, g:i A" }}
+
+
+ {% else %} +
+

No registrations yet

+

Once attendees submit the public form, their records will appear here instantly.

+
+ {% endif %} +
+
+
+{% endblock %} diff --git a/core/templates/core/organizer_event_form.html b/core/templates/core/organizer_event_form.html new file mode 100644 index 0000000..a7bfa0e --- /dev/null +++ b/core/templates/core/organizer_event_form.html @@ -0,0 +1,113 @@ +{% extends "base.html" %} + +{% block title %}{{ page_title }}{% endblock %} +{% block meta_description %}{{ meta_description }}{% endblock %} + +{% block content %} +
+
+
+
+
+ Organizer tools +

{{ form_title }}

+

{{ form_intro }}

+
+ +
+
+ +
+
+
+
+

Event setup

+

{{ form_section_title }}

+
+
+ +
+ {% csrf_token %} + {% if form.non_field_errors %} +
{{ form.non_field_errors }}
+ {% endif %} + +
+
+
+ + {{ form.title }} + {% if form.title.errors %}
{{ form.title.errors|join:', ' }}
{% endif %} +
+
+ + {{ form.summary }} + {% if form.summary.errors %}
{{ form.summary.errors|join:', ' }}
{% endif %} +
+
+ + {{ form.description }} + {% if form.description.errors %}
{{ form.description.errors|join:', ' }}
{% endif %} +
+
+ + {{ form.venue }} + {% if form.venue.errors %}
{{ form.venue.errors|join:', ' }}
{% endif %} +
+
+ +
+
+
+

Schedule & access

+
+ + {{ form.start_at }} + {% if form.start_at.errors %}
{{ form.start_at.errors|join:', ' }}
{% endif %} +
+
+ + {{ form.end_at }} + {% if form.end_at.errors %}
{{ form.end_at.errors|join:', ' }}
{% endif %} +
+
+ + {{ form.capacity }} +
Leave blank for open attendance.
+ {% if form.capacity.errors %}
{{ form.capacity.errors|join:', ' }}
{% endif %} +
+
+ + {{ form.registration_opens }} + {% if form.registration_opens.errors %}
{{ form.registration_opens.errors|join:', ' }}
{% endif %} +
+
+ + {{ form.registration_closes }} + {% if form.registration_closes.errors %}
{{ form.registration_closes.errors|join:', ' }}
{% endif %} +
+
+ {{ form.is_published }} + + {% if form.is_published.errors %}
{{ form.is_published.errors|join:', ' }}
{% endif %} +
+
+
+
+
+ +
+ + Cancel +
+
+
+
+
+
+{% endblock %} diff --git a/core/templates/core/registration_success.html b/core/templates/core/registration_success.html new file mode 100644 index 0000000..185b743 --- /dev/null +++ b/core/templates/core/registration_success.html @@ -0,0 +1,26 @@ +{% extends "base.html" %} + +{% block title %}{{ page_title }}{% endblock %} +{% block meta_description %}{{ meta_description }}{% endblock %} + +{% block content %} +
+
+
+ Registration received +

{% if registration.status == 'waitlist' %}You are on the waitlist.{% else %}Your spot is secured.{% endif %}

+

{{ registration.full_name }}, your registration for {{ event.title }} has been saved. {% if registration.status == 'waitlist' %}We will contact you if a seat opens up.{% else %}Check your inbox for the confirmation email details.{% endif %}

+
+
Event{{ event.title }}
+
When{{ event.start_at|date:"M d, Y · g:i A" }}
+
Where{{ event.venue }}
+
Status{{ registration.get_status_display }}
+
+ +
+
+
+{% endblock %} diff --git a/core/tests.py b/core/tests.py index 7ce503c..4aab592 100644 --- a/core/tests.py +++ b/core/tests.py @@ -1,3 +1,117 @@ -from django.test import TestCase +from datetime import timedelta -# Create your tests here. +from django.contrib.auth import get_user_model +from django.test import TestCase +from django.urls import reverse +from django.utils import timezone + +from .models import Event, Registration + + +class EventFlowTests(TestCase): + def setUp(self): + now = timezone.now() + timedelta(days=7) + self.user = get_user_model().objects.create_user(username='organizer', password='testpass123') + + self.event = Event.objects.create( + title='Design Systems Lab', + summary='Hands-on session for collaborative design systems.', + description='A practical session for teams building consistent digital products.', + venue='Northstar Studio', + start_at=now, + end_at=now + timedelta(hours=2), + capacity=2, + ) + + def test_home_page_loads_event(self): + response = self.client.get(reverse('home')) + self.assertEqual(response.status_code, 200) + self.assertContains(response, self.event.title) + + def test_registration_creates_attendee_and_redirects(self): + response = self.client.post( + reverse('event_detail', args=[self.event.slug]), + { + 'full_name': 'Jordan Lee', + 'email': 'jordan@example.com', + 'company': 'Studio North', + 'notes': 'Vegetarian meal', + }, + follow=True, + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(Registration.objects.count(), 1) + self.assertContains(response, 'Your spot is secured') + + def test_capacity_overflow_goes_to_waitlist(self): + Registration.objects.create(event=self.event, full_name='One', email='one@example.com') + Registration.objects.create(event=self.event, full_name='Two', email='two@example.com') + self.client.post( + reverse('event_detail', args=[self.event.slug]), + { + 'full_name': 'Three', + 'email': 'three@example.com', + 'company': '', + 'notes': '', + }, + ) + self.assertEqual(Registration.objects.get(email='three@example.com').status, Registration.Status.WAITLIST) + def test_dashboard_requires_login(self): + response = self.client.get(reverse('organizer_dashboard')) + self.assertEqual(response.status_code, 302) + self.assertIn(reverse('login'), response.url) + + def test_logged_in_organizer_can_open_dashboard(self): + self.client.force_login(self.user) + response = self.client.get(reverse('organizer_dashboard')) + self.assertEqual(response.status_code, 200) + self.assertContains(response, 'Organizer dashboard') + + + + def test_logged_in_organizer_can_create_event_from_dashboard_form(self): + self.client.force_login(self.user) + response = self.client.post( + reverse('organizer_event_create'), + { + 'title': 'Community Breakfast', + 'summary': 'Meet peers before the keynote.', + 'description': 'A low-key networking breakfast for early arrivals.', + 'venue': 'Northstar Cafe', + 'start_at': (timezone.localtime(self.event.start_at) + timedelta(days=3)).strftime('%Y-%m-%dT%H:%M'), + 'end_at': (timezone.localtime(self.event.end_at) + timedelta(days=3)).strftime('%Y-%m-%dT%H:%M'), + 'capacity': 24, + 'is_published': 'on', + 'registration_opens': '', + 'registration_closes': '', + }, + follow=True, + ) + self.assertEqual(response.status_code, 200) + self.assertTrue(Event.objects.filter(title='Community Breakfast', venue='Northstar Cafe').exists()) + self.assertContains(response, 'created successfully') + + def test_logged_in_organizer_can_edit_event_from_dashboard_form(self): + self.client.force_login(self.user) + response = self.client.post( + reverse('organizer_event_edit', args=[self.event.slug]), + { + 'title': 'Design Systems Lab Updated', + 'summary': 'Hands-on session for collaborative design systems.', + 'description': 'A practical session for teams building consistent digital products.', + 'venue': 'Northstar Auditorium', + 'start_at': timezone.localtime(self.event.start_at).strftime('%Y-%m-%dT%H:%M'), + 'end_at': timezone.localtime(self.event.end_at).strftime('%Y-%m-%dT%H:%M'), + 'capacity': 40, + 'is_published': 'on', + 'registration_opens': '', + 'registration_closes': '', + }, + follow=True, + ) + self.assertEqual(response.status_code, 200) + self.event.refresh_from_db() + self.assertEqual(self.event.title, 'Design Systems Lab Updated') + self.assertEqual(self.event.venue, 'Northstar Auditorium') + self.assertEqual(self.event.capacity, 40) + self.assertContains(response, 'updated successfully') diff --git a/core/urls.py b/core/urls.py index 6299e3d..d5d2696 100644 --- a/core/urls.py +++ b/core/urls.py @@ -1,7 +1,16 @@ +from django.contrib.auth import views as auth_views from django.urls import path -from .views import home +from .views import event_detail, export_attendees_csv, home, organizer_dashboard, organizer_event_create, organizer_event_edit, registration_success urlpatterns = [ - path("", home, name="home"), + path('login/', auth_views.LoginView.as_view(template_name='core/login.html', redirect_authenticated_user=True), name='login'), + path('logout/', auth_views.LogoutView.as_view(next_page='home'), name='logout'), + path('', home, name='home'), + path('events//', event_detail, name='event_detail'), + path('events//registered//', registration_success, name='registration_success'), + path('dashboard/', organizer_dashboard, name='organizer_dashboard'), + path('dashboard/events/new/', organizer_event_create, name='organizer_event_create'), + path('dashboard/events//edit/', organizer_event_edit, name='organizer_event_edit'), + path('dashboard/events//attendees.csv', export_attendees_csv, name='export_attendees_csv'), ] diff --git a/core/views.py b/core/views.py index c9aed12..5a1e538 100644 --- a/core/views.py +++ b/core/views.py @@ -1,25 +1,251 @@ +import csv +import logging import os import platform -from django import get_version as django_version -from django.shortcuts import render +from django.contrib import messages +from django.contrib.auth.decorators import login_required +from django.core.mail import send_mail +from django.db import transaction +from django.db.models import Count, Q +from django.http import Http404, HttpResponse +from django.shortcuts import get_object_or_404, redirect, render from django.utils import timezone +from .forms import EventSearchForm, OrganizerEventForm, RegistrationForm +from .models import Event, Registration + +logger = logging.getLogger(__name__) + + +def _base_context(request): + host_name = request.get_host().lower() + agent_brand = 'AppWizzy' if host_name == 'appwizzy.com' else 'Flatlogic' + now = timezone.now() + return { + 'project_name': 'Northstar Events', + 'agent_brand': agent_brand, + 'django_version': __import__('django').get_version(), + 'python_version': platform.python_version(), + 'current_time': now, + 'host_name': host_name, + 'project_description': os.getenv('PROJECT_DESCRIPTION', 'Modern event publishing and registration for a single organizer team.'), + 'project_image_url': os.getenv('PROJECT_IMAGE_URL', ''), + } + def home(request): - """Render the landing screen with loader and environment details.""" - host_name = request.get_host().lower() - agent_brand = "AppWizzy" if host_name == "appwizzy.com" else "Flatlogic" now = timezone.now() + search_form = EventSearchForm(request.GET or None) + events = Event.objects.filter(is_published=True, end_at__gte=now).annotate( + confirmed_registrations=Count('registrations', filter=Q(registrations__status=Registration.Status.CONFIRMED)), + waitlist_registrations=Count('registrations', filter=Q(registrations__status=Registration.Status.WAITLIST)), + ) + query = '' + if search_form.is_valid(): + query = search_form.cleaned_data.get('q', '').strip() + if query: + events = events.filter( + Q(title__icontains=query) + | Q(summary__icontains=query) + | Q(description__icontains=query) + | Q(venue__icontains=query) + ) + + event_list = list(events) + featured_event = event_list[0] if event_list else None + stats = { + 'upcoming_events': Event.objects.filter(is_published=True, end_at__gte=now).count(), + 'confirmed_attendees': Registration.objects.filter(status=Registration.Status.CONFIRMED).count(), + 'waitlist_total': Registration.objects.filter(status=Registration.Status.WAITLIST).count(), + } + context = { + **_base_context(request), + 'page_title': 'Northstar Events | Browse upcoming events and register online', + 'meta_description': 'Discover upcoming workshops, talks, and community events, then register in minutes with instant confirmation.', + 'search_form': search_form, + 'events': event_list, + 'featured_event': featured_event, + 'stats': stats, + 'query': query, + } + return render(request, 'core/index.html', context) + + +def event_detail(request, slug): + event = get_object_or_404( + Event.objects.annotate( + confirmed_registrations=Count('registrations', filter=Q(registrations__status=Registration.Status.CONFIRMED)), + waitlist_registrations=Count('registrations', filter=Q(registrations__status=Registration.Status.WAITLIST)), + ), + slug=slug, + is_published=True, + ) + form = RegistrationForm(request.POST or None, event=event) + if request.method == 'POST': + if not event.registration_is_open: + form.add_error(None, 'Registration is not open for this event right now.') + elif form.is_valid(): + with transaction.atomic(): + locked_event = Event.objects.select_for_update().get(pk=event.pk) + if Registration.objects.filter(event=locked_event, email__iexact=form.cleaned_data['email']).exists(): + form.add_error('email', 'This email is already registered for this event.') + else: + registration = form.save(commit=False) + registration.event = locked_event + if locked_event.capacity and locked_event.confirmed_registrations_count >= locked_event.capacity: + registration.status = Registration.Status.WAITLIST + else: + registration.status = Registration.Status.CONFIRMED + registration.save() + email_sent = _send_registration_email(registration) + if email_sent: + messages.success(request, 'Registration saved and confirmation email sent.') + else: + messages.warning(request, 'Registration saved. Email delivery is not configured yet, so no confirmation email was sent.') + return redirect('registration_success', slug=locked_event.slug, registration_id=registration.pk) context = { - "project_name": "New Style", - "agent_brand": agent_brand, - "django_version": django_version(), - "python_version": platform.python_version(), - "current_time": now, - "host_name": host_name, - "project_description": os.getenv("PROJECT_DESCRIPTION", ""), - "project_image_url": os.getenv("PROJECT_IMAGE_URL", ""), + **_base_context(request), + 'page_title': f'{event.title} | Register with Northstar Events', + 'meta_description': event.summary, + 'event': event, + 'form': form, + 'related_events': Event.objects.filter(is_published=True, end_at__gte=timezone.now()).exclude(pk=event.pk)[:3], } - return render(request, "core/index.html", context) + return render(request, 'core/event_detail.html', context) + + +def registration_success(request, slug, registration_id): + registration = get_object_or_404( + Registration.objects.select_related('event'), + pk=registration_id, + event__slug=slug, + ) + context = { + **_base_context(request), + 'page_title': f'Registration confirmed | {registration.event.title}', + 'meta_description': f'Confirmation details for {registration.event.title}.', + 'registration': registration, + 'event': registration.event, + } + return render(request, 'core/registration_success.html', context) + + +@login_required +def organizer_dashboard(request): + now = timezone.now() + events = Event.objects.annotate( + confirmed_registrations=Count('registrations', filter=Q(registrations__status=Registration.Status.CONFIRMED)), + waitlist_registrations=Count('registrations', filter=Q(registrations__status=Registration.Status.WAITLIST)), + total_registrations=Count('registrations'), + ).order_by('start_at') + recent_registrations = Registration.objects.select_related('event').order_by('-created_at')[:8] + context = { + **_base_context(request), + 'page_title': 'Organizer dashboard | Northstar Events', + 'meta_description': 'Review upcoming events, attendee counts, and recent registrations for your organizer team.', + 'events': events, + 'recent_registrations': recent_registrations, + 'dashboard_stats': { + 'upcoming_events': events.filter(end_at__gte=now, is_published=True).count(), + 'confirmed_attendees': Registration.objects.filter(status=Registration.Status.CONFIRMED).count(), + 'waitlist_total': Registration.objects.filter(status=Registration.Status.WAITLIST).count(), + }, + } + return render(request, 'core/organizer_dashboard.html', context) + + +@login_required +def organizer_event_create(request): + form = OrganizerEventForm(request.POST or None) + if request.method == 'POST' and form.is_valid(): + event = form.save() + messages.success(request, f'Event "{event.title}" created successfully.') + return redirect('organizer_dashboard') + + context = { + **_base_context(request), + 'page_title': 'Create event | Northstar Events', + 'meta_description': 'Create a new public event from the organizer dashboard.', + 'form': form, + 'form_mode': 'create', + 'form_title': 'Create a new event without opening Django Admin.', + 'form_intro': 'This custom organizer form covers the core publishing fields: schedule, venue, capacity, registration window, and public visibility.', + 'form_section_title': 'Fill in the event details.', + 'submit_label': 'Create event', + } + return render(request, 'core/organizer_event_form.html', context) + + +@login_required +def organizer_event_edit(request, slug): + event = get_object_or_404(Event, slug=slug) + form = OrganizerEventForm(request.POST or None, instance=event) + if request.method == 'POST' and form.is_valid(): + updated_event = form.save() + messages.success(request, f'Event "{updated_event.title}" updated successfully.') + return redirect('organizer_dashboard') + + context = { + **_base_context(request), + 'page_title': f'Edit {event.title} | Northstar Events', + 'meta_description': f'Update the event details for {event.title} from the organizer dashboard.', + 'form': form, + 'event': event, + 'form_mode': 'edit', + 'form_title': f'Edit “{event.title}” from the organizer dashboard.', + 'form_intro': 'Update the same publishing fields used during event creation, while keeping the public event URL stable.', + 'form_section_title': 'Update the event details.', + 'submit_label': 'Save changes', + } + return render(request, 'core/organizer_event_form.html', context) + + +@login_required +def export_attendees_csv(request, slug): + event = get_object_or_404(Event, slug=slug) + registrations = event.registrations.order_by('created_at') + response = HttpResponse(content_type='text/csv') + response['Content-Disposition'] = f'attachment; filename="{event.slug}-attendees.csv"' + writer = csv.writer(response) + writer.writerow(['Full name', 'Email', 'Company', 'Status', 'Registered at']) + for registration in registrations: + writer.writerow([ + registration.full_name, + registration.email, + registration.company, + registration.get_status_display(), + timezone.localtime(registration.created_at).strftime('%Y-%m-%d %H:%M'), + ]) + return response + + + +def _send_registration_email(registration): + subject = f'Your registration for {registration.event.title}' + if registration.status == Registration.Status.WAITLIST: + intro = 'You have been added to the waitlist.' + else: + intro = 'Your spot is confirmed.' + message = ( + f'Hi {registration.full_name},\n\n' + f'{intro}\n\n' + f'Event: {registration.event.title}\n' + f'When: {timezone.localtime(registration.event.start_at).strftime("%B %d, %Y at %I:%M %p")}\n' + f'Where: {registration.event.venue}\n\n' + 'We look forward to seeing you.\n' + 'Northstar Events' + ) + try: + send_mail( + subject, + message, + os.getenv('DEFAULT_FROM_EMAIL', 'no-reply@example.com'), + [registration.email], + fail_silently=False, + ) + return True + except Exception as exc: + logger.exception('Registration email failed for %s: %s', registration.email, exc) + return False diff --git a/static/css/custom.css b/static/css/custom.css index 925f6ed..f6cf2e3 100644 --- a/static/css/custom.css +++ b/static/css/custom.css @@ -1,4 +1,501 @@ -/* Custom styles for the application */ -body { - font-family: system-ui, -apple-system, sans-serif; +:root { + --bg: #f6f8fb; + --surface: rgba(255, 255, 255, 0.88); + --surface-strong: #ffffff; + --surface-muted: #eef3f8; + --border: rgba(27, 47, 74, 0.1); + --text: #132238; + --muted: #617086; + --primary: #0f8f7a; + --primary-deep: #0a6f61; + --secondary: #ff8457; + --accent: #ffd166; + --shadow: 0 30px 60px rgba(19, 34, 56, 0.12); + --radius-xl: 28px; + --radius-lg: 20px; + --radius-md: 14px; +} + +* { + box-sizing: border-box; +} + +html { + scroll-behavior: smooth; +} + +body.app-shell { + margin: 0; + font-family: 'Inter', sans-serif; + color: var(--text); + background: + radial-gradient(circle at top left, rgba(15, 143, 122, 0.14), transparent 28%), + radial-gradient(circle at 80% 10%, rgba(255, 132, 87, 0.16), transparent 22%), + linear-gradient(180deg, #fbfdff 0%, #f4f7fb 100%); + min-height: 100vh; +} + +h1, +h2, +h3, +h4, +.brand-wordmark, +.section-title, +.preview-title, +.footer-brand { + font-family: 'Fraunces', serif; + letter-spacing: -0.03em; +} + +p, +a, +span, +label, +button, +input, +textarea, +table, +div { + font-family: 'Inter', sans-serif; +} + +img { + max-width: 100%; +} + +.site-header { + background: rgba(246, 248, 251, 0.82); + backdrop-filter: blur(14px); + border-bottom: 1px solid rgba(255, 255, 255, 0.55); +} + +.navbar { + --bs-navbar-color: var(--muted); + --bs-navbar-hover-color: var(--text); + --bs-navbar-active-color: var(--text); + --bs-navbar-brand-color: var(--text); +} + +.brand-wordmark { + font-size: 1.25rem; + font-weight: 700; + display: inline-flex; + align-items: center; + gap: 0.6rem; +} + +.brand-mark { + width: 2rem; + height: 2rem; + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 999px; + background: linear-gradient(135deg, rgba(15, 143, 122, 0.14), rgba(255, 209, 102, 0.45)); + color: var(--primary); +} + +.nav-link { + font-weight: 600; +} + +.section-space { + padding: 4.5rem 0; +} + +.hero-section { + padding: 5rem 0 3rem; + overflow: hidden; +} + +.hero-title, +.detail-title { + font-size: clamp(2.8rem, 5vw, 4.8rem); + line-height: 0.98; + max-width: 12ch; +} + +.detail-title { + font-size: clamp(2.4rem, 4vw, 4rem); +} + +.hero-copy, +.detail-copy { + max-width: 62ch; + color: var(--muted); + font-size: 1.08rem; + line-height: 1.8; +} + +.eyebrow-pill, +.status-badge, +.date-chip { + display: inline-flex; + align-items: center; + gap: 0.35rem; + border-radius: 999px; + padding: 0.55rem 0.9rem; + font-size: 0.84rem; + font-weight: 700; + letter-spacing: 0.02em; +} + +.eyebrow-pill, +.status-badge { + background: rgba(15, 143, 122, 0.12); + color: var(--primary-deep); +} + +.badge-warm, +.date-chip { + background: rgba(255, 132, 87, 0.14); + color: #b14c26; +} + +.section-kicker, +.mini-label { + font-size: 0.82rem; + letter-spacing: 0.14em; + text-transform: uppercase; + color: var(--primary); + font-weight: 800; +} + +.section-title { + font-size: clamp(2rem, 3vw, 3rem); +} + +.card-surface { + background: var(--surface); + border: 1px solid rgba(255, 255, 255, 0.6); + border-radius: var(--radius-xl); + box-shadow: var(--shadow); + padding: 2rem; + position: relative; +} + +.card-surface-inner { + background: var(--surface-strong); + border-radius: calc(var(--radius-xl) - 8px); + box-shadow: 0 16px 32px rgba(19, 34, 56, 0.08); +} + +.hero-spotlight { + min-height: 100%; + padding: 1.25rem; + background: linear-gradient(180deg, rgba(255, 255, 255, 0.92), rgba(244, 247, 251, 0.92)); +} + +.event-preview-card { + position: relative; + z-index: 2; + padding: 1.25rem; + border-radius: 24px; + background: rgba(255, 255, 255, 0.88); + border: 1px solid rgba(255, 255, 255, 0.8); +} + +.preview-title { + font-size: 2rem; + margin: 0.2rem 0 0; +} + +.preview-list li { + display: flex; + justify-content: space-between; + gap: 1.2rem; + padding: 0.85rem 0; + border-bottom: 1px solid var(--border); +} + +.preview-list li:last-child { + border-bottom: 0; +} + +.preview-list strong, +.preview-list span { + display: block; +} + +.preview-list span { + text-align: right; + color: var(--muted); +} + +.orb { + position: absolute; + border-radius: 999px; + filter: blur(4px); +} + +.orb-one { + width: 190px; + height: 190px; + right: -1.5rem; + top: -1.5rem; + background: radial-gradient(circle, rgba(255, 209, 102, 0.52), rgba(255, 209, 102, 0)); +} + +.orb-two { + width: 230px; + height: 230px; + left: -2rem; + bottom: -3rem; + background: radial-gradient(circle, rgba(15, 143, 122, 0.26), rgba(15, 143, 122, 0)); +} + +.stat-card, +.metric-box { + padding: 1.2rem; + border-radius: var(--radius-lg); + background: rgba(255, 255, 255, 0.8); + border: 1px solid rgba(255, 255, 255, 0.75); +} + +.stat-card-light { + background: rgba(255, 255, 255, 0.92); +} + +.stat-value { + font-size: clamp(1.8rem, 3vw, 2.4rem); + font-weight: 800; + color: var(--text); +} + +.stat-label, +.panel-note, +.event-card-summary, +.footer-copy, +.metric-box span, +.text-muted, +.meta-list dd, +.detail-meta-item span { + color: var(--muted) !important; +} + +.search-panel { + padding: 1.5rem; +} + +.event-card, +.dashboard-card, +.feature-card, +.success-shell, +.detail-hero, +.dashboard-hero, +.empty-state { + height: 100%; +} + +.event-card, +.dashboard-card, +.feature-card { + display: flex; + flex-direction: column; + gap: 0.8rem; +} + +.event-card-top { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; +} + +.event-card-title { + font-size: 1.6rem; + margin: 0; +} + +.meta-list { + display: grid; + gap: 0.75rem; +} + +.meta-list div { + display: flex; + justify-content: space-between; + gap: 1rem; + padding-bottom: 0.7rem; + border-bottom: 1px solid var(--border); +} + +.meta-list dt { + font-weight: 700; + color: var(--text); +} + +.meta-list dd { + margin: 0; + text-align: right; +} + +.feature-number { + width: 3rem; + height: 3rem; + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 18px; + background: linear-gradient(135deg, rgba(15, 143, 122, 0.14), rgba(255, 132, 87, 0.18)); + color: var(--primary); + font-weight: 800; +} + +.section-muted { + background: linear-gradient(180deg, rgba(238, 243, 248, 0.55), rgba(246, 248, 251, 0)); +} + +.text-link { + color: var(--primary-deep); + font-weight: 700; + text-decoration: none; +} + +.detail-meta-grid, +.success-meta-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + gap: 1rem; +} + +.detail-meta-item { + padding: 1rem 1.1rem; + border-radius: 18px; + background: rgba(255, 255, 255, 0.72); + border: 1px solid rgba(255, 255, 255, 0.72); + display: flex; + flex-direction: column; + gap: 0.4rem; +} + +.detail-meta-item strong { + font-size: 1rem; +} + +.registration-panel { + position: sticky; + top: 6.5rem; +} + +.form-label { + font-weight: 700; + color: var(--text); +} + +.form-control { + border-radius: 14px; + border: 1px solid rgba(19, 34, 56, 0.12); + padding: 0.95rem 1rem; + background: rgba(255, 255, 255, 0.95); +} + +.form-control:focus { + border-color: rgba(15, 143, 122, 0.5); + box-shadow: 0 0 0 0.25rem rgba(15, 143, 122, 0.12); +} + +.btn { + border-radius: 999px; + padding: 0.86rem 1.3rem; + font-weight: 700; + letter-spacing: 0.01em; +} + +.btn-primary { + --bs-btn-bg: var(--primary); + --bs-btn-border-color: var(--primary); + --bs-btn-hover-bg: var(--primary-deep); + --bs-btn-hover-border-color: var(--primary-deep); + --bs-btn-active-bg: var(--primary-deep); + --bs-btn-active-border-color: var(--primary-deep); + --bs-btn-disabled-bg: rgba(15, 143, 122, 0.4); + --bs-btn-disabled-border-color: rgba(15, 143, 122, 0.4); + box-shadow: 0 14px 30px rgba(15, 143, 122, 0.18); +} + +.btn-ghost { + background: rgba(255, 255, 255, 0.72); + border: 1px solid rgba(19, 34, 56, 0.08); + color: var(--text); +} + +.btn-ghost:hover, +.text-link:hover, +.footer-links a:hover { + color: var(--primary-deep); +} + +.dashboard-metrics .metric-box strong { + display: block; + margin-top: 0.4rem; + font-size: 1.8rem; +} + +.dashboard-table { + --bs-table-bg: transparent; + --bs-table-color: var(--text); + --bs-table-border-color: rgba(19, 34, 56, 0.08); +} + +.dashboard-table thead th { + font-size: 0.82rem; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--muted); + padding: 1rem 1.2rem; +} + +.dashboard-table tbody td { + padding: 1rem 1.2rem; +} + +.app-alert { + border-radius: 18px; + border: 0; + box-shadow: 0 16px 30px rgba(19, 34, 56, 0.08); +} + +.site-footer { + border-top: 1px solid rgba(19, 34, 56, 0.06); + margin-top: 3rem; +} + +.footer-brand { + font-size: 1.2rem; +} + +.footer-links a { + text-decoration: none; + color: var(--muted); + font-weight: 600; +} + +@media (max-width: 991.98px) { + .section-space { + padding: 3.5rem 0; + } + + .hero-section { + padding-top: 3rem; + } + + .registration-panel { + position: static; + } +} + +@media (max-width: 575.98px) { + .card-surface, + .hero-spotlight, + .event-preview-card, + .registration-panel, + .success-shell { + padding: 1.3rem; + } + + .hero-title, + .detail-title { + max-width: none; + } } diff --git a/staticfiles/css/custom.css b/staticfiles/css/custom.css index 108056f..f6cf2e3 100644 --- a/staticfiles/css/custom.css +++ b/staticfiles/css/custom.css @@ -1,21 +1,501 @@ - :root { - --bg-color-start: #6a11cb; - --bg-color-end: #2575fc; - --text-color: #ffffff; - --card-bg-color: rgba(255, 255, 255, 0.01); - --card-border-color: rgba(255, 255, 255, 0.1); + --bg: #f6f8fb; + --surface: rgba(255, 255, 255, 0.88); + --surface-strong: #ffffff; + --surface-muted: #eef3f8; + --border: rgba(27, 47, 74, 0.1); + --text: #132238; + --muted: #617086; + --primary: #0f8f7a; + --primary-deep: #0a6f61; + --secondary: #ff8457; + --accent: #ffd166; + --shadow: 0 30px 60px rgba(19, 34, 56, 0.12); + --radius-xl: 28px; + --radius-lg: 20px; + --radius-md: 14px; } -body { + +* { + box-sizing: border-box; +} + +html { + scroll-behavior: smooth; +} + +body.app-shell { margin: 0; font-family: 'Inter', sans-serif; - background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end)); - color: var(--text-color); - display: flex; - justify-content: center; - align-items: center; + color: var(--text); + background: + radial-gradient(circle at top left, rgba(15, 143, 122, 0.14), transparent 28%), + radial-gradient(circle at 80% 10%, rgba(255, 132, 87, 0.16), transparent 22%), + linear-gradient(180deg, #fbfdff 0%, #f4f7fb 100%); min-height: 100vh; - text-align: center; +} + +h1, +h2, +h3, +h4, +.brand-wordmark, +.section-title, +.preview-title, +.footer-brand { + font-family: 'Fraunces', serif; + letter-spacing: -0.03em; +} + +p, +a, +span, +label, +button, +input, +textarea, +table, +div { + font-family: 'Inter', sans-serif; +} + +img { + max-width: 100%; +} + +.site-header { + background: rgba(246, 248, 251, 0.82); + backdrop-filter: blur(14px); + border-bottom: 1px solid rgba(255, 255, 255, 0.55); +} + +.navbar { + --bs-navbar-color: var(--muted); + --bs-navbar-hover-color: var(--text); + --bs-navbar-active-color: var(--text); + --bs-navbar-brand-color: var(--text); +} + +.brand-wordmark { + font-size: 1.25rem; + font-weight: 700; + display: inline-flex; + align-items: center; + gap: 0.6rem; +} + +.brand-mark { + width: 2rem; + height: 2rem; + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 999px; + background: linear-gradient(135deg, rgba(15, 143, 122, 0.14), rgba(255, 209, 102, 0.45)); + color: var(--primary); +} + +.nav-link { + font-weight: 600; +} + +.section-space { + padding: 4.5rem 0; +} + +.hero-section { + padding: 5rem 0 3rem; overflow: hidden; +} + +.hero-title, +.detail-title { + font-size: clamp(2.8rem, 5vw, 4.8rem); + line-height: 0.98; + max-width: 12ch; +} + +.detail-title { + font-size: clamp(2.4rem, 4vw, 4rem); +} + +.hero-copy, +.detail-copy { + max-width: 62ch; + color: var(--muted); + font-size: 1.08rem; + line-height: 1.8; +} + +.eyebrow-pill, +.status-badge, +.date-chip { + display: inline-flex; + align-items: center; + gap: 0.35rem; + border-radius: 999px; + padding: 0.55rem 0.9rem; + font-size: 0.84rem; + font-weight: 700; + letter-spacing: 0.02em; +} + +.eyebrow-pill, +.status-badge { + background: rgba(15, 143, 122, 0.12); + color: var(--primary-deep); +} + +.badge-warm, +.date-chip { + background: rgba(255, 132, 87, 0.14); + color: #b14c26; +} + +.section-kicker, +.mini-label { + font-size: 0.82rem; + letter-spacing: 0.14em; + text-transform: uppercase; + color: var(--primary); + font-weight: 800; +} + +.section-title { + font-size: clamp(2rem, 3vw, 3rem); +} + +.card-surface { + background: var(--surface); + border: 1px solid rgba(255, 255, 255, 0.6); + border-radius: var(--radius-xl); + box-shadow: var(--shadow); + padding: 2rem; position: relative; } + +.card-surface-inner { + background: var(--surface-strong); + border-radius: calc(var(--radius-xl) - 8px); + box-shadow: 0 16px 32px rgba(19, 34, 56, 0.08); +} + +.hero-spotlight { + min-height: 100%; + padding: 1.25rem; + background: linear-gradient(180deg, rgba(255, 255, 255, 0.92), rgba(244, 247, 251, 0.92)); +} + +.event-preview-card { + position: relative; + z-index: 2; + padding: 1.25rem; + border-radius: 24px; + background: rgba(255, 255, 255, 0.88); + border: 1px solid rgba(255, 255, 255, 0.8); +} + +.preview-title { + font-size: 2rem; + margin: 0.2rem 0 0; +} + +.preview-list li { + display: flex; + justify-content: space-between; + gap: 1.2rem; + padding: 0.85rem 0; + border-bottom: 1px solid var(--border); +} + +.preview-list li:last-child { + border-bottom: 0; +} + +.preview-list strong, +.preview-list span { + display: block; +} + +.preview-list span { + text-align: right; + color: var(--muted); +} + +.orb { + position: absolute; + border-radius: 999px; + filter: blur(4px); +} + +.orb-one { + width: 190px; + height: 190px; + right: -1.5rem; + top: -1.5rem; + background: radial-gradient(circle, rgba(255, 209, 102, 0.52), rgba(255, 209, 102, 0)); +} + +.orb-two { + width: 230px; + height: 230px; + left: -2rem; + bottom: -3rem; + background: radial-gradient(circle, rgba(15, 143, 122, 0.26), rgba(15, 143, 122, 0)); +} + +.stat-card, +.metric-box { + padding: 1.2rem; + border-radius: var(--radius-lg); + background: rgba(255, 255, 255, 0.8); + border: 1px solid rgba(255, 255, 255, 0.75); +} + +.stat-card-light { + background: rgba(255, 255, 255, 0.92); +} + +.stat-value { + font-size: clamp(1.8rem, 3vw, 2.4rem); + font-weight: 800; + color: var(--text); +} + +.stat-label, +.panel-note, +.event-card-summary, +.footer-copy, +.metric-box span, +.text-muted, +.meta-list dd, +.detail-meta-item span { + color: var(--muted) !important; +} + +.search-panel { + padding: 1.5rem; +} + +.event-card, +.dashboard-card, +.feature-card, +.success-shell, +.detail-hero, +.dashboard-hero, +.empty-state { + height: 100%; +} + +.event-card, +.dashboard-card, +.feature-card { + display: flex; + flex-direction: column; + gap: 0.8rem; +} + +.event-card-top { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; +} + +.event-card-title { + font-size: 1.6rem; + margin: 0; +} + +.meta-list { + display: grid; + gap: 0.75rem; +} + +.meta-list div { + display: flex; + justify-content: space-between; + gap: 1rem; + padding-bottom: 0.7rem; + border-bottom: 1px solid var(--border); +} + +.meta-list dt { + font-weight: 700; + color: var(--text); +} + +.meta-list dd { + margin: 0; + text-align: right; +} + +.feature-number { + width: 3rem; + height: 3rem; + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 18px; + background: linear-gradient(135deg, rgba(15, 143, 122, 0.14), rgba(255, 132, 87, 0.18)); + color: var(--primary); + font-weight: 800; +} + +.section-muted { + background: linear-gradient(180deg, rgba(238, 243, 248, 0.55), rgba(246, 248, 251, 0)); +} + +.text-link { + color: var(--primary-deep); + font-weight: 700; + text-decoration: none; +} + +.detail-meta-grid, +.success-meta-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + gap: 1rem; +} + +.detail-meta-item { + padding: 1rem 1.1rem; + border-radius: 18px; + background: rgba(255, 255, 255, 0.72); + border: 1px solid rgba(255, 255, 255, 0.72); + display: flex; + flex-direction: column; + gap: 0.4rem; +} + +.detail-meta-item strong { + font-size: 1rem; +} + +.registration-panel { + position: sticky; + top: 6.5rem; +} + +.form-label { + font-weight: 700; + color: var(--text); +} + +.form-control { + border-radius: 14px; + border: 1px solid rgba(19, 34, 56, 0.12); + padding: 0.95rem 1rem; + background: rgba(255, 255, 255, 0.95); +} + +.form-control:focus { + border-color: rgba(15, 143, 122, 0.5); + box-shadow: 0 0 0 0.25rem rgba(15, 143, 122, 0.12); +} + +.btn { + border-radius: 999px; + padding: 0.86rem 1.3rem; + font-weight: 700; + letter-spacing: 0.01em; +} + +.btn-primary { + --bs-btn-bg: var(--primary); + --bs-btn-border-color: var(--primary); + --bs-btn-hover-bg: var(--primary-deep); + --bs-btn-hover-border-color: var(--primary-deep); + --bs-btn-active-bg: var(--primary-deep); + --bs-btn-active-border-color: var(--primary-deep); + --bs-btn-disabled-bg: rgba(15, 143, 122, 0.4); + --bs-btn-disabled-border-color: rgba(15, 143, 122, 0.4); + box-shadow: 0 14px 30px rgba(15, 143, 122, 0.18); +} + +.btn-ghost { + background: rgba(255, 255, 255, 0.72); + border: 1px solid rgba(19, 34, 56, 0.08); + color: var(--text); +} + +.btn-ghost:hover, +.text-link:hover, +.footer-links a:hover { + color: var(--primary-deep); +} + +.dashboard-metrics .metric-box strong { + display: block; + margin-top: 0.4rem; + font-size: 1.8rem; +} + +.dashboard-table { + --bs-table-bg: transparent; + --bs-table-color: var(--text); + --bs-table-border-color: rgba(19, 34, 56, 0.08); +} + +.dashboard-table thead th { + font-size: 0.82rem; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--muted); + padding: 1rem 1.2rem; +} + +.dashboard-table tbody td { + padding: 1rem 1.2rem; +} + +.app-alert { + border-radius: 18px; + border: 0; + box-shadow: 0 16px 30px rgba(19, 34, 56, 0.08); +} + +.site-footer { + border-top: 1px solid rgba(19, 34, 56, 0.06); + margin-top: 3rem; +} + +.footer-brand { + font-size: 1.2rem; +} + +.footer-links a { + text-decoration: none; + color: var(--muted); + font-weight: 600; +} + +@media (max-width: 991.98px) { + .section-space { + padding: 3.5rem 0; + } + + .hero-section { + padding-top: 3rem; + } + + .registration-panel { + position: static; + } +} + +@media (max-width: 575.98px) { + .card-surface, + .hero-spotlight, + .event-preview-card, + .registration-panel, + .success-shell { + padding: 1.3rem; + } + + .hero-title, + .detail-title { + max-width: none; + } +}