Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a023c97618 | |||
| 67c475f14c | |||
| be5be81cbd | |||
| bc75334590 | |||
| 7fdadf4b0b | |||
| 97198efb1c | |||
| 2d0bce143a | |||
| 524e517066 | |||
| d07f18d380 | |||
| f32bb49972 | |||
| 8290f77889 | |||
| ce8145a42e | |||
| 3064235a80 | |||
| 17068aa28c | |||
| aee6fbaf73 | |||
| 0d591c0fa9 | |||
| 898846c654 | |||
| 65127c2273 | |||
| 55e80181df | |||
| f01dfa01cb | |||
| d398de1b47 | |||
| 8a707610bf | |||
| 2a1d8e13c8 | |||
| 93757126e1 | |||
| e8e2df3c43 | |||
| 28e86e5032 |
@@ -3,7 +3,7 @@ FROM node:lts AS buildfront
|
|||||||
COPY front .
|
COPY front .
|
||||||
RUN npm install && npm run build
|
RUN npm install && npm run build
|
||||||
|
|
||||||
FROM golang:1.25 AS build
|
FROM golang:1.26 AS build
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
COPY . .
|
COPY . .
|
||||||
COPY --from=buildfront ./dist front/dist
|
COPY --from=buildfront ./dist front/dist
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ Or with a volume, for example if you created a volume named `bibliomane_data`:
|
|||||||
|
|
||||||
`--add-user` or `-a` can be used to create an account on startup. It requires a string following htpasswd format `[username]:[bcrypt hashed password]`.
|
`--add-user` or `-a` can be used to create an account on startup. It requires a string following htpasswd format `[username]:[bcrypt hashed password]`.
|
||||||
|
|
||||||
The password can be generated using `htpasswd -nB [username]`.
|
The password can be generated using `htpasswd -nBC10 [username]`.
|
||||||
|
|
||||||
For example, to create an user account `demo`:
|
For example, to create an user account `demo`:
|
||||||
|
|
||||||
|
|||||||
@@ -69,9 +69,9 @@ INSERT INTO user_books(created_at, user_id, book_id, read, rating) VALUES ('NOW'
|
|||||||
INSERT INTO books(created_at, title, author_id, added_by_id, cover_id) VALUES ('NOW', 'Le petit bleu de la côte Ouest',(SELECT id FROM authors WHERE name = 'Jean-Patrick Manchette'), (SELECT id FROM users WHERE name = 'demo'),(SELECT id FROM static_files WHERE name = 'le-petit-bleu-de-la-cote-ouest.jpg'));
|
INSERT INTO books(created_at, title, author_id, added_by_id, cover_id) VALUES ('NOW', 'Le petit bleu de la côte Ouest',(SELECT id FROM authors WHERE name = 'Jean-Patrick Manchette'), (SELECT id FROM users WHERE name = 'demo'),(SELECT id FROM static_files WHERE name = 'le-petit-bleu-de-la-cote-ouest.jpg'));
|
||||||
INSERT INTO user_books(created_at, user_id, book_id, want_read, rating) VALUES ('NOW',(SELECT id FROM users WHERE name = 'demo'),(SELECT id FROM books WHERE title = 'Le petit bleu de la côte Ouest'), true,0);
|
INSERT INTO user_books(created_at, user_id, book_id, want_read, rating) VALUES ('NOW',(SELECT id FROM users WHERE name = 'demo'),(SELECT id FROM books WHERE title = 'Le petit bleu de la côte Ouest'), true,0);
|
||||||
INSERT INTO books(created_at, title, author_id, added_by_id, cover_id) VALUES ('NOW', 'D''un château l''autre',(SELECT id FROM authors WHERE name = 'Louis-Ferdinand Céline'), (SELECT id FROM users WHERE name = 'demo'),(SELECT id FROM static_files WHERE name = 'dunchateaulautre.jpg'));
|
INSERT INTO books(created_at, title, author_id, added_by_id, cover_id) VALUES ('NOW', 'D''un château l''autre',(SELECT id FROM authors WHERE name = 'Louis-Ferdinand Céline'), (SELECT id FROM users WHERE name = 'demo'),(SELECT id FROM static_files WHERE name = 'dunchateaulautre.jpg'));
|
||||||
INSERT INTO user_books(created_at, user_id, book_id, read, rating) VALUES ('NOW',(SELECT id FROM users WHERE name = 'demo'),(SELECT id FROM books WHERE title = 'D''un château l''autre'), true,10);
|
INSERT INTO user_books(created_at, user_id, book_id, read, rating, review) VALUES ('NOW',(SELECT id FROM users WHERE name = 'demo'),(SELECT id FROM books WHERE title = 'D''un château l''autre'), true,10, "Lorem ipsum dolor sit amet, consectetur adipisci elit, sed eiusmod tempor incidunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur. Quis aute iure reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint obcaecat cupiditat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.");
|
||||||
INSERT INTO books(created_at, title, author_id, added_by_id, cover_id) VALUES ('NOW', 'Les dieux ont soif',(SELECT id FROM authors WHERE name = 'Anatole France'), (SELECT id FROM users WHERE name = 'demo'),(SELECT id FROM static_files WHERE name = 'lesdieuxontsoif.jpg'));
|
INSERT INTO books(created_at, title, author_id, added_by_id, cover_id) VALUES ('NOW', 'Les dieux ont soif',(SELECT id FROM authors WHERE name = 'Anatole France'), (SELECT id FROM users WHERE name = 'demo'),(SELECT id FROM static_files WHERE name = 'lesdieuxontsoif.jpg'));
|
||||||
INSERT INTO user_books(created_at, user_id, book_id, read, rating) VALUES ('NOW',(SELECT id FROM users WHERE name = 'demo'),(SELECT id FROM books WHERE title = 'Les dieux ont soif'), true,7);
|
INSERT INTO user_books(created_at, user_id, book_id, read, start_read_date, end_read_date, rating) VALUES ('NOW',(SELECT id FROM users WHERE name = 'demo'),(SELECT id FROM books WHERE title = 'Les dieux ont soif'), true,'2026-01-30 00:00:00+00:00','2026-02-13 00:00:00+00:00',7);
|
||||||
INSERT INTO books(created_at, title, author_id, added_by_id, cover_id) VALUES ('NOW', 'Rigodon',(SELECT id FROM authors WHERE name = 'Louis-Ferdinand Céline'), (SELECT id FROM users WHERE name = 'demo'),(SELECT id FROM static_files WHERE name = 'rigodon.jpg'));
|
INSERT INTO books(created_at, title, author_id, added_by_id, cover_id) VALUES ('NOW', 'Rigodon',(SELECT id FROM authors WHERE name = 'Louis-Ferdinand Céline'), (SELECT id FROM users WHERE name = 'demo'),(SELECT id FROM static_files WHERE name = 'rigodon.jpg'));
|
||||||
INSERT INTO user_books(created_at, user_id, book_id, read, rating) VALUES ('NOW',(SELECT id FROM users WHERE name = 'demo2'),(SELECT id FROM books WHERE title = 'Rigodon'),true, 10);
|
INSERT INTO user_books(created_at, user_id, book_id, read, rating) VALUES ('NOW',(SELECT id FROM users WHERE name = 'demo2'),(SELECT id FROM books WHERE title = 'Rigodon'),true, 10);
|
||||||
INSERT INTO books(created_at, title, author_id, added_by_id, cover_id) VALUES ('NOW', 'Un barrage contre le Pacifique',(SELECT id FROM authors WHERE name = 'Marguerite Duras'), (SELECT id FROM users WHERE name = 'demo'),(SELECT id FROM static_files WHERE name = 'Un_barrage_contre_le_Pacifique.jpg'));
|
INSERT INTO books(created_at, title, author_id, added_by_id, cover_id) VALUES ('NOW', 'Un barrage contre le Pacifique',(SELECT id FROM authors WHERE name = 'Marguerite Duras'), (SELECT id FROM users WHERE name = 'demo'),(SELECT id FROM static_files WHERE name = 'Un_barrage_contre_le_Pacifique.jpg'));
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<html lang="">
|
<html lang="">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/favicon.svg" />
|
||||||
<link rel="stylesheet" href="/css/bulma.min.css" />
|
<link rel="stylesheet" href="/css/bulma.min.css" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Bibliomane</title>
|
<title>Bibliomane</title>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "bibliomane",
|
"name": "bibliomane",
|
||||||
"version": "0.2.0",
|
"version": "0.4.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 4.2 KiB |
15
front/public/favicon.svg
Normal file
15
front/public/favicon.svg
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
<svg width="125.95975mm" height="132.42345mm" viewBox="0 0 125.95975 132.42345" version="1.1" id="svg1008" inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20)" sodipodi:docname="logo_carré.svg" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<sodipodi:namedview id="namedview1010" pagecolor="#505050" bordercolor="#ffffff" borderopacity="1" inkscape:pageshadow="0" inkscape:pageopacity="0" inkscape:pagecheckerboard="1" inkscape:document-units="mm" showgrid="false" fit-margin-top="0" fit-margin-left="0" fit-margin-right="0" fit-margin-bottom="0" inkscape:zoom="0.77771465" inkscape:cx="80.363666" inkscape:cy="297.66702" inkscape:window-width="2560" inkscape:window-height="1369" inkscape:window-x="1912" inkscape:window-y="-8" inkscape:window-maximized="1" inkscape:current-layer="layer1"/>
|
||||||
|
<defs id="defs1005">
|
||||||
|
<linearGradient inkscape:collect="always" xlink:href="#linearGradient29626-5" id="linearGradient41043" gradientUnits="userSpaceOnUse" x1="-274.68173" y1="86.602715" x2="-136.50122" y2="141.29916" gradientTransform="matrix(0,-0.975265,0.97958512,0,44.404193,-64.657592)"/>
|
||||||
|
<linearGradient id="linearGradient29626-5" inkscape:swatch="gradient">
|
||||||
|
<stop style="stop-color:#cd82d1;stop-opacity:1;" offset="0" id="stop29622"/>
|
||||||
|
<stop style="stop-color:#45e4be;stop-opacity:1" offset="1" id="stop29624"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<g inkscape:label="Layer 1" inkscape:groupmode="layer" id="layer1" transform="translate(-83.64899,-69.870748)">
|
||||||
|
<path id="rect33397" style="fill:url(#linearGradient41043);stroke-width:14.6613;stroke-linecap:round;paint-order:markers fill stroke" d="m 123.59901,69.870748 c -22.13223,0 -39.95002,17.817785 -39.95002,39.950012 v 52.52341 c 0,22.13223 17.81779,39.95003 39.95002,39.95003 h 46.05972 c 22.13224,0 39.95001,-17.8178 39.95001,-39.95003 v -52.52341 c 0,-22.132227 -17.81777,-39.950012 -39.95001,-39.950012 z m 16.04451,10.09246 c 2.89943,-0.025 5.27029,2.304907 5.2958,5.204333 l 0.093,10.535274 c 0.0259,2.899434 -2.30336,5.270995 -5.20278,5.297355 -2.89984,0.0256 -5.27133,-2.304505 -5.29684,-5.204338 l -0.093,-10.536825 c -0.025,-2.89923 2.30459,-5.270008 5.20381,-5.295799 z m 22.37162,9.874874 c 1.39244,-0.02341 2.73714,0.507288 3.73828,1.475362 2.08486,2.015053 2.14177,5.338588 0.12712,7.423838 l -8.88628,9.193748 c -2.01549,2.08523 -5.33978,2.14171 -7.42487,0.12609 -2.08423,-2.01528 -2.14067,-5.33841 -0.12609,-7.42332 l 8.8868,-9.194266 c 0.96744,-1.001274 2.29295,-1.577317 3.68504,-1.601452 z m -36.12428,8.403542 c 0.077,-0.0011 0.15435,-0.0011 0.23151,5.3e-4 0.30864,0.006 0.61792,0.03233 0.92604,0.08113 2.46499,0.38998 4.30183,1.812396 5.71283,3.422536 l 0.03,0.0341 9.57203,11.31249 h 19.29081 c 5.4245,0 10.14805,2.49331 13.13356,6.02857 2.98552,3.53526 4.46133,7.94487 4.60799,12.361 0.13508,4.06728 -0.94484,8.23683 -3.47576,11.72228 1.71448,1.01562 3.30442,2.15465 4.61729,3.52227 4.26817,4.44617 6.19882,10.33751 6.02082,16.07241 -0.35602,11.46979 -9.7863,22.94071 -24.73595,22.94071 h -24.1298 l -18.22317,-18.95904 0.063,0.063 c -11.56249,-11.62674 -15.08369,-24.70477 -13.06949,-34.38395 1.0071,-4.83959 3.55434,-9.28947 8.20829,-11.16469 0.37422,-0.15075 0.77822,-0.13389 1.1622,-0.23771 -0.63978,-3.80679 -0.50295,-7.69555 0.10542,-11.12801 0.64248,-3.62486 1.54593,-6.92763 4.57957,-9.60974 1.13761,-1.005806 2.85512,-1.845387 4.68291,-2.037607 v 5.29e-4 c 0.22848,-0.02416 0.45874,-0.03788 0.68989,-0.04082 z m -0.38396,11.934156 c -0.60269,0.009 -1.13008,0.50778 -1.28416,1.3069 -1.14194,5.72294 -0.28389,9.83155 3.99252,14.50919 0,0 5.3e-4,7.9e-4 0.001,10e-4 l 0.077,0.0739 0.0119,0.0124 6.21357,6.22855 -0.01,-12.24059 -7.90029,-9.33742 c -0.35161,-0.388 -0.74031,-0.55941 -1.10174,-0.55397 z m 19.50372,13.41779 v 15.93959 h 16.68012 0.0574 c 3.0387,0.0385 4.41726,-0.84879 5.48907,-2.13579 1.07456,-1.29032 1.73633,-3.34125 1.66243,-5.56658 -0.0739,-2.22533 -0.90166,-4.47159 -2.13579,-5.93297 -1.23413,-1.46138 -2.64641,-2.30425 -5.10976,-2.30425 z m -25.58914,8.07651 c -0.20944,2.6e-4 -0.43094,0.0238 -0.66714,0.0734 -1.64588,0.45199 -2.71995,2.05325 -3.14503,4.38784 -1.47473,6.99382 0.018,11.95115 11.36778,23.30814 l 0.0315,0.0315 7.50031,7.80211 v -20.75429 l -12.90825,-13.80742 c -0.54958,-0.56585 -1.16751,-0.97593 -1.97352,-1.03405 -0.0672,-0.005 -0.13586,-0.007 -0.20568,-0.007 z m 25.58914,18.36425 v 25.20363 H 161.822 c 9.92523,0 14.04184,-6.38226 14.23995,-12.7646 0.0991,-3.19116 -0.94119,-6.225 -3.09955,-8.47338 -2.15835,-2.24838 -5.52091,-3.96565 -11.1404,-3.96565 h -0.2434 z" sodipodi:nodetypes="sssssssssccccccccccsccccssccccssscssscccsscssccssccccccccsccccsssscscssccccssccssssccscs"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 4.8 KiB |
17
front/public/image/logo.svg
Normal file
17
front/public/image/logo.svg
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
<svg width="80.642197mm" height="104.19308mm" viewBox="0 0 80.642197 104.19308" version="1.1" id="svg863" sodipodi:docname="logo_trait.svg" inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20)" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<sodipodi:namedview id="namedview865" pagecolor="#505050" bordercolor="#ffffff" borderopacity="1" inkscape:pageshadow="0" inkscape:pageopacity="0" inkscape:pagecheckerboard="1" inkscape:document-units="mm" showgrid="false" inkscape:zoom="1.0998546" inkscape:cx="111.37836" inkscape:cy="219.57448" inkscape:window-width="2560" inkscape:window-height="1369" inkscape:window-x="1912" inkscape:window-y="-8" inkscape:window-maximized="1" inkscape:current-layer="layer1" fit-margin-top="0" fit-margin-left="0" fit-margin-right="0" fit-margin-bottom="0"/>
|
||||||
|
<defs id="defs860">
|
||||||
|
<linearGradient inkscape:collect="always" xlink:href="#linearGradient29626-5" id="linearGradient29653" gradientUnits="userSpaceOnUse" x1="180.15134" y1="639.77362" x2="387.26251" y2="162.77214" gradientTransform="matrix(0.18523379,0,0,0.18523379,50.465238,49.46306)"/>
|
||||||
|
<linearGradient id="linearGradient29626-5" inkscape:swatch="gradient">
|
||||||
|
<stop style="stop-color:#cd82d1;stop-opacity:1;" offset="0" id="stop29622"/>
|
||||||
|
<stop style="stop-color:#45e4be;stop-opacity:1" offset="1" id="stop29624"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<g inkscape:label="Layer 1" inkscape:groupmode="layer" id="layer1" transform="translate(-76.709803,-68.336456)">
|
||||||
|
<path id="path11547" style="color:#000000;fill:url(#linearGradient29653);fill-opacity:1;fill-rule:nonzero;stroke-width:0.700097;-inkscape-stroke:none" d="m 96.905008,85.032291 c -0.30864,-0.0061 -0.61647,0.0081 -0.9211,0.04016 v -3.61e-4 c -1.82779,0.192211 -3.54569,1.031778 -4.6833,2.037572 -3.03364,2.682116 -3.937,5.984852 -4.57947,9.609724 -0.60837,3.432464 -0.74506,7.320994 -0.10528,11.127784 -0.38397,0.10382 -0.78784,0.0873 -1.16206,0.23805 -4.65395,1.87521 -7.20143,6.32509 -8.20853,11.16468 -2.01421,9.67918 1.50698,22.75728 13.06947,34.38402 l -0.063,-0.063 18.223102,18.95861 h 24.12996 c 14.94966,0 24.37993,-11.47061 24.73595,-22.94041 0.178,-5.7349 -1.75266,-11.62612 -6.02083,-16.07229 -1.31287,-1.36763 -2.90298,-2.50673 -4.61746,-3.52233 2.53092,-3.48546 3.61075,-7.65527 3.47567,-11.72255 -0.14666,-4.41613 -1.62253,-8.82548 -4.60805,-12.36074 -2.98551,-3.53526 -7.70901,-6.028784 -13.13351,-6.028784 h -19.29073 l -9.57173,-11.312289 -0.03,-0.03437 c -1.411,-1.610146 -3.24796,-3.032503 -5.712952,-3.422483 -0.30812,-0.04875 -0.61753,-0.07496 -0.92617,-0.08104 z m 0.48588,12.487005 7.900662,9.338024 0.01,12.24061 -6.213462,-6.22873 -0.0123,-0.0123 -0.0767,-0.0738 c -3.7e-4,-3.6e-4 -0.001,-0.001 -0.001,-0.001 -4.27641,-4.67764 -5.13453,-8.78607 -3.9926,-14.509014 0.24652,-1.27859 1.44807,-1.7891 2.38607,-0.75336 z m 18.402182,12.864344 h 16.64355 c 2.46335,0 3.87571,0.84282 5.10984,2.3042 1.23413,1.46139 2.06207,3.70758 2.13598,5.93291 0.0739,2.22533 -0.5882,4.27646 -1.66276,5.56678 -1.07181,1.28701 -2.44994,2.17408 -5.48864,2.13562 h -0.0575 -16.68045 z m -26.256232,8.15022 c 1.25974,-0.2642 2.11382,0.21302 2.84659,0.96748 l 12.908122,13.80751 v 20.75451 l -7.500162,-7.80225 -0.0315,-0.0315 c -11.34978,-11.35699 -12.84262,-16.31443 -11.3679,-23.30824 0.42508,-2.33459 1.49895,-3.93552 3.14483,-4.38751 z m 26.256232,18.29082 h 16.5683 0.24348 c 5.61949,0 8.98172,1.71714 11.14007,3.96552 2.15836,2.24838 3.19883,5.2822 3.09978,8.47336 -0.19811,6.38234 -4.31462,12.76449 -14.23985,12.76449 h -16.81178 z" sodipodi:nodetypes="cccssccscccssscssscccsccccccccccccssssccccscccccscccssssccc"/>
|
||||||
|
<path style="color:#000000;fill:#60d1c2;fill-opacity:1;stroke-width:0.653215;stroke-linecap:round;-inkscape-stroke:none" d="m 111.75634,68.336639 a 4.8991144,4.8991144 0 0 0 -4.85575,4.941216 l 0.0868,9.831401 a 4.8991144,4.8991144 0 0 0 4.9425,4.855737 4.8991144,4.8991144 0 0 0 4.85445,-4.942493 l -0.0868,-9.830124 a 4.8991144,4.8991144 0 0 0 -4.94122,-4.855737 z" id="path23529"/>
|
||||||
|
<path style="color:#000000;fill:#60d1c2;fill-opacity:1;stroke-width:0.606037;stroke-linecap:round;-inkscape-stroke:none" d="m 132.27231,78.931592 a 4.442115,4.6508356 0 0 0 -3.16127,1.307975 l -7.77474,7.875191 a 4.442115,4.6508356 0 0 0 -0.10429,6.5754 4.442115,4.6508356 0 0 0 6.28146,0.109238 l 7.77474,-7.87519 a 4.442115,4.6508356 0 0 0 0.10314,-6.57544 4.442115,4.6508356 0 0 0 -3.11904,-1.417174 z" id="path23531"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 4.6 KiB |
@@ -13,7 +13,7 @@ const router = useRouter()
|
|||||||
const isMenuActive = ref(false)
|
const isMenuActive = ref(false)
|
||||||
const isSearchBarShown = ref(false)
|
const isSearchBarShown = ref(false)
|
||||||
|
|
||||||
const appVersion = import.meta.env.VITE_APP_VERSION;
|
const appVersion = import.meta.env.VITE_APP_VERSION
|
||||||
|
|
||||||
function logout() {
|
function logout() {
|
||||||
authStore.logout()
|
authStore.logout()
|
||||||
@@ -47,7 +47,9 @@ onMounted(() => {
|
|||||||
<template>
|
<template>
|
||||||
<nav class="navbar">
|
<nav class="navbar">
|
||||||
<div class="navbar-brand">
|
<div class="navbar-brand">
|
||||||
<RouterLink to="/" class="navbar-item" :title="'bibliomane v' + appVersion" activeClass="is-active"> B </RouterLink>
|
<RouterLink to="/" class="navbar-item" :title="'bibliomane v' + appVersion">
|
||||||
|
<img class="ml-3" src="/image/logo.svg" />
|
||||||
|
</RouterLink>
|
||||||
<div class="navbar-item is-hidden-desktop">
|
<div class="navbar-item is-hidden-desktop">
|
||||||
<a
|
<a
|
||||||
@click="isSearchBarShown = !isSearchBarShown"
|
@click="isSearchBarShown = !isSearchBarShown"
|
||||||
|
|||||||
@@ -5,16 +5,13 @@ const props = defineProps({
|
|||||||
icon: String,
|
icon: String,
|
||||||
legend: String,
|
legend: String,
|
||||||
isSet: Boolean,
|
isSet: Boolean,
|
||||||
isReadonly: Boolean,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const hovered = ref(false)
|
const hovered = ref(false)
|
||||||
const isOnMobile = ref(computeIsOnMobile())
|
const isOnMobile = ref(computeIsOnMobile())
|
||||||
|
|
||||||
const computedIcon = computed(
|
const computedIcon = computed(
|
||||||
() =>
|
() => props.icon + ((hovered.value && !isOnMobile.value) || props.isSet ? 'Fill' : ''),
|
||||||
props.icon +
|
|
||||||
(!props.isReadonly && ((hovered.value && !isOnMobile.value) || props.isSet) ? 'Fill' : ''),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
function computeIsOnMobile() {
|
function computeIsOnMobile() {
|
||||||
@@ -34,8 +31,7 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="bigiconandlegend"
|
class="bigiconandlegend showcanclick"
|
||||||
:class="props.isReadonly ? '' : 'showcanclick'"
|
|
||||||
@mouseover="hovered = true"
|
@mouseover="hovered = true"
|
||||||
@mouseout="hovered = false"
|
@mouseout="hovered = false"
|
||||||
>
|
>
|
||||||
@@ -91,7 +87,7 @@ onUnmounted(() => {
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding-bottom: 0px;
|
padding-bottom: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bigiconandlegend {
|
.bigiconandlegend {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import BigIcon from './BigIcon.vue'
|
import BigIcon from './BigIcon.vue'
|
||||||
|
import DateWidget from './DateWidget.vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
icon: String,
|
icon: String,
|
||||||
@@ -8,7 +9,6 @@ const props = defineProps({
|
|||||||
startReadDate: String,
|
startReadDate: String,
|
||||||
endReadDate: String,
|
endReadDate: String,
|
||||||
isExpanded: Boolean,
|
isExpanded: Boolean,
|
||||||
isReadonly: Boolean,
|
|
||||||
useEndDate: Boolean,
|
useEndDate: Boolean,
|
||||||
lastWidget: Boolean,
|
lastWidget: Boolean,
|
||||||
})
|
})
|
||||||
@@ -26,9 +26,6 @@ function computeParentClasses() {
|
|||||||
} else {
|
} else {
|
||||||
classNames += ' border-radius-right'
|
classNames += ' border-radius-right'
|
||||||
}
|
}
|
||||||
if (props.isReadonly) {
|
|
||||||
classNames += ' widget-readonly'
|
|
||||||
}
|
|
||||||
return classNames
|
return classNames
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -37,35 +34,24 @@ function computeParentClasses() {
|
|||||||
<div :class="computeParentClasses()">
|
<div :class="computeParentClasses()">
|
||||||
<BigIcon
|
<BigIcon
|
||||||
:icon="props.icon"
|
:icon="props.icon"
|
||||||
:is-readonly="props.isReadonly"
|
|
||||||
:legend="props.legend"
|
:legend="props.legend"
|
||||||
:isSet="props.isExpanded"
|
:isSet="props.isExpanded"
|
||||||
@click="props.isReadonly ? null : $emit('onIconClick')"
|
@click="$emit('onIconClick')"
|
||||||
/>
|
/>
|
||||||
<div v-if="props.isExpanded" class="inputdate">
|
<div v-if="props.isExpanded" class="inputdate">
|
||||||
<div class="ontopofinput">
|
<div class="ontopofinput">
|
||||||
<label class="datelabel" for="startread">
|
<DateWidget
|
||||||
{{ $t('bookdatewidget.started') }}
|
dateinputid="startread"
|
||||||
</label>
|
dateinputlabel="bookdatewidget.started"
|
||||||
<input
|
:initdate="props.startReadDate"
|
||||||
class="datepicker has-background-dark has-text-light"
|
@onDateChange="(d) => $emit('onStartDateChange', d)"
|
||||||
id="startread"
|
|
||||||
type="date"
|
|
||||||
@change="(e) => $emit('onStartDateChange', e.target.value)"
|
|
||||||
:value="props.startReadDate"
|
|
||||||
:max="today"
|
|
||||||
/>
|
/>
|
||||||
<div v-if="props.useEndDate">
|
<div v-if="props.useEndDate">
|
||||||
<label class="datelabel" for="endread">
|
<DateWidget
|
||||||
{{ $t('bookdatewidget.finished') }}
|
dateinputid="endread"
|
||||||
</label>
|
dateinputlabel="bookdatewidget.finished"
|
||||||
<input
|
:initdate="props.endReadDate"
|
||||||
class="datepicker has-background-dark has-text-light"
|
@onDateChange="(d) => $emit('onEndDateChange', d)"
|
||||||
id="endread"
|
|
||||||
type="date"
|
|
||||||
@change="(e) => $emit('onEndDateChange', e.target.value)"
|
|
||||||
:value="props.endReadDate"
|
|
||||||
:max="today"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -93,24 +79,6 @@ function computeParentClasses() {
|
|||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.datelabel {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
font-size: 26px;
|
|
||||||
border: none;
|
|
||||||
padding-bottom: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.datepicker {
|
|
||||||
font-size: 26px;
|
|
||||||
border-radius: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.widget-readonly {
|
|
||||||
opacity: 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 1024px) {
|
@media (max-width: 1024px) {
|
||||||
.bookdatewidget {
|
.bookdatewidget {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|||||||
@@ -3,9 +3,7 @@ import { ref, computed } from 'vue'
|
|||||||
import {
|
import {
|
||||||
getBook,
|
getBook,
|
||||||
getImagePathOrDefault,
|
getImagePathOrDefault,
|
||||||
putReadBook,
|
putUpdateBook,
|
||||||
putWantReadBook,
|
|
||||||
putRateBook,
|
|
||||||
putStartReadDate,
|
putStartReadDate,
|
||||||
putStartReadDateUnset,
|
putStartReadDateUnset,
|
||||||
putEndReadDate,
|
putEndReadDate,
|
||||||
@@ -14,8 +12,8 @@ import {
|
|||||||
} from './api.js'
|
} from './api.js'
|
||||||
import { useRouter, onBeforeRouteUpdate } from 'vue-router'
|
import { useRouter, onBeforeRouteUpdate } from 'vue-router'
|
||||||
import { VRating } from 'vuetify/components/VRating'
|
import { VRating } from 'vuetify/components/VRating'
|
||||||
import BigIcon from './BigIcon.vue'
|
import BookFormIcons from './BookFormIcons.vue'
|
||||||
import BookDateWidget from './BookDateWidget.vue'
|
import ReviewWidget from './ReviewWidget.vue'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -38,10 +36,15 @@ function onRatingUpdate(rating) {
|
|||||||
data.value.read = true
|
data.value.read = true
|
||||||
data.value.wantread = false
|
data.value.wantread = false
|
||||||
}
|
}
|
||||||
putRateBook(props.id, { rating: data.value.rating })
|
putUpdateBook(props.id, { rating: data.value.rating })
|
||||||
}
|
}
|
||||||
|
|
||||||
function onReadIconClick() {
|
function onReviewUpdate(review) {
|
||||||
|
data.value.review = review
|
||||||
|
putUpdateBook(props.id, { review: data.value.review })
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onReadIconClick() {
|
||||||
data.value.read = !data.value.read
|
data.value.read = !data.value.read
|
||||||
if (data.value.read) {
|
if (data.value.read) {
|
||||||
data.value.wantread = false
|
data.value.wantread = false
|
||||||
@@ -54,17 +57,21 @@ function onReadIconClick() {
|
|||||||
|
|
||||||
function onWantReadIconClick() {
|
function onWantReadIconClick() {
|
||||||
data.value.wantread = !data.value.wantread
|
data.value.wantread = !data.value.wantread
|
||||||
putWantReadBook(props.id, { wantread: data.value.wantread })
|
putUpdateBook(props.id, { wantread: data.value.wantread })
|
||||||
}
|
}
|
||||||
|
|
||||||
function onStartReadIconClick() {
|
async function onStartReadIconClick() {
|
||||||
if (!data.value.startReadDate) {
|
if (!data.value.startReadDate) {
|
||||||
data.value.startReadDate = today
|
data.value.startReadDate = today
|
||||||
putStartReadDate(props.id, data.value.startReadDate)
|
putStartReadDate(props.id, data.value.startReadDate)
|
||||||
} else {
|
} else if (!data.value.read) {
|
||||||
data.value.startReadDate = null
|
data.value.startReadDate = null
|
||||||
putStartReadDateUnset(props.id)
|
putStartReadDateUnset(props.id)
|
||||||
}
|
}
|
||||||
|
if (data.value.read) {
|
||||||
|
data.value.read = false
|
||||||
|
putUnreadBook(props.id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onStartReadDateChange(d) {
|
function onStartReadDateChange(d) {
|
||||||
@@ -85,12 +92,6 @@ function onEndReadDateChange(d) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function isStartReadExpanded() {
|
|
||||||
let isStartReadDateSet = data.value.startReadDate ? true : false
|
|
||||||
let isReadUnset = !data.value.read ? true : false
|
|
||||||
return isStartReadDateSet && isReadUnset
|
|
||||||
}
|
|
||||||
|
|
||||||
function goToAuthor() {
|
function goToAuthor() {
|
||||||
router.push('/author/' + data.value.authorId)
|
router.push('/author/' + data.value.authorId)
|
||||||
}
|
}
|
||||||
@@ -103,17 +104,6 @@ function goToAuthor() {
|
|||||||
<figure class="image">
|
<figure class="image">
|
||||||
<img v-bind:src="imagePathOrDefault" v-bind:alt="data.title" />
|
<img v-bind:src="imagePathOrDefault" v-bind:alt="data.title" />
|
||||||
</figure>
|
</figure>
|
||||||
<VRating
|
|
||||||
half-increments
|
|
||||||
hover
|
|
||||||
:length="5"
|
|
||||||
size="x-large"
|
|
||||||
density="compact"
|
|
||||||
:model-value="data.rating / 2"
|
|
||||||
@update:modelValue="onRatingUpdate"
|
|
||||||
active-color="bulma-body-color"
|
|
||||||
class="centered"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="column">
|
<div class="column">
|
||||||
<h3 class="title">{{ data.title }}</h3>
|
<h3 class="title">{{ data.title }}</h3>
|
||||||
@@ -122,39 +112,22 @@ function goToAuthor() {
|
|||||||
<div class="my-5" v-if="data.isbn">ISBN: {{ data.isbn }}</div>
|
<div class="my-5" v-if="data.isbn">ISBN: {{ data.isbn }}</div>
|
||||||
<div class="my-5" v-if="data.inventaireid">Inventaire ID: {{ data.inventaireid }}</div>
|
<div class="my-5" v-if="data.inventaireid">Inventaire ID: {{ data.inventaireid }}</div>
|
||||||
<div class="my-5" v-if="data.openlibraryid">OLID: {{ data.openlibraryid }}</div>
|
<div class="my-5" v-if="data.openlibraryid">OLID: {{ data.openlibraryid }}</div>
|
||||||
|
<ReviewWidget
|
||||||
|
:reviewtext="data.review"
|
||||||
|
:rating="data.rating"
|
||||||
|
@on-review-update="onReviewUpdate"
|
||||||
|
@on-rating-update="onRatingUpdate"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="column">
|
<div class="column">
|
||||||
<div class="iconscontainer" :class="data.read ? 'remove-border-bottom' : ''">
|
<BookFormIcons
|
||||||
<div class="bigiconcontainer">
|
v-bind="data"
|
||||||
<BigIcon
|
@on-want-read-icon-click="onWantReadIconClick"
|
||||||
icon="BIconEye"
|
@on-start-read-icon-click="onStartReadIconClick"
|
||||||
:legend="$t('bookform.wantread')"
|
@on-read-icon-click="onReadIconClick"
|
||||||
:isSet="data.wantread"
|
@on-start-read-date-change="onStartReadDateChange"
|
||||||
@click="onWantReadIconClick"
|
@on-end-read-date-change="onEndReadDateChange"
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
<BookDateWidget
|
|
||||||
icon="BIconBook"
|
|
||||||
:legend="$t('bookform.startread')"
|
|
||||||
:start-read-date="data.startReadDate"
|
|
||||||
:is-expanded="isStartReadExpanded()"
|
|
||||||
:is-readonly="data.read"
|
|
||||||
@onStartDateChange="onStartReadDateChange"
|
|
||||||
@onIconClick="onStartReadIconClick"
|
|
||||||
/>
|
|
||||||
<BookDateWidget
|
|
||||||
icon="BIconCheckCircle"
|
|
||||||
:legend="$t('bookform.read')"
|
|
||||||
:start-read-date="data.startReadDate"
|
|
||||||
use-end-date
|
|
||||||
last-widget
|
|
||||||
:endReadDate="data.endReadDate"
|
|
||||||
:isExpanded="data.read"
|
|
||||||
@onStartDateChange="onStartReadDateChange"
|
|
||||||
@onEndDateChange="onEndReadDateChange"
|
|
||||||
@onIconClick="onReadIconClick"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -167,22 +140,6 @@ img {
|
|||||||
width: auto;
|
width: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.centered {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.iconscontainer {
|
|
||||||
border: solid;
|
|
||||||
border-radius: 50px;
|
|
||||||
width: 250px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.remove-border-bottom {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
@media (min-width: 1024px) {
|
||||||
.left-panel {
|
.left-panel {
|
||||||
margin-left: 3rem;
|
margin-left: 3rem;
|
||||||
@@ -193,10 +150,7 @@ img {
|
|||||||
img {
|
img {
|
||||||
max-height: 250px;
|
max-height: 250px;
|
||||||
max-width: 250px;
|
max-width: 250px;
|
||||||
}
|
padding: 20px;
|
||||||
|
|
||||||
.bigiconcontainer {
|
|
||||||
flex: 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.image {
|
.image {
|
||||||
@@ -204,10 +158,5 @@ img {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.iconscontainer {
|
|
||||||
display: flex;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
174
front/src/BookFormIcons.vue
Normal file
174
front/src/BookFormIcons.vue
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
<script setup>
|
||||||
|
import { useTemplateRef, nextTick } from 'vue'
|
||||||
|
import BigIcon from './BigIcon.vue'
|
||||||
|
import BookDateWidget from './BookDateWidget.vue'
|
||||||
|
import DateWidget from './DateWidget.vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
wantread: Boolean,
|
||||||
|
startReadDate: String,
|
||||||
|
endReadDate: String,
|
||||||
|
read: Boolean,
|
||||||
|
})
|
||||||
|
|
||||||
|
const mobiledatesel = useTemplateRef('mobiledates')
|
||||||
|
|
||||||
|
const emit = defineEmits([
|
||||||
|
'onWantReadIconClick',
|
||||||
|
'onStartReadIconClick',
|
||||||
|
'onReadIconClick',
|
||||||
|
'onStartReadDateChange',
|
||||||
|
'onEndReadDateChange',
|
||||||
|
])
|
||||||
|
|
||||||
|
function isStartReadExpanded() {
|
||||||
|
let isStartReadDateSet = props.startReadDate ? true : false
|
||||||
|
let isReadUnset = !props.read ? true : false
|
||||||
|
return isStartReadDateSet && isReadUnset
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onReadIconClick() {
|
||||||
|
emit('onReadIconClick')
|
||||||
|
await nextTick()
|
||||||
|
mobiledatesel.value.scrollIntoView()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onStartReadIconClick() {
|
||||||
|
emit('onStartReadIconClick')
|
||||||
|
await nextTick()
|
||||||
|
mobiledatesel.value.scrollIntoView()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="iconscontainer" :class="props.read ? 'remove-border-bottom' : ''">
|
||||||
|
<div
|
||||||
|
class="bigiconcontainer"
|
||||||
|
:class="props.wantread ? 'has-text-dark has-background-text border-radius-wantread-fill' : ''"
|
||||||
|
>
|
||||||
|
<BigIcon
|
||||||
|
icon="BIconEye"
|
||||||
|
:legend="$t('bookform.wantread')"
|
||||||
|
:isSet="props.wantread"
|
||||||
|
@click="$emit('onWantReadIconClick')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="bigiconcontainer is-hidden-desktop"
|
||||||
|
:class="isStartReadExpanded() ? 'has-text-dark has-background-text' : ''"
|
||||||
|
>
|
||||||
|
<BigIcon
|
||||||
|
icon="BIconBook"
|
||||||
|
:legend="$t('bookform.startread')"
|
||||||
|
:is-set="isStartReadExpanded()"
|
||||||
|
@click="onStartReadIconClick"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="bigiconcontainer is-hidden-desktop"
|
||||||
|
:class="props.read ? 'has-text-dark has-background-text border-radius-right-fill' : ''"
|
||||||
|
>
|
||||||
|
<BigIcon
|
||||||
|
icon="BIconCheckCircle"
|
||||||
|
:legend="$t('bookform.read')"
|
||||||
|
:isSet="props.read"
|
||||||
|
@click="onReadIconClick"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<BookDateWidget
|
||||||
|
class="is-hidden-mobile"
|
||||||
|
icon="BIconBook"
|
||||||
|
:legend="$t('bookform.startread')"
|
||||||
|
:start-read-date="props.startReadDate"
|
||||||
|
:is-expanded="isStartReadExpanded()"
|
||||||
|
@onStartDateChange="(d) => $emit('onStartReadDateChange', d)"
|
||||||
|
@onIconClick="onStartReadIconClick"
|
||||||
|
/>
|
||||||
|
<BookDateWidget
|
||||||
|
class="is-hidden-mobile"
|
||||||
|
icon="BIconCheckCircle"
|
||||||
|
:legend="$t('bookform.read')"
|
||||||
|
:start-read-date="props.startReadDate"
|
||||||
|
use-end-date
|
||||||
|
last-widget
|
||||||
|
:endReadDate="props.endReadDate"
|
||||||
|
:isExpanded="props.read"
|
||||||
|
@onStartDateChange="(d) => $emit('onStartReadDateChange', d)"
|
||||||
|
@onEndDateChange="(d) => $emit('onEndReadDateChange', d)"
|
||||||
|
@onIconClick="onReadIconClick"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div ref="mobiledates" class="mobile-dates pt-3 is-hidden-desktop">
|
||||||
|
<div class="mobiledate">
|
||||||
|
<DateWidget
|
||||||
|
v-if="isStartReadExpanded() || props.read"
|
||||||
|
dateinputid="startread"
|
||||||
|
dateinputlabel="bookdatewidget.started"
|
||||||
|
:initdate="props.startReadDate"
|
||||||
|
is-horizontal
|
||||||
|
@onDateChange="(d) => $emit('onStartReadDateChange', d)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="mobiledate pt-2">
|
||||||
|
<DateWidget
|
||||||
|
v-if="props.read"
|
||||||
|
dateinputid="endread"
|
||||||
|
dateinputlabel="bookdatewidget.finished"
|
||||||
|
:initdate="props.endReadDate"
|
||||||
|
is-horizontal
|
||||||
|
@onDateChange="(d) => $emit('onEndReadDateChange', d)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.iconscontainer {
|
||||||
|
border: solid;
|
||||||
|
border-radius: 50px;
|
||||||
|
width: 250px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-dates {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobiledate {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.border-radius-wantread-fill {
|
||||||
|
border-radius: 45px 45px 0px 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove-border-bottom {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bigiconcontainer {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.border-radius-wantread-fill {
|
||||||
|
border-radius: 38px 0px 0px 38px;
|
||||||
|
}
|
||||||
|
.bigiconcontainer {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.iconscontainer {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.border-radius-right-fill {
|
||||||
|
border-radius: 0px 38px 38px 0px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
51
front/src/DateWidget.vue
Normal file
51
front/src/DateWidget.vue
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
dateinputid: String,
|
||||||
|
dateinputlabel: String,
|
||||||
|
initdate: String,
|
||||||
|
isHorizontal: Boolean,
|
||||||
|
})
|
||||||
|
defineEmits(['onDateChange'])
|
||||||
|
|
||||||
|
const today = new Date().toISOString().slice(0, 10)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<label class="datelabel" :class="props.isHorizontal ? 'pr-2' : 'pb-1'" :for="props.dateinputid">
|
||||||
|
{{ $t(props.dateinputlabel) }}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
class="datepicker has-background-dark has-text-light"
|
||||||
|
:id="props.dateinputid"
|
||||||
|
type="date"
|
||||||
|
@change="(e) => $emit('onDateChange', e.target.value)"
|
||||||
|
:value="props.initdate"
|
||||||
|
:max="today"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.datelabel {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 26px;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.datepicker {
|
||||||
|
font-size: 26px;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.datelabel {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
.datepicker {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
134
front/src/ReviewWidget.vue
Normal file
134
front/src/ReviewWidget.vue
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { VRating } from 'vuetify/components/VRating'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
rating: Number,
|
||||||
|
reviewtext: String,
|
||||||
|
})
|
||||||
|
const isTextareaExpanded = ref(false)
|
||||||
|
const isTextareaTransitionEnabled = ref(true)
|
||||||
|
|
||||||
|
defineEmits('onRatingUpdate', 'onReviewUpdate')
|
||||||
|
|
||||||
|
function computeTextareaClass() {
|
||||||
|
let classAttr =
|
||||||
|
isTextareaExpanded && isTextareaExpanded.value ? 'textarea-expanded' : 'textarea-normal'
|
||||||
|
if (isTextareaTransitionEnabled && isTextareaTransitionEnabled.value) {
|
||||||
|
classAttr += ' transition-height'
|
||||||
|
}
|
||||||
|
return classAttr
|
||||||
|
}
|
||||||
|
|
||||||
|
function onTextAreaFocus() {
|
||||||
|
isTextareaExpanded.value = true
|
||||||
|
setTimeout(() => {
|
||||||
|
isTextareaTransitionEnabled.value = false
|
||||||
|
}, 500)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="maincontainer py-5">
|
||||||
|
<div class="widget-header mb-5 full-width">
|
||||||
|
<div class="widget-title ml-3">
|
||||||
|
<h2>{{ $t('review.title') }}</h2>
|
||||||
|
<span class="ml-3">
|
||||||
|
<b-icon-pen />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<VRating
|
||||||
|
half-increments
|
||||||
|
hover
|
||||||
|
:length="5"
|
||||||
|
size="x-large"
|
||||||
|
density="compact"
|
||||||
|
:model-value="rating / 2"
|
||||||
|
@update:modelValue="(r) => $emit('onRatingUpdate', r)"
|
||||||
|
active-color="bulma-body-color"
|
||||||
|
class="widget-rating centered"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="full-width centered">
|
||||||
|
<textarea
|
||||||
|
:placeholder="$t('review.textplaceholder')"
|
||||||
|
class="widget-textarea mx-4"
|
||||||
|
@change="(e) => $emit('onReviewUpdate', e.target.value)"
|
||||||
|
@focus="onTextAreaFocus"
|
||||||
|
:class="computeTextareaClass()"
|
||||||
|
>{{ reviewtext }}</textarea
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.maincontainer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
border: solid;
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.widget-header {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.widget-title {
|
||||||
|
flex: 2;
|
||||||
|
font-size: 2em;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.widget-title h2 {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.widget-rating {
|
||||||
|
flex: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.widget-textarea {
|
||||||
|
color: var(--bulma-body-color);
|
||||||
|
background-color: var(--bulma-text-20);
|
||||||
|
width: 95%;
|
||||||
|
border-radius: 30px;
|
||||||
|
border: none;
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.textarea-normal {
|
||||||
|
height: 80px;
|
||||||
|
resize: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.textarea-expanded {
|
||||||
|
height: 350px;
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transition-height {
|
||||||
|
transition: height 0.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.full-width {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.widget-header {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.widget-title {
|
||||||
|
font-size: 1.5em;
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,21 +1,38 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { useTemplateRef, onMounted, ref } from 'vue'
|
import { useTemplateRef, onMounted, onUnmounted, ref } from 'vue'
|
||||||
import { BrowserMultiFormatReader } from '@zxing/library'
|
import { BrowserMultiFormatReader } from '@zxing/library'
|
||||||
|
import { i18n } from '@/main'
|
||||||
|
|
||||||
const emit = defineEmits('readBarcode')
|
const emit = defineEmits('readBarcode')
|
||||||
|
|
||||||
|
const { t } = i18n.global
|
||||||
|
|
||||||
const scanResult = ref(null)
|
const scanResult = ref(null)
|
||||||
|
const scanErr = ref(null)
|
||||||
const codeReader = new BrowserMultiFormatReader()
|
const codeReader = new BrowserMultiFormatReader()
|
||||||
const scannerElement = useTemplateRef('scanner')
|
const scannerElement = useTemplateRef('scanner')
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
codeReader.decodeFromVideoDevice(undefined, scannerElement.value, (result, err) => {
|
if (!codeReader.isMediaDevicesSuported || !codeReader.canEnumerateDevices) {
|
||||||
if (result) {
|
scanErr.value = 'This browser does not support this feature.'
|
||||||
emit('readBarcode', result.text)
|
return
|
||||||
|
}
|
||||||
|
codeReader.listVideoInputDevices().then((mediaDevicesInfoArray) => {
|
||||||
|
if (mediaDevicesInfoArray.length > 0) {
|
||||||
|
codeReader.decodeFromVideoDevice(undefined, scannerElement.value, (result, err) => {
|
||||||
|
if (result) {
|
||||||
|
emit('readBarcode', result.text)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
scanErr.value = t('barcode.nocamera')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
codeReader.reset()
|
||||||
|
})
|
||||||
function onResult(result) {
|
function onResult(result) {
|
||||||
scanResult.value = result
|
scanResult.value = result
|
||||||
}
|
}
|
||||||
@@ -23,6 +40,7 @@ function onResult(result) {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<h1 class="subtitle">{{ $t('barcode.title') }}</h1>
|
<h1 class="subtitle">{{ $t('barcode.title') }}</h1>
|
||||||
|
<div v-if="scanErr">{{ scanErr }}</div>
|
||||||
<div v-if="scanResult">{{ scanResult }}</div>
|
<div v-if="scanResult">{{ scanResult }}</div>
|
||||||
<video poster="data:image/gif,AAAA" ref="scanner"></video>
|
<video poster="data:image/gif,AAAA" ref="scanner"></video>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -99,35 +99,31 @@ export async function postImportBook(id, language) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function putReadBook(bookId) {
|
export async function putReadBook(bookId) {
|
||||||
return genericPayloadCall('/ws/book/' + bookId + '/read', { read: true }, 'PUT')
|
return genericPayloadCall('/ws/book/' + bookId, { read: true }, 'PUT')
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function putUnreadBook(bookId) {
|
export async function putUnreadBook(bookId) {
|
||||||
return genericPayloadCall('/ws/book/' + bookId + '/read', { read: false }, 'PUT')
|
return genericPayloadCall('/ws/book/' + bookId, { read: false }, 'PUT')
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function putEndReadDate(bookId, enddate) {
|
export async function putEndReadDate(bookId, enddate) {
|
||||||
return genericPayloadCall('/ws/book/' + bookId + '/read', { read: true, endDate: enddate }, 'PUT')
|
return genericPayloadCall('/ws/book/' + bookId, { read: true, endDate: enddate }, 'PUT')
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function putEndReadDateUnset(bookId) {
|
export async function putEndReadDateUnset(bookId) {
|
||||||
return genericPayloadCall('/ws/book/' + bookId + '/read', { read: true, endDate: 'null' }, 'PUT')
|
return genericPayloadCall('/ws/book/' + bookId, { read: true, endDate: 'null' }, 'PUT')
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function putStartReadDateUnset(bookId) {
|
export async function putStartReadDateUnset(bookId) {
|
||||||
return genericPayloadCall('/ws/book/' + bookId + '/startread', { startDate: 'null' }, 'PUT')
|
return genericPayloadCall('/ws/book/' + bookId, { startDate: 'null' }, 'PUT')
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function putStartReadDate(bookId, startdate) {
|
export async function putStartReadDate(bookId, startdate) {
|
||||||
return genericPayloadCall('/ws/book/' + bookId + '/startread', { startDate: startdate }, 'PUT')
|
return genericPayloadCall('/ws/book/' + bookId, { startDate: startdate }, 'PUT')
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function putWantReadBook(bookId, payload) {
|
export async function putUpdateBook(bookId, payload) {
|
||||||
return genericPayloadCall('/ws/book/' + bookId + '/wantread', payload, 'PUT')
|
return genericPayloadCall('/ws/book/' + bookId, payload, 'PUT')
|
||||||
}
|
|
||||||
|
|
||||||
export async function putRateBook(bookId, payload) {
|
|
||||||
return genericPayloadCall('/ws/book/' + bookId + '/rate', payload, 'PUT')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function postLogin(user) {
|
export function postLogin(user) {
|
||||||
|
|||||||
@@ -13,7 +13,8 @@
|
|||||||
},
|
},
|
||||||
"barcode": {
|
"barcode": {
|
||||||
"title": "Scan barcode",
|
"title": "Scan barcode",
|
||||||
"barcode": "Scan barcode"
|
"barcode": "Scan barcode",
|
||||||
|
"nocamera": "No camera found."
|
||||||
},
|
},
|
||||||
"addbook": {
|
"addbook": {
|
||||||
"title": "Title",
|
"title": "Title",
|
||||||
@@ -58,6 +59,7 @@
|
|||||||
},
|
},
|
||||||
"bookform": {
|
"bookform": {
|
||||||
"error": "Error when loading book: {error}",
|
"error": "Error when loading book: {error}",
|
||||||
|
"reviewbtn": "My review",
|
||||||
"read": "Read",
|
"read": "Read",
|
||||||
"startread": "Started",
|
"startread": "Started",
|
||||||
"wantread": "Interested"
|
"wantread": "Interested"
|
||||||
@@ -79,5 +81,9 @@
|
|||||||
"releasedate": "Release date:",
|
"releasedate": "Release date:",
|
||||||
"publisher": "Publisher:",
|
"publisher": "Publisher:",
|
||||||
"importing": "Importing..."
|
"importing": "Importing..."
|
||||||
|
},
|
||||||
|
"review": {
|
||||||
|
"title": "My review",
|
||||||
|
"textplaceholder": "Write my review..."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,8 @@
|
|||||||
},
|
},
|
||||||
"barcode": {
|
"barcode": {
|
||||||
"title": "Scanner le code-barres",
|
"title": "Scanner le code-barres",
|
||||||
"barcode": "Scanner le code-barres"
|
"barcode": "Scanner le code-barres",
|
||||||
|
"nocamera": "Impossible de détecter la caméra."
|
||||||
},
|
},
|
||||||
"addbook": {
|
"addbook": {
|
||||||
"title": "Titre",
|
"title": "Titre",
|
||||||
@@ -58,6 +59,7 @@
|
|||||||
},
|
},
|
||||||
"bookform": {
|
"bookform": {
|
||||||
"error": "Erreur pendant le chargement du livre: {error}",
|
"error": "Erreur pendant le chargement du livre: {error}",
|
||||||
|
"reviewbtn": "Ma critique",
|
||||||
"read": "Lu",
|
"read": "Lu",
|
||||||
"startread": "Commencé",
|
"startread": "Commencé",
|
||||||
"wantread": "À lire"
|
"wantread": "À lire"
|
||||||
@@ -79,5 +81,9 @@
|
|||||||
"releasedate": "Date de publication : ",
|
"releasedate": "Date de publication : ",
|
||||||
"publisher": "Maison d'édition : ",
|
"publisher": "Maison d'édition : ",
|
||||||
"importing": "Import en cours..."
|
"importing": "Import en cours..."
|
||||||
|
},
|
||||||
|
"review": {
|
||||||
|
"title": "Ma critique",
|
||||||
|
"textplaceholder": "Écrire ma critique..."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import fr from './locales/fr.json'
|
|||||||
import en from './locales/en.json'
|
import en from './locales/en.json'
|
||||||
|
|
||||||
// configure i18n
|
// configure i18n
|
||||||
const i18n = createI18n({
|
export const i18n = createI18n({
|
||||||
locale: navigator.language,
|
locale: navigator.language,
|
||||||
fallbackLocale: 'en',
|
fallbackLocale: 'en',
|
||||||
messages: { fr, en },
|
messages: { fr, en },
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
.clickable {
|
.clickable {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.centered {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|||||||
2
go.mod
2
go.mod
@@ -1,6 +1,6 @@
|
|||||||
module git.artlef.fr/bibliomane
|
module git.artlef.fr/bibliomane
|
||||||
|
|
||||||
go 1.25.1
|
go 1.26
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/alecthomas/kong v1.14.0
|
github.com/alecthomas/kong v1.14.0
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ func TestGetBook_Ok(t *testing.T) {
|
|||||||
Rating: 10,
|
Rating: 10,
|
||||||
Read: true,
|
Read: true,
|
||||||
CoverPath: "/static/bookcover/dunchateaulautre.jpg",
|
CoverPath: "/static/bookcover/dunchateaulautre.jpg",
|
||||||
|
Review: "Lorem ipsum dolor sit amet, consectetur adipisci elit, sed eiusmod tempor incidunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur. Quis aute iure reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint obcaecat cupiditat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
|
||||||
}, book)
|
}, book)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,87 +0,0 @@
|
|||||||
package apitest
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"git.artlef.fr/bibliomane/internal/testutils"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestPutRatingUserBooksHandler_UpdateRating(t *testing.T) {
|
|
||||||
payload :=
|
|
||||||
`{
|
|
||||||
"rating": 5
|
|
||||||
}`
|
|
||||||
bookId := "17"
|
|
||||||
testPutRateUserBooks(t, payload, bookId, http.StatusOK)
|
|
||||||
book := testGetBook(t, bookId, http.StatusOK)
|
|
||||||
assert.Equal(t, 5, book.Rating)
|
|
||||||
assert.Equal(t, true, book.Read)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPutRatingUserBooksHandler_RateNewBookMakeItRead(t *testing.T) {
|
|
||||||
payload :=
|
|
||||||
`{
|
|
||||||
"rating": 7
|
|
||||||
}`
|
|
||||||
bookId := "18"
|
|
||||||
testPutRateUserBooks(t, payload, bookId, http.StatusOK)
|
|
||||||
book := testGetBook(t, bookId, http.StatusOK)
|
|
||||||
assert.Equal(t, 7, book.Rating)
|
|
||||||
assert.Equal(t, true, book.Read)
|
|
||||||
assert.Equal(t, false, book.WantRead)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPutRatingUserBooksHandler_RateWantedBook(t *testing.T) {
|
|
||||||
payload :=
|
|
||||||
`{
|
|
||||||
"rating": 6
|
|
||||||
}`
|
|
||||||
bookId := "2"
|
|
||||||
testPutRateUserBooks(t, payload, bookId, http.StatusOK)
|
|
||||||
book := testGetBook(t, bookId, http.StatusOK)
|
|
||||||
assert.Equal(t, 6, book.Rating)
|
|
||||||
assert.Equal(t, true, book.Read)
|
|
||||||
assert.Equal(t, false, book.WantRead)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPutRatingUserBooksHandler_RatingTypeWrong(t *testing.T) {
|
|
||||||
payload :=
|
|
||||||
`{
|
|
||||||
"rating": "bad"
|
|
||||||
}`
|
|
||||||
bookId := "18"
|
|
||||||
testPutRateUserBooks(t, payload, bookId, http.StatusInternalServerError)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPutRatingUserBooksHandler_RatingMin(t *testing.T) {
|
|
||||||
payload :=
|
|
||||||
`{
|
|
||||||
"rating": -3
|
|
||||||
}`
|
|
||||||
bookId := "18"
|
|
||||||
testPutRateUserBooks(t, payload, bookId, http.StatusBadRequest)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPutRatingUserBooksHandler_RatingMax(t *testing.T) {
|
|
||||||
payload :=
|
|
||||||
`{
|
|
||||||
"rating": 15
|
|
||||||
}`
|
|
||||||
bookId := "18"
|
|
||||||
testPutRateUserBooks(t, payload, bookId, http.StatusBadRequest)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPutRatingUserBooksHandler_BadBookId(t *testing.T) {
|
|
||||||
payload :=
|
|
||||||
`{
|
|
||||||
"rating": 15
|
|
||||||
}`
|
|
||||||
bookId := "18574"
|
|
||||||
testPutRateUserBooks(t, payload, bookId, http.StatusNotFound)
|
|
||||||
}
|
|
||||||
|
|
||||||
func testPutRateUserBooks(t *testing.T, payload string, bookId string, expectedCode int) {
|
|
||||||
testutils.TestBookPutCallWithDemoPayload(t, payload, bookId, expectedCode, "/ws/book/"+bookId+"/rate")
|
|
||||||
}
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
package apitest
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"git.artlef.fr/bibliomane/internal/testutils"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestPutReadUserBooks_NewReadOk(t *testing.T) {
|
|
||||||
payload :=
|
|
||||||
`{
|
|
||||||
"read": true
|
|
||||||
}`
|
|
||||||
bookId := "21"
|
|
||||||
testPutReadUserBooks(t, payload, bookId, http.StatusOK)
|
|
||||||
book := testGetBook(t, bookId, http.StatusOK)
|
|
||||||
assert.Equal(t, true, book.Read)
|
|
||||||
assert.Equal(t, false, book.WantRead)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPutReadUserBooks_NewReadDateOk(t *testing.T) {
|
|
||||||
payload :=
|
|
||||||
`{
|
|
||||||
"read": true,
|
|
||||||
"endDate": "2025-10-20"
|
|
||||||
}`
|
|
||||||
bookId := "9"
|
|
||||||
testPutReadUserBooks(t, payload, bookId, http.StatusOK)
|
|
||||||
book := testGetBook(t, bookId, http.StatusOK)
|
|
||||||
assert.Equal(t, true, book.Read)
|
|
||||||
assert.Equal(t, "2025-10-20", book.EndReadDate)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPutReadUserBooks_UnsetEndDate(t *testing.T) {
|
|
||||||
payload :=
|
|
||||||
`{
|
|
||||||
"read": true,
|
|
||||||
"endDate": "null"
|
|
||||||
}`
|
|
||||||
bookId := "9"
|
|
||||||
testPutReadUserBooks(t, payload, bookId, http.StatusOK)
|
|
||||||
book := testGetBook(t, bookId, http.StatusOK)
|
|
||||||
assert.Equal(t, true, book.Read)
|
|
||||||
assert.Equal(t, "", book.EndReadDate)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPutReadUserBooks_UnsetReadOk(t *testing.T) {
|
|
||||||
payload :=
|
|
||||||
`{
|
|
||||||
"read": false
|
|
||||||
}`
|
|
||||||
bookId := "9"
|
|
||||||
testPutReadUserBooks(t, payload, bookId, http.StatusOK)
|
|
||||||
book := testGetBook(t, bookId, http.StatusOK)
|
|
||||||
assert.Equal(t, false, book.Read)
|
|
||||||
assert.Equal(t, "", book.EndReadDate)
|
|
||||||
}
|
|
||||||
|
|
||||||
func testPutReadUserBooks(t *testing.T, payload string, bookId string, expectedCode int) {
|
|
||||||
testutils.TestBookPutCallWithDemoPayload(t, payload, bookId, expectedCode, "/ws/book/"+bookId+"/read")
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
package apitest
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"git.artlef.fr/bibliomane/internal/testutils"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestPutStartReadUserBooks_NoDate(t *testing.T) {
|
|
||||||
payload :=
|
|
||||||
`{
|
|
||||||
"date": "2025-11-19"
|
|
||||||
}`
|
|
||||||
bookId := "6"
|
|
||||||
testPutStartReadUserBooks(t, payload, bookId, http.StatusBadRequest)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPutStartReadUserBooks_WrongDateFormat(t *testing.T) {
|
|
||||||
payload :=
|
|
||||||
`{
|
|
||||||
"startDate": "19/11/2025"
|
|
||||||
}`
|
|
||||||
bookId := "6"
|
|
||||||
testPutStartReadUserBooks(t, payload, bookId, http.StatusInternalServerError)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPutStartReadUserBooks_NewReadOk(t *testing.T) {
|
|
||||||
payload :=
|
|
||||||
`{
|
|
||||||
"startDate": "2025-11-19"
|
|
||||||
}`
|
|
||||||
bookId := "6"
|
|
||||||
testPutStartReadUserBooks(t, payload, bookId, http.StatusOK)
|
|
||||||
book := testGetBook(t, bookId, http.StatusOK)
|
|
||||||
assert.Equal(t, "2025-11-19", book.StartReadDate)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPutStartReadUserBooks_Unset(t *testing.T) {
|
|
||||||
payload :=
|
|
||||||
`{
|
|
||||||
"startDate": "null"
|
|
||||||
}`
|
|
||||||
bookId := "6"
|
|
||||||
testPutStartReadUserBooks(t, payload, bookId, http.StatusOK)
|
|
||||||
book := testGetBook(t, bookId, http.StatusOK)
|
|
||||||
assert.Equal(t, "", book.StartReadDate)
|
|
||||||
}
|
|
||||||
|
|
||||||
func testPutStartReadUserBooks(t *testing.T, payload string, bookId string, expectedCode int) {
|
|
||||||
testutils.TestBookPutCallWithDemoPayload(t, payload, bookId, expectedCode, "/ws/book/"+bookId+"/startread")
|
|
||||||
}
|
|
||||||
190
internal/apitest/put_userbook_test.go
Normal file
190
internal/apitest/put_userbook_test.go
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
package apitest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.artlef.fr/bibliomane/internal/testutils"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPutRatingUserBooksHandler_UpdateRating(t *testing.T) {
|
||||||
|
payload :=
|
||||||
|
`{
|
||||||
|
"rating": 5
|
||||||
|
}`
|
||||||
|
bookId := "17"
|
||||||
|
testPutUserBooks(t, payload, bookId, http.StatusOK)
|
||||||
|
book := testGetBook(t, bookId, http.StatusOK)
|
||||||
|
assert.Equal(t, 5, book.Rating)
|
||||||
|
assert.Equal(t, true, book.Read)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPutRatingUserBooksHandler_RateNewBookMakeItRead(t *testing.T) {
|
||||||
|
payload :=
|
||||||
|
`{
|
||||||
|
"rating": 7
|
||||||
|
}`
|
||||||
|
bookId := "18"
|
||||||
|
testPutUserBooks(t, payload, bookId, http.StatusOK)
|
||||||
|
book := testGetBook(t, bookId, http.StatusOK)
|
||||||
|
assert.Equal(t, 7, book.Rating)
|
||||||
|
assert.Equal(t, true, book.Read)
|
||||||
|
assert.Equal(t, false, book.WantRead)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPutRatingUserBooksHandler_RateWantedBook(t *testing.T) {
|
||||||
|
payload :=
|
||||||
|
`{
|
||||||
|
"rating": 6
|
||||||
|
}`
|
||||||
|
bookId := "2"
|
||||||
|
testPutUserBooks(t, payload, bookId, http.StatusOK)
|
||||||
|
book := testGetBook(t, bookId, http.StatusOK)
|
||||||
|
assert.Equal(t, 6, book.Rating)
|
||||||
|
assert.Equal(t, true, book.Read)
|
||||||
|
assert.Equal(t, false, book.WantRead)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPutRatingUserBooksHandler_RatingTypeWrong(t *testing.T) {
|
||||||
|
payload :=
|
||||||
|
`{
|
||||||
|
"rating": "bad"
|
||||||
|
}`
|
||||||
|
bookId := "18"
|
||||||
|
testPutUserBooks(t, payload, bookId, http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPutRatingUserBooksHandler_RatingMin(t *testing.T) {
|
||||||
|
payload :=
|
||||||
|
`{
|
||||||
|
"rating": -3
|
||||||
|
}`
|
||||||
|
bookId := "18"
|
||||||
|
testPutUserBooks(t, payload, bookId, http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPutRatingUserBooksHandler_RatingMax(t *testing.T) {
|
||||||
|
payload :=
|
||||||
|
`{
|
||||||
|
"rating": 15
|
||||||
|
}`
|
||||||
|
bookId := "18"
|
||||||
|
testPutUserBooks(t, payload, bookId, http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPutRatingUserBooksHandler_BadBookId(t *testing.T) {
|
||||||
|
payload :=
|
||||||
|
`{
|
||||||
|
"rating": 15
|
||||||
|
}`
|
||||||
|
bookId := "18574"
|
||||||
|
testPutUserBooks(t, payload, bookId, http.StatusNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPutReadUserBooks_NewReadOk(t *testing.T) {
|
||||||
|
payload :=
|
||||||
|
`{
|
||||||
|
"read": true
|
||||||
|
}`
|
||||||
|
bookId := "21"
|
||||||
|
testPutUserBooks(t, payload, bookId, http.StatusOK)
|
||||||
|
book := testGetBook(t, bookId, http.StatusOK)
|
||||||
|
assert.Equal(t, true, book.Read)
|
||||||
|
assert.Equal(t, false, book.WantRead)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPutReadUserBooks_NewReadDateOk(t *testing.T) {
|
||||||
|
payload :=
|
||||||
|
`{
|
||||||
|
"read": true,
|
||||||
|
"endDate": "2025-10-20"
|
||||||
|
}`
|
||||||
|
bookId := "9"
|
||||||
|
testPutUserBooks(t, payload, bookId, http.StatusOK)
|
||||||
|
book := testGetBook(t, bookId, http.StatusOK)
|
||||||
|
assert.Equal(t, true, book.Read)
|
||||||
|
assert.Equal(t, "2025-10-20", book.EndReadDate)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPutReadUserBooks_UnsetEndDate(t *testing.T) {
|
||||||
|
payload :=
|
||||||
|
`{
|
||||||
|
"read": true,
|
||||||
|
"endDate": "null"
|
||||||
|
}`
|
||||||
|
bookId := "9"
|
||||||
|
testPutUserBooks(t, payload, bookId, http.StatusOK)
|
||||||
|
book := testGetBook(t, bookId, http.StatusOK)
|
||||||
|
assert.Equal(t, true, book.Read)
|
||||||
|
assert.Equal(t, "", book.EndReadDate)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPutReadUserBooks_UnsetReadOk(t *testing.T) {
|
||||||
|
payload :=
|
||||||
|
`{
|
||||||
|
"read": false
|
||||||
|
}`
|
||||||
|
bookId := "9"
|
||||||
|
testPutUserBooks(t, payload, bookId, http.StatusOK)
|
||||||
|
book := testGetBook(t, bookId, http.StatusOK)
|
||||||
|
assert.Equal(t, false, book.Read)
|
||||||
|
assert.Equal(t, "", book.EndReadDate)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPutStartReadUserBooks_WrongDateFormat(t *testing.T) {
|
||||||
|
payload :=
|
||||||
|
`{
|
||||||
|
"startDate": "19/11/2025"
|
||||||
|
}`
|
||||||
|
bookId := "6"
|
||||||
|
testPutUserBooks(t, payload, bookId, http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPutStartReadUserBooks_NewReadOk(t *testing.T) {
|
||||||
|
payload :=
|
||||||
|
`{
|
||||||
|
"startDate": "2025-11-19"
|
||||||
|
}`
|
||||||
|
bookId := "6"
|
||||||
|
testPutUserBooks(t, payload, bookId, http.StatusOK)
|
||||||
|
book := testGetBook(t, bookId, http.StatusOK)
|
||||||
|
assert.Equal(t, "2025-11-19", book.StartReadDate)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPutStartReadUserBooks_Unset(t *testing.T) {
|
||||||
|
payload :=
|
||||||
|
`{
|
||||||
|
"startDate": "null"
|
||||||
|
}`
|
||||||
|
bookId := "6"
|
||||||
|
testPutUserBooks(t, payload, bookId, http.StatusOK)
|
||||||
|
book := testGetBook(t, bookId, http.StatusOK)
|
||||||
|
assert.Equal(t, "", book.StartReadDate)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPutWantRead_SetTrue(t *testing.T) {
|
||||||
|
payload :=
|
||||||
|
`{
|
||||||
|
"wantread": true
|
||||||
|
}`
|
||||||
|
bookId := "17"
|
||||||
|
testPutUserBooks(t, payload, bookId, http.StatusOK)
|
||||||
|
book := testGetBook(t, bookId, http.StatusOK)
|
||||||
|
assert.Equal(t, true, book.WantRead)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPutWantRead_SetFalse(t *testing.T) {
|
||||||
|
payload :=
|
||||||
|
`{
|
||||||
|
"wantread": false
|
||||||
|
}`
|
||||||
|
bookId := "2"
|
||||||
|
testPutUserBooks(t, payload, bookId, http.StatusOK)
|
||||||
|
book := testGetBook(t, bookId, http.StatusOK)
|
||||||
|
assert.Equal(t, false, book.WantRead)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testPutUserBooks(t *testing.T, payload string, bookId string, expectedCode int) {
|
||||||
|
testutils.TestBookPutCallWithDemoPayload(t, payload, bookId, expectedCode, "/ws/book/"+bookId)
|
||||||
|
}
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
package apitest
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"git.artlef.fr/bibliomane/internal/testutils"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestPutWantRead_SetTrue(t *testing.T) {
|
|
||||||
payload :=
|
|
||||||
`{
|
|
||||||
"wantread": true
|
|
||||||
}`
|
|
||||||
bookId := "17"
|
|
||||||
testPutWantReadUserBooks(t, payload, bookId, http.StatusOK)
|
|
||||||
book := testGetBook(t, bookId, http.StatusOK)
|
|
||||||
assert.Equal(t, true, book.WantRead)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPutWantRead_SetFalse(t *testing.T) {
|
|
||||||
payload :=
|
|
||||||
`{
|
|
||||||
"wantread": false
|
|
||||||
}`
|
|
||||||
bookId := "2"
|
|
||||||
testPutWantReadUserBooks(t, payload, bookId, http.StatusOK)
|
|
||||||
book := testGetBook(t, bookId, http.StatusOK)
|
|
||||||
assert.Equal(t, false, book.WantRead)
|
|
||||||
}
|
|
||||||
|
|
||||||
func testPutWantReadUserBooks(t *testing.T, payload string, bookId string, expectedCode int) {
|
|
||||||
testutils.TestBookPutCallWithDemoPayload(t, payload, bookId, expectedCode, "/ws/book/"+bookId+"/wantread")
|
|
||||||
}
|
|
||||||
@@ -21,6 +21,15 @@ type BookPostImport struct {
|
|||||||
Lang string `json:"lang" binding:"required,max=5"`
|
Lang string `json:"lang" binding:"required,max=5"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type UserBookPutUpdate struct {
|
||||||
|
Read *bool `json:"read"`
|
||||||
|
EndDate *string `json:"endDate"`
|
||||||
|
WantRead *bool `json:"wantread"`
|
||||||
|
Rating *int `json:"rating"`
|
||||||
|
StartDate *string `json:"startDate"`
|
||||||
|
Review *string `json:"review"`
|
||||||
|
}
|
||||||
|
|
||||||
type FileInfoPost struct {
|
type FileInfoPost struct {
|
||||||
FileID uint `json:"fileId"`
|
FileID uint `json:"fileId"`
|
||||||
FilePath string `json:"filepath"`
|
FilePath string `json:"filepath"`
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ type BookGet struct {
|
|||||||
InventaireId string `json:"inventaireid"`
|
InventaireId string `json:"inventaireid"`
|
||||||
OpenLibraryId string `json:"openlibraryid"`
|
OpenLibraryId string `json:"openlibraryid"`
|
||||||
Summary string `json:"summary"`
|
Summary string `json:"summary"`
|
||||||
|
Review string `json:"review"`
|
||||||
Rating int `json:"rating"`
|
Rating int `json:"rating"`
|
||||||
Read bool `json:"read"`
|
Read bool `json:"read"`
|
||||||
WantRead bool `json:"wantread"`
|
WantRead bool `json:"wantread"`
|
||||||
|
|||||||
@@ -132,6 +132,27 @@ func TestCallInventaireEdition(t *testing.T) {
|
|||||||
result)
|
result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCalInventaireEditionNoAuthor(t *testing.T) {
|
||||||
|
|
||||||
|
result, err := CallInventaireEdition(getBaseInventaireUrl(), "isbn:9782226487162", "fr")
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
assert.Equal(t,
|
||||||
|
InventaireEditionDetailedSingleResult{
|
||||||
|
Id: "isbn:9782226487162",
|
||||||
|
Title: "Les Yeux de Mona",
|
||||||
|
Author: nil,
|
||||||
|
Description: "",
|
||||||
|
ISBN: "978-2-226-48716-2",
|
||||||
|
Publisher: "éditions Albin Michel",
|
||||||
|
ReleaseDate: "2024-02-01",
|
||||||
|
Image: "https://inventaire.io/img/entities/3ca857913983d694be03dee712bb2af9e2c51747",
|
||||||
|
Lang: "fr",
|
||||||
|
},
|
||||||
|
result)
|
||||||
|
}
|
||||||
|
|
||||||
func TestCallInventaireEditionFromISBN(t *testing.T) {
|
func TestCallInventaireEditionFromISBN(t *testing.T) {
|
||||||
result, err := CallInventaireFromISBN(getBaseInventaireUrl(), "9782070379248", "fr")
|
result, err := CallInventaireFromISBN(getBaseInventaireUrl(), "9782070379248", "fr")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package inventaire
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"math"
|
"math"
|
||||||
|
"slices"
|
||||||
"sort"
|
"sort"
|
||||||
|
|
||||||
"git.artlef.fr/bibliomane/internal/callapiutils"
|
"git.artlef.fr/bibliomane/internal/callapiutils"
|
||||||
@@ -32,14 +33,15 @@ func CallInventaireEditionFromWork(inventaireUrl string, workId string, lang str
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return queryResult, err
|
return queryResult, err
|
||||||
}
|
}
|
||||||
queryResult.Count = int64(len(uris.Uris))
|
|
||||||
sort.Strings(uris.Uris)
|
sort.Strings(uris.Uris)
|
||||||
limitedUris := uris.Uris
|
listUris := slices.Compact(uris.Uris)
|
||||||
|
queryResult.Count = int64(len(listUris))
|
||||||
|
limitedUris := listUris
|
||||||
if limit != 0 {
|
if limit != 0 {
|
||||||
l := len(uris.Uris)
|
l := len(listUris)
|
||||||
startIndex := int(math.Min(float64(offset), float64(l)))
|
startIndex := int(math.Min(float64(offset), float64(l)))
|
||||||
endIndex := int(math.Min(float64(limit+offset), float64(l)))
|
endIndex := int(math.Min(float64(limit+offset), float64(l)))
|
||||||
limitedUris = uris.Uris[startIndex:endIndex]
|
limitedUris = listUris[startIndex:endIndex]
|
||||||
}
|
}
|
||||||
editionEntities, err := callInventaireEditionEntities(inventaireUrl, limitedUris)
|
editionEntities, err := callInventaireEditionEntities(inventaireUrl, limitedUris)
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ type UserBook struct {
|
|||||||
Rating int
|
Rating int
|
||||||
Read bool
|
Read bool
|
||||||
WantRead bool
|
WantRead bool
|
||||||
|
Review string
|
||||||
StartReadDate *time.Time
|
StartReadDate *time.Time
|
||||||
EndReadDate *time.Time
|
EndReadDate *time.Time
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,6 +79,8 @@ func computeValidationMessage(ac *appcontext.AppContext, fe *validator.FieldErro
|
|||||||
return i18nresource.GetTranslatedMessage(ac, "ValidationRequired")
|
return i18nresource.GetTranslatedMessage(ac, "ValidationRequired")
|
||||||
case "min":
|
case "min":
|
||||||
return fmt.Sprintf(i18nresource.GetTranslatedMessage(ac, "ValidationTooShort"), (*fe).Param())
|
return fmt.Sprintf(i18nresource.GetTranslatedMessage(ac, "ValidationTooShort"), (*fe).Param())
|
||||||
|
case "gte":
|
||||||
|
return fmt.Sprintf("Should be greater than %s", (*fe).Param())
|
||||||
case "max":
|
case "max":
|
||||||
return fmt.Sprintf(i18nresource.GetTranslatedMessage(ac, "ValidationTooLong"), (*fe).Param())
|
return fmt.Sprintf(i18nresource.GetTranslatedMessage(ac, "ValidationTooLong"), (*fe).Param())
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ func FetchBookGet(db *gorm.DB, userId uint, bookId uint64) (dto.BookGet, error)
|
|||||||
var book dto.BookGet
|
var book dto.BookGet
|
||||||
query := db.Model(&model.Book{})
|
query := db.Model(&model.Book{})
|
||||||
selectQueryString := "books.title, authors.name as author, authors.id as author_id, books.isbn, books.inventaire_id, books.open_library_id, books.summary, " +
|
selectQueryString := "books.title, authors.name as author, authors.id as author_id, books.isbn, books.inventaire_id, books.open_library_id, books.summary, " +
|
||||||
"user_books.rating, user_books.read, user_books.want_read, " +
|
"user_books.review, user_books.rating, user_books.read, user_books.want_read, " +
|
||||||
"DATE(user_books.start_read_date) as start_read_date, " +
|
"DATE(user_books.start_read_date) as start_read_date, " +
|
||||||
"DATE(user_books.end_read_date) AS end_read_date, " +
|
"DATE(user_books.end_read_date) AS end_read_date, " +
|
||||||
selectStaticFilesPath()
|
selectStaticFilesPath()
|
||||||
@@ -63,7 +63,7 @@ func fetchReadUserBookQuery(db *gorm.DB, userId uint) *gorm.DB {
|
|||||||
}
|
}
|
||||||
func fetchReadingUserBookQuery(db *gorm.DB, userId uint) *gorm.DB {
|
func fetchReadingUserBookQuery(db *gorm.DB, userId uint) *gorm.DB {
|
||||||
query := fetchUserBookGet(db, userId)
|
query := fetchUserBookGet(db, userId)
|
||||||
query = query.Where("user_books.start_read_date IS NOT NULL")
|
query = query.Where("user_books.start_read_date IS NOT NULL AND (user_books.read IS NULL OR user_books.read IS FALSE)")
|
||||||
return query
|
return query
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,18 +40,21 @@ func PostImportBookHandler(ac appcontext.AppContext) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func saveInventaireBookToDb(ac appcontext.AppContext, inventaireEdition inventaire.InventaireEditionDetailedSingleResult, user *model.User) (*model.Book, error) {
|
func saveInventaireBookToDb(ac appcontext.AppContext, inventaireEdition inventaire.InventaireEditionDetailedSingleResult, user *model.User) (*model.Book, error) {
|
||||||
author, err := fetchOrCreateInventaireAuthor(ac, inventaireEdition.Author)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
book := model.Book{
|
book := model.Book{
|
||||||
Title: inventaireEdition.Title,
|
Title: inventaireEdition.Title,
|
||||||
SmallDescription: inventaireEdition.Description,
|
SmallDescription: inventaireEdition.Description,
|
||||||
InventaireID: inventaireEdition.Id,
|
InventaireID: inventaireEdition.Id,
|
||||||
Author: *author,
|
|
||||||
AddedBy: *user,
|
AddedBy: *user,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if inventaireEdition.Author != nil {
|
||||||
|
author, err := fetchOrCreateInventaireAuthor(ac, inventaireEdition.Author)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
book.Author = *author
|
||||||
|
}
|
||||||
|
|
||||||
if inventaireEdition.Image != "" {
|
if inventaireEdition.Image != "" {
|
||||||
cover, err := fileutils.DownloadFile(ac, inventaireEdition.Image)
|
cover, err := fileutils.DownloadFile(ac, inventaireEdition.Image)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -59,7 +62,7 @@ func saveInventaireBookToDb(ac appcontext.AppContext, inventaireEdition inventai
|
|||||||
}
|
}
|
||||||
book.Cover = cover
|
book.Cover = cover
|
||||||
}
|
}
|
||||||
err = ac.Db.Save(&book).Error
|
err := ac.Db.Save(&book).Error
|
||||||
return &book, err
|
return &book, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,21 +7,35 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.artlef.fr/bibliomane/internal/appcontext"
|
"git.artlef.fr/bibliomane/internal/appcontext"
|
||||||
|
"git.artlef.fr/bibliomane/internal/dto"
|
||||||
"git.artlef.fr/bibliomane/internal/model"
|
"git.artlef.fr/bibliomane/internal/model"
|
||||||
"git.artlef.fr/bibliomane/internal/myvalidator"
|
"git.artlef.fr/bibliomane/internal/myvalidator"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/go-playground/validator/v10"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
func PutReadUserBookHandler(ac appcontext.AppContext) {
|
func PutUserBookHandler(ac appcontext.AppContext) {
|
||||||
data, err := retrieveDataFromContext(ac)
|
bookId64, err := strconv.ParseUint(ac.C.Param("id"), 10, 64)
|
||||||
|
bookId := uint(bookId64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
ac.C.JSON(http.StatusBadRequest, gin.H{"error": err})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
bookId := data.BookId
|
err = myvalidator.ValidateId(ac.Db, bookId, &model.Book{})
|
||||||
user := data.User
|
if err != nil {
|
||||||
var read userbookPutRead
|
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||||
err = ac.C.ShouldBindJSON(&read)
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := ac.GetAuthenticatedUser()
|
||||||
|
if err != nil {
|
||||||
|
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var userBookPut dto.UserBookPutUpdate
|
||||||
|
err = ac.C.ShouldBindJSON(&userBookPut)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||||
return
|
return
|
||||||
@@ -32,14 +46,56 @@ func PutReadUserBookHandler(ac appcontext.AppContext) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
userbook.Read = read.Read
|
if userBookPut.Read != nil {
|
||||||
|
err = updateReadStatus(&userbook, &userBookPut)
|
||||||
if read.EndDate != "" {
|
|
||||||
d, err := parseDate(read.EndDate)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
if userBookPut.WantRead != nil {
|
||||||
|
userbook.WantRead = *userBookPut.WantRead
|
||||||
|
}
|
||||||
|
if userBookPut.StartDate != nil {
|
||||||
|
d, err := parseDate(*userBookPut.StartDate)
|
||||||
|
if err != nil {
|
||||||
|
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
userbook.StartReadDate = d
|
||||||
|
}
|
||||||
|
if userBookPut.Rating != nil {
|
||||||
|
err = validateRating(*userBookPut.Rating)
|
||||||
|
if err != nil {
|
||||||
|
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||||
|
}
|
||||||
|
updateRating(&userbook, &userBookPut)
|
||||||
|
}
|
||||||
|
if userBookPut.Review != nil {
|
||||||
|
userbook.Review = *userBookPut.Review
|
||||||
|
}
|
||||||
|
ac.Db.Save(&userbook)
|
||||||
|
ac.C.String(http.StatusOK, "Success")
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateRating(rating int) error {
|
||||||
|
//struct used for validation
|
||||||
|
var ratingStruct struct {
|
||||||
|
Rating int `validate:"gte=0,lte=10"`
|
||||||
|
}
|
||||||
|
ratingStruct.Rating = rating
|
||||||
|
validate := validator.New()
|
||||||
|
return validate.Struct(ratingStruct)
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateReadStatus(userbook *model.UserBook, userBookPut *dto.UserBookPutUpdate) error {
|
||||||
|
userbook.Read = *userBookPut.Read
|
||||||
|
|
||||||
|
if userBookPut.EndDate != nil && *userBookPut.EndDate != "" {
|
||||||
|
d, err := parseDate(*userBookPut.EndDate)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
userbook.EndReadDate = d
|
userbook.EndReadDate = d
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,84 +108,11 @@ func PutReadUserBookHandler(ac appcontext.AppContext) {
|
|||||||
if !userbook.Read {
|
if !userbook.Read {
|
||||||
userbook.EndReadDate = nil
|
userbook.EndReadDate = nil
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
ac.Db.Save(&userbook)
|
|
||||||
ac.C.String(http.StatusOK, "Success")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func PutWantReadUserBookHandler(ac appcontext.AppContext) {
|
func updateRating(userbook *model.UserBook, userBookPut *dto.UserBookPutUpdate) {
|
||||||
data, err := retrieveDataFromContext(ac)
|
userbook.Rating = *userBookPut.Rating
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
bookId := data.BookId
|
|
||||||
user := data.User
|
|
||||||
var wantread userbookPutWantRead
|
|
||||||
err = ac.C.ShouldBindJSON(&wantread)
|
|
||||||
if err != nil {
|
|
||||||
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
userbook, err := fetchOrCreateUserBook(ac, bookId, &user)
|
|
||||||
if err != nil {
|
|
||||||
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
userbook.WantRead = wantread.WantRead
|
|
||||||
ac.Db.Save(&userbook)
|
|
||||||
ac.C.String(http.StatusOK, "Success")
|
|
||||||
}
|
|
||||||
|
|
||||||
func PutStartReadUserBookHandler(ac appcontext.AppContext) {
|
|
||||||
data, err := retrieveDataFromContext(ac)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
bookId := data.BookId
|
|
||||||
user := data.User
|
|
||||||
|
|
||||||
var startDateToParse userbookPutStartRead
|
|
||||||
err = ac.C.ShouldBindJSON(&startDateToParse)
|
|
||||||
if err != nil {
|
|
||||||
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
userbook, err := fetchOrCreateUserBook(ac, bookId, &user)
|
|
||||||
if err != nil {
|
|
||||||
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
d, err := parseDate(startDateToParse.StartDate)
|
|
||||||
if err != nil {
|
|
||||||
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
userbook.StartReadDate = d
|
|
||||||
|
|
||||||
ac.Db.Save(&userbook)
|
|
||||||
ac.C.String(http.StatusOK, "Success")
|
|
||||||
}
|
|
||||||
|
|
||||||
func PutRateUserBookHandler(ac appcontext.AppContext) {
|
|
||||||
data, err := retrieveDataFromContext(ac)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
bookId := data.BookId
|
|
||||||
user := data.User
|
|
||||||
var rating userbookPutRating
|
|
||||||
err = ac.C.ShouldBindJSON(&rating)
|
|
||||||
if err != nil {
|
|
||||||
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
userbook, err := fetchOrCreateUserBook(ac, bookId, &user)
|
|
||||||
if err != nil {
|
|
||||||
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
userbook.Rating = rating.Rating
|
|
||||||
|
|
||||||
//if rated, set to "read" (a rating = 0 means unrated)
|
//if rated, set to "read" (a rating = 0 means unrated)
|
||||||
if userbook.Rating > 0 {
|
if userbook.Rating > 0 {
|
||||||
@@ -137,30 +120,6 @@ func PutRateUserBookHandler(ac appcontext.AppContext) {
|
|||||||
//if set to read, remove want read
|
//if set to read, remove want read
|
||||||
userbook.WantRead = false
|
userbook.WantRead = false
|
||||||
}
|
}
|
||||||
ac.Db.Save(&userbook)
|
|
||||||
ac.C.String(http.StatusOK, "Success")
|
|
||||||
}
|
|
||||||
|
|
||||||
type userbookPutRead struct {
|
|
||||||
Read bool `json:"read"`
|
|
||||||
EndDate string `json:"endDate"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type userbookPutWantRead struct {
|
|
||||||
WantRead bool `json:"wantread"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type userbookPutRating struct {
|
|
||||||
Rating int `json:"rating" binding:"min=0,max=10"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type userbookPutStartRead struct {
|
|
||||||
StartDate string `json:"startDate" binding:"required"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type apiCallData struct {
|
|
||||||
BookId uint
|
|
||||||
User model.User
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseDate(dateToParse string) (*time.Time, error) {
|
func parseDate(dateToParse string) (*time.Time, error) {
|
||||||
@@ -173,27 +132,6 @@ func parseDate(dateToParse string) (*time.Time, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func retrieveDataFromContext(ac appcontext.AppContext) (apiCallData, error) {
|
|
||||||
bookId64, err := strconv.ParseUint(ac.C.Param("id"), 10, 64)
|
|
||||||
bookId := uint(bookId64)
|
|
||||||
if err != nil {
|
|
||||||
ac.C.JSON(http.StatusBadRequest, gin.H{"error": err})
|
|
||||||
return apiCallData{}, err
|
|
||||||
}
|
|
||||||
err = myvalidator.ValidateId(ac.Db, bookId, &model.Book{})
|
|
||||||
if err != nil {
|
|
||||||
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
|
||||||
return apiCallData{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
user, fetchUserErr := ac.GetAuthenticatedUser()
|
|
||||||
if fetchUserErr != nil {
|
|
||||||
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
|
||||||
return apiCallData{}, fetchUserErr
|
|
||||||
}
|
|
||||||
return apiCallData{BookId: bookId, User: user}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func fetchOrCreateUserBook(ac appcontext.AppContext, bookId uint, user *model.User) (model.UserBook, error) {
|
func fetchOrCreateUserBook(ac appcontext.AppContext, bookId uint, user *model.User) (model.UserBook, error) {
|
||||||
var userbook model.UserBook
|
var userbook model.UserBook
|
||||||
res := ac.Db.Where("user_id = ? AND book_id = ?", user.ID, bookId).First(&userbook)
|
res := ac.Db.Where("user_id = ? AND book_id = ?", user.ID, bookId).First(&userbook)
|
||||||
|
|||||||
@@ -58,17 +58,8 @@ func Setup(config *config.Config) *gin.Engine {
|
|||||||
ws.GET("/book/:id", func(c *gin.Context) {
|
ws.GET("/book/:id", func(c *gin.Context) {
|
||||||
routes.GetBookHandler(appcontext.AppContext{C: c, Db: db, I18n: bundle, Config: config})
|
routes.GetBookHandler(appcontext.AppContext{C: c, Db: db, I18n: bundle, Config: config})
|
||||||
})
|
})
|
||||||
ws.PUT("/book/:id/read", func(c *gin.Context) {
|
ws.PUT("/book/:id", func(c *gin.Context) {
|
||||||
routes.PutReadUserBookHandler(appcontext.AppContext{C: c, Db: db, I18n: bundle, Config: config})
|
routes.PutUserBookHandler(appcontext.AppContext{C: c, Db: db, I18n: bundle, Config: config})
|
||||||
})
|
|
||||||
ws.PUT("/book/:id/wantread", func(c *gin.Context) {
|
|
||||||
routes.PutWantReadUserBookHandler(appcontext.AppContext{C: c, Db: db, I18n: bundle, Config: config})
|
|
||||||
})
|
|
||||||
ws.PUT("/book/:id/startread", func(c *gin.Context) {
|
|
||||||
routes.PutStartReadUserBookHandler(appcontext.AppContext{C: c, Db: db, I18n: bundle, Config: config})
|
|
||||||
})
|
|
||||||
ws.PUT("/book/:id/rate", func(c *gin.Context) {
|
|
||||||
routes.PutRateUserBookHandler(appcontext.AppContext{C: c, Db: db, I18n: bundle, Config: config})
|
|
||||||
})
|
})
|
||||||
ws.POST("/book", func(c *gin.Context) {
|
ws.POST("/book", func(c *gin.Context) {
|
||||||
routes.PostBookHandler(appcontext.AppContext{C: c, Db: db, I18n: bundle, Config: config})
|
routes.PostBookHandler(appcontext.AppContext{C: c, Db: db, I18n: bundle, Config: config})
|
||||||
@@ -104,7 +95,7 @@ func Setup(config *config.Config) *gin.Engine {
|
|||||||
r.StaticFS("/"+folder, http.FS(subFs))
|
r.StaticFS("/"+folder, http.FS(subFs))
|
||||||
}
|
}
|
||||||
|
|
||||||
r.StaticFileFS("/favicon.ico", "favicon.ico", http.FS(front.Frontend))
|
r.StaticFileFS("/favicon.svg", "favicon.svg", http.FS(front.Frontend))
|
||||||
r.GET("/", serveIndexHtml)
|
r.GET("/", serveIndexHtml)
|
||||||
|
|
||||||
r.NoRoute(serveIndexHtml)
|
r.NoRoute(serveIndexHtml)
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import (
|
|||||||
"git.artlef.fr/bibliomane/internal/config"
|
"git.artlef.fr/bibliomane/internal/config"
|
||||||
"git.artlef.fr/bibliomane/internal/setup"
|
"git.artlef.fr/bibliomane/internal/setup"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestSetup() *gin.Engine {
|
func TestSetup() *gin.Engine {
|
||||||
@@ -62,5 +61,7 @@ func TestBookPutCallWithDemoPayload(t *testing.T, payload string, bookId string,
|
|||||||
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))
|
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
router.ServeHTTP(w, req)
|
router.ServeHTTP(w, req)
|
||||||
assert.Equal(t, expectedCode, w.Code)
|
if w.Code != expectedCode {
|
||||||
|
t.Errorf("%s", w.Body.String())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
2
main.go
2
main.go
@@ -6,7 +6,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
applicationVersion := "0.2.0"
|
applicationVersion := "0.4.0"
|
||||||
c := config.LoadConfig(applicationVersion)
|
c := config.LoadConfig(applicationVersion)
|
||||||
r := setup.Setup(&c)
|
r := setup.Setup(&c)
|
||||||
r.Run(":" + c.Port)
|
r.Run(":" + c.Port)
|
||||||
|
|||||||
Reference in New Issue
Block a user