From b90d6d0f03cf7ebc81b966879f2a884160d23ce9 Mon Sep 17 00:00:00 2001 From: ScriptSorcerer20 Date: Mon, 30 Mar 2026 19:22:14 +0200 Subject: [PATCH] Improved save and load system, added description and sort options --- app/__pycache__/__init__.cpython-311.pyc | Bin 2477 -> 2477 bytes app/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 4439 bytes .../__pycache__/gamestate.cpython-311.pyc | Bin 1108 -> 1747 bytes .../__pycache__/gamestate.cpython-312.pyc | Bin 0 -> 1743 bytes app/models/gamestate.py | 22 ++- .../__pycache__/main_routes.cpython-311.pyc | Bin 9286 -> 13583 bytes .../__pycache__/main_routes.cpython-312.pyc | Bin 8027 -> 11962 bytes app/routes/main_routes.py | 106 ++++++++--- app/static/css/dashboard.css | 61 +++++++ app/static/js/dashboard.js | 165 ++++++++++++++++-- app/templates/dashboard.html | 57 +++++- database_setup.sql | 13 +- 12 files changed, 378 insertions(+), 46 deletions(-) create mode 100644 app/__pycache__/__init__.cpython-312.pyc create mode 100644 app/models/__pycache__/gamestate.cpython-312.pyc diff --git a/app/__pycache__/__init__.cpython-311.pyc b/app/__pycache__/__init__.cpython-311.pyc index 3a6324cf054d49b95230387a0b02f799fe94505e..c75a2cb53ecd25c07bde4182f6d7f95ab4362cfc 100644 GIT binary patch delta 22 ccmZ20yjGZZIWI340}$L_e=76lM&1RS082~bk#_+n07Mf8lmGw# diff --git a/app/__pycache__/__init__.cpython-312.pyc b/app/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ae97128424de79a8757e3b913bbd82e78df38ceb GIT binary patch literal 4439 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 n)bIe+{T|i#o2AOKcrAz!T6P(*X literal 0 HcmV?d00001 diff --git a/app/models/__pycache__/gamestate.cpython-311.pyc b/app/models/__pycache__/gamestate.cpython-311.pyc index 02fc1f9e2bb7ad5c99298bffc0f3636e79837b3f..9a1f79a2f5a8535fcb9975de9cf305e3db026cce 100644 GIT binary patch literal 1747 zcmb_cO=#Oz9G7I-mi$#qO&YSSqXgE@1BVdruw^3L&uZB=QmE7&O7d z5zOAmxDcHpBJNYHkB~Nna(PFGzupVx{1*Tm5E79g5s{$~nQ>8sL_c!EW7^{wBI?-H z1pHAyXvX_5X!_Vagl0z2Y~S}N+Bb;qa{fNv(4Cmw1SF)#09vIn;;WCNM{R!Es=y z{_+3Q`+v?R*rXvln7xs?hC*z_s7tnQ14yBa>3eXd{UxnX6t!*P2Wfj5 zct)wnvY3;lQ+%yX8CW8ehCj9!N$X-o)=!hbJqe^01w5e}DREP>`=C%+(l#YcxWGnS zU0W@L9Wbg=DIo}aH!@)}f`IJ>!S)G4QAHIQ(*Z$vToL69O;8YYoJ)_@@Wr$}->Pz@ zsMy|wqDvc+W>W=Zdn*uD0D`_J{ixgSoGNO__M~*JplsMQxQD{KLZMuEBH3&ilJ3TL zNEKS+jjO6GiHhwnl!bM1Q`HI}WtMQO3Ps7L*DFfib}8zX?aONth>;-bwy#$drhg;O zAxGlfQhq+0%0GmfvUgJ7WfM!u#jKcrs7Dc$tVH#Sq!p!TNmHS?Vpc4bvN%P#oW+~d z-J6-xwub*7-31q3PQW!a`j0hs!&rOG@_T%BvNmVVAAZqdXWQ&-hn+RzR%l``S1r`3 zhVL-l3dP!?SSJ)SR$d3j_u|z=EoW{Vx?6#nc3`Fxm@#f!{;|Ee>Q}Y?f@yCoMi~Fd?o0Ijq#{7x<`QsM1*ya{H+@i5+g(qv2$<`zF=|<#P{O4#R zdh+RuPg>z*JDluB<#8>=VdFWfDDxy>(k z_+{hHWxTIJqrKdEwe#oBAG*Ujr^tef||Az*on7h@Ga+AXKIcO$eG#7y^ex!3SM^g4eNOS`zwfO^M# t(>tXL+ULOHe`a|NmNOSgk`}?W2H!K9Cp~Ax=mPCx-eCFLe^7SAy$5p?km~>d literal 1108 zcmaJi(Qw+;d_FH_Gm^T}6FGHPz!9><{19Jd`}8DzX@6`&F$qZW%9mR>Jq z^58GA;$y`vON3PYfj!Am1BUWh?@1$-k?Lq|vOc{#)s`kY(nMF9D9`l5w<_7H*5G#n zyUXoxyc3Rh!}0RmLGWf}raE8C*7LjGb}-fn#=5~+`B_iCQJJhhsIAmq*H;?Wht>Kj z@Z@+$j(6pFIe8GdT^p&7HYRr-e&P4U@A5bKYiK{zjwCvfL^qNszhK?E>q=dW`Y0Nl zvb?6Nr42=Bk!7>)#v<9aC49{zf0 ctUdf4c|`8<5xP9oB+!2UH}m|t!rhnf7vUZSivR!s diff --git a/app/models/__pycache__/gamestate.cpython-312.pyc b/app/models/__pycache__/gamestate.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..db13518392aa5ee34fb3b8da4780cd331dbc795d GIT binary patch literal 1743 zcmcIkO=ufO7@b}1N~_gREH$>Ox(%*r>u#Y*ae9eT0#%b3yCj4vpd-`jcL z_swHxev8FqK!^Ry3;o>y0DrJxG{hk|&QRb22q0&GBA4Shz=I_K`NseT5Vs|a1gC3{ z=3*lks0oUA#8aqngopVJpB_%3;d*?DZg&9!ISzsx54l``2T0iRTN@Vu#zz0iun6)8 zfgUSDM=ta)wZegTqLt3kCI|V)XE?5vCux0tW?VPGFxcaUXgp7JQl1HC7hvc>Ix##p zNjkX-c z=!s|3|9`dr&|j=KyjZ!JT3oq6!b@5at=gK6h+LphEi-(d-m6Fn>qQOM)bCL3HjO2- zVi;Q9Ku7Eb9Z7Ujc+3_Of@o8#81~UU8a<4znI_Ey=T^0CL{2uv%Ls2HtPb}o0bh&b zEP-U1l@g?d?tzsP_Arp(SC&yJnk2Mr z+Ngjq;dMwt6-w~3gyGz>{_)yBG6AOcNK5AKs{S$~K~xJ;Y&>yMyCtqEl< z%Rrh*B6?Zf)V3|GQ&zgktkR2!2%8mig9J>gO5_cUC=FH3Ci37=4F3#{X^-G_N-LGF zu&PG+3iI2y`*Nv<*}LdZ>@4N?=^cIyyo+=7J1@h@-DES}NVgW1A|!H``opEVuG*!71#E{pjWTEiZC$cd>b`ajmu9`o>*% z*4udRTdIg$t>5$#@3fL`+DW$;yO~brP<-;BpZK!A;!UMnyc>4HZKXZmRXWN~*&jbW z{P5|e{?xtt^6A(4?m}l_|JIYm{?rZ1CudvpuHq=|Y?d#6zaFo0y>p%N z_<8kN_4&@TonIgI6Kk~Is1Hw)n~_GuOU!twv|Dwm?vAsw_sC1U<))mJJL}Buz4t0C zCWAk$;DwQAcNGE0Sm6H%;vMGiz)^O82m3yz=(Y64;D@?_KcOLeD9dlt>CXVialeDa SDA59G`U_C#EB^*Ewp?xi literal 0 HcmV?d00001 diff --git a/app/models/gamestate.py b/app/models/gamestate.py index d68aa08..7aef035 100644 --- a/app/models/gamestate.py +++ b/app/models/gamestate.py @@ -1,11 +1,21 @@ from .. import db -from flask_login import UserMixin -class GameState(UserMixin, db.Model): +class GameState(db.Model): + __tablename__ = "game_state" + id = db.Column(db.Integer, primary_key=True) user_id = db.Column(db.Integer, nullable=False) - board = db.Column(db.Text, nullable=False) # JSON String - mode = db.Column(db.String(10), nullable=False) # queens / rooks - - created_at = db.Column(db.DateTime, default=db.func.now()) + board = db.Column(db.Text, nullable=False) + mode = db.Column(db.String(10), nullable=False) + board_size = db.Column(db.Integer, nullable=False, default=8) + save_name = db.Column(db.String(80), nullable=False, default="Unnamed Save") + save_note = db.Column(db.Text, nullable=False, default="") + is_favorite = db.Column(db.Boolean, nullable=False, default=False) + created_at = db.Column(db.DateTime, nullable=False, server_default=db.func.now()) + updated_at = db.Column( + db.DateTime, + nullable=False, + server_default=db.func.now(), + onupdate=db.func.now() + ) diff --git a/app/routes/__pycache__/main_routes.cpython-311.pyc b/app/routes/__pycache__/main_routes.cpython-311.pyc index c5a0bbff6a3d1f6c64b0d3e6c19412e50f45f3d0..ec65ad5f4128bc316292c594bb4bf37706520277 100644 GIT binary patch delta 5741 zcmb7HeQX=ab>AU(Ns&wOWBDmcqDa}4Oi~stAN^6;)!BFFvrcEp7fa3|mk);4u4JmT zsLoP8%cVosFd8Q@8r$QV zB=?X1xxQIar09Ud-Kcj?=INi>Fl&J(B8TCFWC=**X#lG{|n&+tuG@> zh6LBQx$J#IiVL?SBW;o$#sn8>FuNIXo4EbSJ;TRE^^fas2*URdv~LbX26d5bEs-5h zh{ULfdAo?FnOOY(sPX#-ml2MIKEE3Wa+4__^0_r=SVhzdzv^<7M0Az`X~WbQ3j!J% z(G2-T&A1}T8e722Qp}(kec7$B<^hnK#pCGr46+rq&7L9;)YF%3xVa&t=Z$ zakc=RTxK2*04E5K90pQm)_MEom2Zt*8LRPqRlZN*BP#ERRHDyU_~$F^^S=Wj=Keo% z|B6sQA?9TR9wgR||JWacY1oA+v$P~Fia0@sX1o*r@GvFal(HrzlTl(fL{f^Ck!A%xzBfK6v zbZ(WW#(B@?B3->U!{*3?tfXoix6$LkDIimLKN;x!0-7S{I+I;=Q|DdLFYSz;*%=kX z(ecsfCnNHAZBIzGmSh&1=Tmn-K#D-&>3mxQN!t0H zo5Ih~w?DzRik&}TFL3aQ4p@<6irlIhI^9f>ea4&hFoB&l&Xmk|^`XtVQu{dr!r7

FLRV^P8-SSV(~r|1HmvXql% zX)K?~151z5*D1qHKNSQuo8HMU6|(t7S*C9d>#dIMzEjKDoS4=jWGSCrEKr2SD>w&o zGw{gg01N@%ZQd)#Yqn_B7QL4%-OX2QQN=c<+Q!O9KJj$lI9Bs)uX?txeo^rZsh*+o z{Yz`RPVm>)Vke!C0DbDs`k*W zv#aM6`!3bKt9;x;;5Q9N(IMN=lEsr6Lav^m|~8r=J*DKxZMre+b>a(5!E?znfuJdxO-G#pe7`$ zLP8OCs>057p@*OYwn?H$vvdFhgJ9Rx;LbH<27%x>P;%%5!E}g zVc|RgSwPCiHtdYe_1&>=kKG!o1>@CV{Pw<|y`rYZYpKapr2pYWK@BB6Tx{m*? zT&yR7FM|xGX-eauPRNQc&wwXVr@~W|NR(rd7MEc&6!(YdBrQH-7LA2Glc^__`l4i7 z_&WCuV4f!rgwG)%KXCubhefJF*U|MbP1yKrjmOk!W(W2E^!9U?pnD6{$3S(;=jSaju4fMeWRG!FyzU7gBiZHZCjahqxbL#< zFmu631-x84v@Rjhw8}ML(zN6&8dgn>k>#%$(fpPH(g4+a4EML{Iqh7*0h?(weIFL` z1y9fJ3#P5fpdf6DKw@ixc!7RHqb=~{a4?W68j8juTjYu+$QOV(?^4v!+XZtWu-S)2 zT1ak(O(&S5x&7-9^9*qc(L6I+PYmFXXO|+52z3x)yRNV=vKWP@-^`^7u zC4g62PeL65<;)b64)`lDJkN!vVceLdg^e{Uzuc%qp;Y`L$h4MwYK_9t1w~qA(pqGB zT-K~DxQhu}yjS-k-Gz-g3CbX-XtGNhSI8D}lE$9O=W{x&H$a^Hw?AIOw5Ex5;B(D= ztkB+5Y9)LE7&0vk>-ZaPRM+*Ijn+!|d%*k~Ds@-~UBD%q`^v%cOAl?%tNtsKl6OZWpdB-0~{+KGoj0*6Ck!_pS-u^ylf@ zaC8W!PZ=P;r=&dbnTO%bmnObBb#dzIsKSO+HdJ9lBk9;0Fx4VcInvrjV;xpYG6=;HJv;_);dRw(N z!rBR8-NXh+3Vu>?21(lEDsfYH4E6ejMxMq=80YaL&oqQsSg#+#i}^x$F2B4e{`LlV zam-c^nNuK*X%cHyKpHof#SmijK=twlQ%u9xDY-!jHOU==W^Q~u4WKzjHo)v=>;wz> z`S~1NGT`;;dik<>K=|)K?jv~QzW|~eMCiWJS98UyuK4Zw_lo~ms<!jj(U3I-)o_NT6$)ATq_J@rfIr-B+Ga*&y}AnUCFrx2=l%Io17~!lzU|Rbf+g zn+2b3GtKnUkz+?@j6@RmI25@fPAy+NbEWdHOf?$@XOKcjh)NVKt zE+tvij_FV?o2T$w@P16O2LF(fRZ6}CL^F#}P(nQ!~!0R~HmEqOy?+8v#279odR|hr-8H zK2~94fD0dXhtZ|RL8lSD6AqDlm!CZ7+O~PYSha=7y&kX24V!5+(J+q+9`bKJ(atTi z5DcGNcN~Xfxft%~_k$3eI;E$5^${BvLcR59`hMC68%{C(tS0z!un+!tE3 zgQWXI5j%p^(2j}vj;iRWu4_SiHW(mH%@Si6 zN5I`+DJ#%aP5k8aPu`%ahg5W^p$eBG3yL>Y8N*@FJVZ6)P0h=1D#))Qzpi3Mdp8&$ sTT}wT+h8dpsK4vL#omfzyMm%Bit0Khx delta 2675 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 C1s!Vu delta 2366 zcmZ`)du&^06~CYF!}0Yaj$d*7N*ph7ZsIhZC~bkRTdO{3*-~|FS=Fhyce!_|tt3vq zYZp4wkxGbWAn2@v)Jhvn`v+9gO`D1Y0)aFM@sv;^4<))pz{G^aKj5zY10f_h=UO+0 zpwVOU8(nahb2V7)6YQ$#Q-T0cPT0uh*@YJThSL6%>{$!N%Zmp{_sbLq=}3nvHgp4`ixm# zYMAB%FM{eDWs~qI(s4NNyIjx+NHw`5Xma-&pCo;e$S(M^FE8&#HU&QQQwrUfVX#z; z!ROQ}%fqkKqw-!7^}1=4 z^}6P>rK(k1u;sa$Zdm*=a&v&lK_s>~XD-;H)tKi`kl16@`SbP4yop6sXZ3lrY}L^b ze+*5mrMb?{;F~_6SsAD^v*F4|)SiTky&X*WgC{RB8%w+^K4Y97~sL z^Cmxu?32)rCzuA_L}TznAfKUIm!GTY+~7|k@-)1XNHgu;4-%_v#)-SzSSNVJ>88c0 zG`4(2pQ{+h&T0x*Q09k7DbAX8+h;B`@Q+g-!(@?=P+1JUu6OFu@e8jdFUilM@fnZ? z)-lB24SavVmUKRA@iXp4P7`p4@ie@aD#$ttHTZkF2!Bfj@y5SW-x*Ajk%s|6_TD&o{p5}5>(ejqc~?qqOA5)GQhY;-zbo~>?~lCv>D#mG z(b1N_xGoiOb|d*B?wIEAKKM!TID9)2V?kJoq#1+0v0>?&Q164Ke40h!>-pUQ%`=Uo zU}8*~3YBPf4?Qfqr^XGe%X3$8#7t|(7WKI~XXrRRAbeSVr#Mbh7C#1m%@3COD3Vh|3DjXqrmhe+7^ARjN|>!&QRcYNgV4U;I|8nY#U% z&CKLRX0p|{Yb%`I3=eIDhwjT_OnD%P0dHFnWiNJH9sfr^ri1Z2E0H>B%O>YF{sL?i z;wLKK#Q(cm&qrtU1qzX}aC_@@wr0h!gO)jG&d?Gj4eXuBe{uzJZ{V@cB3Th0DndMW z>s0g9W^8gJHu+Zn&q}|TdS_}qHra|jy%{^d5j)?CU08YQJ%19K6WL_272VtN?^~Dl zIg3@xcdGO;`1!=gnGOG*$g!)CD~+C@6~&!3Ljz~aywz_dQ;Aj_sfZ+Y-s`m<#)Si}gFOAoK2^l5Gja_7F68 R#p3&S)T8$$h3$8W@E>KLEdl@l diff --git a/app/routes/main_routes.py b/app/routes/main_routes.py index cf8405c..c1b77b2 100644 --- a/app/routes/main_routes.py +++ b/app/routes/main_routes.py @@ -23,6 +23,10 @@ def parse_board_size(raw_size, default=DEFAULT_BOARD_SIZE): return max(MIN_BOARD_SIZE, min(MAX_BOARD_SIZE, board_size)) +def parse_favorite_flag(raw_value): + return str(raw_value).lower() in {"1", "true", "yes", "on"} + + def solve_queens(board_size): queen_positions = [-1] * board_size queen_solutions = [] @@ -67,12 +71,43 @@ def serialize_game_state(game_state): "id": game_state.id, "board": board, "mode": game_state.mode, - "board_size": len(board), + "board_size": game_state.board_size, + "save_name": game_state.save_name, + "save_note": game_state.save_note, + "is_favorite": game_state.is_favorite, "pieces_placed": sum(1 for column in board if column != -1), - "created_at": game_state.created_at.strftime("%Y-%m-%d %H:%M:%S") + "created_at": game_state.created_at.strftime("%Y-%m-%d %H:%M:%S"), + "updated_at": game_state.updated_at.strftime("%Y-%m-%d %H:%M:%S") } +def build_save_point_query(): + query = GameState.query.filter_by(user_id=current_user.id) + + selected_mode = request.args.get("mode") + board_size = request.args.get("board_size") + favorites_only = request.args.get("favorites_only") + sort_by = request.args.get("sort", "newest") + + if selected_mode in {"queens", "rooks"}: + query = query.filter_by(mode=selected_mode) + + if board_size not in {None, "", "all"}: + query = query.filter_by(board_size=parse_board_size(board_size)) + + if parse_favorite_flag(favorites_only): + query = query.filter_by(is_favorite=True) + + sort_options = { + "newest": GameState.created_at.desc(), + "oldest": GameState.created_at.asc(), + "updated": GameState.updated_at.desc(), + "favorites": GameState.is_favorite.desc() + } + + return query.order_by(sort_options.get(sort_by, GameState.created_at.desc()), GameState.id.desc()) + + @main_bp.route("/") def home(): return render_template("index.html") @@ -127,29 +162,63 @@ def save_game(): board = data["board"] piece_mode = data["mode"] + board_size = parse_board_size(data.get("boardSize"), default=len(board)) + save_name = (data.get("saveName") or "").strip() or f"{piece_mode.title()} Save" + save_note = (data.get("saveNote") or "").strip() + is_favorite = bool(data.get("isFavorite")) - game = GameState( + game_state = GameState( user_id=current_user.id, board=json.dumps(board), - mode=piece_mode + mode=piece_mode, + board_size=board_size, + save_name=save_name, + save_note=save_note, + is_favorite=is_favorite ) - db.session.add(game) + db.session.add(game_state) db.session.commit() - return jsonify({"status": "saved"}) + return jsonify({ + "status": "saved", + "save_point": serialize_game_state(game_state) + }) @main_bp.route("/save_points") @login_required def list_save_points(): - saved_games = ( - GameState.query.filter_by(user_id=current_user.id) - .order_by(GameState.id.desc()) - .all() - ) + save_points = build_save_point_query().all() + return jsonify([serialize_game_state(game_state) for game_state in save_points]) - return jsonify([serialize_game_state(game_state) for game_state in saved_games]) + +@main_bp.route("/save_points//favorite", methods=["POST"]) +@login_required +def toggle_save_point_favorite(game_id): + game_state = GameState.query.filter_by(id=game_id, user_id=current_user.id).first() + + if not game_state: + return jsonify({"error": "Save point not found"}), 404 + + game_state.is_favorite = not game_state.is_favorite + db.session.commit() + + return jsonify(serialize_game_state(game_state)) + + +@main_bp.route("/save_points/", methods=["DELETE"]) +@login_required +def delete_save_point(game_id): + game_state = GameState.query.filter_by(id=game_id, user_id=current_user.id).first() + + if not game_state: + return jsonify({"error": "Save point not found"}), 404 + + db.session.delete(game_state) + db.session.commit() + + return jsonify({"status": "deleted"}) @main_bp.route("/load") @@ -157,25 +226,22 @@ def list_save_points(): def load_game(): latest_game = ( GameState.query.filter_by(user_id=current_user.id) - .order_by(GameState.id.desc()) + .order_by(GameState.created_at.desc(), GameState.id.desc()) .first() ) if not latest_game: return jsonify({"board": [-1] * DEFAULT_BOARD_SIZE, "mode": "queens"}) - return jsonify({ - "board": json.loads(latest_game.board), - "mode": latest_game.mode - }) + return jsonify(serialize_game_state(latest_game)) @main_bp.route("/load/") @login_required def load_specific_game(game_id): - selected_game = GameState.query.filter_by(id=game_id, user_id=current_user.id).first() + game_state = GameState.query.filter_by(id=game_id, user_id=current_user.id).first() - if not selected_game: + if not game_state: return jsonify({"error": "Save point not found"}), 404 - return jsonify(serialize_game_state(selected_game)) + return jsonify(serialize_game_state(game_state)) diff --git a/app/static/css/dashboard.css b/app/static/css/dashboard.css index ee0e1b3..c3ca90f 100644 --- a/app/static/css/dashboard.css +++ b/app/static/css/dashboard.css @@ -190,6 +190,10 @@ body { width: min(380px, 92vw); } +.save-sidebar { + width: min(360px, 90vw); +} + .sidebar-header { display: flex; justify-content: space-between; @@ -212,6 +216,29 @@ body { width: 100%; } +.sidebar-input { + width: 100%; + padding: 10px 12px; + border: 1px solid #cbd5e1; + border-radius: 8px; + font: inherit; + box-sizing: border-box; + background: #fff; +} + +.sidebar-textarea { + resize: vertical; + min-height: 96px; +} + +.sidebar-checkbox { + display: flex; + align-items: center; + gap: 10px; + margin: 16px 0 18px; + color: #334155; +} + .sidebar-size-row { margin: 12px 0 18px; font-size: 18px; @@ -228,6 +255,14 @@ body { margin-top: 18px; } +.save-point-filters { + display: grid; + gap: 8px; + margin-top: 18px; + padding-bottom: 16px; + border-bottom: 1px solid #e2e8f0; +} + .save-point-card { border: 1px solid #dbe4ee; border-radius: 10px; @@ -253,6 +288,32 @@ body { word-break: break-word; } +.save-point-note { + margin: 10px 0 0; + color: #475569; + font-size: 14px; +} + +.save-point-actions { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-top: 14px; +} + +.save-point-actions .btn { + margin: 0; +} + +.btn.ghost { + background: #e2e8f0; + color: #1e293b; +} + +.btn.ghost:hover { + background: #cbd5e1; +} + .sidebar-backdrop { position: fixed; inset: 0; diff --git a/app/static/js/dashboard.js b/app/static/js/dashboard.js index abb2334..437d712 100644 --- a/app/static/js/dashboard.js +++ b/app/static/js/dashboard.js @@ -15,6 +15,8 @@ const btnQueens = document.getElementById("btnQueens"); const btnRooks = document.getElementById("btnRooks"); const settingsSidebar = document.getElementById("settingsSidebar"); const sidebarBackdrop = document.getElementById("sidebarBackdrop"); +const saveSidebar = document.getElementById("saveSidebar"); +const saveBackdrop = document.getElementById("saveBackdrop"); const savePointsSidebar = document.getElementById("savePointsSidebar"); const savePointsBackdrop = document.getElementById("savePointsBackdrop"); const boardSizeInput = document.getElementById("boardSizeInput"); @@ -23,6 +25,13 @@ const boardSizeValueMirror = document.getElementById("boardSizeValueMirror"); const sidebarBoardSizeValue = document.getElementById("sidebarBoardSizeValue"); const sidebarBoardSizeValueMirror = document.getElementById("sidebarBoardSizeValueMirror"); const savePointsList = document.getElementById("savePointsList"); +const saveNameInput = document.getElementById("saveNameInput"); +const saveNoteInput = document.getElementById("saveNoteInput"); +const saveFavoriteInput = document.getElementById("saveFavoriteInput"); +const savePointModeFilter = document.getElementById("savePointModeFilter"); +const savePointSizeFilter = document.getElementById("savePointSizeFilter"); +const savePointSortFilter = document.getElementById("savePointSortFilter"); +const savePointFavoritesOnly = document.getElementById("savePointFavoritesOnly"); function createEmptyBoard(size) { return Array(size).fill(EMPTY_CELL); @@ -67,6 +76,12 @@ function setMode(newMode) { updateModeButtons(); } +function closeAllPanels() { + toggleSidebar(false); + toggleSavePanel(false); + toggleSavePointsPanel(false); +} + function getConflicts() { const conflicts = []; @@ -178,6 +193,7 @@ function toggleSidebar(forceOpen) { const shouldOpen = typeof forceOpen === "boolean" ? forceOpen : !settingsSidebar.classList.contains("open"); if (shouldOpen) { + toggleSavePanel(false); toggleSavePointsPanel(false); } @@ -186,9 +202,27 @@ function toggleSidebar(forceOpen) { settingsSidebar.setAttribute("aria-hidden", String(!shouldOpen)); } +function toggleSavePanel(forceOpen) { + const shouldOpen = typeof forceOpen === "boolean" ? forceOpen : !saveSidebar.classList.contains("open"); + + if (shouldOpen) { + toggleSidebar(false); + toggleSavePointsPanel(false); + } + + saveSidebar.classList.toggle("open", shouldOpen); + saveBackdrop.classList.toggle("visible", shouldOpen); + saveSidebar.setAttribute("aria-hidden", String(!shouldOpen)); +} + function toggleSavePointsPanel(forceOpen) { const shouldOpen = typeof forceOpen === "boolean" ? forceOpen : !savePointsSidebar.classList.contains("open"); + if (shouldOpen) { + toggleSidebar(false); + toggleSavePanel(false); + } + savePointsSidebar.classList.toggle("open", shouldOpen); savePointsBackdrop.classList.toggle("visible", shouldOpen); savePointsSidebar.setAttribute("aria-hidden", String(!shouldOpen)); @@ -198,6 +232,21 @@ function getPieceLabel(selectedMode) { return selectedMode === "queens" ? "Dame" : "Turm"; } +function getPlacedPieceCount() { + return board.filter((col) => col !== EMPTY_CELL).length; +} + +function resetSaveForm() { + saveNameInput.value = `${getPieceLabel(mode)} ${boardSize}x${boardSize} - ${getPlacedPieceCount()} gesetzt`; + saveNoteInput.value = ""; + saveFavoriteInput.checked = false; +} + +function openSavePanel() { + resetSaveForm(); + toggleSavePanel(true); +} + function getBoardPreview(savedBoard, limit = 4) { const preview = []; @@ -216,11 +265,40 @@ function getBoardPreview(savedBoard, limit = 4) { return preview.length > 0 ? preview.join(", ") : "Noch keine Figuren gesetzt"; } +function getSavePointFilters() { + const params = new URLSearchParams(); + + if (savePointModeFilter.value !== "all") { + params.set("mode", savePointModeFilter.value); + } + + if (savePointSizeFilter.value !== "all") { + params.set("board_size", savePointSizeFilter.value); + } + + params.set("sort", savePointSortFilter.value); + + if (savePointFavoritesOnly.checked) { + params.set("favorites_only", "true"); + } + + return params.toString(); +} + +function createSavePointActionButton(label, className, onClick) { + const button = document.createElement("button"); + button.className = className; + button.type = "button"; + button.textContent = label; + button.onclick = onClick; + return button; +} + function renderSavePoints(savePoints) { savePointsList.innerHTML = ""; if (savePoints.length === 0) { - savePointsList.innerHTML = '

'; + savePointsList.innerHTML = ''; return; } @@ -228,42 +306,54 @@ function renderSavePoints(savePoints) { const item = document.createElement("article"); const title = document.createElement("h4"); const createdAt = document.createElement("p"); + const updatedAt = document.createElement("p"); const boardMeta = document.createElement("p"); const preview = document.createElement("p"); - const loadButton = document.createElement("button"); + const note = document.createElement("p"); + const actions = document.createElement("div"); item.className = "save-point-card"; - title.textContent = `Save Point #${savePoint.id}`; + title.textContent = `${savePoint.is_favorite ? "★ " : ""}${savePoint.save_name}`; createdAt.className = "save-point-meta"; - createdAt.textContent = `Gespeichert: ${savePoint.created_at}`; + createdAt.textContent = `Erstellt: ${savePoint.created_at}`; + updatedAt.className = "save-point-meta"; + updatedAt.textContent = `Aktualisiert: ${savePoint.updated_at}`; boardMeta.className = "save-point-meta"; boardMeta.textContent = `${getPieceLabel(savePoint.mode)} | ${savePoint.board_size} x ${savePoint.board_size} | ${savePoint.pieces_placed}/${savePoint.board_size} gesetzt`; preview.className = "save-point-preview"; preview.textContent = `Vorschau: ${getBoardPreview(savePoint.board)}`; + note.className = "save-point-note"; + note.textContent = savePoint.save_note ? `Notiz: ${savePoint.save_note}` : "Notiz: Keine Notiz"; + actions.className = "save-point-actions"; - loadButton.className = "btn"; - loadButton.type = "button"; - loadButton.textContent = "Diesen laden"; - loadButton.onclick = () => loadSavePoint(savePoint.id); + actions.appendChild(createSavePointActionButton("Laden", "btn", () => loadSavePoint(savePoint.id))); + actions.appendChild(createSavePointActionButton( + savePoint.is_favorite ? "Favorit entfernen" : "Als Favorit", + "btn secondary", + () => toggleSavePointFavorite(savePoint.id) + )); + actions.appendChild(createSavePointActionButton("Loeschen", "btn ghost", () => deleteSavePoint(savePoint.id))); item.appendChild(title); item.appendChild(createdAt); + item.appendChild(updatedAt); item.appendChild(boardMeta); item.appendChild(preview); - item.appendChild(loadButton); + item.appendChild(note); + item.appendChild(actions); savePointsList.appendChild(item); } } async function refreshSavePoints() { - const response = await fetch("/save_points"); + const queryString = getSavePointFilters(); + const response = await fetch(`/save_points${queryString ? `?${queryString}` : ""}`); const savePoints = await response.json(); renderSavePoints(savePoints); } async function openSavePointsPanel() { await refreshSavePoints(); - toggleSidebar(false); toggleSavePointsPanel(true); } @@ -316,7 +406,7 @@ function updateQueenList() { } async function saveGame() { - await fetch("/save", { + const response = await fetch("/save", { method: "POST", headers: { "Content-Type": "application/json" @@ -324,12 +414,23 @@ async function saveGame() { body: JSON.stringify({ board, mode, - boardSize + boardSize, + saveName: saveNameInput.value, + saveNote: saveNoteInput.value, + isFavorite: saveFavoriteInput.checked }) }); + if (!response.ok) { + setStatus("❌ Der Speicherstand konnte nicht gespeichert werden.", "red"); + return; + } + + const data = await response.json(); + + toggleSavePanel(false); await refreshSavePoints(); - alert("💾 Spiel gespeichert!"); + setStatus(`Speicherpunkt "${data.save_point.save_name}" gespeichert.`, "green"); } async function loadGame() { @@ -349,10 +450,44 @@ async function loadSavePoint(savePointId) { const data = await response.json(); applyLoadedGame(data); - setStatus(`Speicherstand #${savePointId} geladen.`, "green"); + setStatus(`Speicherpunkt "${data.save_name}" geladen.`, "green"); toggleSavePointsPanel(false); } +async function toggleSavePointFavorite(savePointId) { + const response = await fetch(`/save_points/${savePointId}/favorite`, { + method: "POST" + }); + + if (!response.ok) { + setStatus("❌ Favorit konnte nicht aktualisiert werden.", "red"); + return; + } + + await refreshSavePoints(); + setStatus("Favoritenstatus aktualisiert.", "#2563eb"); +} + +async function deleteSavePoint(savePointId) { + const confirmed = window.confirm("Diesen Speicherpunkt wirklich loeschen?"); + + if (!confirmed) { + return; + } + + const response = await fetch(`/save_points/${savePointId}`, { + method: "DELETE" + }); + + if (!response.ok) { + setStatus("❌ Speicherpunkt konnte nicht geloescht werden.", "red"); + return; + } + + await refreshSavePoints(); + setStatus("Speicherpunkt geloescht.", "#2563eb"); +} + pendingBoardSize = boardSize; updateModeButtons(); updateBoardSizeDisplays(boardSize); diff --git a/app/templates/dashboard.html b/app/templates/dashboard.html index 9c988c1..1c82d34 100644 --- a/app/templates/dashboard.html +++ b/app/templates/dashboard.html @@ -66,7 +66,7 @@ - + @@ -95,6 +95,29 @@ + + + diff --git a/database_setup.sql b/database_setup.sql index c2ece83..161cd8b 100644 --- a/database_setup.sql +++ b/database_setup.sql @@ -9,7 +9,12 @@ CREATE TABLE users ( CREATE TABLE game_state ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, - board TEXT NOT NULL, -- JSON String (z.B. "[0,4,7,5,2,6,1,3]") - mode TEXT NOT NULL, -- "queens" oder "rooks" - created_at DATETIME DEFAULT CURRENT_TIMESTAMP -); \ No newline at end of file + board TEXT NOT NULL, -- JSON String (z.B. "[0,4,7,5,2,6,1,3]") + mode TEXT NOT NULL, -- "queens" oder "rooks" + board_size INTEGER NOT NULL DEFAULT 8, + save_name VARCHAR(80) NOT NULL DEFAULT 'Unnamed Save', + save_note TEXT NOT NULL DEFAULT '', + is_favorite BOOLEAN NOT NULL DEFAULT 0, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP +);