From 49959e22ac1885258613d40aff4a631ed8e5b876 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Mon, 6 Apr 2026 09:15:29 +0000 Subject: [PATCH] karl thelen 1.0 --- config/__pycache__/__init__.cpython-311.pyc | Bin 159 -> 159 bytes config/__pycache__/settings.cpython-311.pyc | Bin 5552 -> 5827 bytes config/__pycache__/urls.cpython-311.pyc | Bin 1557 -> 1557 bytes config/__pycache__/wsgi.cpython-311.pyc | Bin 679 -> 679 bytes config/settings.py | 11 +- core/__pycache__/__init__.cpython-311.pyc | Bin 157 -> 157 bytes core/__pycache__/admin.cpython-311.pyc | Bin 212 -> 2080 bytes core/__pycache__/apps.cpython-311.pyc | Bin 524 -> 524 bytes .../context_processors.cpython-311.pyc | Bin 763 -> 1343 bytes core/__pycache__/forms.cpython-311.pyc | Bin 0 -> 3794 bytes core/__pycache__/models.cpython-311.pyc | Bin 209 -> 5776 bytes core/__pycache__/tests.cpython-311.pyc | Bin 0 -> 3056 bytes core/__pycache__/urls.cpython-311.pyc | Bin 347 -> 724 bytes core/__pycache__/views.cpython-311.pyc | Bin 1364 -> 8928 bytes core/admin.py | 33 +- core/context_processors.py | 11 +- core/forms.py | 65 ++ core/migrations/0001_initial.py | 67 ++ core/migrations/0002_seed_catalog.py | 120 ++++ .../__pycache__/0001_initial.cpython-311.pyc | Bin 0 -> 3893 bytes .../0002_seed_catalog.cpython-311.pyc | Bin 0 -> 4675 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 168 -> 168 bytes core/models.py | 80 ++- core/templates/base.html | 80 ++- core/templates/core/catalog.html | 73 ++ core/templates/core/contact.html | 72 ++ core/templates/core/contact_success.html | 24 + core/templates/core/index.html | 306 +++++---- core/templates/core/product_detail.html | 76 +++ core/tests.py | 46 +- core/urls.py | 6 +- core/views.py | 195 +++++- static/css/custom.css | 634 ++++++++++++++++- staticfiles/css/custom.css | 643 +++++++++++++++++- 34 files changed, 2346 insertions(+), 196 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_catalog.py create mode 100644 core/migrations/__pycache__/0001_initial.cpython-311.pyc create mode 100644 core/migrations/__pycache__/0002_seed_catalog.cpython-311.pyc create mode 100644 core/templates/core/catalog.html create mode 100644 core/templates/core/contact.html create mode 100644 core/templates/core/contact_success.html create mode 100644 core/templates/core/product_detail.html diff --git a/config/__pycache__/__init__.cpython-311.pyc b/config/__pycache__/__init__.cpython-311.pyc index 896bb4f1b8949c96d2f1bf743293342b70a9d0e4..f0d36cce8bb132fdacf0d71574e2e55a2d1e9da8 100644 GIT binary patch delta 19 ZcmbQwIG>SwIWI340}wQpU!KT41pqA#1rh)N delta 19 ZcmbQwIG>SwIWI340}#~tteD6>1pq5}1i1hJ diff --git a/config/__pycache__/settings.cpython-311.pyc b/config/__pycache__/settings.cpython-311.pyc index d79d6a79890a58443ee5f65d8d558823a19e9aed..91c91d61d10886e0e40c7a0217f070e6dd803cab 100644 GIT binary patch delta 1190 zcmah{OH30%7@pbPw%f|K-2#R3Xpx7dU{Ub_6huVOXyU=(#fcsa{{o93WV$|MQVzs; zAW@uy8aQ#lgNdj;8BfNW2Nfle5D%U`tcN&c6Y7Y~A8+^01e zeUcb~9x{*3o~*YdEC3-EMAM;=h0qEI2^7syR)oG7eKCYt7)n?Pl;TYYB8X*k)TB^8 zs|-?B3Kc8@l@QIF8p~%d&PqD6Qo8wRiyKriJ)?-pu2KA7@ zQ#~}~urY_bO^O0a`OjtXyt?)+8TEM?9n-%K(}WmTs)U;+#h4O| zAAzpTDzHX4y5$FSi+LqZkBN6moz}w=(94=Y?jwogFI=;9htD$7R@&fK4K8NfT~rfa z+~-_6eu1b+dEOo>#$AH0_|n!zZl$l8)=)y?e!N^!hQ_ec`F~YaJcy1vRXL~?DfJ8u zWvA3vvODJ7He^p1G}G#Siih>3;6_IKkwSPt2FWdQp;SEel!^C#e`X+`)}a--iG<2SY^@d&HZ9S9S|CTfxN55B6p?IWuG*;De-Q?lrZEO{b! z08^F~&6fYB7OWNU5O&CBG=uJ2qv1+h+#FwLp_rC2x;=pfQ5-mdDqINsrUzTF4R1wQ zk81i}#4TdTEU_Hpe0-@~YY~ItEupj7~m`0Y}gD zs(7ovEDs|4iSEghcP>3_UR0t>O4RncbZJ7f{n?q&Yuce~h~Oq({a)*$TE46XH+<)! ey3_V3VYltV#P5z|KOOi$f_6BYDN0hs(c%xmTJR14 delta 865 zcmZuuOHWfl6rQ;=Em-eudrK+LwrIstu_*6n)FY#c?!Jzjv_~UpcC(mJNzlRA&(27{zGp+Am8yg z9h6^ry)h&ba9%V4yFwDFT*$RX<$HHO)#VTORhJ)sA}ps$K6M1qOVGS0>s+^;>^;kw zNocl#!91Ut&pKK4-F0=g56Mw(`Fq5*wU4C zY-?e^o8_eE{@-m=>i$veHsSwHms(Y~HiSI0Zaa3n+IA}FQqZlSM?oI~j#!wr9D6|d zfz1x6_?XdcQ7 zL~aK*D~;u$m9f=Z&suhPWS2(@CEV1Oje?$gTK0j)+{9(9-rwOBpSixzi#t4w=YrZc O@fWg#RdQ|p#-o1 delta 20 acmbQrGnI#XIWI340}#~ttk}rS#RdQ`vjkTF diff --git a/config/__pycache__/wsgi.cpython-311.pyc b/config/__pycache__/wsgi.cpython-311.pyc index a1b4aa75cef92df0f6f3c8f77e2be44ec1b36534..59f60c7d0502349f706ad71ac255a07d00297d2f 100644 GIT binary patch delta 20 acmZ3^x}24JIWI340}wQpU*5<)hY0{QaRpQW delta 20 acmZ3^x}24JIWI340}#~ttk}pshY0{Og9Q5k diff --git a/config/settings.py b/config/settings.py index 291d043..7b53c10 100644 --- a/config/settings.py +++ b/config/settings.py @@ -23,6 +23,7 @@ DEBUG = os.getenv("DJANGO_DEBUG", "true").lower() == "true" ALLOWED_HOSTS = [ "127.0.0.1", "localhost", + "testserver", os.getenv("HOST_FQDN", ""), ] @@ -150,9 +151,13 @@ STATIC_URL = 'static/' STATIC_ROOT = BASE_DIR / 'staticfiles' STATICFILES_DIRS = [ - BASE_DIR / 'static', - BASE_DIR / 'assets', - BASE_DIR / 'node_modules', + directory + for directory in [ + BASE_DIR / 'static', + BASE_DIR / 'assets', + BASE_DIR / 'node_modules', + ] + if directory.exists() ] # Email diff --git a/core/__pycache__/__init__.cpython-311.pyc b/core/__pycache__/__init__.cpython-311.pyc index 3f553f6185fe97866ef932cffccb11efab5e8ef0..d4d602b536697107e8b847cee401d0b40f68c1db 100644 GIT binary patch delta 19 ZcmbQsIG2%oIWI340}wQpU!KT42>>j^1q=WH delta 19 ZcmbQsIG2%oIWI340}#~tteD6>2>>fD1hW7D diff --git a/core/__pycache__/admin.cpython-311.pyc b/core/__pycache__/admin.cpython-311.pyc index 5e8987a0cd478c226e501b5189550e758662c560..727fc13785802fd4e1f391028a85fc038671060d 100644 GIT binary patch literal 2080 zcma)6OK;mo5Zx{dW1y?EYCU7YST{ z9sJ||trGGtI@y&U85fKa@|-Zjs7HKiQc9S@a-QPn%$%>9s;`+EB?{qmKN~}M?i5D7 zgg3nu@;zbdF<}}+6|&aT9K^tzUZ?PRnK{JNrx>*>81ukb_+ZAFDMs}QMjaSuKA5q{ ziV$1M7H!RD7AsA(m&fh16YZ68d-)e)R9>TNP^sC>cZ1ico{^LKLp$cJP#nt2!!U?# zC;l;b(sjWv{3Jrwbz&!@!LKrMEhm&GU)u@wr&g;XQ2+j>C-mW!bX{Nu#lH40LYFG`%qRpf_K7B*MlEpR@3Dki)1P>cszⅇG^4r zd~sIWJX3EB7H;)+26Jn@AMh4S+0d(XU5D;zT{BIY2Kwe%-Yl>JXsgTFQHoM&wL~X0~GdTkeWZHeojhv|Af^H(*0s#HTw!n%LSYC#ja$k$u zm^Q{EN4TBX4Fg%a?C3tsEnM5%HFSaXC*~055v~EqQj)5s>p_?-Ck|(BZ0~toT*s&? z!XiQ<)D5&&GC;;?6X3GiKZDxWzwA94EIa_6FYErew}W@egd`J^XUE5trJPBQWejrX zLGFUAPVSHi)eBK~Z=XA{T!!*%ase`1PP(BipU3Tx$$S)p{*f#?0ullJb`0`k!o>yx zmXSdd+Zg3iKC0XB>RA@N1iUJg9HS+;h38 zCsG|%CR>kgKC)xm3tRQ|`gYJ1@KZtkDHqRdsE%OQn3EN@ZwIaLJ_;$^J%OjW%)=?b zy+~-_2;6@Hk9?^M-hyKQ%Br}70m+o5=yu^W5P;RcK! zScx;G%sc(7^z`{!d3o|YlELKJ zu=$Lwa!yzKbQRe)kZc-$U+dTIA<+i%Y#>d296wKL4|>{hu}_u;%Xd?2aj^3FsO^j_ I!IM0#ks&dbZi00fQYmp5`VG64WD6$F6* delta 20 acmeBS>0#ks&dbZi00ebDD>ia7G64WBCj=+} diff --git a/core/__pycache__/context_processors.cpython-311.pyc b/core/__pycache__/context_processors.cpython-311.pyc index 75bf2234fb21a6b62efc5cec11af9512dd0c9616..558bb2ab9e79e95829775e5df4330519e1cc3b61 100644 GIT binary patch literal 1343 zcma)5%}*Og6rWw2jj?eECB-qsWt6t6tR|SRs7e%pU9cJ_e95MYN@!Qh&Va}4S7&Dl zwur=`e?p}6-~&}t+6qPLvB&-iw(0{K$)}upLqfUa)LDN}Xob`_+TXnQ_Ra6jdvEvW zXfzCjyx#qj%me^<;~QO(JId)tLU{=gK#~F0+PkzSwQPA!M)G%HEr6r~NCy7$k1Xj@ z=cK+RMG0Q0-okjsCS zi%0)-GFlCZNnEWw2}22rSF}%y*mSJybhEX1CG4e=Zbvd6^R~{}eWh>$F$%$z=Jux{wRZ*UPvyMLf zngc$#l3#w5om*AsvxT{QZe=yMyu=5(&fH?=i>&%(eu0PER7;t~Y^DE^_Y4~ZQ@C2h z2DZ9+vWuDALS=|pn|8X?2@%&65n~ARXqzE3KcCMQ3YCwa5bPqGZVH+eQA7pay>}bl zotXM;3NGLsOt(E3Tgea~?DRxM*wra<7_lur&~*}1+rp|x4IXxxO|fceCgy`}-6`4@ zRuOSUm&*Lwd8a|FZ60ZRu&EJ)N85&m5XG*`uOsXjcG+*d%7}?wrkPGs;j-;=WeYPg z7%o%H1FpBh1H@uH=!Yc({0hb2dLo`)6@#b!0$l(57bAXmKUK6%obon2i+L%$i*-+2 zI`zz^+peSOIHijc1hl59iW+rs6`Q(aPMP<2OYXd;BZ5@%*?$zU58j5rNTQlNSp4ll zeK_41PS=95W-R_<=X>(Yd$l|1dMwk3Wop6eZxsMz1IFV1nK?ca1&XB^8!kEJl1)&+E7*m*97^0X`7=sx! znO}l5_$4!fC@5e9(#$~oc^bpSJe|oojN;nNDNM_l7#LOqF$9#b!dMIpCF}@x7W3qF zjH>J`bC`jKvrfLn7^N*75ajRW>Kqd9;u`E6F;-o;g%pw*wfe1-8DWm$VZcT z@((7>$py?}8Z5V%@{4b=rKgsp=9S%I&de*h#Zr=)o2topi@hkdur#%}q!{Gn$%mQ! z1qFZ%79cKm0TK-iU%4jxuqf0&RZw2)vcz+t_Y&`m3Ij z{x1TBSP?IX0GYFrp-2$K<_8kLIBatBQ%ZAE?TUnfT#$Xm20-EiGb1D84F=8zFucLQ V-2jFkSU4Gh=0gbq1d|PHBmm@)T(tlI diff --git a/core/__pycache__/forms.cpython-311.pyc b/core/__pycache__/forms.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c2087361077de4bc1a38f09e6edfea16c2353cdb GIT binary patch literal 3794 zcma)9Uu+Y}8K1Si_Byc>$00a(;b2Kpf(>!5BBww(dPz7qA|Oyey~;hbw|FMWChJ`@ zyC%VPI37-_ucB?PN=2ljl2x^o3M%EH4}G9d_i~T6YbmR(kdXSsn@_oi6HonSH+Etm z(v5e&oj>1v-#6dP_nXgu3y0eflz%M!N6~`_{f{*oh2N@do`cGLgc0Ucl;Khwhp-2G zRZqs7@@9M~ABQ~XBZPgQBg_+SzA+Cy^dj^aUbm$D*nbtp120%J&e{BoPBX>}j_=dFTP1@VX`C zEut9?dp{RaKBwk`8ec?9T#DJk+tF6FXThWvjl!0o&0j!eTOaYJe6!$>KXWM_gLdMd z4{UKF?8QEXd%{12l|5#wbBe;H1nkEFEZ`t+!|gbPxd(w%5Qo8zj+e35?MPlErW_C0 zfvoEZ+o#DHV)G;;E2_Yh($86WIn!jy8b-y&N8y(=%iwcO$2nxe>BN(m7M7Ba1p+FtT!*jHGo+Mx3LU%-*(r%*m9+ z0NX!f4#?f+&x-6X@^z08ZL6aODf;VsPz&&&ivFUX9t34{07$xdN}%C~w-esYaX7_) zcou4x-a+#0fCXGru{dw4=Athu{o9@wA@QQZs(H zxq#2x%2hz~%u=Ud-&>6;lD6Z3-<78kRS5fc+EHrAbf>zS0V5f`jfZB;_?$B^jbGLS z%?^s?+gSh)8vsLTGaqsq4F0GNz?fEL!?4jyPPE&zDun!;u3|#tQJXh%Swa~CDVkzR z5@jdY4(JX}0+P%qstMyAu3U){6?}#1KE@^7%n^FqAf_!)!mcQS?RJvMnztnffNWkd zNd|EIQxN4$}?C z5pZELB&4HEKrn3>lDkrYEGdQw&bEV++a*Z`iwm()?$Ul}(xX7~eQ&sE8vZx{48|Bh z%J+AXCG`H5{~~ZNP!oFB2FpUcbO?z1JWKre$;d`xyqXyQX0)6buOu$l5|=j;QZ*r! zKb@~67HWxwqVHb&%2Z9*UlWF`#7IkXcydQGQTL;s!&c9b)wAD+SwtfLtKO zP`v}~6Ca%1=sQ~NJ6h>W*7}k#5}e>%`o;O;hbsWHZ?Aq*Jo_wuZ2j!N&pf?QiBHwy zQ{~?4YvHxqqW#*@tid@QXV`7&wAYbZVaBT4xX+I zo~aFTR|@6M0eGzBPyRJpOOCJjS3_&pE0H7L*TdV!IEdZ?<_vNf z0B=48uQD*-9m-2hCJv%RS1dgwT? z8O(t$fNa6XBy{9^-vA$uJcdffe}MpYJd5r7a&dL>{_-D|zx-_Vvtq~^7}*#&RvkE2 z9-XcXT&fLRDhs03_0C4u!D`pR(om%t2w z7s`hw;VE8Nd#e%}c;4Ap7W$m{1~>M?GAYX)C`%cf6wU!x%3NfJw@^4uR8^AVoa;^& z-Qz9}vqMH1Vb_mw2MGAA@vD3G@_>D>lW$qyciRK+cJmAbccF8blqa$x@JWUc4Hfp1y=cW)SU%=kZ^6prlx literal 0 HcmV?d00001 diff --git a/core/__pycache__/models.cpython-311.pyc b/core/__pycache__/models.cpython-311.pyc index a251b5fb7c2b603c587f8162d643eb10596d2858..0c3eda37e9eea53b8a6d8d3d58700eadcc7e9d2c 100644 GIT binary patch literal 5776 zcmbVQT}&HS7M`&^<9`n}hLVI72M8NC)RdN8P_x}C;ipL(oV1Xix=Ck^=K@a0Ho0S` z3Cc>=Di1v50cj((QdNnSR#~D@iHAP)v5#G8AI8~{VvY8p>b`9D$q1f!+H>yM7!NdC z?KN|_KWFamIp6v2{52T#age^b`;Y7$$o&fojU?1F+pCcIf>St!SGgRY;(5$FR7cL4 za`K#mdyi9`4>-j|oVQ$!mmfPh?kPN0N(oF$04?z)S|ZbOgO=wdT5iQV!^M3su|lJs9L5zD*M-%(e`&x{DLD~idVRl!@=zzLE#n0hV!ul<_wROaL%A3 zHp-ykdd!0cJar8>)9^IPpdr{ayi9}bF={E|hTEp$XKVJrn!UF11B&kfmkKI=SZ%=c zjHioap+N7wxC{2mX$eYp21ua|5-XRzAUpbAnc83)<#DfkCqcok~eV~s3n^h2&z|v!oPwRP(ONDHp;ZcMQ zyX#9#2hWxz#i#fc0rUco!G@lq9kG-O+xiL~(pQA(peeGXgf=AGilR0R;ebX5(?FcC z`VcnSY<;|PKqG9^=wd(@0ibKQ0o@TL3P5+198)?TaJJp*RyqNOUNPO5Dd0+`XnK~c zj0OnkE#zeYd{QK4fD#n|Qei;!VhyGV-i(JdOVK%B{1XIqW3_`-Q$NqFTMrsDT)vTI1xw#X36=&=W<$60Chc$a*OREj$qnG zC}`TQ!Y}Xuvkq9Fb2m_5A46G&3`rURb-PC+sL?YZPQarftnBmX=+V`&{~rNqH}-uT z#jDskPTPV%hAqodV~Ta;>qkwT26NdKq($n$koMn@X+_$yL*Zvyuy?JoKcL%8#RmO= zD#g)iORc3p@QU+3ueiQ)DZ^Z^$6v+dQj7IL|txg9M!(cUxBiA8$_A= z(JwuGSNHc;+aYESRHI#wExYeV9YTGsD^X^-F50q(dat0Ij(n&Rl3HE zt})%!(Fo<0NWT&3hbS)f;8;EDZ*3~NbjCVX7$)40!M%Ue@%``*6Z*AJ^ze;J_=XX_ zQGUPLIj}l!bPjCxeK)Ijo~v}8GdjPV|^{r3n zZNrteVWVxhe4!dXiJhMW;t?LJgvX5V81!1NTrcY3kxF>P2#;8Fq8n*~T_Bz4MmkY% zu+a&T6GJtJOZYus<4|rnq2j^y{Z&?jMK|yqGVtsrj1-u*{{sipglrbVcnD#R{kI1j z$*}G^|LaC1vVhqbha-67LGI2H1bdO)6#xXE?g3%S$KJunWodbUIKce&-AH~3^=Q8D zgE9QAFqX2YM^h(oh)x@u2XbYWY$4ks9L*krM7Xs_KOm1$kfn`~ZRaeS(R@=P;=VV< zTYCiLz?&BRK;p|1%-K#*j;Q|Ljpp=JfVOl^?JC+EEW`?FT&DdAx}*AEKr#FLLO^KE}d1E z#mU#F;eRqN*dWssE}y@YoHT{W>9ZFu+91X3JbWa24JQ2tJoY6c=oqu-jo<_Dm5l6j_=xPd+!DNm7EmH)HKCK&$@)%zjqP z0z^WKm{AKFR0q$_Ex@g35lR}#C*z`h3!KTxYjBIYl$<>~K07&KisKhA%}h?%H$$|> zr!vfY1Q(CQW*@|cj;=VX(FDY}@M|j~{;VCyf*Qa+JN+!tX5%h;8mj0Z3N%gnYZU0X zGy&qpDa_&a($`T8gNQpB$28rU#R7zNjpAM41=>9w1#$QWzl0pN+qM>O!pr{Gn-S0##y?DVj%XV2KB{8XCm_tOr3M1Xb@P{V3ZeVKzIGS!hV7hIKDDv z1ddk%Jw~9XJXr-LD0iU!VitR8y15_;PRyi^HV_CU{iIxCg(#v2TxRC^@-=$=vD}hYe6cX-=wL5eHC=5r~Ur Vfy4)9Mn=XP3^Et6p&~Y*3IJ?MBhUZ< diff --git a/core/__pycache__/tests.cpython-311.pyc b/core/__pycache__/tests.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c2315bf0a5b9000c3b8fe8b384353b4bd9334804 GIT binary patch literal 3056 zcmbtW&2Jn<7O$R}p01g(9e*tAnYGt$AOVjcGbY|(CqWn|3LzH@K@eyp)M~oRn;KGSl-JXsoByiZ8 zn%7mY>U~td-+R@c4-V!LwEtcE*7`1s&<|qLYx3dX!7>cCk%>&nMvhdIBr#6eDJNY^ z!#Hhb?2IGVWG7q8I!aBEPzqf_ru+#qvqaiWBeVzqxYe+!yp78EM=>o+9A6>Kzo0V$ zV}+8dgfjA@B-YX#U(kJ0_vlA_=z{0^y5V1TKMX9GD{oTI3=H3hmBv&^iS|GRvW*C; zNhYeLOz9I;OPeX688Z!3HZ$OX%!fg}ZhQ6DZEr((!Hk{;p+&qG;q`;BfNUclXs@j$ z`*u=2-qEKinJMvB?#R0tI6M5~wu!XlJ@~lCMd*+8ZKAbA&di#MiOrmuHw)&#oxxqu zvzO=s3%#g8jNp_*%RG05>H)RfI^j9LH*BhxUDLX1nE|nFqP``T)xb5?^MvYt;MOZt zC@N=o#?>9dGt3U^ykHV$P^;lvp3C#hqrT=*lhEeq+g9BLIZD)?wdN@6@lN*4HfJtc zuDWjPj9JWBB{VXuI+m|)=+q&Oddag*-*YR$1+cypusYeedwbRPAL=F1_DfX18c;9t zB=Wlfz`6USYg>`pm{gJ>(%b9ljG+6sw|YnHPV@&<9m`(TY2KI2(~!V;llLb(VGR~j)`0pS@& z>=h>9qyBQTy6QQk8hjAAeo!UXh!OZ6t!{X9oi%iWR1J@ksxN?9X?(*!ul?G$GYiq-vMH5{1~y?8bpFLraNGuBPT?kdLt~pA3ik^j;mpDvO9u`mHQ}H1W6zuIj{x42Vf5M<&1z# zlrtR|Y$L#}*b=p!BxiaY@$?h4BB(CAuxBsX*FXDM+C)1^797>F2`g(!j;$pccOf_T zlKhmwo8OTaF^t-i<)eH`E%S^BTUmiX+ZJ(so~{$03j2Hra80Nm)hf$n^e@1SK_$|HneY2lg(I z>nI!>zj1DR@n4Id&42Ooe->{qw#Vi=V{=EJW1UZ+!_yAK2fU_>AgA^(3taJ2M2402pvO*t-xx0L=+M^H&%0Nx~R zQ|bd;t{-CkS|Y$Cut-$Ez!!Dnu&pJfcPH259ewV?WG`Wu`A2O$L05DsCt`<#eqA4@ z0ML*1Z!`7qx;Xm&9i>o9qZT+^L~O)7@xG?f?r zc1C^a)Et)^tDZ|Z?p4`f20XlMtg6o4FO5~G)x>g%t6m}0fpWic_iLKm3Zzn29_9s? ziKAOK74p2;pk#$mO3VoG;E?7Jf7LU&!h)!{a>XHxL3;3Ghc0U@Fbv?umsNp2Efi%M z9*Z+Fvcyx3b$%>P&9{hY(Wp!pV#dV}1Kp+yh|!{ul?@NdF_%{?%KUOV%2;|_tSDCe z?ZE`L)n3{@9c5t8n-z~bT3r8`cDTQJ>hW3odot5tf_($zFli6Qp4}gN_TIqQre?YES?BUFNCFM!WZYli4)<(#qKdYIMhWz?#qQgLA?gDg}X(R8$7_1 z`*`x^d!KEzs;{-5U+O%+)W+vK_XfPnrPKSR)9uoHr!@c7NE@H) z;Bzf}?%t@{8J#&8t?Z9h+M~0b(b+8}5xhBmYx;}hcV2Ac`3|0M^?Fnumdja=HBEd- zXd2IJngbtiwipj+ng~F=LjMXUrY8h>8OVQ8Bpj7TDiR_hB2;V%$o1scO%2QWE=tJp zw0yRU5|SU57sTqLB-SnzFPC%hX|txgb+01gNkxbgPC#uI*Y_;=O<*FjF2;<={@Ay( zVkBx|wENeYqL>hB?%ZuR%?(dpKcdpA2H!Et^|KD~Bd LaDRHtQsmsPHmv51 literal 0 HcmV?d00001 diff --git a/core/__pycache__/urls.cpython-311.pyc b/core/__pycache__/urls.cpython-311.pyc index f705988a223abc7b5af333902925bc196186bbf5..886b59871b7fb20f55e15bfdd9771cbe34f5dcd4 100644 GIT binary patch literal 724 zcma)2y=xRf6n}I3_ABe%gO@ZJMlOK{aKex~%>LqhumC>me?>Loa*KH@ z5udwkZ6y+LWY<VQ+-^kF8Jl1{&-Su_s?R?txtAM?t*)ZQMMN$ZJwaR`y;`>EizfP?~>nZBO zRiH!lYLbhr_yiX^1upM|X85ptS!S{f`2mkhnTv2g7vo|=V;;u2(6H{YIeByGZKrz2 zgztzAoop^teAi-b5kk)3mim)_!V~p>Ja@qMj~*Rw96vvC6n3WAnPKM)J?*>s?#1br z!rl~nGwjtq<#$eRt9y?X4yHJm;ou*UH%=a?yAKuir`Vrie;zpP#!+CNSl6Tf2I6qT AIsgCw delta 255 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 Xr9HlS=yH2}4P;gNa)91vejfh;`i@)& diff --git a/core/admin.py b/core/admin.py index 8c38f3f..5b2d8a2 100644 --- a/core/admin.py +++ b/core/admin.py @@ -1,3 +1,34 @@ from django.contrib import admin -# Register your models here. +from .models import Category, ContactInquiry, Product + + +@admin.register(Category) +class CategoryAdmin(admin.ModelAdmin): + list_display = ("name", "sort_order") + prepopulated_fields = {"slug": ("name",)} + search_fields = ("name",) + ordering = ("sort_order", "name") + + +@admin.register(Product) +class ProductAdmin(admin.ModelAdmin): + list_display = ("name", "category", "price_from", "is_featured", "is_active") + list_filter = ("category", "is_featured", "is_active") + list_editable = ("is_featured", "is_active") + prepopulated_fields = {"slug": ("name",)} + search_fields = ("name", "short_description", "description", "material") + + +@admin.register(ContactInquiry) +class ContactInquiryAdmin(admin.ModelAdmin): + list_display = ("name", "subject", "preferred_contact_method", "status", "created_at") + list_filter = ("preferred_contact_method", "status", "created_at") + list_editable = ("status",) + search_fields = ("name", "email", "phone", "subject", "message") + autocomplete_fields = ("product",) + + +admin.site.site_header = "Thelen Atelier Admin" +admin.site.site_title = "Thelen Atelier" +admin.site.index_title = "Katalog & Anfragen verwalten" diff --git a/core/context_processors.py b/core/context_processors.py index 0bf87c3..72b929d 100644 --- a/core/context_processors.py +++ b/core/context_processors.py @@ -1,13 +1,22 @@ import os +import re import time + def project_context(request): """ Adds project-specific environment variables to the template context globally. """ + store_phone_display = os.getenv("STORE_PHONE", "0214 41243") + store_phone_link = re.sub(r"[^\d+]", "", store_phone_display) + return { "project_description": os.getenv("PROJECT_DESCRIPTION", ""), "project_image_url": os.getenv("PROJECT_IMAGE_URL", ""), - # Used for cache-busting static assets + "store_name": os.getenv("STORE_NAME", "Juwelier Thelen"), + "store_phone_display": store_phone_display, + "store_phone_link": store_phone_link, + "store_email": os.getenv("STORE_EMAIL", "info@juwelierthelen.de"), + "store_address": os.getenv("STORE_ADDRESS", "Wiesdorfer Platz 59, 51373 Leverkusen"), "deployment_timestamp": int(time.time()), } diff --git a/core/forms.py b/core/forms.py new file mode 100644 index 0000000..eeff850 --- /dev/null +++ b/core/forms.py @@ -0,0 +1,65 @@ +from django import forms + +from .models import ContactInquiry, Product + + +class ContactInquiryForm(forms.ModelForm): + class Meta: + model = ContactInquiry + fields = [ + "product", + "name", + "email", + "phone", + "preferred_contact_method", + "subject", + "message", + ] + widgets = { + "message": forms.Textarea(attrs={"rows": 5}), + } + labels = { + "product": "Produkt / Kollektion", + "name": "Ihr Name", + "email": "E-Mail-Adresse", + "phone": "Telefonnummer", + "preferred_contact_method": "Bevorzugte Kontaktart", + "subject": "Betreff", + "message": "Nachricht", + } + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields["product"].queryset = Product.objects.filter(is_active=True).select_related("category") + self.fields["product"].required = False + self.fields["product"].empty_label = "Allgemeine Beratung" + + placeholders = { + "name": "z. B. Anna Becker", + "email": "anna@example.com", + "phone": "+49 ...", + "subject": "z. B. Beratung zu Trauringen", + "message": "Worum geht es? Teilen Sie uns kurz Ihre Wünsche mit.", + } + for name, field in self.fields.items(): + widget = field.widget + css_class = "form-select" if isinstance(widget, forms.Select) else "form-control" + existing = widget.attrs.get("class", "") + widget.attrs["class"] = f"{existing} {css_class}".strip() + if name in placeholders: + widget.attrs["placeholder"] = placeholders[name] + + def clean(self): + cleaned_data = super().clean() + preferred_contact_method = cleaned_data.get("preferred_contact_method") + phone = (cleaned_data.get("phone") or "").strip() + product = cleaned_data.get("product") + subject = (cleaned_data.get("subject") or "").strip() + + if preferred_contact_method == ContactInquiry.PreferredContactMethod.PHONE and not phone: + self.add_error("phone", "Bitte geben Sie eine Telefonnummer an, wenn Sie einen Rückruf wünschen.") + + if product and not subject: + cleaned_data["subject"] = f"Anfrage zu {product.name}" + + return cleaned_data diff --git a/core/migrations/0001_initial.py b/core/migrations/0001_initial.py new file mode 100644 index 0000000..b938c73 --- /dev/null +++ b/core/migrations/0001_initial.py @@ -0,0 +1,67 @@ +# Generated by Django 5.2.7 on 2026-04-06 09:11 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Category', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=120, unique=True)), + ('slug', models.SlugField(unique=True)), + ('description', models.TextField(blank=True)), + ('sort_order', models.PositiveIntegerField(default=0)), + ], + options={ + 'ordering': ['sort_order', 'name'], + }, + ), + migrations.CreateModel( + name='Product', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=160)), + ('slug', models.SlugField(unique=True)), + ('short_description', models.CharField(max_length=220)), + ('description', models.TextField()), + ('material', models.CharField(blank=True, max_length=120)), + ('price_from', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True)), + ('collection_note', models.CharField(blank=True, max_length=180)), + ('is_featured', models.BooleanField(default=False)), + ('is_active', models.BooleanField(default=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('category', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='products', to='core.category')), + ], + options={ + 'ordering': ['name'], + }, + ), + migrations.CreateModel( + name='ContactInquiry', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=120)), + ('email', models.EmailField(max_length=254)), + ('phone', models.CharField(blank=True, max_length=40)), + ('subject', models.CharField(blank=True, max_length=160)), + ('message', models.TextField()), + ('preferred_contact_method', models.CharField(choices=[('phone', 'Telefon'), ('email', 'E-Mail')], default='phone', max_length=10)), + ('status', models.CharField(choices=[('new', 'Neu'), ('contacted', 'Kontaktiert'), ('closed', 'Abgeschlossen')], default='new', max_length=20)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('product', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='inquiries', to='core.product')), + ], + options={ + 'ordering': ['-created_at'], + }, + ), + ] diff --git a/core/migrations/0002_seed_catalog.py b/core/migrations/0002_seed_catalog.py new file mode 100644 index 0000000..63667d6 --- /dev/null +++ b/core/migrations/0002_seed_catalog.py @@ -0,0 +1,120 @@ +from django.db import migrations + + +def seed_catalog(apps, schema_editor): + Category = apps.get_model("core", "Category") + Product = apps.get_model("core", "Product") + + categories = { + "trauringe": Category.objects.update_or_create( + slug="trauringe", + defaults={ + "name": "Trauringe", + "description": "Beratung für Trauringe, Materialien, Oberflächen und Gravuren.", + "sort_order": 1, + }, + )[0], + "schmuck": Category.objects.update_or_create( + slug="schmuck", + defaults={ + "name": "Schmuck", + "description": "Ringe, Ohrschmuck und Perlenstücke für besondere Momente.", + "sort_order": 2, + }, + )[0], + "uhren": Category.objects.update_or_create( + slug="uhren", + defaults={ + "name": "Uhren", + "description": "Ausgewählte Uhren mit persönlicher Beratung und Service.", + "sort_order": 3, + }, + )[0], + "service": Category.objects.update_or_create( + slug="service", + defaults={ + "name": "Services", + "description": "Goldschmiedearbeiten, Reparaturen, Reinigung und Extras.", + "sort_order": 4, + }, + )[0], + } + + products = [ + { + "slug": "signature-trauringe", + "category": categories["trauringe"], + "name": "Signature Trauringe", + "short_description": "Warme Goldtöne, klare Linien und Platz für Ihre persönliche Gravur.", + "description": "Diese Beispielkollektion steht für die Trauring-Beratung von Juwelier Thelen: verschiedene Legierungen, angenehme Profile und eine ruhige, persönliche Auswahl im Geschäft.", + "material": "Gelb- oder Roségold", + "price_from": "1290.00", + "collection_note": "Gravur und Größenanpassung möglich", + "is_featured": True, + }, + { + "slug": "perlen-ohrschmuck-edit", + "category": categories["schmuck"], + "name": "Perlen & Ohrschmuck Edit", + "short_description": "Leichte, elegante Schmuckstücke mit klassischer Note und moderner Wirkung.", + "description": "Inspiriert von den Serviceleistungen rund um Perlen und Ohrlochstechen: eine kuratierte Auswahl an Ohrschmuck und Perlendesigns für Alltag und Anlass.", + "material": "Perlen, Gold, Silber", + "price_from": "149.00", + "collection_note": "Ideal für Geschenkberatung", + "is_featured": True, + }, + { + "slug": "atelier-automatik-uhr", + "category": categories["uhren"], + "name": "Atelier Automatik Uhr", + "short_description": "Zeitloses Zifferblatt, ausgewogenes Gehäuse und Beratung zu Pflege & Revision.", + "description": "Die neue Katalogstrecke macht Uhrenpräsentation und Service greifbarer: ausgewählte Modelle, klare Preisindikationen und direkter Kontakt für Rückfragen.", + "material": "Edelstahl", + "price_from": "895.00", + "collection_note": "Revision und Armbandservice auf Anfrage", + "is_featured": True, + }, + { + "slug": "goldschmiede-service-paket", + "category": categories["service"], + "name": "Goldschmiede Servicepaket", + "short_description": "Ringweiten, Reinigung, Umarbeitung und Reparatur – digital angefragt, persönlich umgesetzt.", + "description": "Auch ohne Checkout bleibt der Ablauf komplett: Leistung entdecken, Kontakt aufnehmen und den Auftrag im Geschäft besprechen. Dieses Beispiel zeigt den Service-Fokus der Website.", + "material": "Service & Beratung", + "price_from": None, + "collection_note": "Preis nach Sichtprüfung", + "is_featured": True, + }, + ] + + for item in products: + Product.objects.update_or_create( + slug=item["slug"], + defaults=item, + ) + + +def unseed_catalog(apps, schema_editor): + Product = apps.get_model("core", "Product") + Category = apps.get_model("core", "Category") + + Product.objects.filter( + slug__in=[ + "signature-trauringe", + "perlen-ohrschmuck-edit", + "atelier-automatik-uhr", + "goldschmiede-service-paket", + ] + ).delete() + Category.objects.filter(slug__in=["trauringe", "schmuck", "uhren", "service"]).delete() + + +class Migration(migrations.Migration): + + dependencies = [ + ("core", "0001_initial"), + ] + + operations = [ + migrations.RunPython(seed_catalog, unseed_catalog), + ] 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..90add7c1a13e23e17f80feccb832aa78dd0cc40f GIT binary patch literal 3893 zcmbtXO;8)j6`mOh{UNYqgRz7_vaJvcjQBMhY`nx^WaG6BYgiwM?b$)yh{2;7d1i!- zD^>9!haC5m<><*;CIdPNAoc55)>lsPdNR{1`2kA}s z^w+Q7`@Yw$;je9N0S>P3U;I=4bCBcy%{sNKrP_FZ6B^%gh(o->t?+Oc6d~_gapm1B zZk`jkTO4xzkwb3m+NjOpc3d3y2ENX<(t`LUF4c0#y7yB84dmsVE}62X8q|~55LS$A z6>HzlRy*%Q;PG1y+v~0fh+lE#xD~g+;g$_gZ2}UI3!Zp4eDxs;U|Xk%JyPXN=_fN8fez^rqj zQ@f#)m|P1Aqju!Y2{ONX`eWW;_J?_%`Gh<>b~{h5jDAU$XYCA*`RZ!o=L8cwknEdU z${a9t?t~8>`F0}>oJVa`TWumJ*o|VqYEOfPy$$~2P5dQnRD0c@yODcz{se|^g1KO0 zPSySJyB8anOg3PC)qv@s34;Z73OcI+%zZ^O-;<|7{(B7E$DJy z(znE?c#HZCtjm%jzr-}SiS<>@z@jSUF?BP0_yUBrPiX0ds9-f`u2WA@m7f&>U?|00 zir2#cp{)oTSzRu$Lp;UPmQ_VkpVEM#>87aZ2=$_5zJXT|>pJdHC9%sxx9~*jQ&iKUnVYU+lRr54o{SQi9I*ih~Y7s#z?q z>017f1#rj?@s!&}&~}8gK!hR|6e)`h_!)vEk(`rF1F|nF3O$w86a{BlQlhGv`0(#+ zsaDw#*MQ2R4rKbFBLQL)(?FGAQ8FP>hjUeXE=mZ|K(T-r`OcWn)&s_X-i>U zQ}N*?*7h33)eXpndh^&Yq#UN_3OZf`Wgx1o<3r42b6rC~;T3zzGAz0Vf(rT=^j~`B z2JeAyh{&5=*ML|(V| z++(jNI(vtp!5KMs12$HM)h+eUu1k8gxdc0-+FZsjOsCngs2SjW6VI!#Lva-wyy+C7 zg9*-Pnt~9ySish8zEcDjKK)P?p+$t3b^{7^*8Q4q`>_&J+!Xn@lbKKsG7wg`~qhoA${n%rav_b zuq%cb-9yLkXG6oo!y{sqrgULTXYXCMVLw_%6o`3M|NF81^s8GHH{aoTb+_W<{P8~p zEMIRq-2G#?lXUfevuSnp@4Me7$%;r+opc$cF2m|FNXRUQyGSIpeb$Pk_LdGhNF-B= zWUNSrgl>H}1OpSL$b=P{AfeC7;a-v$e(kXm!+R!4OqCK-R$_{Tu9w3-Bz|>!(~4g` z7$NbwQhd&e&yi53((37GuY|a8_x7XjCP-|w;u1RBD`$fKz&j4i5i^zOap2MaFK`l> zEJY@*$Rr6}{U2c`N#DflHLGvp;3DbEl=?DOpOa`kpHgDfN{o`w7;r~=2DcwtJ%f8U z50*&Je5q&N>X|2@J9d_%EX&dTj<*td^n_rU#5YRu4J*DuLQl)#3nV%II%6fr_s8GP zlH_73xo9O9N$5+Px-mxG*!~borVy4o6tkL;T(dZnB4s7>X z(SbdKM6Z>i*R1F@5}GQ9VW6K4^zU^XTp-c8QgqIWvKlIfqa=2DJ7dKz?~NbKlGyE1 z?6wuVO+xb@43%POE0!jqp>lYD3|{~4p*47&-1;4PTp*cekmc|BS(g5y@POUYON<4u zzyest!m>~fM@aX;_Jq|vK*s;zd|>2`u)qM1geUA46WL6#FcU1B`8CX}OL9^xh<%X7 z4%M`8zTNy`%UTevg>`Fz1$9HX%RGE3*p7O>X0E}4Yp~3fYsZ-W6~L&!K5n1C_L1wHHjkJW z+tm|j{p6c+PV>)o3A*fmGGi4?#l`b{#l!LE372p_W$qGb_NjQLcu$3EG~WcQQ1Trc KIBGI#$M9cToe-V? literal 0 HcmV?d00001 diff --git a/core/migrations/__pycache__/0002_seed_catalog.cpython-311.pyc b/core/migrations/__pycache__/0002_seed_catalog.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..79eb48ef680654c47cfdba9a2767cc199938baac GIT binary patch literal 4675 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 Mb$s_dy}ncb0tdVoWB>pF literal 0 HcmV?d00001 diff --git a/core/migrations/__pycache__/__init__.cpython-311.pyc b/core/migrations/__pycache__/__init__.cpython-311.pyc index 799581567d2858c54584d0675ede42b2d728f5cb..39dfe21efef834e3ed8030d111998b7c85b2d22a 100644 GIT binary patch delta 19 ZcmZ3%xPp;;IWI340}wQpU!KT47XU6=1uXyo delta 19 ZcmZ3%xPp;;IWI340}#~tteD6>7XU291k?Zk diff --git a/core/models.py b/core/models.py index 71a8362..a13c606 100644 --- a/core/models.py +++ b/core/models.py @@ -1,3 +1,81 @@ +from decimal import Decimal + from django.db import models -# Create your models here. + +class Category(models.Model): + name = models.CharField(max_length=120, unique=True) + slug = models.SlugField(unique=True) + description = models.TextField(blank=True) + sort_order = models.PositiveIntegerField(default=0) + + class Meta: + ordering = ["sort_order", "name"] + + def __str__(self): + return self.name + + +class Product(models.Model): + category = models.ForeignKey(Category, on_delete=models.PROTECT, related_name="products") + name = models.CharField(max_length=160) + slug = models.SlugField(unique=True) + short_description = models.CharField(max_length=220) + description = models.TextField() + material = models.CharField(max_length=120, blank=True) + price_from = models.DecimalField(max_digits=10, decimal_places=2, blank=True, null=True) + collection_note = models.CharField(max_length=180, blank=True) + is_featured = models.BooleanField(default=False) + is_active = models.BooleanField(default=True) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + ordering = ["name"] + + def __str__(self): + return self.name + + @property + def price_label(self): + if self.price_from is None: + return "Preis auf Anfrage" + quantized = self.price_from.quantize(Decimal("1.00")) + return f"ab {quantized:,.2f} €".replace(",", "X").replace(".", ",").replace("X", ".") + + +class ContactInquiry(models.Model): + class PreferredContactMethod(models.TextChoices): + PHONE = "phone", "Telefon" + EMAIL = "email", "E-Mail" + + class Status(models.TextChoices): + NEW = "new", "Neu" + CONTACTED = "contacted", "Kontaktiert" + CLOSED = "closed", "Abgeschlossen" + + product = models.ForeignKey( + Product, + on_delete=models.SET_NULL, + related_name="inquiries", + blank=True, + null=True, + ) + name = models.CharField(max_length=120) + email = models.EmailField() + phone = models.CharField(max_length=40, blank=True) + subject = models.CharField(max_length=160, blank=True) + message = models.TextField() + preferred_contact_method = models.CharField( + max_length=10, + choices=PreferredContactMethod.choices, + default=PreferredContactMethod.PHONE, + ) + status = models.CharField(max_length=20, choices=Status.choices, default=Status.NEW) + created_at = models.DateTimeField(auto_now_add=True) + + class Meta: + ordering = ["-created_at"] + + def __str__(self): + return f"{self.name} – {self.subject or 'Allgemeine Anfrage'}" diff --git a/core/templates/base.html b/core/templates/base.html index 1e7e5fb..7ee44b3 100644 --- a/core/templates/base.html +++ b/core/templates/base.html @@ -1,25 +1,81 @@ +{% load static %} - - + - {% block title %}Knowledge Base{% endblock %} - {% if project_description %} - - - - {% endif %} + + {{ meta_title|default:store_name }} + {% if project_image_url %} {% endif %} - {% load static %} + + + + {% block head %}{% endblock %} - - {% block content %}{% endblock %} - +
+ +
+ {% block content %}{% endblock %} +
+ + +
+ + + diff --git a/core/templates/core/catalog.html b/core/templates/core/catalog.html new file mode 100644 index 0000000..41bc786 --- /dev/null +++ b/core/templates/core/catalog.html @@ -0,0 +1,73 @@ +{% extends "base.html" %} + +{% block content %} +
+
+ Katalog +

Entdecken Sie Schmuck, Uhren und Trauringe.

+

Filterbar, fokussiert und bereit für die direkte Kontaktaufnahme – ohne komplizierten Checkout.

+
+
+ +
+
+
+
+
+ + +
+
+ + +
+
+ + Zurücksetzen +
+
+
+ +
+ {% for product in products %} +
+
+
+ {{ product.category.name }} +
+
+

{{ product.category.name }}

+

{{ product.name }}

+

{{ product.short_description }}

+
+ {{ product.price_label }} + {% if product.material %}{{ product.material }}{% endif %} +
+ +
+
+
+ {% empty %} +
+
+

Keine Treffer gefunden

+

Passen Sie die Filter an oder nehmen Sie direkt Kontakt auf, damit wir Ihnen passende Stücke empfehlen können.

+ +
+
+ {% endfor %} +
+
+
+{% endblock %} diff --git a/core/templates/core/contact.html b/core/templates/core/contact.html new file mode 100644 index 0000000..2174c82 --- /dev/null +++ b/core/templates/core/contact.html @@ -0,0 +1,72 @@ +{% extends "base.html" %} + +{% block content %} +
+
+ Kontakt +

Schnell anfragen, persönlich beraten lassen.

+

Die Anfrage wird gespeichert, im Admin sichtbar gemacht und auf Wunsch zusätzlich per E-Mail versendet.

+
+
+ +
+
+
+
+ +
+
+
+
+ Anfrageformular +

Per E-Mail anfragen

+
+
+ {% csrf_token %} + {% if form.non_field_errors %} +
{{ form.non_field_errors }}
+ {% endif %} +
+ {% for field in form %} +
+ + {{ field }} + {% if field.help_text %}
{{ field.help_text }}
{% endif %} + {% for error in field.errors %} +
{{ error }}
+ {% endfor %} +
+ {% endfor %} +
+
+ +

Wir melden uns zeitnah telefonisch oder per E-Mail zurück.

+
+
+
+
+
+
+
+{% endblock %} diff --git a/core/templates/core/contact_success.html b/core/templates/core/contact_success.html new file mode 100644 index 0000000..6338cd9 --- /dev/null +++ b/core/templates/core/contact_success.html @@ -0,0 +1,24 @@ +{% extends "base.html" %} + +{% block content %} +
+
+
+ Vielen Dank +

Ihre Anfrage ist eingegangen.

+

Das Team von {{ store_name }} meldet sich so schnell wie möglich bei Ihnen.

+ {% if inquiry %} +
+
Kontakt: {{ inquiry.name }}
+
Rückmeldung via: {{ inquiry.get_preferred_contact_method_display }}
+
Produkt: {% if inquiry.product %}{{ inquiry.product.name }}{% else %}Allgemeine Beratung{% endif %}
+
+ {% endif %} + +
+
+
+{% endblock %} diff --git a/core/templates/core/index.html b/core/templates/core/index.html index faec813..942ee3d 100644 --- a/core/templates/core/index.html +++ b/core/templates/core/index.html @@ -1,145 +1,171 @@ {% extends "base.html" %} - -{% block title %}{{ project_name }}{% endblock %} - -{% block head %} - - - - -{% endblock %} +{% load static %} {% block content %} -
-
-

Analyzing your requirements and generating your app…

-
- Loading… +
+
+
+
+ Seit über 80 Jahren Vertrauen, Qualität und persönliche Beratung +

Ein moderner Schmuckkatalog für Trauringe, Uhren und Goldschmiedeservice.

+

Entdecken Sie ausgewählte Kollektionen, lernen Sie unsere Services kennen und wechseln Sie mit einem Klick vom Stöbern zur persönlichen Beratung – telefonisch oder per E-Mail.

+ + +
+
+
+
+
+

Store Snapshot

+
+
+ 80+ + Jahre Erfahrung +
+
+ 4 + klare Katalogbereiche +
+
+ 1 + zentrale Anfrage pro Produkt +
+
+
+

Wiesdorfer Platz 59

+

51373 Leverkusen · Beratung per Telefon und E-Mail

+
+
+
-

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" }} -

-
-
- Page updated: {{ current_time|date:"Y-m-d H:i:s" }} (UTC) -
-{% endblock %} \ No newline at end of file + + +
+ +
+ +
+
+
+ Leistungen +

Die wichtigsten Inhalte der bisherigen Website – jetzt klarer, schneller, moderner.

+

Die neue Startseite bündelt Trauringe, Schmuck, Uhren und Serviceleistungen in einem ruhigen, leicht navigierbaren Ablauf.

+
+
+ {% for item in service_highlights %} +
+
+
{{ forloop.counter|stringformat:"02d" }}
+

{{ item.title }}

+

{{ item.text }}

+
+
+ {% endfor %} +
+
+
+ +
+
+
+
+ Auswahl +

Featured Katalog

+
+ Alle Produkte ansehen +
+
+ {% for product in featured_products %} +
+
+
+ {{ product.category.name }} +
+
+
+
+

{{ product.category.name }}

+

{{ product.name }}

+
+ {{ product.price_label }} +
+

{{ product.short_description }}

+ Details & Anfrage +
+
+
+ {% empty %} +
+
+

Der Katalog ist bereit.

+

Pflegen Sie jetzt Produkte im Admin ein, damit die Startseite automatisch die ersten Highlights zeigt.

+ Produkte im Admin anlegen +
+
+ {% endfor %} +
+
+
+ +
+
+
+
+ Ablauf +

Vom Produkt zur persönlichen Beratung in drei einfachen Schritten.

+
+
+ {% for step in process_steps %} +
+
+ {{ step.step }} +

{{ step.title }}

+

{{ step.text }}

+
+
+ {% endfor %} +
+
+
+
+ +
+
+
+
+ Kontakt +

Sie möchten lieber direkt sprechen?

+

Rufen Sie an oder senden Sie eine E-Mail – alle Produktseiten führen direkt in die Kontaktstrecke.

+
+ +
+
+
+{% endblock %} diff --git a/core/templates/core/product_detail.html b/core/templates/core/product_detail.html new file mode 100644 index 0000000..16d60a8 --- /dev/null +++ b/core/templates/core/product_detail.html @@ -0,0 +1,76 @@ +{% extends "base.html" %} + +{% block content %} +
+
+ ← Zurück zum Katalog +
+
+ {{ product.category.name }} +

{{ product.name }}

+

{{ product.short_description }}

+
+
+
+

{{ product.price_label }}

+ {% if product.material %}

Material · {{ product.material }}

{% endif %} + {% if product.collection_note %}

{{ product.collection_note }}

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

Über dieses Stück

+

{{ product.description }}

+
+
+
+ +
+
+
+
+ +{% if related_products %} +
+
+
+ Passend dazu +

Weitere Stücke aus {{ product.category.name }}

+
+
+ {% for item in related_products %} +
+
+
{{ item.category.name }}
+
+

{{ item.category.name }}

+

{{ item.name }}

+

{{ item.short_description }}

+ Details ansehen +
+
+
+ {% endfor %} +
+
+
+{% endif %} +{% endblock %} diff --git a/core/tests.py b/core/tests.py index 7ce503c..931b5ce 100644 --- a/core/tests.py +++ b/core/tests.py @@ -1,3 +1,47 @@ from django.test import TestCase +from django.urls import reverse -# Create your tests here. +from .models import Category, ContactInquiry, Product + + +class CatalogFlowTests(TestCase): + def setUp(self): + self.category = Category.objects.create( + name="Trauringe", + slug="trauringe", + description="Individuelle Ringe und Beratung.", + sort_order=1, + ) + self.product = Product.objects.create( + category=self.category, + name="Signature Trauring", + slug="signature-trauring", + short_description="Ein klassischer Ring mit warmem Goldton.", + description="Ausgewählte Trauringe mit Gravuroption und persönlicher Beratung.", + material="Gold", + price_from="1290.00", + is_featured=True, + ) + + def test_homepage_uses_catalog_content(self): + response = self.client.get(reverse("home")) + self.assertContains(response, self.product.name) + self.assertContains(response, self.category.name) + + def test_contact_form_creates_inquiry(self): + response = self.client.post( + reverse("contact"), + { + "product": self.product.pk, + "name": "Anna Becker", + "email": "anna@example.com", + "phone": "+49 123", + "preferred_contact_method": "phone", + "subject": "Beratung", + "message": "Ich möchte einen Termin für Trauringe.", + }, + ) + self.assertRedirects(response, reverse("contact_success")) + self.assertEqual(ContactInquiry.objects.count(), 1) + inquiry = ContactInquiry.objects.first() + self.assertEqual(inquiry.product, self.product) diff --git a/core/urls.py b/core/urls.py index 6299e3d..442aad9 100644 --- a/core/urls.py +++ b/core/urls.py @@ -1,7 +1,11 @@ from django.urls import path -from .views import home +from .views import catalog, contact, contact_success, home, product_detail urlpatterns = [ path("", home, name="home"), + path("katalog/", catalog, name="catalog"), + path("katalog//", product_detail, name="product_detail"), + path("kontakt/", contact, name="contact"), + path("kontakt/danke/", contact_success, name="contact_success"), ] diff --git a/core/views.py b/core/views.py index c9aed12..c2fc1ff 100644 --- a/core/views.py +++ b/core/views.py @@ -1,25 +1,186 @@ -import os -import platform +from django.conf import settings +from django.core.mail import send_mail +from django.db.models import Count, Q +from django.shortcuts import get_object_or_404, redirect, render -from django import get_version as django_version -from django.shortcuts import render -from django.utils import timezone +from .forms import ContactInquiryForm +from .models import Category, ContactInquiry, Product + + +SERVICE_HIGHLIGHTS = [ + { + "title": "Trauringe & Beratung", + "text": "Individuelle Beratung für Trauringe, Materialien und Gravuren – persönlich im Geschäft oder per Anfrage.", + }, + { + "title": "Goldschmiedeservice", + "text": "Ringweitenänderung, Reinigung, Umarbeitung und Reparaturen mit viel Erfahrung und Fingerspitzengefühl.", + }, + { + "title": "Uhren & Revision", + "text": "Ausgewählte Uhren und fachkundige Unterstützung bei Pflege, Wartung und Reparatur wertvoller Zeitmesser.", + }, + { + "title": "Perlen, Ohrschmuck & Extras", + "text": "Perlkettenservice, Ohrlochstechen mit STUDEX-System sowie Geschenkgutscheine für besondere Anlässe.", + }, +] + + +PROCESS_STEPS = [ + { + "step": "01", + "title": "Kollektion entdecken", + "text": "Filtern Sie die Kollektion nach Kategorie und lassen Sie sich inspirieren.", + }, + { + "step": "02", + "title": "Per Telefon oder E-Mail anfragen", + "text": "Zu jedem Produkt gelangen Sie in wenigen Klicks zur passenden Kontaktmöglichkeit.", + }, + { + "step": "03", + "title": "Persönliche Beratung im Store", + "text": "Im Geschäft in Leverkusen klärt das Team Details, Anpassungen und Verfügbarkeit.", + }, +] + + +def _catalog_categories(): + return Category.objects.annotate( + product_total=Count("products", filter=Q(products__is_active=True)) + ) + + +def _send_inquiry_notification(inquiry): + if not settings.CONTACT_EMAIL_TO: + return + + subject = inquiry.subject or "Neue Anfrage über die Website" + lines = [ + f"Name: {inquiry.name}", + f"E-Mail: {inquiry.email}", + f"Telefon: {inquiry.phone or '-'}", + f"Bevorzugte Kontaktart: {inquiry.get_preferred_contact_method_display()}", + f"Produkt: {inquiry.product.name if inquiry.product else 'Allgemeine Beratung'}", + "", + inquiry.message, + ] + + try: + send_mail( + subject=f"Website-Anfrage: {subject}", + message="\n".join(lines), + from_email=settings.DEFAULT_FROM_EMAIL, + recipient_list=settings.CONTACT_EMAIL_TO, + fail_silently=True, + reply_to=[inquiry.email], + ) + except Exception: + return 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() + featured_products = Product.objects.filter(is_active=True, is_featured=True).select_related("category")[:4] + categories = _catalog_categories().filter(product_total__gt=0) 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", ""), + "meta_title": "Juwelier Thelen | Moderner Schmuck- & Trauringkatalog", + "meta_description": "Moderner Katalog für Trauringe, Schmuck, Uhren und Goldschmiedeservice – mit schneller Kontaktaufnahme per Telefon oder E-Mail.", + "featured_products": featured_products, + "categories": categories, + "service_highlights": SERVICE_HIGHLIGHTS, + "process_steps": PROCESS_STEPS, } return render(request, "core/index.html", context) + + +def catalog(request): + selected_category = request.GET.get("category", "").strip() + query = request.GET.get("q", "").strip() + + products = Product.objects.filter(is_active=True).select_related("category") + if selected_category: + products = products.filter(category__slug=selected_category) + if query: + products = products.filter( + Q(name__icontains=query) + | Q(short_description__icontains=query) + | Q(description__icontains=query) + | Q(material__icontains=query) + | Q(category__name__icontains=query) + ) + + context = { + "meta_title": "Katalog | Juwelier Thelen", + "meta_description": "Entdecken Sie Trauringe, Schmuck und Uhren – klar gefiltert und bereit für eine persönliche Beratung.", + "products": products, + "categories": _catalog_categories(), + "selected_category": selected_category, + "query": query, + } + return render(request, "core/catalog.html", context) + + +def product_detail(request, slug): + product = get_object_or_404( + Product.objects.filter(is_active=True).select_related("category"), + slug=slug, + ) + related_products = ( + Product.objects.filter(is_active=True, category=product.category) + .exclude(pk=product.pk) + .select_related("category")[:3] + ) + + context = { + "meta_title": f"{product.name} | Juwelier Thelen", + "meta_description": product.short_description, + "product": product, + "related_products": related_products, + } + return render(request, "core/product_detail.html", context) + + +def contact(request): + selected_product = None + product_slug = request.GET.get("product", "").strip() + + if product_slug: + selected_product = Product.objects.filter(is_active=True, slug=product_slug).first() + + if request.method == "POST": + form = ContactInquiryForm(request.POST) + if form.is_valid(): + inquiry = form.save() + _send_inquiry_notification(inquiry) + request.session["contact_inquiry_id"] = inquiry.pk + return redirect("contact_success") + else: + initial = {} + if selected_product: + initial["product"] = selected_product + initial["subject"] = f"Anfrage zu {selected_product.name}" + form = ContactInquiryForm(initial=initial) + + context = { + "meta_title": "Kontakt | Juwelier Thelen", + "meta_description": "Fragen zu Trauringen, Schmuck, Uhren oder Serviceleistungen? Kontaktieren Sie Juwelier Thelen per Telefon oder E-Mail.", + "form": form, + "selected_product": selected_product, + } + return render(request, "core/contact.html", context) + + +def contact_success(request): + inquiry_id = request.session.get("contact_inquiry_id") + inquiry = None + if inquiry_id: + inquiry = ContactInquiry.objects.filter(pk=inquiry_id).select_related("product").first() + + context = { + "meta_title": "Anfrage erhalten | Juwelier Thelen", + "meta_description": "Vielen Dank für Ihre Anfrage. Das Team meldet sich zeitnah telefonisch oder per E-Mail zurück.", + "inquiry": inquiry, + } + return render(request, "core/contact_success.html", context) diff --git a/static/css/custom.css b/static/css/custom.css index 925f6ed..ce06a33 100644 --- a/static/css/custom.css +++ b/static/css/custom.css @@ -1,4 +1,632 @@ -/* Custom styles for the application */ -body { - font-family: system-ui, -apple-system, sans-serif; +/* Brand system */ +:root { + --color-bg: #f5efe7; + --color-bg-soft: #ece4d7; + --color-surface: rgba(255, 250, 244, 0.82); + --color-surface-strong: #fffaf4; + --color-ink: #171616; + --color-muted: #5f605f; + --color-brand: #b08a52; + --color-brand-dark: #8d6a37; + --color-accent: #1d4339; + --color-accent-soft: #dce9e3; + --color-line: rgba(23, 22, 22, 0.08); + --shadow-soft: 0 24px 70px rgba(28, 24, 19, 0.12); + --shadow-card: 0 18px 40px rgba(43, 31, 18, 0.1); + --radius-xl: 32px; + --radius-lg: 24px; + --radius-md: 18px; + --radius-sm: 14px; + --space-section: clamp(4.5rem, 7vw, 7rem); +} + +* { + box-sizing: border-box; +} + +html { + scroll-behavior: smooth; +} + +body { + margin: 0; + font-family: 'Inter', system-ui, -apple-system, sans-serif; + color: var(--color-ink); + background: + radial-gradient(circle at top left, rgba(176, 138, 82, 0.18), transparent 30%), + radial-gradient(circle at 85% 15%, rgba(29, 67, 57, 0.14), transparent 26%), + linear-gradient(180deg, #fbf7f1 0%, var(--color-bg) 46%, #efe8dc 100%); + line-height: 1.6; +} + +body::before, +body::after { + content: ""; + position: fixed; + inset: auto; + border-radius: 50%; + filter: blur(10px); + pointer-events: none; + z-index: -1; +} + +body::before { + width: 18rem; + height: 18rem; + top: 6rem; + right: 4vw; + background: rgba(176, 138, 82, 0.12); +} + +body::after { + width: 12rem; + height: 12rem; + bottom: 10rem; + left: 3vw; + background: rgba(29, 67, 57, 0.1); +} + +h1, +h2, +h3, +h4, +h5, +h6, +.navbar-brand strong, +.footer-title { + font-family: 'Cormorant Garamond', Georgia, serif; + letter-spacing: -0.02em; + line-height: 1.05; +} + +a { + color: inherit; + text-decoration: none; +} + +a:hover { + color: var(--color-brand-dark); +} + +.site-header { + background: rgba(251, 247, 241, 0.78); + backdrop-filter: blur(24px); + border-bottom: 1px solid rgba(23, 22, 22, 0.05); +} + +.navbar-brand { + display: inline-flex; + align-items: center; + gap: 0.85rem; + font-size: 1.05rem; +} + +.navbar-brand small { + display: block; + color: var(--color-muted); + font-size: 0.78rem; + letter-spacing: 0.12em; + text-transform: uppercase; +} + +.brand-mark { + width: 2.6rem; + height: 2.6rem; + display: inline-grid; + place-items: center; + border-radius: 50%; + background: linear-gradient(135deg, var(--color-brand), #ddc08b); + color: white; + font-family: 'Cormorant Garamond', Georgia, serif; + font-size: 1.45rem; + box-shadow: 0 12px 30px rgba(176, 138, 82, 0.3); +} + +.nav-link { + color: var(--color-ink); + font-weight: 500; + padding: 0.75rem 1rem !important; +} + +.nav-link:hover, +.nav-link:focus { + color: var(--color-brand-dark); +} + +.section-shell { + padding: var(--space-section) 0; +} + +.page-hero-compact { + padding-bottom: 2.5rem; +} + +.hero-section { + position: relative; + overflow: hidden; +} + +.hero-title, +.page-title { + font-size: clamp(3rem, 5vw, 5.4rem); + margin-bottom: 1.4rem; +} + +.hero-copy, +.page-copy, +.section-heading p, +.product-body p, +.service-card p, +.process-card p, +.contact-info p, +.detail-copy p, +.detail-side p, +.form-note, +.footer-title + p { + color: var(--color-muted); + font-size: 1.05rem; +} + +.hero-copy, +.page-copy { + max-width: 44rem; +} + +.eyebrow { + display: inline-block; + margin-bottom: 1rem; + color: var(--color-accent); + text-transform: uppercase; + letter-spacing: 0.18em; + font-size: 0.77rem; + font-weight: 700; +} + +.glass-panel, +.product-card, +.service-card, +.category-card, +.empty-state, +.process-shell, +.success-card { + background: linear-gradient(180deg, rgba(255, 250, 244, 0.9), rgba(255, 255, 255, 0.72)); + border: 1px solid rgba(255, 255, 255, 0.7); + box-shadow: var(--shadow-soft); + backdrop-filter: blur(18px); + border-radius: var(--radius-xl); +} + +.hero-search, +.filter-panel, +.form-panel { + padding: 1.35rem; +} + +.hero-actions { + margin-top: 1.8rem; +} + +.hero-showcase { + padding: 2rem; + min-height: 27rem; + position: relative; + overflow: hidden; +} + +.hero-orb { + position: absolute; + border-radius: 50%; + background: linear-gradient(180deg, rgba(255, 255, 255, 0.9), rgba(176, 138, 82, 0.12)); + border: 1px solid rgba(176, 138, 82, 0.16); +} + +.hero-orb-lg { + width: 13rem; + height: 13rem; + top: 2rem; + right: -1rem; +} + +.hero-orb-sm { + width: 5rem; + height: 5rem; + left: 1rem; + bottom: 2rem; +} + +.stat-grid { + position: relative; + z-index: 1; + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 1rem; +} + +.stat-card { + padding: 1.4rem; + border-radius: var(--radius-md); + background: rgba(255, 255, 255, 0.72); + border: 1px solid rgba(23, 22, 22, 0.05); + box-shadow: var(--shadow-card); +} + +.stat-card strong { + display: block; + font-family: 'Cormorant Garamond', Georgia, serif; + font-size: 2.4rem; +} + +.stat-card span, +.showcase-note, +.product-category, +.detail-label { + color: var(--color-muted); +} + +.showcase-note { + position: absolute; + left: 2rem; + right: 2rem; + bottom: 2rem; + padding: 1.1rem 1.2rem; + border-radius: var(--radius-md); + background: rgba(29, 67, 57, 0.08); +} + +.category-card, +.service-card, +.product-card { + padding: 1.6rem; + display: flex; + flex-direction: column; + height: 100%; + transition: transform 0.25s ease, box-shadow 0.25s ease; +} + +.category-card:hover, +.service-card:hover, +.product-card:hover, +.contact-banner:hover { + transform: translateY(-4px); + box-shadow: 0 28px 70px rgba(28, 24, 19, 0.16); +} + +.category-chip, +.product-visual span, +.process-step { + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 999px; + padding: 0.35rem 0.75rem; + font-size: 0.78rem; + font-weight: 700; + letter-spacing: 0.06em; +} + +.category-chip, +.process-step { + background: rgba(29, 67, 57, 0.09); + color: var(--color-accent); +} + +.product-visual { + min-height: 12rem; + border-radius: var(--radius-lg); + margin-bottom: 1.25rem; + background: + radial-gradient(circle at 28% 28%, rgba(255, 255, 255, 0.95), transparent 36%), + linear-gradient(140deg, rgba(176, 138, 82, 0.22), rgba(29, 67, 57, 0.18)); + position: relative; + overflow: hidden; +} + +.product-visual::before, +.product-visual::after { + content: ""; + position: absolute; + border-radius: 999px; + border: 1px solid rgba(255, 255, 255, 0.75); +} + +.product-visual::before { + width: 7rem; + height: 7rem; + right: 1.25rem; + top: 1.25rem; +} + +.product-visual::after { + width: 3.6rem; + height: 3.6rem; + left: 1.4rem; + bottom: 1.1rem; +} + +.product-visual span { + position: absolute; + left: 1rem; + top: 1rem; + background: rgba(255, 250, 244, 0.9); + color: var(--color-accent); +} + +.product-body { + display: flex; + flex-direction: column; + flex: 1; +} + +.product-body h2, +.product-body h3, +.service-card h3, +.category-card h2, +.process-card h3, +.contact-banner h2, +.contact-info h2, +.detail-copy h2, +.detail-side h2, +.form-panel h2 { + font-size: clamp(1.8rem, 2vw, 2.4rem); + margin-bottom: 0.9rem; +} + +.product-category { + margin-bottom: 0.25rem; + text-transform: uppercase; + letter-spacing: 0.12em; + font-size: 0.75rem; + font-weight: 700; +} + +.product-price, +.detail-price { + color: var(--color-brand-dark); + font-size: 1.1rem; +} + +.product-meta { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + margin-top: auto; + padding-top: 1rem; + color: var(--color-muted); +} + +.text-link, +.back-link { + color: var(--color-accent); + font-weight: 700; +} + +.text-link:hover, +.back-link:hover { + color: var(--color-brand-dark); +} + +.section-heading { + max-width: 42rem; + margin-bottom: 2rem; +} + +.section-heading h2, +.footer-title { + font-size: clamp(2.4rem, 3vw, 3.6rem); + margin-bottom: 0.8rem; +} + +.service-icon { + width: 3rem; + height: 3rem; + display: inline-grid; + place-items: center; + border-radius: 50%; + margin-bottom: 1rem; + background: rgba(176, 138, 82, 0.12); + color: var(--color-brand-dark); + font-weight: 700; +} + +.process-shell, +.contact-banner, +.success-card, +.detail-card, +.detail-copy, +.detail-side, +.contact-info { + padding: 2rem; +} + +.process-card { + padding: 1.2rem; + border-radius: var(--radius-md); + background: rgba(255, 255, 255, 0.55); + border: 1px solid rgba(23, 22, 22, 0.05); + height: 100%; +} + +.contact-banner { + padding: 2rem; + display: flex; + align-items: center; + justify-content: space-between; + gap: 2rem; +} + +.contact-method { + margin-bottom: 1.5rem; +} + +.contact-method span, +.footer-heading { + display: block; + margin-bottom: 0.35rem; + color: var(--color-muted); + font-size: 0.84rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.12em; +} + +.contact-method a, +.site-footer a { + font-size: 1.05rem; + font-weight: 600; +} + +.selected-product-note { + margin-top: 2rem; + padding: 1.25rem; + border-radius: var(--radius-md); + background: rgba(29, 67, 57, 0.08); +} + +.site-footer { + border-top: 1px solid rgba(23, 22, 22, 0.06); + background: rgba(255, 250, 244, 0.65); + backdrop-filter: blur(18px); +} + +.btn { + border-radius: 999px; + padding: 0.9rem 1.45rem; + font-weight: 700; + letter-spacing: 0.01em; + border-width: 1px; + transition: transform 0.2s ease, box-shadow 0.2s ease, background-color 0.2s ease; +} + +.btn:hover, +.btn:focus { + transform: translateY(-1px); +} + +.btn-brand { + color: #fff; + background: linear-gradient(135deg, var(--color-brand-dark), var(--color-brand)); + border-color: transparent; + box-shadow: 0 16px 32px rgba(176, 138, 82, 0.28); +} + +.btn-brand:hover, +.btn-brand:focus { + color: #fff; + background: linear-gradient(135deg, #7a5b2f, var(--color-brand-dark)); +} + +.btn-ghost { + color: var(--color-ink); + background: rgba(255, 255, 255, 0.58); + border-color: rgba(23, 22, 22, 0.08); +} + +.btn-ghost:hover, +.btn-ghost:focus { + background: rgba(255, 255, 255, 0.88); + border-color: rgba(23, 22, 22, 0.14); +} + +.form-control, +.form-select { + min-height: 3.4rem; + border-radius: 1rem; + border: 1px solid rgba(23, 22, 22, 0.1); + background: rgba(255, 255, 255, 0.92); + color: var(--color-ink); + padding: 0.85rem 1rem; +} + +textarea.form-control { + min-height: 9rem; +} + +.form-control:focus, +.form-select:focus, +.btn:focus, +.nav-link:focus, +a:focus { + outline: none; + border-color: rgba(176, 138, 82, 0.5); + box-shadow: 0 0 0 0.2rem rgba(176, 138, 82, 0.16); +} + +.field-error { + color: #9b2d2d; + font-size: 0.88rem; + margin-top: 0.45rem; +} + +.empty-state, +.success-card { + text-align: center; + padding: 3rem 2rem; +} + +.success-card { + max-width: 48rem; +} + +.success-summary { + margin-top: 1.5rem; + padding: 1.2rem; + border-radius: var(--radius-md); + background: rgba(29, 67, 57, 0.08); +} + +.filter-panel { + position: relative; + z-index: 2; +} + +.detail-list { + padding-left: 1.1rem; + margin: 0; +} + +.detail-list li { + margin-bottom: 0.8rem; + color: var(--color-muted); +} + +.back-link { + display: inline-block; + margin-bottom: 1.25rem; +} + +@media (max-width: 991.98px) { + .hero-showcase { + min-height: auto; + } + + .contact-banner { + flex-direction: column; + align-items: flex-start; + } +} + +@media (max-width: 767.98px) { + .section-shell { + padding: 4rem 0; + } + + .hero-title, + .page-title { + font-size: 2.6rem; + } + + .glass-panel, + .product-card, + .service-card, + .category-card, + .contact-banner, + .success-card, + .process-shell, + .detail-card, + .detail-copy, + .detail-side, + .contact-info { + border-radius: 1.5rem; + } } diff --git a/staticfiles/css/custom.css b/staticfiles/css/custom.css index 108056f..ce06a33 100644 --- a/staticfiles/css/custom.css +++ b/staticfiles/css/custom.css @@ -1,21 +1,632 @@ - +/* Brand system */ :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); + --color-bg: #f5efe7; + --color-bg-soft: #ece4d7; + --color-surface: rgba(255, 250, 244, 0.82); + --color-surface-strong: #fffaf4; + --color-ink: #171616; + --color-muted: #5f605f; + --color-brand: #b08a52; + --color-brand-dark: #8d6a37; + --color-accent: #1d4339; + --color-accent-soft: #dce9e3; + --color-line: rgba(23, 22, 22, 0.08); + --shadow-soft: 0 24px 70px rgba(28, 24, 19, 0.12); + --shadow-card: 0 18px 40px rgba(43, 31, 18, 0.1); + --radius-xl: 32px; + --radius-lg: 24px; + --radius-md: 18px; + --radius-sm: 14px; + --space-section: clamp(4.5rem, 7vw, 7rem); } + +* { + box-sizing: border-box; +} + +html { + scroll-behavior: smooth; +} + body { 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; - min-height: 100vh; - text-align: center; - overflow: hidden; - position: relative; + font-family: 'Inter', system-ui, -apple-system, sans-serif; + color: var(--color-ink); + background: + radial-gradient(circle at top left, rgba(176, 138, 82, 0.18), transparent 30%), + radial-gradient(circle at 85% 15%, rgba(29, 67, 57, 0.14), transparent 26%), + linear-gradient(180deg, #fbf7f1 0%, var(--color-bg) 46%, #efe8dc 100%); + line-height: 1.6; +} + +body::before, +body::after { + content: ""; + position: fixed; + inset: auto; + border-radius: 50%; + filter: blur(10px); + pointer-events: none; + z-index: -1; +} + +body::before { + width: 18rem; + height: 18rem; + top: 6rem; + right: 4vw; + background: rgba(176, 138, 82, 0.12); +} + +body::after { + width: 12rem; + height: 12rem; + bottom: 10rem; + left: 3vw; + background: rgba(29, 67, 57, 0.1); +} + +h1, +h2, +h3, +h4, +h5, +h6, +.navbar-brand strong, +.footer-title { + font-family: 'Cormorant Garamond', Georgia, serif; + letter-spacing: -0.02em; + line-height: 1.05; +} + +a { + color: inherit; + text-decoration: none; +} + +a:hover { + color: var(--color-brand-dark); +} + +.site-header { + background: rgba(251, 247, 241, 0.78); + backdrop-filter: blur(24px); + border-bottom: 1px solid rgba(23, 22, 22, 0.05); +} + +.navbar-brand { + display: inline-flex; + align-items: center; + gap: 0.85rem; + font-size: 1.05rem; +} + +.navbar-brand small { + display: block; + color: var(--color-muted); + font-size: 0.78rem; + letter-spacing: 0.12em; + text-transform: uppercase; +} + +.brand-mark { + width: 2.6rem; + height: 2.6rem; + display: inline-grid; + place-items: center; + border-radius: 50%; + background: linear-gradient(135deg, var(--color-brand), #ddc08b); + color: white; + font-family: 'Cormorant Garamond', Georgia, serif; + font-size: 1.45rem; + box-shadow: 0 12px 30px rgba(176, 138, 82, 0.3); +} + +.nav-link { + color: var(--color-ink); + font-weight: 500; + padding: 0.75rem 1rem !important; +} + +.nav-link:hover, +.nav-link:focus { + color: var(--color-brand-dark); +} + +.section-shell { + padding: var(--space-section) 0; +} + +.page-hero-compact { + padding-bottom: 2.5rem; +} + +.hero-section { + position: relative; + overflow: hidden; +} + +.hero-title, +.page-title { + font-size: clamp(3rem, 5vw, 5.4rem); + margin-bottom: 1.4rem; +} + +.hero-copy, +.page-copy, +.section-heading p, +.product-body p, +.service-card p, +.process-card p, +.contact-info p, +.detail-copy p, +.detail-side p, +.form-note, +.footer-title + p { + color: var(--color-muted); + font-size: 1.05rem; +} + +.hero-copy, +.page-copy { + max-width: 44rem; +} + +.eyebrow { + display: inline-block; + margin-bottom: 1rem; + color: var(--color-accent); + text-transform: uppercase; + letter-spacing: 0.18em; + font-size: 0.77rem; + font-weight: 700; +} + +.glass-panel, +.product-card, +.service-card, +.category-card, +.empty-state, +.process-shell, +.success-card { + background: linear-gradient(180deg, rgba(255, 250, 244, 0.9), rgba(255, 255, 255, 0.72)); + border: 1px solid rgba(255, 255, 255, 0.7); + box-shadow: var(--shadow-soft); + backdrop-filter: blur(18px); + border-radius: var(--radius-xl); +} + +.hero-search, +.filter-panel, +.form-panel { + padding: 1.35rem; +} + +.hero-actions { + margin-top: 1.8rem; +} + +.hero-showcase { + padding: 2rem; + min-height: 27rem; + position: relative; + overflow: hidden; +} + +.hero-orb { + position: absolute; + border-radius: 50%; + background: linear-gradient(180deg, rgba(255, 255, 255, 0.9), rgba(176, 138, 82, 0.12)); + border: 1px solid rgba(176, 138, 82, 0.16); +} + +.hero-orb-lg { + width: 13rem; + height: 13rem; + top: 2rem; + right: -1rem; +} + +.hero-orb-sm { + width: 5rem; + height: 5rem; + left: 1rem; + bottom: 2rem; +} + +.stat-grid { + position: relative; + z-index: 1; + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 1rem; +} + +.stat-card { + padding: 1.4rem; + border-radius: var(--radius-md); + background: rgba(255, 255, 255, 0.72); + border: 1px solid rgba(23, 22, 22, 0.05); + box-shadow: var(--shadow-card); +} + +.stat-card strong { + display: block; + font-family: 'Cormorant Garamond', Georgia, serif; + font-size: 2.4rem; +} + +.stat-card span, +.showcase-note, +.product-category, +.detail-label { + color: var(--color-muted); +} + +.showcase-note { + position: absolute; + left: 2rem; + right: 2rem; + bottom: 2rem; + padding: 1.1rem 1.2rem; + border-radius: var(--radius-md); + background: rgba(29, 67, 57, 0.08); +} + +.category-card, +.service-card, +.product-card { + padding: 1.6rem; + display: flex; + flex-direction: column; + height: 100%; + transition: transform 0.25s ease, box-shadow 0.25s ease; +} + +.category-card:hover, +.service-card:hover, +.product-card:hover, +.contact-banner:hover { + transform: translateY(-4px); + box-shadow: 0 28px 70px rgba(28, 24, 19, 0.16); +} + +.category-chip, +.product-visual span, +.process-step { + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 999px; + padding: 0.35rem 0.75rem; + font-size: 0.78rem; + font-weight: 700; + letter-spacing: 0.06em; +} + +.category-chip, +.process-step { + background: rgba(29, 67, 57, 0.09); + color: var(--color-accent); +} + +.product-visual { + min-height: 12rem; + border-radius: var(--radius-lg); + margin-bottom: 1.25rem; + background: + radial-gradient(circle at 28% 28%, rgba(255, 255, 255, 0.95), transparent 36%), + linear-gradient(140deg, rgba(176, 138, 82, 0.22), rgba(29, 67, 57, 0.18)); + position: relative; + overflow: hidden; +} + +.product-visual::before, +.product-visual::after { + content: ""; + position: absolute; + border-radius: 999px; + border: 1px solid rgba(255, 255, 255, 0.75); +} + +.product-visual::before { + width: 7rem; + height: 7rem; + right: 1.25rem; + top: 1.25rem; +} + +.product-visual::after { + width: 3.6rem; + height: 3.6rem; + left: 1.4rem; + bottom: 1.1rem; +} + +.product-visual span { + position: absolute; + left: 1rem; + top: 1rem; + background: rgba(255, 250, 244, 0.9); + color: var(--color-accent); +} + +.product-body { + display: flex; + flex-direction: column; + flex: 1; +} + +.product-body h2, +.product-body h3, +.service-card h3, +.category-card h2, +.process-card h3, +.contact-banner h2, +.contact-info h2, +.detail-copy h2, +.detail-side h2, +.form-panel h2 { + font-size: clamp(1.8rem, 2vw, 2.4rem); + margin-bottom: 0.9rem; +} + +.product-category { + margin-bottom: 0.25rem; + text-transform: uppercase; + letter-spacing: 0.12em; + font-size: 0.75rem; + font-weight: 700; +} + +.product-price, +.detail-price { + color: var(--color-brand-dark); + font-size: 1.1rem; +} + +.product-meta { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + margin-top: auto; + padding-top: 1rem; + color: var(--color-muted); +} + +.text-link, +.back-link { + color: var(--color-accent); + font-weight: 700; +} + +.text-link:hover, +.back-link:hover { + color: var(--color-brand-dark); +} + +.section-heading { + max-width: 42rem; + margin-bottom: 2rem; +} + +.section-heading h2, +.footer-title { + font-size: clamp(2.4rem, 3vw, 3.6rem); + margin-bottom: 0.8rem; +} + +.service-icon { + width: 3rem; + height: 3rem; + display: inline-grid; + place-items: center; + border-radius: 50%; + margin-bottom: 1rem; + background: rgba(176, 138, 82, 0.12); + color: var(--color-brand-dark); + font-weight: 700; +} + +.process-shell, +.contact-banner, +.success-card, +.detail-card, +.detail-copy, +.detail-side, +.contact-info { + padding: 2rem; +} + +.process-card { + padding: 1.2rem; + border-radius: var(--radius-md); + background: rgba(255, 255, 255, 0.55); + border: 1px solid rgba(23, 22, 22, 0.05); + height: 100%; +} + +.contact-banner { + padding: 2rem; + display: flex; + align-items: center; + justify-content: space-between; + gap: 2rem; +} + +.contact-method { + margin-bottom: 1.5rem; +} + +.contact-method span, +.footer-heading { + display: block; + margin-bottom: 0.35rem; + color: var(--color-muted); + font-size: 0.84rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.12em; +} + +.contact-method a, +.site-footer a { + font-size: 1.05rem; + font-weight: 600; +} + +.selected-product-note { + margin-top: 2rem; + padding: 1.25rem; + border-radius: var(--radius-md); + background: rgba(29, 67, 57, 0.08); +} + +.site-footer { + border-top: 1px solid rgba(23, 22, 22, 0.06); + background: rgba(255, 250, 244, 0.65); + backdrop-filter: blur(18px); +} + +.btn { + border-radius: 999px; + padding: 0.9rem 1.45rem; + font-weight: 700; + letter-spacing: 0.01em; + border-width: 1px; + transition: transform 0.2s ease, box-shadow 0.2s ease, background-color 0.2s ease; +} + +.btn:hover, +.btn:focus { + transform: translateY(-1px); +} + +.btn-brand { + color: #fff; + background: linear-gradient(135deg, var(--color-brand-dark), var(--color-brand)); + border-color: transparent; + box-shadow: 0 16px 32px rgba(176, 138, 82, 0.28); +} + +.btn-brand:hover, +.btn-brand:focus { + color: #fff; + background: linear-gradient(135deg, #7a5b2f, var(--color-brand-dark)); +} + +.btn-ghost { + color: var(--color-ink); + background: rgba(255, 255, 255, 0.58); + border-color: rgba(23, 22, 22, 0.08); +} + +.btn-ghost:hover, +.btn-ghost:focus { + background: rgba(255, 255, 255, 0.88); + border-color: rgba(23, 22, 22, 0.14); +} + +.form-control, +.form-select { + min-height: 3.4rem; + border-radius: 1rem; + border: 1px solid rgba(23, 22, 22, 0.1); + background: rgba(255, 255, 255, 0.92); + color: var(--color-ink); + padding: 0.85rem 1rem; +} + +textarea.form-control { + min-height: 9rem; +} + +.form-control:focus, +.form-select:focus, +.btn:focus, +.nav-link:focus, +a:focus { + outline: none; + border-color: rgba(176, 138, 82, 0.5); + box-shadow: 0 0 0 0.2rem rgba(176, 138, 82, 0.16); +} + +.field-error { + color: #9b2d2d; + font-size: 0.88rem; + margin-top: 0.45rem; +} + +.empty-state, +.success-card { + text-align: center; + padding: 3rem 2rem; +} + +.success-card { + max-width: 48rem; +} + +.success-summary { + margin-top: 1.5rem; + padding: 1.2rem; + border-radius: var(--radius-md); + background: rgba(29, 67, 57, 0.08); +} + +.filter-panel { + position: relative; + z-index: 2; +} + +.detail-list { + padding-left: 1.1rem; + margin: 0; +} + +.detail-list li { + margin-bottom: 0.8rem; + color: var(--color-muted); +} + +.back-link { + display: inline-block; + margin-bottom: 1.25rem; +} + +@media (max-width: 991.98px) { + .hero-showcase { + min-height: auto; + } + + .contact-banner { + flex-direction: column; + align-items: flex-start; + } +} + +@media (max-width: 767.98px) { + .section-shell { + padding: 4rem 0; + } + + .hero-title, + .page-title { + font-size: 2.6rem; + } + + .glass-panel, + .product-card, + .service-card, + .category-card, + .contact-banner, + .success-card, + .process-shell, + .detail-card, + .detail-copy, + .detail-side, + .contact-info { + border-radius: 1.5rem; + } }