From a85507f7dc2e200b99ee1f7520403fdbef1f17ea Mon Sep 17 00:00:00 2001 From: Leon Morival Date: Sun, 24 May 2026 21:29:44 +0200 Subject: [PATCH] feat: landing --- .idea/.gitignore | 10 + .idea/misc.xml | 4 + public/bowli-icon.png | Bin 0 -> 19568 bytes public/favicon.png | Bin 0 -> 2197 bytes src/app/delete-account/page.tsx | 64 ++++++ src/app/globals.css | 34 ++- src/app/layout.tsx | 24 ++- src/app/legal/[slug]/page.tsx | 57 +++++ src/app/page.tsx | 202 +++++++++++++----- src/app/privacy-policy/page.tsx | 3 + src/app/privacy/page.tsx | 36 ++++ src/app/robots.ts | 13 ++ src/app/sitemap.ts | 18 ++ src/app/support/page.tsx | 56 +++++ src/app/terms-and-conditions/page.tsx | 3 + src/app/terms-of-service/page.tsx | 3 + src/app/terms/page.tsx | 36 ++++ src/components/legal-document-unavailable.tsx | 34 +++ src/components/legal-document-view.tsx | 90 ++++++++ src/components/simple-page-shell.tsx | 43 ++++ src/components/site-footer.tsx | 31 +++ src/components/site-header.tsx | 51 +++++ src/content/legal-documents.ts | 140 ++++++++++++ src/content/site.ts | 35 +++ 24 files changed, 919 insertions(+), 68 deletions(-) create mode 100644 .idea/.gitignore create mode 100644 .idea/misc.xml create mode 100644 public/bowli-icon.png create mode 100644 public/favicon.png create mode 100644 src/app/delete-account/page.tsx create mode 100644 src/app/legal/[slug]/page.tsx create mode 100644 src/app/privacy-policy/page.tsx create mode 100644 src/app/privacy/page.tsx create mode 100644 src/app/robots.ts create mode 100644 src/app/sitemap.ts create mode 100644 src/app/support/page.tsx create mode 100644 src/app/terms-and-conditions/page.tsx create mode 100644 src/app/terms-of-service/page.tsx create mode 100644 src/app/terms/page.tsx create mode 100644 src/components/legal-document-unavailable.tsx create mode 100644 src/components/legal-document-view.tsx create mode 100644 src/components/simple-page-shell.tsx create mode 100644 src/components/site-footer.tsx create mode 100644 src/components/site-header.tsx create mode 100644 src/content/legal-documents.ts create mode 100644 src/content/site.ts diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..30cf57e --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,10 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Ignored default folder with query files +/queries/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..90dee70 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,4 @@ + + + {} + \ No newline at end of file diff --git a/public/bowli-icon.png b/public/bowli-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..618aa053d6dcb5556ac7c18377aeca12c5370f00 GIT binary patch literal 19568 zcmeIacT`i^_b7gm5o{QHLkNx(5h((Kw4j5cfQSNubm>xq^p*q%6$?da5fHF|NN*yY z2q-NeQbUhOCxp-vAR);+!THX2=KX$uy!GBc@2&M=t*m?RxqI((&OUqZb9T8rxpDmp z-;QHDAPC~qx_U_;f_?+Hzd_r0zy%{95ehCJ?Qd&2Xlb2>wt)v8Xdf3h#0Bm+zhhim zH?KzEp8v0VOK^YcFPJUhe*c#LhIb14Z4=%Bv~&LMfFFm>e_cVZ&v`e8AR~KLhU>+z zdoDl$di_`XSs59w*S{#T*$}kl^)HGXB<_FQ1ELT!=UE!KZ1=co;srswzi@tBaT2@v zA!sYr$?&%KZQX0iwn#TAYdfTky_CP32L}sMRrmL>wso=h7P7H-baGb{qg3L=gq-Zu z#Ej4B%IJD%*xz-!8t7?n5P03tHqgaZ$xciiu|w5g84z%@_qG=DcXM_3QubF9+oY=u z?m5lUVgSg~&OurKlIC9^;7LvFuD7>`vb40HpP!VUoD|a2QTnWsl9IHHthB7GB!H0g z3UK$f_Lp?`IteK35Za`1$==J>)5*iz3F$7xp=oV{^zl{`6Wby57wL=M-kwf2e^Yn& zlCs`x@!_CIfk{hS+j&W!m6G8MOGrrd7rgRCJ1@@TuTBi??`=N(dUAEz?8(F0)63p4 zz{6fmOyAxM>Emf@ze$0E^cN8gPkU=`dpm@zjI4sB%sI)k=T)Wur&pXl{u|+vwYN1Q zNc5f$(p$w2Y5VI&`Pb#r#j83R9>%sl#y5X8ocVR}@v`^)7vQanmwjygF9!O$YyAt* z#?#u}?q87Ckk;<@LVqBAeC@qOPs%B&cnNviTf6-W%iG?~!xhZpUx<2ulq10OMEbc4 z8Q*pCwinX1_jj_jb{8_VwnYj7hIm{103dH6O(fDz=o-=y`HKMRZw_qE_21LK?C-5* zhfuP%u~Bk3drtD)IR{xuc^er;$@8}7Y$a{&WNZ{=6%@}aoR|Nb$}g0ElhL#RRL;qs zlT|pUbXNYXqSCqZGJhj%zWg`5A=1b8E@veXvVVj9ciSecsx(KqT%CT&#OA|aLh(1o zFNygD_z$W4Uzb!n+rOpM!^hKgQv>X5rR`nq-R#}HIWl_oFB!G7RrYrBcD4U+3BBm* z{ci~cs;BG<7D-LaU((Lr!P>{wTMS|2YVRX->a2{cypX@p=EcKbTu4#~O#A;cy}u}` zO8=v?|AWbS{6oVzI=E?QINAqZfSJktH8b#}e9_1IF47b6PvKPMfc@7Cp!u7_{oi6T zFin4R`CsGkxAypl6#!t$o7#)?G(;j@5gOL+zScrOCDs0+hC&?GQ~QUy32myF+CNAB z548fi^-Go1{&&7;cr`R?+y&OC$Q9?l?x6+kPxJG z>7t?ktGQv`m*)0w)<(-$1FHk$;_cc`o&NCp_fzIvI|-gQ4;-vEo?-31d13F?-_q>c zUGK~Y{6W#(^2Tq=8|?QWzUuvf+xIB9VrsTOJK?IJA~!YX=RiZHPOUx{`X;WTGL@9z zRbz0Pv%&r6?>`*)4+s9kf&Xye|78yBbvxT0oZj+5L`UsiycE@2(l|>PkL|pP6Tek~ zw$hIA(RL#6lweAo#3hZLF~ls|>imGZVkVn0Ki*!#Zci*>GfPX@=n4JwjqCv&JAheO z5tHuB9)6qN(R^5^dLpgcjNaat#PP8zWxgQZDc=q+-Q=qUiRSl`iQ6%TCk*BKtODl|)dl%8Ctg#*BQT zQT(!*DBJ+s_cpn`v0J&k_rU;BI+3ON>T=xp(g4N}MQ-=&79`ytAf|p`GQaj>;L(Sj zqr5QWc0vy;z#Wx*dZOByJ;!vk7%p+MsH9O8Pg7&rhGtCQ$`iBWL_^tBL6guQ2ZP#XBLyN^~|;us6zT!qMzJPxWom z2)(cXeJuyhUz&+rU4h{+H)hFn!#Vf~=z?wC>wK1`N?p>%oVvV!k-~Ikd9P9zZ>+^& zk7>TbU5hugRZ6U8J9E9SGUzr*5|z`fvN}|fYEW*N5X2hbUzFBUXV{J_5;0?YM_w66 zA0#KoIh| zV3to1x!o97s;8IAI-%j|K~4>-TCKfvzSyxC2vZIdH9Q0VERv0x4o_J)p|ck7DAmQ^ zpx=P?htu-S<@k!JBPQ%SkM2{ePf(7o_srl9zhgPlDVGRYUFtu!BP;Mu%+M@)*oBFD zXSQk{t-aWpUFClW`ADqC>Tm`wfe)T_UGlS6zE>jaVr|+g;=FmsJs#*(p3LWPHUFmu zYeqA)fje$CxhF~R0}m7vM+bB1h@uP2!qsSeai87vx6fsXKoymvw5Mf?7T=X zZ5)lT@DDX+(%2Jm&J})tawKH%LWv#Pygf?~*US{nHq8 zl5UuLWfploL*1%QvD8?e6+vlrw2hU{@$n@WUe8jS?C4T&Jw;)TEPpIV=G2$sM!8?m zm#b(mM_ZpSSdZnjZ!7nv(_$$(Oy=k+!ZkpWg*Ki~pUB8aEsW1G!$@w09!r_Ks51}X z*Fw|(SRF}oD z@KB%c<36qxEuWqDOwAC2lrNvJmSO$Sj23Ronzwt{SHBf{z27j}=N-%y;N|GVkZfUx zp^)p(;~O>MeOIH??hgicR~3fsf-Y3*;K$yy&g_)mfU)zn}etwqxB0fI~M+A6Nnot8r8Uomlv(N7j*-SpGQg(cPyw0M3pMF z*Au2gkAa~wAjni-%knYeHHR<^dM)C#WV6B*A*^+*Og=!I_M8j4>*uuI{|mZO@Ufa5 z1Pxdus+xpc+yo-oz>Y#8zQ$UD5}#j#sqz^nzT*GYiF~;_ozv9z-p+jP1UK}3{&aKO zBJa6qVW-cXdF1~>l#2Q_t4x=k|C-i=wavlZ89sR^@>hrH!R9NQ9iE3m;?tXJIyT9K zL?`{lLec6)UIjoHyz3|o+X-C|yIR@%3{e1P#U1M^7*qy9-hawud#v0CqfYd(AU;zw z2PLT9FRIn5~QjZhHZl`WXx#AMA{*SwUNSFI|yvONVPMd z$wZ`Ijm{D5p4kC8S^)%2+c_0o-{6W8);gU&VQ&N538ved9IUtrr29-w7$8|731T~C zre6ph#F-i@aGF#tnV; zxS{16fmi^TW%2R(5o!k@=+QHI6E*seE757ArircxgH8czhix^_>_aU8)B?Be=)$lq z0Q0Zsxr9Cq5S}c{jLjGSECl8|dMokx-k@y&OEGRdWI9v`jKRV}qEQfpI_q`8?z^$6 z$T^ae2@**6ZHgFbK$mnUH11K4=G6mxOsCU8nQ~I6uMi9DFq3vn9Qp+KB z+cf1kLk0#d=s6W>t#Czini7v6{M8h9Q%4gHSkwK&Cl|iibTMvxVl6c7n|mfVHaqX$ zPkK+s{?hEo>YqPm@0F&QR!_6=*#y-<4V-S^bmHvM7%`)$s>L!o21mIS*26~koFud_ z;na(1I5?{7Nl$g2YFWi(Gr!ScK?ZYrP*SVDVt%RA7XFh9LY&gQ9)-9%TQE87)G<3b zcjOi#-Fndj!~IthpDe`DTlR=8F4`euA7d?(1kKJOg&bS z1l&XEEE2r3oW8o%$CB|0#Gw`NNziS_?-tG^wa3ox5Xf;z?q2CLoS0`_*sP%}fS}D+#U5p}3Wod5& zpw}2Dg6Sf!4!NJ%t`_?>Xki+S>E!8HP4B)N@d~wHs>38;1O6_5`2ra+6Y>b-=T~Ou znn(Irch;p>`e?=YeTgiwVYEZZYMljYLFOd{?QnS(F!>COuAb%5NjxT)bGRo7i8En- zy@`vc(MzxW#7Y?EQ*W;igVdE>zfO}ztVT`Q4rJ8CiG=)iDJ)T6{1yL#vbNHY$am^HDw&P-W-@HUm()*#_ z@3K+l>*Ldme1DOo21FV+wCjv6`WWVWH^S}S{HKInea|M-308T(WYEgV$Sr75FhP}o z?W2fwl&0sGTpu|V;}*E~@QRtTB)cUn-^ICnlsmkSQE-DQtR9KF$_u@$?hAddFdN>y zU+!qZ;SP5!1&(5CGY?+ZOQq9w5>(q^t7Kqh^n*aqIh1@VXpP-mt?GZZqrn3HAdux9ke{6$=jAiBPgs<>oQ#xXkT zYBoL7?J#>{(UqL&W-mLOc^5YvB;EYu9+%F2@rT z-(k+rAbY|r{q34Qovf<$a=PGnVDf#FslQaiNZyT?q~Bc*5Bc@GVuPqB743?0^2moH z!8p67^S0W;8vqZGD?_PKh>+9%&*rPWMMEcl-x-r@U*2Bh^SNC%sJP<;$B3=E#Mbxo zjMBWb1#s?Eugc()bgz&5gOr#k{3CDLxv*}(jN^39!Q9VX*EU!8kWg!t` zKg4WBb*x(4Pdvofvm5$ertJyN82S$-hn6e4+yZL9C6MzcqSiHh29mmLkowdn{wVxL z`5sZJVAt7Ln54w3fIWy)EuJRos@@OjynL4t@2qPq4&~4L&sg@vxT_Z-2#&%Q;hcSvK+B~5#Q@W%-)?%{4-B|H18;X@^IIRZ9clrsyJw0 z>$sm--eKlBk(H8ame{h}PW35P{6W$6R_(HLj&msESJA_^n=>&!x>&x(o%vIt{gc%< z*M(qu@>H+b!Q-u-9D_lHZFBeEh;NNoml!=0X0+PXdzc*02vj(kV|=JRbY!QWK=)E& z^}9dHl4|GNANn_Q8}FKpV8C{--(n^%+{^jrzoA%}dQ+~BHy-3FUVTb^=&>5v$H=*n{tmsLvA_p?fAul==1Q&MeSF<`2E{3G zeKc^n>Vetgk_t?Pn0x9Vy<~RSkC@9McHmqB_<4g!bsp;)Wy97dqnPU!*c|q8p0P}x^87S)7g7qIO24EL1xI|m>unXr7?`pT(`nGesLhbD?AvO~ z)KlhaRJ*d7!)3!5NG*o}^*L;?R4oZDOS#caMG~t3D{+XKJ!( zSFzNVP6Mz*B>QDtP8w}olH2pz$3fHfgo=FZQ-Yj zftN{sj)gaSLKis&EIM&v>01S<>Y_SY4lWasI=ai?~VrSCSE3bo$geb_vSsOj0z&F^yZONm)mp2+Oq{-Z7}qU}g) zFoYK@)b1KAZg@f>z-{v-VD9v@fAXec{6G7&{@fQu=J#hgGIQ6$>-x7ATIo4R1-qoC zd&Fz3RquAJu{#mWNd2mMLg3b>0abu5xPZ7B@Fh>*we}?&l>}y2tBUS&nJ9W-zuWBW zGPQZIHRB5G6R$J!dD7lIZ0fLWSpQPeL8)_Vi=z_fnC#S>FCP@(sEnJM>Qj{|<$m88 zD%n~jde^LobU-YXqJLf@qoZ7iNB<*UbjMA z?b>DG_A-p1g8fnoCpV-T(P}gK29%f22Ng}(hU%;S=UB~2ha)YMgZ7ITiQ8>Xo%5V&keR*T z7v>CwxP}X^w{6*#x5N15=lolatv{0*J?NDDMrKXL&q9|T>HX(NKXu6^c-;{iH5-#A zFDAcVz2d)rIBx_!MRpDdyUbIayK|0}8;>mqr?_uRi}gc$E_-y69np8H>%u7}xDyuh zA9{F7+EWu~uJ7^59XV*&vm1dUyUX4D~-iumZ%jxwA)IZ<6YG z_2lvhCkW?!UFvGLl^PsxMPPgfyb`1<+gk@sJ|6&bWUR%bXFwSz>IL zI!V%wMd1HFuO7p~Q)N7j%ZN(oS(^@TY?A;E?%Vwd2q2h-jTWmg|{ z>>V-0*tJ$A%!QG?s_cgY9m54z0)rVLsijp_UX=2?-|=|QpY_0G&V%jgIxfm--IjI3 zDoO@+!4i!gXY;SH(5D;m*I0oKhdQkEcm{6}e=ib{ih67bA{n;uL@sElqpQSxrPk=; z!1B}3N8Umyar==fC%c%>3FCG+rTKQgeEeR;aHLa0shU#2cf7Xm?KBqo?jeVBJDIiy z89)?zG9u|$ff z{KHXFQn9Mp)h-}LutGG3jN&-DUT(!d!UWl$T_Rh?7x@q7t;6u_33a1(x1$-v0Cx?B z^0k*Qu?5Ls3ph6OlUH=&lQjJS>?k^TO}nGCgl|BCcH$@`WMy6VZY?x#`XTA-U{AoR zWpqEfjM}UsXHn|WhLIpGe_n=_e%#QEKbR-b-V$CqM3QK*hpgYP%aq&PW+|(qc7jqj1z!AY8_)O%H#%VFNJlfA(ng)ONUXxD*5iP5?OfK?YA;f69P_JR{&jqfdJ4*84irm>RburtI{tcC9fORxc^BEI*~N^AbkQ>0_1XiRz!D zF2Lnr#GZjOZtQ|&ygQfH*0>A&9}dp={JGO{t+zBm5S$MkNYP*0Ma(OQo^rg5dVH(r z?_Yqc#{51urNoa(^$@SWp<0@>{(Nf8tPPw-e=D@oNp#B4S^Z{jbvzI?hIQOESjRgG zVt1N}8}fAMjExu(Z6w9L@aE9umT8LFRIM*Cv35m**0Qx{&{DZICg%1VsmClQ6%LbA zni1RbFmsb=?28FrNu~+&0j?LjzlN{frGcpfuPVw+bt=1TV_9bbv%$s78(4a2xcbye zb(H3U^&&JNegp1swS0gq{lXdjjFGb^uw4GTB1WR!8u9{G9$o7_@oYQ9aQ?cZF{giF z2~mnyoQ0>^*#jqe?T!7$*!%smK^qCWC}#TXmC_AYloIJxg_b*heP(aA0(JR#`S2g* zxn00iT*62?y>DwD7R53Pzm^Yb5sYCnDCLv>WSF* zs~Rt_%Ivhh{t!%dG{RLPUd6fOeS8pbHyn!Kf2!r5pVo+CHyW@~`p4SmKJZ6jFiKOy zL0VGyGWrYtL{>t)-)&*6&kl||GMz8d*6qcLET4nHpy}VHq5_^&Z=a25z=*D}jvQzwRO3Mh+gj*}nXF?%T{01bW5iy0xGo%swpttAKupa!2>d7|g)rJxI{ zaZBt5_;Ws#Nh{Z$ngA3iBvsj%z9&!?23L>a$NQ;Nb>&DR%vDC(@Vu)JpbqeP{JDM{wFnB&r1ed|T2w?L%2 zSjO-dc-GxXg^2omyRN=43n-+l(@uNiV*mH}uhspBiK~sek}9oF(FD0w2bm#uhJDoS z^h_U<+0PTQCM=GZ*xa7p6Vz8)GzY>4dF`iD9|9M7ojZ!jaYthsllxyN3L0ZJ)_qM> zv5o2=bY#X#_!MZ;c)_>IJD;<_-oU|{DpIc_Y0tSy1and)s3wqIlhFP26mg3GXPx55 z+_I^DhM6h4?2M3(OLEms1JdG)v&%-rz$tLX#Gy2?kgK<^UKeR$D7*a%#eMCQh*Jfv{2_F?I1swZ$tpciT8vt5W#Q_Sg*9G!HBA37tXis z80?r`!uc(*KUGQ7Tm4Ryb8g13M#^QbaA>>wtBwMkY3hT5ernTC zGm3|{*CBPR(fqgPJxR4L@an<|W``ro0{elOAS691wcDB%mP7Ru3pHxNQ=^8DGdW$o z+9Pk?vM-3S!%TVha3QP&bhU>^vyj(Oqi@7mu(MN0<(XoYHP33!8NY64-TX$;{hyoz z*5%NH!`h>rPaW)i4%Jbvo9Aaqq_G~(XAucTe z=kp!MPOXKANbihdqF2Zgt`O8JB&mKJ6{2v-&2KS9lCOy4amL5vK*+;hekj#_Mgxls zw5UOSZa1?;uW1(dq-vE#*hrX`@3NVk$IQQpU)Azhm0Pt}DEx5&-_$7rGk>v88I(ZU zty&vn>e8N#wnlguKkx{P;zaaWwY@E4?AuIg8POQC9@UfmjJ#Ywov(s=NNv+83m2i$ z4i=BU>I=IKh1h6SR*b`8o zW31k|Wg~XML*ZgJ!*=Bh+0-)1O?u>Ay3aPpTk;amDrH-TfNwImgm4lwR`=9!$00`x zpGjS7Deu0IpI{Ae4lPYSDs^E=0Z0vgU)?zk3FyMvh)wUY*E7Fouv(4UBF85w9sI6} z*b`<<1P)^Oqjh%d*Kxm{^DJ^a;ayw+WXd%19xHCNV;$d)y)}+vqHjLXIlok;i6H4% zF73-}WB0$nHB(C*o0-@)AAW`Na&11$!D5cFGJ-;WW(g}czzZhK{JjWI7aBZd@Zyw$ zt3Hi7t67*k&#^gq>(x#zZj$*r#l+XXdP6no6H*0?QqO>t;{jv$Bq=ez&sW748i|3X zbGoig`3<=*25Z(Rq39Q+3Z%c0UYm8gAv)aT`+^MKu#K$1 zq1+53D-f>^e?(;lfl#~LAlzF2qfs(?svMZjSLorK!PLZ8gI>bH5+gHpVFsLy$s9x2 zT{A!3m(>59vvJXzb8*k{HQW5=nhXRT>pun%z9Q1LKnJYSH_W+s+{=4sb4Q{D4rQX< zu}DMCCPdF!47#RV5C~3w6LdFrt8axjrIps`x7(HJ8&Yd^omll|;*rit)4lF9FQ=A; zhm7)Ka{U^nhcJYOS2%;YHdl_?N8@YOnnxw3Dn|H|$-6>evA%A586G>J8^Li)v<_Z( zCBJnI<5KzW=;?=7WRDdoQaA~U8AP*9QeDD934u!5>-fV6Rq+AD%W@GKL+ibt+o@=? z)GDv5XhNMhl`LvLxqouL3AoIa_Tx^uF^W%F1Ex8zSTIN?$Vp%7R&?o4*vJxz;{f;w zrIb48mBK$5;UZEyXxMN850g>f$GH{Y^cFT=E;~BEQ?~Eq*JWlX8+nyRbo8}6Lwm0< z0TW|NE~EBNHaB%WZOl7J^(nb6Qrdus-2#1#`WPI%!DWulMYS}P@BXrUU|zIfdPK*u zMi1@X_cS4ID#E}$mo@)|iYN&8$gBk^n3U$RFxcKlKg*+6zqp-!Kw1i0T9q_6Cc0m* zV79id)?;aF*Yiln(P6f@Plw)r%vPaUAAnw?yv2wSh!A|?L)wOW9`~Z^LzAB60qIo) zqsU!gwCIF<_8%I+oA91&OG%m@TLjlvdEg@U0;Q* z^r|)t=h}{c2N_b$fEsgZ6Sdsk_@%!T^6)Sw;3KhA0$wAvO2&6C=hr;PqqjNMk6VLu z$|Xksq5M3mEgZoG4H$rOqm5c~M6TDYX!))$%h9Wn6#6VUcg*P~r)TEl3~@}G@F_ev zc_xrPJ?vkqr%lY@XzMot^>mm;P44^$526=rvIyQV9IP)zs>ItfGy(nW@mzh%Vr%0`W`Ksuw*xfOT`OUKF|zq=OH;1n-_OeEI6kkWv2Q zx|vRntM(D))kkgoGP0FY36f=5aH2D7x*dC?*O9AL_08yg!tuD7u`v7&P6}t5k(&>a z4{s$AzcqQK4lk@pCgizHR0+IgoA117iat*p-3Ps9Qo}c##dSv_5M}txlSoymu3G;? zb(gF;(bjI}Qep0-#FsB_1T|lSlB)XOJ5t3^%#9{Cw*M0+CkYSS@NjD3>@KcHIXUT$ z)&ACpFs`~ZpUVeDgIm2A#>&3r*4}f$40ea4IzQ9V^l@jNqzzn(3sTld5B6QDwYc6^ z-nOOOVsQ9_<9juJrt<1h{=#A7EI(BrjgJO-U~M@;#Si<5 zxoaTS|6+3D~th2nI6CW+jl_?eRvkc0#iE%AJ=Xtu7xEKZHEW)UesNqYK(#y#VuS_qkc!JpYKDT zV0xV6v5}g})bf1vj*UCUedNuJvUFf((j$Nt?-e<~Oe}?<7p% zm#iK|BDy%!r}|z?E*0->!yejUpgbE^2zvoIAfjs&g=oT;d*lX;`0;Eb(jOunjgPW3 zrUwV_j%q~9DE-Jr)+DZ@w_}fOfHQ|Cu|bnQQp+|&EtX`4jx^xm6%_*|%(_tYhDU(* zJNTBBS2)`hW~Z~Y-~(cEnWax^_U_zvjlJKtHJcrC`n}qt{K`AWEf3f%kh&J5w`ZOg zS^bJo-2!>1jC5Ync3Qf-QESP1SWC4u7BuJN{1d9mw-uwqkl(Su*P9-UznnUqdY$uGmHGw25C_I5V>4$zLh&*hKkKLvnbH%!fzvnx6kj3lZm>V zp^BrEvTbq37NalEk&xZ>Uy?_^?aAE%w-W#LwwV=Q(z?98w3>rkH( z9Xv%F-3z^zOK*1AdSs5zY4q3xwS6v6(4*}IH3465zoosJzQM~g5v7NEiwJ@3uN1(L zoO(j@m{*v+=IV&x^l4J6AG@SIDvRR;Gr1%8yQ@^AcdRDs5iM|4)|#Vc_yTqvdSlrs z$p}|Sk?kO;;p=FeWFbZG)OugHc#oW@UJh$wrA^MPVIafR9MihcXN|F3TqvQ+$<`%r zw?SfZIMo#NWhBTz^{QX102L*qWjWQ2x_4AXyj&nTmyiWRusx_745{3_AiBfDB(E{W zMrL^+ZFp*x(V(8g0O7p+Z306rmLzsEHDxod6swc0)Li416XaZh#V*bs8KJl6B;|8vRI^$l`{+Oq_wV^hY?&uZ23t$EQOzwDhNX z4Oj+q8Uhe>zZeqM3WyVpiZ_yCnr$|rTDJ;mwY-xzIY~RQ8wxq9wDUrAED9DIsKY+S z*ux8bZrubZ`sfnZ7p4F~^e(wsiFECy>nmLEgfqFd=bxf(b3=E8oIDLTE`k=a4j%sn zk;VndJa4-a{3%uxsR=)?9X(Js{;@Ai4t&dcQZgz+-9J{i#GVR!aV1!a_GT9pQahbu zYbOpW-%xwRwVZLCOS=nUwh$!$yY8nah{pif*hw~hvrs!s27Ccq=(N5+{o{t~N^P2O zCQ2Kh{S^_+1$__ySaol+Nmo1XF{*D1^xA|J%c@rR5Oj+5T0hzl_^V|7_7C}A5Uv3F z>HIjMgEu}p&F1jDAUAY@_bMKH32v?({d7Psz3ocdUdD~B(1k-4SI(Gg$N24(X^Is- zn9Jdl^$sZHOnUR)4&H?5w6{L@=IZxDkO>LYGrPT3%!NXAwezVU3loE& zrRSWSxQBN14*eqcx%zz&6kt-mf-K=8S}gJ=N2fI@QNenR-HK*Qeg13^eIVOo7Nb1d zRS3HRO5PsIE1jl8hS3KyQu5B(!XJV1w$q?$-B8Oi60t2#n9s+t5%C*fcX#K~qpe3C zYezrI8GoYB3GE{`AZU+FII-eW z1geP(68fZFWWU+cm?ut+XPu*s@<1WKU&R}PWYTG^mMS}ECr&95>LnNS`qXK!b}Rl< zHHR>-!K)nkfYR=mjq|h<0OM_@aFQlGM?1Pmf;<4$cPnVp#yehy7idSzo+|J50q#Mo zX_>mWNWdA^=UdU;-cC#YzaqEd5Olx-R4@-fpyZ7!M>w+_6zki-wIS&4VKoc&XNZtE z;d3_$S%t6@pqe!aaAXW-C!!T9-LFi0!wt$&Z)y2Ps=o$oc{^qU2gS2@k!*c_!xWQ{ z`?;V-RKz&0SDYC%#|^%DCcBSyHo9=Py8MKBh-kr2>&{DVRtJZHF9DRl9$Id_#cwyV z5A|9?E3q&`J8p04GZd)j1ogi`+8GlnJ7j*u3Ezr7tj#H7WPFD}3FmBbkh|lStNJUJ z5*&xF4}5kF>L|hKjI{syS2&~tEI>eZcgz`n#tjG*J9dE5?E#^cr)%8STE{fgn=4BQ zBL{8aprrALyMiR^_a z5VVHX7g0J-|8YB7xirEC_Z`$=s^5yv-6?Z5KJ7X_2?Hv?PVF)z#TZ_O%joFy#=0J5 z*aKVOkQ=?BzXV*=fTLksQFBup4{L;9(9uu2rT#3-h1n+!N@QgG-+}P7 z!t-h&`W@5gf%o9lsXVJ#Xa(v$Uf-)u$K>+e^@~*h5TEuZHr`5-DvWPH_X9!bHLNho zAFpQGu35>~tH(pRyk znGVWw^hNes}b}5<-1c16m8b*|^ohAJ@4= zTI3y#mX8&o#B$DCI1ugq{QX$W86*B(#Dka%Qd%0FANBPJo`qI{K-wccv@Vz;(jaax zt(JWGa-9784QiisInWs^P#RRiwWQO*I};u2HHD+4)~IL80o-@3>ceQ<|-7R^Ptcmt;EaCY7XnXz}Tc|fsa7e zLW8vuGv$>&pmhvD{zSv6D6qL7ir#T^>5t3|l4GIOAutp#@U@J2N$n~E>=7y~en_jn zc)U{|NyxCl9S0@03*Xf|-{_mx2($RPz^0<8?Nnq!J~(X!c*S|!m7H^f|GA#{XMS$2 zvYfEMTG>;^+v72Qvu z4%v9;g2NyBK`WZMWKupH=u=W=_?mfU^ee*<#Y53g%@bdl&d$*F=dhrp`SmSO>)Ok+ zPPmMTIgxqS!g%(24L?H!C~lJ}Sh{p=Pb;nw@2gKu5{Im^ymOB+M7D!hW~L)$G|?ZK z)5t%B`P@KhyD5MCrZvCkWF8zR0=@S+`cBgBW|YrF0jYiz~@1Q zK>YM)XH1J}JfkUVNPS`ZA+Hi5SyIQWRnB-0dkhTh6lY%**X@;mGccXxq@;T!g;edlc??1-Ok4vY^ud~U+W*;UD zj`J?0U$uUz&e+P?H%ZmYfd$9!b43YYy=yL)NlhxL=%^Qt$vppdV3Zy~julU&}u z%*nTmy!2iUer3`?rP7K#0m{c^i~=R}ldKX#>V%8FFQdi_4e1$QTLv;|9HHr}DG$Ek zyeyD8E-m)@>XS3CVmBJrw&BusT%^ItAOz>`y?Z^_tFVjv5=%&geS9o?_+C;yIFEb$;GpDeegp?|IrVhHTe$*zI*@nHsWolgg_{j#Q|G+Zq zO}E07w+mS1J2#yiqn_BT+&SDSX6=KQT?j_H9>qln?_CVJW4okC>m4r&# zZrz->{9q}c?z!YR7VsSTOzEDrbI_HZloZh8#^Fa!rSk$Kofx4Vqwnbt4O#ErvTu!3 zPcc>-{Oym)i@hIhO)1F$`U{opT3XDMSG;mFxESpeCvb|cp5Dn8TJkU5kD1B6nvE5> z9RzGAB-5;^#~T|Q7~5>tp!nNyx$|em19Zld*XV^ukdIS40-W;v?8qEb@mans564)0 zbm71^1>AXbPVknNg>KP2%7<|*YyVh=Y@&|-;H%h2Z6hTaK$oCrZPRnB9rr>-h=(sz zb5G?Fd!@ha6c6w+FDJIz(O1#*%L_uIvODn7kF*%t)qGfc-d4URkAP zJZct8)&`3o&TkVe-omnQdY=E-#oO60835lo&F26kE4+7E&RT5dUN1SKo@v?XH#f|% zdfSZ*&ml#2l340(y2DZoH-rT1+r43)V2LiN5@{8HdoA*&iF?xi3O=%Sv2sm~+*9hi zF~4t>?R8P^2R0b+v!|Fyn+=-{$`q{hAiZR9K<(`AF{;ZLb!UU3)Z1=YU|8B{v?Uo2 z3<}hREc13U?HN>TQ{>G!xBmFlG`=gC5=(aQomFbs*)>VXSXHPhHu}jqw_@70*24a8gXKL>;%n1h47C<@@Xx=vTG>U;L8YB!MV|@}^gjVb#ssJ#3RhM$ ze>(PRn0>F5-dLBk(c9rkN$+pG^;AAfLBIwb~wtQ`Bb-jE-x2|N}KA)SyUAQ zqns>RbuNP}ohP483Edg8EA5P|GD0aYwBj})P@P_3(@=WepzC#OkNM=uVa|wJ!tJl4 zX7pZDzY%Lz%JI%pr)=VmWj$#Q*BOKJYRIQcOa?~Gp1e6sF;!H9G8qf%5CL14KFIK_ zm%EOQ74(J;HR1dZy7E%HQYDRs)UfU5REZT?br0~sj}3u);YLM==Fut7Azc}&tm|r; z75PE#xDJb!T!7x(4Lh z{3EQP!+Q&&S*>8y_|gZZCii|cF`|wll$ECF^rH4?tDTh{sv9=_6EuM=A7@S8Ia_0X zSh3NN1I~TiIfFiJ_n07(QaI=_Lgabv#-FzKxtpZea}rlAZ&7XVU~^8#H&b)Q@|N~C zDH0e8w6&6ZjN?^Oy-c<}+g2&EMvj&FC{kP)9k>g(k`|vF0+n0@{4%*u*dZOi+w;@C z>e>3+Q2CETa6KUfLf4FL#iC&cWh@u}m(i5GU#Rawk4CDQz5kd1{|pBqf7pGQu?2_5D>Ps8|$CLV6&43p6ePad0pZ+9b3JF-wFfB$TP z(`>l6FTgD@IEFXeGj&WYxeJ$a&CAzy*=6uae)9D2T)yzFYQ0FPg(E_>bhoeYefxIbENL zUz8OhzQz9LJ)p(xm~mN%nCFgEbsX`wN^f1zNUn&@VYim_O@~UZaNMV2LXyz2Bktp4 z_|zYFVACtx`qySD=|%B=wIxl(Se2hV0l9;E$Cm@>h068X1?DM>1Iv5%V*1vvRVi2-JK^O4^1LqNDL}C#d^v za&o)2_T=|5b$~7<6p5CXWQ5Cddj%CX?L9p`2aR40V&oGhRucd6^g|?XeCu>P1|+!bW6BA-tFQbdZ5(O-MfPKH}4C+ z|DKN8|7T?vcb0k`L$7)&08{tu@VS z1e*CrKUw|}x1yxyl54@3s2X2oFURbAh!u()xH4~Mer`g|ZJN6{^ZPiY-^}$vB8x7U zAH)i6%j@b=cWhmhPcI7`eg3MxqcQWcDm3T+D6{fVX>~cSaSTjRIbgz(aZueJNepc71vBY^<~C9A35W$a zL9}oNXbVN1V!(@UmNdmk#yTRNlID08g9bw^G6aPJcv|==8c2r%EbVL@iFl@GK$NE& z7!}E2SyF85qXJ@V*r&n?;Bgi$oNoEwNw-IWFoPB*-olunG?-y|M2rN9OR}KoaM1Ha zFw6j*88N{Dv~ZA23xGfpBNC=Xi6Iy~1VMDr2?~NllHH18WdOn6#qPeX|Ji>5=FO=V literal 0 HcmV?d00001 diff --git a/src/app/delete-account/page.tsx b/src/app/delete-account/page.tsx new file mode 100644 index 0000000..9279cb5 --- /dev/null +++ b/src/app/delete-account/page.tsx @@ -0,0 +1,64 @@ +import type { Metadata } from "next"; + +import { SimplePageShell } from "@/components/simple-page-shell"; +import { siteConfig } from "@/content/site"; + +export const metadata: Metadata = { + title: "Suppression de compte", + description: `Demander la suppression d'un compte ${siteConfig.name}.`, + alternates: { + canonical: "/delete-account", + }, +}; + +export default function DeleteAccountPage() { + return ( + +
+
+

+ {"Depuis l'application"} +

+

+ Ouvre Bowli, va dans Profil, Réglages, puis choisis Supprimer mon + compte. Cette action supprime le compte et les données associées + dans les bases principales. +

+
+ +
+

+ Depuis le web +

+

+ { + "Si tu n'as plus accès à l'application, envoie une demande depuis l'adresse email liée au compte. L'équipe vérifiera la demande avant suppression." + } +

+ + + Demander la suppression + +
+ +
+

+ Données concernées +

+

+ { + "La suppression concerne le compte, les repas, les photos, les préférences, les tokens d'accès, les notifications et les données de suivi associées. Certaines informations peuvent être conservées temporairement si la loi, la sécurité ou la prévention des abus l'exige." + } +

+
+
+
+ ); +} diff --git a/src/app/globals.css b/src/app/globals.css index a2dc41e..a77f880 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1,8 +1,24 @@ @import "tailwindcss"; :root { - --background: #ffffff; - --foreground: #171717; + --background: #f7faf5; + --surface: #ffffff; + --foreground: #172016; + --ink: #172016; + --body: #2d3a2f; + --muted: #5f6f64; + --soft-text: #7a887d; + --border: #dce7dc; + --border-strong: #b8c8ba; + --brand: #007f3a; + --brand-ink: #0a4525; + --brand-soft: #eaf6e8; + --orange: #ff6b35; + --orange-ink: #833313; + --orange-soft: #fff0e8; + --blue: #3b82f6; + --blue-ink: #1e4f9f; + --blue-soft: #eaf2ff; } @theme inline { @@ -12,15 +28,17 @@ --font-mono: var(--font-geist-mono); } -@media (prefers-color-scheme: dark) { - :root { - --background: #0a0a0a; - --foreground: #ededed; - } +* { + box-sizing: border-box; } body { background: var(--background); color: var(--foreground); - font-family: Arial, Helvetica, sans-serif; + font-family: var(--font-geist-sans), Arial, Helvetica, sans-serif; +} + +a { + color: inherit; + text-decoration: none; } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 976eb90..5ac3fe9 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,5 +1,6 @@ import type { Metadata } from "next"; import { Geist, Geist_Mono } from "next/font/google"; +import { siteConfig } from "@/content/site"; import "./globals.css"; const geistSans = Geist({ @@ -13,8 +14,25 @@ const geistMono = Geist_Mono({ }); export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", + metadataBase: new URL(siteConfig.siteUrl), + title: { + default: `${siteConfig.name} - Suivi repas et sport`, + template: `%s | ${siteConfig.name}`, + }, + description: siteConfig.description, + applicationName: siteConfig.name, + icons: { + icon: "/favicon.png", + apple: "/bowli-icon.png", + }, + openGraph: { + title: siteConfig.name, + description: siteConfig.description, + siteName: siteConfig.name, + images: ["/bowli-icon.png"], + locale: "fr_FR", + type: "website", + }, }; export default function RootLayout({ @@ -24,7 +42,7 @@ export default function RootLayout({ }>) { return ( {children} diff --git a/src/app/legal/[slug]/page.tsx b/src/app/legal/[slug]/page.tsx new file mode 100644 index 0000000..fdb569e --- /dev/null +++ b/src/app/legal/[slug]/page.tsx @@ -0,0 +1,57 @@ +import type { Metadata } from "next"; + +import { LegalDocumentUnavailable } from "@/components/legal-document-unavailable"; +import { LegalDocumentView } from "@/components/legal-document-view"; +import { + fetchLegalDocument, + getLegalDocumentDescription, + getLocalizedTitle, +} from "@/content/legal-documents"; +import { siteConfig } from "@/content/site"; + +type LegalPageProps = { + params: Promise<{ slug: string }>; +}; + +export const dynamic = "force-dynamic"; + +export async function generateMetadata({ + params, +}: LegalPageProps): Promise { + const { slug } = await params; + const document = await fetchLegalDocument(slug); + + if (!document) { + return { + title: "Document légal", + }; + } + + const title = getLocalizedTitle(document); + const description = getLegalDocumentDescription(document); + + return { + title, + description, + alternates: { + canonical: `/legal/${document.slug}`, + }, + openGraph: { + title: `${title} - ${siteConfig.name}`, + description, + type: "article", + publishedTime: document.publishedAt ?? undefined, + }, + }; +} + +export default async function LegalPage({ params }: LegalPageProps) { + const { slug } = await params; + const document = await fetchLegalDocument(slug); + + if (!document) { + return ; + } + + return ; +} diff --git a/src/app/page.tsx b/src/app/page.tsx index 3f36f7c..00cc9f7 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,65 +1,153 @@ import Image from "next/image"; +import Link from "next/link"; + +import { SiteFooter } from "@/components/site-footer"; +import { SiteHeader } from "@/components/site-header"; +import { siteConfig } from "@/content/site"; + +const features = [ + { + title: "Journal repas", + body: "Ajoute tes repas, photos, calories et macros dans un historique lisible.", + accent: "bg-[var(--brand-soft)] text-[var(--brand-ink)]", + }, + { + title: "Objectifs nutritionnels", + body: "Suis tes habitudes avec des indicateurs simples pour garder le cap.", + accent: "bg-[var(--blue-soft)] text-[var(--blue-ink)]", + }, + { + title: "Sport connecté", + body: "Importe tes activités Strava pour rapprocher nutrition et entraînement.", + accent: "bg-[var(--orange-soft)] text-[var(--orange-ink)]", + }, +] as const; + +const previewMeals = [ + { label: "Petit-déj", kcal: "420 kcal", color: "bg-[var(--orange)]" }, + { label: "Déjeuner", kcal: "680 kcal", color: "bg-[var(--brand)]" }, + { label: "Séance", kcal: "Strava", color: "bg-[var(--blue)]" }, +] as const; export default function Home() { return ( -
-
- Next.js logo -
-

- To get started, edit the page.tsx file. -

-

- Looking for a starting point or more instructions? Head over to{" "} - - Templates - {" "} - or the{" "} - - Learning - {" "} - center. -

-
- +
+ + +
+
+
+

+ {siteConfig.formerName} devient {siteConfig.name} +

+

+ {siteConfig.name} +

+

+ {siteConfig.tagline} Suis tes repas, tes objectifs et tes + activités sans transformer ton journal alimentaire en tableau de + bord compliqué. +

+ +
+ + Voir la confidentialité + + + Contacter le support + +
+
+ +
+
+
+ Icône Bowli +
+

+ {"Aujourd'hui"} +

+

+ 1 850 kcal +

+
+
+ +
+ {previewMeals.map((item) => ( +
+
+
+ + {item.kcal} + +
+ ))} +
+
+
+
+ +
+
+
+

+ Application mobile +

+

+ Le nécessaire pour suivre sans friction. +

+
+ +
+ {features.map((feature) => ( +
+ + {feature.title} + +

+ {feature.body} +

+
+ ))} +
+
+
+
+ +
); } diff --git a/src/app/privacy-policy/page.tsx b/src/app/privacy-policy/page.tsx new file mode 100644 index 0000000..23f01f2 --- /dev/null +++ b/src/app/privacy-policy/page.tsx @@ -0,0 +1,3 @@ +export const dynamic = "force-dynamic"; + +export { default, generateMetadata } from "../privacy/page"; diff --git a/src/app/privacy/page.tsx b/src/app/privacy/page.tsx new file mode 100644 index 0000000..3106767 --- /dev/null +++ b/src/app/privacy/page.tsx @@ -0,0 +1,36 @@ +import type { Metadata } from "next"; + +import { LegalDocumentUnavailable } from "@/components/legal-document-unavailable"; +import { LegalDocumentView } from "@/components/legal-document-view"; +import { + fetchLegalDocument, + getLegalDocumentDescription, + getLocalizedTitle, + legalDocumentSlugs, +} from "@/content/legal-documents"; + +export const dynamic = "force-dynamic"; + +export async function generateMetadata(): Promise { + const document = await fetchLegalDocument(legalDocumentSlugs.privacy); + + return { + title: document ? getLocalizedTitle(document) : "Politique de confidentialité", + description: document + ? getLegalDocumentDescription(document) + : "Politique de confidentialité Bowli.", + alternates: { + canonical: "/privacy", + }, + }; +} + +export default async function PrivacyPage() { + const document = await fetchLegalDocument(legalDocumentSlugs.privacy); + + if (!document) { + return ; + } + + return ; +} diff --git a/src/app/robots.ts b/src/app/robots.ts new file mode 100644 index 0000000..3c6be69 --- /dev/null +++ b/src/app/robots.ts @@ -0,0 +1,13 @@ +import type { MetadataRoute } from "next"; + +import { siteConfig } from "@/content/site"; + +export default function robots(): MetadataRoute.Robots { + return { + rules: { + userAgent: "*", + allow: "/", + }, + sitemap: `${siteConfig.siteUrl}/sitemap.xml`, + }; +} diff --git a/src/app/sitemap.ts b/src/app/sitemap.ts new file mode 100644 index 0000000..6f493f1 --- /dev/null +++ b/src/app/sitemap.ts @@ -0,0 +1,18 @@ +import type { MetadataRoute } from "next"; + +import { siteConfig, storeLinks } from "@/content/site"; + +export default function sitemap(): MetadataRoute.Sitemap { + const updatedAt = new Date(siteConfig.legalPublishedAt); + + return [ + { + url: siteConfig.siteUrl, + lastModified: updatedAt, + }, + ...storeLinks.map((link) => ({ + url: `${siteConfig.siteUrl}${link.href}`, + lastModified: updatedAt, + })), + ]; +} diff --git a/src/app/support/page.tsx b/src/app/support/page.tsx new file mode 100644 index 0000000..7993571 --- /dev/null +++ b/src/app/support/page.tsx @@ -0,0 +1,56 @@ +import type { Metadata } from "next"; + +import { SimplePageShell } from "@/components/simple-page-shell"; +import { siteConfig } from "@/content/site"; + +export const metadata: Metadata = { + title: "Support", + description: `Contacter le support ${siteConfig.name}.`, + alternates: { + canonical: "/support", + }, +}; + +export default function SupportPage() { + return ( + +
+

+ Pour une question sur ton compte, un bug, une demande liée aux données + personnelles ou un problème de connexion, contacte l'équipe Bowli par + email. +

+ + + {siteConfig.supportEmail} + + +
+

+ Informations utiles +

+
    +
  • + { + "Indique l'adresse email liée à ton compte si la demande concerne ton profil." + } +
  • +
  • + { + "Ajoute le modèle de téléphone et la version de l'application si la demande concerne un bug." + } +
  • +
  • Pour les demandes de suppression, utilise aussi la page dédiée.
  • +
+
+
+
+ ); +} diff --git a/src/app/terms-and-conditions/page.tsx b/src/app/terms-and-conditions/page.tsx new file mode 100644 index 0000000..e7b07cc --- /dev/null +++ b/src/app/terms-and-conditions/page.tsx @@ -0,0 +1,3 @@ +export const dynamic = "force-dynamic"; + +export { default, generateMetadata } from "../terms/page"; diff --git a/src/app/terms-of-service/page.tsx b/src/app/terms-of-service/page.tsx new file mode 100644 index 0000000..e7b07cc --- /dev/null +++ b/src/app/terms-of-service/page.tsx @@ -0,0 +1,3 @@ +export const dynamic = "force-dynamic"; + +export { default, generateMetadata } from "../terms/page"; diff --git a/src/app/terms/page.tsx b/src/app/terms/page.tsx new file mode 100644 index 0000000..566ae00 --- /dev/null +++ b/src/app/terms/page.tsx @@ -0,0 +1,36 @@ +import type { Metadata } from "next"; + +import { LegalDocumentUnavailable } from "@/components/legal-document-unavailable"; +import { LegalDocumentView } from "@/components/legal-document-view"; +import { + fetchLegalDocument, + getLegalDocumentDescription, + getLocalizedTitle, + legalDocumentSlugs, +} from "@/content/legal-documents"; + +export const dynamic = "force-dynamic"; + +export async function generateMetadata(): Promise { + const document = await fetchLegalDocument(legalDocumentSlugs.terms); + + return { + title: document ? getLocalizedTitle(document) : "Conditions d'utilisation", + description: document + ? getLegalDocumentDescription(document) + : "Conditions d'utilisation Bowli.", + alternates: { + canonical: "/terms", + }, + }; +} + +export default async function TermsPage() { + const document = await fetchLegalDocument(legalDocumentSlugs.terms); + + if (!document) { + return ; + } + + return ; +} diff --git a/src/components/legal-document-unavailable.tsx b/src/components/legal-document-unavailable.tsx new file mode 100644 index 0000000..9f47540 --- /dev/null +++ b/src/components/legal-document-unavailable.tsx @@ -0,0 +1,34 @@ +import { SimplePageShell } from "@/components/simple-page-shell"; +import { siteConfig } from "@/content/site"; + +type LegalDocumentUnavailableProps = { + title: string; +}; + +export function LegalDocumentUnavailable({ + title, +}: LegalDocumentUnavailableProps) { + return ( + +
+

+ Le contenu légal demandé doit être publié depuis le dashboard Bowli + avant d’apparaître sur cette page. +

+

+ Contact :{" "} + + {siteConfig.supportEmail} + +

+
+
+ ); +} diff --git a/src/components/legal-document-view.tsx b/src/components/legal-document-view.tsx new file mode 100644 index 0000000..e0c3b58 --- /dev/null +++ b/src/components/legal-document-view.tsx @@ -0,0 +1,90 @@ +import Link from "next/link"; + +import { + getLegalDocumentDescription, + getLocalizedContent, + getLocalizedTitle, + getPublishedAtLabel, + type LegalDocument, + type LegalLocale, +} from "@/content/legal-documents"; +import { siteConfig } from "@/content/site"; + +type LegalDocumentViewProps = { + document: LegalDocument; + locale?: LegalLocale; +}; + +export function LegalDocumentView({ + document, + locale = "fr", +}: LegalDocumentViewProps) { + const title = getLocalizedTitle(document, locale); + const description = getLegalDocumentDescription(document); + const content = getLocalizedContent(document, locale); + + return ( +
+
+ + {"Retour à l'accueil"} + + +
+
+

+ Version {document.version} +

+

+ {title} +

+

+ {description} +

+

+ Publié le {getPublishedAtLabel(document)} +

+
+ +
+
+ {content.intro.map((paragraph) => ( +

{paragraph}

+ ))} +
+ +
+ {content.sections.map((section) => ( +
+

+ {section.title} +

+
+ {section.body.map((paragraph) => ( +

{paragraph}

+ ))} +
+
+ ))} +
+
+
+ +
+

+ Contact :{" "} + + {siteConfig.supportEmail} + +

+
+
+
+ ); +} diff --git a/src/components/simple-page-shell.tsx b/src/components/simple-page-shell.tsx new file mode 100644 index 0000000..693b0e9 --- /dev/null +++ b/src/components/simple-page-shell.tsx @@ -0,0 +1,43 @@ +import Link from "next/link"; +import type { ReactNode } from "react"; + +type SimplePageShellProps = { + eyebrow: string; + title: string; + description: string; + children: ReactNode; +}; + +export function SimplePageShell({ + eyebrow, + title, + description, + children, +}: SimplePageShellProps) { + return ( +
+
+ + {"Retour à l'accueil"} + + +
+

+ {eyebrow} +

+

+ {title} +

+

+ {description} +

+ +
{children}
+
+
+
+ ); +} diff --git a/src/components/site-footer.tsx b/src/components/site-footer.tsx new file mode 100644 index 0000000..decd1c6 --- /dev/null +++ b/src/components/site-footer.tsx @@ -0,0 +1,31 @@ +import Link from "next/link"; + +import { siteConfig, storeLinks } from "@/content/site"; + +export function SiteFooter() { + return ( +
+
+
+

{siteConfig.name}

+

{siteConfig.description}

+
+ + +
+
+ ); +} diff --git a/src/components/site-header.tsx b/src/components/site-header.tsx new file mode 100644 index 0000000..e46e9b3 --- /dev/null +++ b/src/components/site-header.tsx @@ -0,0 +1,51 @@ +import Image from "next/image"; +import Link from "next/link"; + +import { siteConfig } from "@/content/site"; + +const navItems = [ + { href: "/#features", label: "Fonctionnalités" }, + { href: "/privacy", label: "Confidentialité" }, + { href: "/support", label: "Support" }, +] as const; + +export function SiteHeader() { + return ( +
+
+ + + + {siteConfig.name} + + + + +
+
+ ); +} diff --git a/src/content/legal-documents.ts b/src/content/legal-documents.ts new file mode 100644 index 0000000..b145f53 --- /dev/null +++ b/src/content/legal-documents.ts @@ -0,0 +1,140 @@ +export type LegalLocale = "fr"; + +export type LegalDocumentType = "privacy_policy" | "terms"; + +export type LegalSection = { + title: string; + body: string[]; +}; + +export type LegalContent = Record< + LegalLocale, + { + intro: string[]; + sections: LegalSection[]; + } +>; + +export type LegalDocument = { + id: number; + slug: string; + type: LegalDocumentType; + typeLabel: string; + version: number; + title: Record; + content: LegalContent; + isActive: boolean; + publishedAt: string | null; + createdAt: string; + updatedAt: string; +}; + +type LegalDocumentApiResponse = { + data: LegalDocument; +}; + +const defaultApiUrl = "http://localhost:8003/api"; + +export const legalDocumentSlugs = { + privacy: "privacy-policy", + terms: "terms-of-service", +} as const; + +const legalDocumentSlugAliases: Record = { + privacy: legalDocumentSlugs.privacy, + "privacy-policy": legalDocumentSlugs.privacy, + confidentialite: legalDocumentSlugs.privacy, + "politique-de-confidentialite": legalDocumentSlugs.privacy, + terms: legalDocumentSlugs.terms, + "terms-of-service": legalDocumentSlugs.terms, + "terms-and-conditions": legalDocumentSlugs.terms, + cgu: legalDocumentSlugs.terms, +}; + +export function getLegalDocumentsApiUrl() { + return ( + process.env.LEGAL_DOCUMENTS_API_URL ?? + process.env.NEXT_PUBLIC_API_URL ?? + defaultApiUrl + ).replace(/\/+$/, ""); +} + +export function resolveLegalDocumentSlug(slug: string) { + return legalDocumentSlugAliases[slug] ?? slug; +} + +export async function fetchLegalDocument( + slug: string +): Promise { + const resolvedSlug = resolveLegalDocumentSlug(slug); + + try { + const response = await fetch( + `${getLegalDocumentsApiUrl()}/legal-documents/${encodeURIComponent(resolvedSlug)}`, + { + headers: { + Accept: "application/json", + }, + next: { + revalidate: 60, + }, + } + ); + + if (response.status === 404) { + return null; + } + + if (!response.ok) { + throw new Error(`Legal document request failed: ${response.status}`); + } + + const payload = (await response.json()) as LegalDocumentApiResponse; + + return payload.data; + } catch (error) { + console.error(error); + + return null; + } +} + +export function getLocalizedTitle( + document: LegalDocument, + locale: LegalLocale = "fr" +) { + return ( + document.title[locale] ?? + Object.values(document.title)[0] ?? + document.slug + ); +} + +export function getLocalizedContent( + document: LegalDocument, + locale: LegalLocale = "fr" +) { + return ( + document.content[locale] ?? + Object.values(document.content)[0] ?? { + intro: [], + sections: [], + } + ); +} + +export function getLegalDocumentDescription(document: LegalDocument) { + const content = getLocalizedContent(document); + + return content.intro[0] ?? getLocalizedTitle(document); +} + +export function getPublishedAtLabel(document: LegalDocument) { + if (!document.publishedAt) { + return "Non publié"; + } + + return new Intl.DateTimeFormat("fr-FR", { + dateStyle: "long", + }).format(new Date(document.publishedAt)); +} diff --git a/src/content/site.ts b/src/content/site.ts new file mode 100644 index 0000000..783bcdf --- /dev/null +++ b/src/content/site.ts @@ -0,0 +1,35 @@ +export const siteConfig = { + name: "Bowli", + formerName: "Daily Meal", + appIdentifier: "com.meal.daily", + tagline: "Le carnet repas et sport qui reste simple.", + description: + "Bowli aide à suivre ses repas, ses objectifs nutritionnels et ses activités sportives depuis une application mobile.", + supportEmail: process.env.NEXT_PUBLIC_SUPPORT_EMAIL ?? "support@bowli.app", + siteUrl: process.env.NEXT_PUBLIC_SITE_URL ?? "https://bowli.app", + legalPublishedAt: "2026-05-24T00:00:00+02:00", + legalUpdatedLabel: "24 mai 2026", +} as const; + +export const storeLinks = [ + { + href: "/privacy", + label: "Politique de confidentialité", + description: "URL publique à renseigner dans App Store Connect et Play Console.", + }, + { + href: "/terms", + label: "Conditions d'utilisation", + description: "Conditions applicables à la création et à l'utilisation d'un compte Bowli.", + }, + { + href: "/support", + label: "Support", + description: "Point de contact public pour les fiches Apple et Google.", + }, + { + href: "/delete-account", + label: "Suppression de compte", + description: "URL web dédiée à la demande de suppression de compte Google Play.", + }, +] as const;