Initial Post list
This commit is contained in:
1 parent
9a2673aba2
commit
bb1e0eebf5
16 files changed
+273
-42
No files matched your search
@@ -124,7 +124,7 @@ paths:
|
||||
[
|
||||
{
|
||||
"id": 0,
|
||||
"user": { "username": "string" },
|
||||
"user": { "username": "string", "image": "string" },
|
||||
"content": "string",
|
||||
"postedAt":
|
||||
{
|
||||
|
||||
@@ -428,7 +428,7 @@ data-styled.g137[id="sc-kvXgyf"]{content:"fBvPoH,"}/*!sc*/
|
||||
" class="sc-euGpHm sc-exayXG fwfkcU jYGAQp"><p>The number of items to return.</p>
|
||||
</div></div></div></td></tr></tbody></table></div><div><h3 class="sc-eiLgtK dbBFCU">Responses</h3><div><button class="sc-giOWAb gcYfHW"><svg class="sc-cBYhjr fqtTpb" version="1.1" viewBox="0 0 24 24" x="0" xmlns="http://www.w3.org/2000/svg" y="0" aria-hidden="true"><polygon points="17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 "></polygon></svg><strong class="sc-catHVh gXPvFO">200<!-- --> </strong><div html="<p>Success.</p>
|
||||
" class="sc-euGpHm sc-exayXG fwfkcU kqJXdD sc-dHrNzZ dRdjww"><p>Success.</p>
|
||||
</div></button></div></div></div><div class="sc-iKTcqh sc-gnpbhQ fTxZsC drGLlX"><div class="sc-dKsqdn dCbPd"><button class="sc-iAlELC gsBSOU"><span type="get" class="sc-oeqTF ffmPnn http-verb get">get</span><span class="sc-eowDPD jcAXWA">/posts</span><svg class="sc-cBYhjr iMxoRf" style="margin-right:-25px" version="1.1" viewBox="0 0 24 24" x="0" xmlns="http://www.w3.org/2000/svg" y="0" aria-hidden="true"><polygon points="17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 "></polygon></svg></button><div aria-hidden="true" class="sc-ezTrPE bFiOkX"><div class="sc-drnuxz hdRKqQ"><div html="" class="sc-euGpHm sc-exayXG fwfkcU jYGAQp"></div><div tabindex="0" role="button"><div class="sc-hDcvty jpmGrk"><span>https://khofmann.userpage.fu-berlin.de/phpCourse/exam/api</span>/posts</div></div></div></div></div><div><h3 class="sc-kkmypM jKNCPF"> <!-- -->Response samples<!-- --> </h3><div class="sc-bSlUec jxQggo" data-rttabs="true"><ul class="react-tabs__tab-list" role="tablist"><li class="tab-success react-tabs__tab--selected" role="tab" id="tab:R174pa:0" aria-selected="true" aria-disabled="false" aria-controls="panel:R174pa:0" tabindex="0" data-rttab="true">200</li></ul><div class="react-tabs__tab-panel react-tabs__tab-panel--selected" role="tabpanel" id="panel:R174pa:0" aria-labelledby="tab:R174pa:0"><div><div class="sc-WChnI iLXIA-d"><span class="sc-foMnoT dmNpjh">Content type</span><div class="sc-ZubPq KmnOC">application/json</div></div><div class="sc-AmOMz gDrQpr"><div class="sc-WChnI iLXIA-d"><span class="sc-foMnoT dmNpjh">Example</span><div class="sc-dorvvM sc-flUsNl dQFPeA hzaARH"><svg class="sc-kTYLvb FdjRy" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg><select class="dropdown-select"><option value="Not authenticated" selected="">Not authenticated</option><option value="Authenticated">Authenticated</option></select><label>Not authenticated</label></div></div><div><div class="sc-eTdEpr dgzSkD"><div class="sc-pFPEP bMXXJy"><button><div class="sc-eHujzY ecCAmX">Copy</div></button><button> Expand all </button><button> Collapse all </button></div><div class="sc-euGpHm fwfkcU sc-fTgapq bDqsIm"><div class="redoc-json"><code><button class="collapser" aria-label="collapse"></button><span class="token punctuation">{</span><span class="ellipsis"></span><ul class="obj collapsible"><li><div class="hoverable "><span class="property token string">"pages"</span>: <span class="token number">0</span><span class="token punctuation">,</span></div></li><li><div class="hoverable "><span class="property token string">"data"</span>: <button class="collapser" aria-label="collapse"></button><span class="token punctuation">[</span><span class="ellipsis"></span><ul class="array collapsible"><li><div class="hoverable collapsed"><button class="collapser" aria-label="expand"></button><span class="token punctuation">{</span><span class="ellipsis"></span><ul class="obj collapsible"><li><div class="hoverable collapsed"><span class="property token string">"id"</span>: <span class="token number">0</span><span class="token punctuation">,</span></div></li><li><div class="hoverable collapsed"><span class="property token string">"user"</span>: <button class="collapser" aria-label="expand"></button><span class="token punctuation">{</span><span class="ellipsis"></span><ul class="obj collapsible"><li><div class="hoverable collapsed"><span class="property token string">"username"</span>: <span class="token string">"string"</span></div></li></ul><span class="token punctuation">}</span><span class="token punctuation">,</span></div></li><li><div class="hoverable collapsed"><span class="property token string">"content"</span>: <span class="token string">"string"</span><span class="token punctuation">,</span></div></li><li><div class="hoverable collapsed"><span class="property token string">"postedAt"</span>: <button class="collapser" aria-label="expand"></button><span class="token punctuation">{</span><span class="ellipsis"></span><ul class="obj collapsible"><li><div class="hoverable collapsed"><span class="property token string">"date"</span>: <span class="token string">"2019-08-24T14:15:22Z"</span><span class="token punctuation">,</span></div></li><li><div class="hoverable collapsed"><span class="property token string">"timezone_type"</span>: <span class="token number">0</span><span class="token punctuation">,</span></div></li><li><div class="hoverable collapsed"><span class="property token string">"timezone"</span>: <span class="token string">"string"</span></div></li></ul><span class="token punctuation">}</span></div></li></ul><span class="token punctuation">}</span></div></li></ul><span class="token punctuation">]</span></div></li></ul><span class="token punctuation">}</span></code></div></div></div></div></div></div></div></div></div>Line truncated
|
||||
</div></button></div></div></div><div class="sc-iKTcqh sc-gnpbhQ fTxZsC drGLlX"><div class="sc-dKsqdn dCbPd"><button class="sc-iAlELC gsBSOU"><span type="get" class="sc-oeqTF ffmPnn http-verb get">get</span><span class="sc-eowDPD jcAXWA">/posts</span><svg class="sc-cBYhjr iMxoRf" style="margin-right:-25px" version="1.1" viewBox="0 0 24 24" x="0" xmlns="http://www.w3.org/2000/svg" y="0" aria-hidden="true"><polygon points="17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 "></polygon></svg></button><div aria-hidden="true" class="sc-ezTrPE bFiOkX"><div class="sc-drnuxz hdRKqQ"><div html="" class="sc-euGpHm sc-exayXG fwfkcU jYGAQp"></div><div tabindex="0" role="button"><div class="sc-hDcvty jpmGrk"><span>https://khofmann.userpage.fu-berlin.de/phpCourse/exam/api</span>/posts</div></div></div></div></div><div><h3 class="sc-kkmypM jKNCPF"> <!-- -->Response samples<!-- --> </h3><div class="sc-bSlUec jxQggo" data-rttabs="true"><ul class="react-tabs__tab-list" role="tablist"><li class="tab-success react-tabs__tab--selected" role="tab" id="tab:R174pa:0" aria-selected="true" aria-disabled="false" aria-controls="panel:R174pa:0" tabindex="0" data-rttab="true">200</li></ul><div class="react-tabs__tab-panel react-tabs__tab-panel--selected" role="tabpanel" id="panel:R174pa:0" aria-labelledby="tab:R174pa:0"><div><div class="sc-WChnI iLXIA-d"><span class="sc-foMnoT dmNpjh">Content type</span><div class="sc-ZubPq KmnOC">application/json</div></div><div class="sc-AmOMz gDrQpr"><div class="sc-WChnI iLXIA-d"><span class="sc-foMnoT dmNpjh">Example</span><div class="sc-dorvvM sc-flUsNl dQFPeA hzaARH"><svg class="sc-kTYLvb FdjRy" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg><select class="dropdown-select"><option value="Not authenticated" selected="">Not authenticated</option><option value="Authenticated">Authenticated</option></select><label>Not authenticated</label></div></div><div><div class="sc-eTdEpr dgzSkD"><div class="sc-pFPEP bMXXJy"><button><div class="sc-eHujzY ecCAmX">Copy</div></button><button> Expand all </button><button> Collapse all </button></div><div class="sc-euGpHm fwfkcU sc-fTgapq bDqsIm"><div class="redoc-json"><code><button class="collapser" aria-label="collapse"></button><span class="token punctuation">{</span><span class="ellipsis"></span><ul class="obj collapsible"><li><div class="hoverable "><span class="property token string">"pages"</span>: <span class="token number">0</span><span class="token punctuation">,</span></div></li><li><div class="hoverable "><span class="property token string">"data"</span>: <button class="collapser" aria-label="collapse"></button><span class="token punctuation">[</span><span class="ellipsis"></span><ul class="array collapsible"><li><div class="hoverable collapsed"><button class="collapser" aria-label="expand"></button><span class="token punctuation">{</span><span class="ellipsis"></span><ul class="obj collapsible"><li><div class="hoverable collapsed"><span class="property token string">"id"</span>: <span class="token number">0</span><span class="token punctuation">,</span></div></li><li><div class="hoverable collapsed"><span class="property token string">"user"</span>: <button class="collapser" aria-label="expand"></button><span class="token punctuation">{</span><span class="ellipsis"></span><ul class="obj collapsible"><li><div class="hoverable collapsed"><span class="property token string">"username"</span>: <span class="token string">"string"</span><span class="token punctuation">,</span></div></li><li><div class="hoverable collapsed"><span class="property token string">"image"</span>: <span class="token string">"string"</span></div></li></ul><span class="token punctuation">}</span><span class="token punctuation">,</span></div></li><li><div class="hoverable collapsed"><span class="property token string">"content"</span>: <span class="token string">"string"</span><span class="token punctuation">,</span></div></li><li><div class="hoverable collapsed"><span class="property token string">"postedAt"</span>: <button class="collapser" aria-label="expand"></button><span class="token punctuation">{</span><span class="ellipsis"></span><ul class="obj collapsible"><li><div class="hoverable collapsed"><span class="property token string">"date"</span>: <span class="token string">"2019-08-24T14:15:22Z"</span><span class="token punctuation">,</span></div></li><li><div class="hoverable collapsed"><span class="property token string">"timezone_type"</span>: <span class="token number">0</span><span class="token punctuation">,</span></div></li><li><div class="hoverable collapsed"><span class="property token string">"timezone"</span>: <span class="token string">"string"</span></div></li></ul><span class="token punctuation">}</span></div></li></ul><span class="token puLine truncated
|
||||
" class="sc-euGpHm sc-exayXG fwfkcU kqJXdD"><p>Create a new post.</p>
|
||||
</div></div><div class="sc-hWiVxH cCfxdS"><div class="sc-jHofgq febKCY"><h5 class="sc-dkjaqt sc-jRHPII gwrByh eGMvsx">Authorizations:</h5><svg class="sc-cBYhjr dJanPw" version="1.1" viewBox="0 0 24 24" x="0" xmlns="http://www.w3.org/2000/svg" y="0" aria-hidden="true"><polygon points="17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 "></polygon></svg></div><div class="sc-iRFtIJ juVMxG"><span class="sc-iHlJgr lkBZak"><span class="sc-blcnQh fyveIh"><i>BasicAuth</i></span></span></div></div><h5 class="sc-dkjaqt gwrByh">Request Body schema: <span class="sc-dwYcXH fafqZb">application/json</span><div class="sc-xuUkR sc-hrDJJk sc-kGqRax bJcEcT gLEAmN HCLfY">required</div></h5><div html="" class="sc-euGpHm sc-exayXG fwfkcU kqJXdD"></div><table class="sc-dENhDJ ceVHDP"><tbody><tr class="last "><td kind="field" title="content" class="sc-tOkKi sc-epPVmt gbdrVc bUksBx"><span class="sc-hfvVTD hTjFRU"></span><span class="property-name">content</span><div class="sc-xuUkR sc-hrDJJk bJcEcT gLEAmN">required</div></td><td class="sc-fpSrms exGrJC"><div><div><span class="sc-xuUkR sc-cvzDha bJcEcT kDPMlG"></span><span class="sc-xuUkR sc-gKROGD bJcEcT etUsjc">string</span></div> <div><div html="" class="sc-euGpHm sc-exayXG fwfkcU jYGAQp"></div></div></div></td></tr></tbody></table><div><h3 class="sc-eiLgtK dbBFCU">Responses</h3><div><button class="sc-giOWAb gcYfHW"><svg class="sc-cBYhjr fqtTpb" version="1.1" viewBox="0 0 24 24" x="0" xmlns="http://www.w3.org/2000/svg" y="0" aria-hidden="true"><polygon points="17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 "></polygon></svg><strong class="sc-catHVh gXPvFO">200<!-- --> </strong><div html="<p>Success.</p>
|
||||
" class="sc-euGpHm sc-exayXG fwfkcU kqJXdD sc-dHrNzZ dRdjww"><p>Success.</p>
|
||||
@@ -506,7 +506,7 @@ data-styled.g137[id="sc-kvXgyf"]{content:"fBvPoH,"}/*!sc*/
|
||||
" class="sc-euGpHm sc-exayXG fwfkcU kqJXdD sc-dHrNzZ dRdjww"><p>User not found.</p>
|
||||
</div></button></div></div></div><div class="sc-iKTcqh sc-gnpbhQ fTxZsC drGLlX"><div class="sc-dKsqdn dCbPd"><button class="sc-iAlELC gsBSOU"><span type="delete" class="sc-oeqTF ByFMv http-verb delete">delete</span><span class="sc-eowDPD jcAXWA">/users/{id}</span><svg class="sc-cBYhjr iMxoRf" style="margin-right:-25px" version="1.1" viewBox="0 0 24 24" x="0" xmlns="http://www.w3.org/2000/svg" y="0" aria-hidden="true"><polygon points="17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 "></polygon></svg></button><div aria-hidden="true" class="sc-ezTrPE bFiOkX"><div class="sc-drnuxz hdRKqQ"><div html="" class="sc-euGpHm sc-exayXG fwfkcU jYGAQp"></div><div tabindex="0" role="button"><div class="sc-hDcvty jpmGrk"><span>https://khofmann.userpage.fu-berlin.de/phpCourse/exam/api</span>/users/{id}</div></div></div></div></div><div><h3 class="sc-kkmypM jKNCPF"> <!-- -->Response samples<!-- --> </h3><div class="sc-bSlUec jxQggo" data-rttabs="true"><ul class="react-tabs__tab-list" role="tablist"><li class="tab-success react-tabs__tab--selected" role="tab" id="tab:R176aa:0" aria-selected="true" aria-disabled="false" aria-controls="panel:R176aa:0" tabindex="0" data-rttab="true">200</li><li class="tab-error" role="tab" id="tab:R176aa:1" aria-selected="false" aria-disabled="false" aria-controls="panel:R176aa:1" data-rttab="true">404</li></ul><div class="react-tabs__tab-panel react-tabs__tab-panel--selected" role="tabpanel" id="panel:R176aa:0" aria-labelledby="tab:R176aa:0"><div><div class="sc-WChnI iLXIA-d"><span class="sc-foMnoT dmNpjh">Content type</span><div class="sc-ZubPq KmnOC">application/json</div></div><div class="sc-AmOMz gDrQpr"><div class="sc-eTdEpr dgzSkD"><div class="sc-pFPEP bMXXJy"><button><div class="sc-eHujzY ecCAmX">Copy</div></button><button> Expand all </button><button> Collapse all </button></div><div class="sc-euGpHm fwfkcU sc-fTgapq bDqsIm"><div class="redoc-json"><code><button class="collapser" aria-label="collapse"></button><span class="token punctuation">{</span><span class="ellipsis"></span><ul class="obj collapsible"><li><div class="hoverable "><span class="property token string">"id"</span>: <span class="token number">0</span><span class="token punctuation">,</span></div></li><li><div class="hoverable "><span class="property token string">"username"</span>: <span class="token string">"string"</span><span class="token punctuation">,</span></div></li><li><div class="hoverable "><span class="property token string">"status"</span>: <span class="token number">0</span><span class="token punctuation">,</span></div></li><li><div class="hoverable "><span class="property token string">"email"</span>: <span class="token string">"string"</span><span class="token punctuation">,</span></div></li><li><div class="hoverable "><span class="property token string">"image"</span>: <span class="token string">"string"</span><span class="token punctuation">,</span></div></li><li><div class="hoverable "><span class="property token string">"isAdmin"</span>: <span class="token boolean">true</span><span class="token punctuation">,</span></div></li><li><div class="hoverable "><span class="property token string">"memberSince"</span>: <button class="collapser" aria-label="collapse"></button><span class="token punctuation">{</span><span class="ellipsis"></span><ul class="obj collapsible"><li><div class="hoverable collapsed"><span class="property token string">"date"</span>: <span class="token string">"2019-08-24T14:15:22Z"</span><span class="token punctuation">,</span></div></li><li><div class="hoverable collapsed"><span class="property token string">"timezone_type"</span>: <span class="token number">0</span><span class="token punctuation">,</span></div></li><li><div class="hoverable collapsed"><span class="property token string">"timezone"</span>: <span class="token string">"string"</span></div></li></ul><span class="token punctuation">}</span><span class="token punctuation">,</span></div></li><li><div class="hoverable "><span class="property token string">"postCount"</span>: <span class="token number">0</span></div></li></ul><span class="token punctuation">}</span></code></div></div></div></div></div></div><div class="react-tabs__tab-panel" role="tabpanel" id="panel:R176aa:1" aria-labelledby="tab:R176aa:1"></div></div></div></div></div></div></div><div class="sc-epzHnm bWkBKa"></div></div></div>
|
||||
<script>
|
||||
const __redoc_state = {"menu":{"activeItemIdx":-1},"spec":{"data":{"openapi":"3.0.0","info":{"title":"PHP Course Exam","version":"1.0.0","contact":{"name":"Kilian Kurt Hofmann","email":"khofmann@zedat.fu-berlin.de"},"description":"PHP Course (ABV FU Berlin) 2024 Exam"},"paths":{"/login":{"post":{"summary":"Login","description":"Log in user.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginRequest"}}}},"responses":{"200":{"description":"Success.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginResponse"}}}},"400":{"description":"Missing fields.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MissingFieldResponse"},"examples":{"Missing fields":{"value":{"code":"MissingField","fields":["email","password"]}}}}}},"401":{"description":"Invalid credentials.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UnauthorizedResponse"},"examples":{"Invalid username or password":{"value":{"code":"Unauthorized","message":"Invalid username or password"}}}}}},"404":{"description":"User not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NotFoundResponse"},"examples":{"User not found":{"value":{"code":"NotFound","entity":"user"}}}}}},"500":{"description":"Failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FailedResponse"},"examples":{"Failed":{"value":{"code":"Failed","message":"Login failed"}}}}}}},"tags":["Login/Logout"]}},"/logout":{"post":{"summary":"Logout","description":"Log out user.","security":[{"BasicAuth":[]}],"responses":{"200":{"description":"Success.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BooleanResponse"},"examples":{"Success":{"value":true}}}}}},"tags":["Login/Logout"]}},"/posts":{"get":{"summary":"List posts","description":"List all posts, return full user data if authenticated.","security":[{},{"BasicAuth":[]}],"parameters":[{"in":"query","name":"p","schema":{"type":"integer","minimum":0,"default":0},"description":"Current page."},{"in":"query","name":"l","schema":{"type":"integer","minimum":0,"maximum":30,"default":10},"description":"The number of items to return."}],"responses":{"200":{"description":"Success.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PostListResponse"},"examples":{"Not authenticated":{"value":{"pages":0,"data":[{"id":0,"user":{"username":"string"},"content":"string","postedAt":{"date":"2019-08-24T14:15:22Z","timezone_type":0,"timezone":"string"}}]}},"Authenticated":{"value":{"pages":1,"data":[{"id":1,"user":{"id":0,"username":"string","status":0,"email":"string","image":"string","isAdmin":true,"memberSince":{"date":"2019-08-24T14:15:22Z","timezone_type":0,"timezone":"string"},"postCount":0},"content":"string","postedAt":{"date":"2019-08-24T14:15:22Z","timezone_type":0,"timezone":"string"}}]}}}}}}},"tags":["Post"]},"post":{"summary":"New post","description":"Create a new post.","security":[{"BasicAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PostCreateRequest"}}}},"responses":{"200":{"description":"Success.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PostResponse"}}}},"400":{"description":"Missing fields.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MissingFieldResponse"},"examples":{"Missing fields":{"value":{"code":"MissingField","fields":["content"]}}}}}}},"tags":["Post"]}},"/posts/{id}":{"patch":{"summary":"Update post","description":"Update post with ID. <br> Requires logged in user to have admin permissions for posts not made by them.","security":[{"BasicAuth":[]},{"BasicAuth":["isAdmin"]}],"parameters":[{"name":"id","in":"path","description":"Post ID","required":true,"schema":{"type":"integer","format":"int14"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PostUpdateRequest"}}}},"responses":{"200":{"description":"Success.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PostResponse"}}}},"401":{"description":"Not allowed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UnauthorizedResponse"},"examples":{"Not allowed":{"value":{"code":"Unauthorized","message":"Not allowed"}}}}}},"404":{"description":"Post not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NotFoundResponse"},"examples":{"Post not found":{"value":{"code":"NotFound","entity":"post"}}}}}},"500":{"description":"Update failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FailedUpdateResponse"},"examples":{"Failed":{"value":{"code":"FailedUpdate","fields":["content"]}}}}}}},"tags":["Post"]},"delete":{"summary":"Delete post","description":"Delete post with ID.","security":[{"BasicAuth":["isAdmin"]}],"parameters":[{"name":"id","in":"path","description":"Post ID","required":true,"schema":{"type":"integer","format":"int14Line truncated
|
||||
const __redoc_state = {"menu":{"activeItemIdx":-1},"spec":{"data":{"openapi":"3.0.0","info":{"title":"PHP Course Exam","version":"1.0.0","contact":{"name":"Kilian Kurt Hofmann","email":"khofmann@zedat.fu-berlin.de"},"description":"PHP Course (ABV FU Berlin) 2024 Exam"},"paths":{"/login":{"post":{"summary":"Login","description":"Log in user.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginRequest"}}}},"responses":{"200":{"description":"Success.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginResponse"}}}},"400":{"description":"Missing fields.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MissingFieldResponse"},"examples":{"Missing fields":{"value":{"code":"MissingField","fields":["email","password"]}}}}}},"401":{"description":"Invalid credentials.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UnauthorizedResponse"},"examples":{"Invalid username or password":{"value":{"code":"Unauthorized","message":"Invalid username or password"}}}}}},"404":{"description":"User not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NotFoundResponse"},"examples":{"User not found":{"value":{"code":"NotFound","entity":"user"}}}}}},"500":{"description":"Failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FailedResponse"},"examples":{"Failed":{"value":{"code":"Failed","message":"Login failed"}}}}}}},"tags":["Login/Logout"]}},"/logout":{"post":{"summary":"Logout","description":"Log out user.","security":[{"BasicAuth":[]}],"responses":{"200":{"description":"Success.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BooleanResponse"},"examples":{"Success":{"value":true}}}}}},"tags":["Login/Logout"]}},"/posts":{"get":{"summary":"List posts","description":"List all posts, return full user data if authenticated.","security":[{},{"BasicAuth":[]}],"parameters":[{"in":"query","name":"p","schema":{"type":"integer","minimum":0,"default":0},"description":"Current page."},{"in":"query","name":"l","schema":{"type":"integer","minimum":0,"maximum":30,"default":10},"description":"The number of items to return."}],"responses":{"200":{"description":"Success.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PostListResponse"},"examples":{"Not authenticated":{"value":{"pages":0,"data":[{"id":0,"user":{"username":"string","image":"string"},"content":"string","postedAt":{"date":"2019-08-24T14:15:22Z","timezone_type":0,"timezone":"string"}}]}},"Authenticated":{"value":{"pages":1,"data":[{"id":1,"user":{"id":0,"username":"string","status":0,"email":"string","image":"string","isAdmin":true,"memberSince":{"date":"2019-08-24T14:15:22Z","timezone_type":0,"timezone":"string"},"postCount":0},"content":"string","postedAt":{"date":"2019-08-24T14:15:22Z","timezone_type":0,"timezone":"string"}}]}}}}}}},"tags":["Post"]},"post":{"summary":"New post","description":"Create a new post.","security":[{"BasicAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PostCreateRequest"}}}},"responses":{"200":{"description":"Success.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PostResponse"}}}},"400":{"description":"Missing fields.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MissingFieldResponse"},"examples":{"Missing fields":{"value":{"code":"MissingField","fields":["content"]}}}}}}},"tags":["Post"]}},"/posts/{id}":{"patch":{"summary":"Update post","description":"Update post with ID. <br> Requires logged in user to have admin permissions for posts not made by them.","security":[{"BasicAuth":[]},{"BasicAuth":["isAdmin"]}],"parameters":[{"name":"id","in":"path","description":"Post ID","required":true,"schema":{"type":"integer","format":"int14"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PostUpdateRequest"}}}},"responses":{"200":{"description":"Success.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PostResponse"}}}},"401":{"description":"Not allowed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UnauthorizedResponse"},"examples":{"Not allowed":{"value":{"code":"Unauthorized","message":"Not allowed"}}}}}},"404":{"description":"Post not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NotFoundResponse"},"examples":{"Post not found":{"value":{"code":"NotFound","entity":"post"}}}}}},"500":{"description":"Update failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FailedUpdateResponse"},"examples":{"Failed":{"value":{"code":"FailedUpdate","fields":["content"]}}}}}}},"tags":["Post"]},"delete":{"summary":"Delete post","description":"Delete post with ID.","security":[{"BasicAuth":["isAdmin"]}],"parameters":[{"name":"id","in":"path","description":"Post ID","required":true,"schema":{"type":"integerLine truncated
|
||||
|
||||
var container = document.getElementById('redoc');
|
||||
Redoc.hydrate(__redoc_state, container);
|
||||
|
||||
@@ -16,16 +16,18 @@ class Post implements JsonSerializable
|
||||
private int $id;
|
||||
// User is set if the post was fetched by an authenticated user
|
||||
private ?User $user;
|
||||
// Name is set if the post was fetched by a non authenticated user
|
||||
// Name and image are set if the post was fetched by a non authenticated user
|
||||
private ?string $name;
|
||||
private ?string $image;
|
||||
private string $content;
|
||||
private DateTime $postedAt;
|
||||
|
||||
private function __construct(int $id, ?User $user, ?string $name, string $content, string $postedAt)
|
||||
private function __construct(int $id, ?User $user, ?string $name, ?string $image, string $content, string $postedAt)
|
||||
{
|
||||
$this->id = $id;
|
||||
$this->user = $user;
|
||||
$this->name = $name;
|
||||
$this->image = $image;
|
||||
$this->content = $content;
|
||||
$this->postedAt = new DateTime($postedAt);
|
||||
}
|
||||
@@ -53,11 +55,13 @@ class Post implements JsonSerializable
|
||||
if (!$data) throw new Exception("NotFound");
|
||||
|
||||
$user = User::getByID($data["benutzer_id"]);
|
||||
return new Post($data["id"], $user, null, $data["beitrag"], $data["zeitstempel"]);
|
||||
return new Post($data["id"], $user, null, null, $data["beitrag"], $data["zeitstempel"]);
|
||||
}
|
||||
|
||||
public static function create(User $user, string $content): Post
|
||||
{
|
||||
$content = substr(trim($content), 0, 250);
|
||||
|
||||
$db = Database::getInstance();
|
||||
|
||||
$stmt = $db->prepare(
|
||||
@@ -98,7 +102,7 @@ class Post implements JsonSerializable
|
||||
$list = array_map(
|
||||
function ($item) use ($authed) {
|
||||
$user = User::getByID($item["benutzer_id"]);
|
||||
return new Post($item["id"], $authed ? $user : null, !$authed ? $user->getUsername() : null, $item["beitrag"], $item["zeitstempel"]);
|
||||
return new Post($item["id"], $authed ? $user : null, !$authed ? $user->getUsername() : null, !$authed ? $user->getImage() : null, $item["beitrag"], $item["zeitstempel"]);
|
||||
},
|
||||
$data
|
||||
);
|
||||
@@ -115,12 +119,14 @@ class Post implements JsonSerializable
|
||||
$db = Database::getInstance();
|
||||
|
||||
if (!empty($content)) {
|
||||
$content = substr(trim($content), 0, 250);
|
||||
|
||||
$stmt = $db->prepare("UPDATE egb_gaestebuch SET beitrag = :CON WHERE id = :ID");
|
||||
$stmt->bindValue(":CON", $content);
|
||||
$stmt->bindValue(":CON", nl2br(htmlspecialchars($content)));
|
||||
$stmt->bindValue(":ID", $this->id);
|
||||
try {
|
||||
if (!$stmt->execute()) throw ApiError::failedUpdate(["content"]);
|
||||
} catch (Exception $e) {
|
||||
} catch (Exception) {
|
||||
throw ApiError::failedUpdate(["content"]);
|
||||
}
|
||||
}
|
||||
@@ -169,6 +175,7 @@ class Post implements JsonSerializable
|
||||
{
|
||||
$user = $this->user ? $this->user : [
|
||||
"username" => $this->name,
|
||||
"image" => $this->image,
|
||||
];
|
||||
|
||||
return [
|
||||
|
||||
@@ -188,7 +188,7 @@ class User implements JsonSerializable
|
||||
egb_benutzer(benutzer, passwort, email, confirmationcode)
|
||||
VALUES(:USR, :PAS, :EMA, :COD)"
|
||||
);
|
||||
$stmt->bindValue(":USR", $username);
|
||||
$stmt->bindValue(":USR", htmlspecialchars($username));
|
||||
$stmt->bindValue(":PAS", password_hash($password, PASSWORD_DEFAULT));
|
||||
$stmt->bindValue(":EMA", $email);
|
||||
$stmt->bindValue(":COD", $guid);
|
||||
@@ -277,7 +277,7 @@ class User implements JsonSerializable
|
||||
$failed = [];
|
||||
if (!empty($username)) {
|
||||
$stmt = $db->prepare("UPDATE egb_benutzer SET benutzer = :USR WHERE id = :ID");
|
||||
$stmt->bindValue(":USR", $username);
|
||||
$stmt->bindValue(":USR", htmlspecialchars($username));
|
||||
$stmt->bindValue(":ID", $this->id);
|
||||
try {
|
||||
if (!$stmt->execute()) array_push($failed, "username");
|
||||
@@ -378,7 +378,7 @@ class User implements JsonSerializable
|
||||
'id' => $this->id,
|
||||
'username' => $this->username,
|
||||
'status' => $this->status,
|
||||
'email' => $this->email,
|
||||
'email' => htmlspecialchars($this->email),
|
||||
'image' => $this->image,
|
||||
'isAdmin' => $this->isAdmin,
|
||||
'memberSince' => $this->memberSince,
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.13.0",
|
||||
"@emotion/styled": "^11.13.0",
|
||||
"@fontsource/roboto": "^5.0.13",
|
||||
"@mui/icons-material": "^5.16.4",
|
||||
"@mui/material": "^5.16.4",
|
||||
"@tanstack/react-form": "^0.26.4",
|
||||
|
||||
Generated
+8
@@ -14,6 +14,9 @@ importers:
|
||||
'@emotion/styled':
|
||||
specifier: ^11.13.0
|
||||
version: 11.13.0(@emotion/react@11.13.0(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1)
|
||||
'@fontsource/roboto':
|
||||
specifier: ^5.0.13
|
||||
version: 5.0.13
|
||||
'@mui/icons-material':
|
||||
specifier: ^5.16.4
|
||||
version: 5.16.4(@mui/material@5.16.4(@emotion/react@11.13.0(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.0(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@18.3.3)(react@18.3.1)
|
||||
@@ -441,6 +444,9 @@ packages:
|
||||
resolution: {integrity: sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==}
|
||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||
|
||||
'@fontsource/roboto@5.0.13':
|
||||
resolution: {integrity: sha512-j61DHjsdUCKMXSdNLTOxcG701FWnF0jcqNNQi2iPCDxU8seN/sMxeh62dC++UiagCWq9ghTypX+Pcy7kX+QOeQ==}
|
||||
|
||||
'@humanwhocodes/config-array@0.11.14':
|
||||
resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==}
|
||||
engines: {node: '>=10.10.0'}
|
||||
@@ -2234,6 +2240,8 @@ snapshots:
|
||||
|
||||
'@eslint/js@8.57.0': {}
|
||||
|
||||
'@fontsource/roboto@5.0.13': {}
|
||||
|
||||
'@humanwhocodes/config-array@0.11.14':
|
||||
dependencies:
|
||||
'@humanwhocodes/object-schema': 2.0.3
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { PostListAuth, PostListNonAuth } from '../types/Post';
|
||||
import { User } from '../types/User';
|
||||
|
||||
const BASE = 'https://khofmann.userpage.fu-berlin.de/phpCourse/exam/api/';
|
||||
@@ -5,7 +6,8 @@ const BASE = 'https://khofmann.userpage.fu-berlin.de/phpCourse/exam/api/';
|
||||
let instance: ApiImpl;
|
||||
|
||||
class ApiImpl {
|
||||
private token: string = '';
|
||||
//FIXME: PRIVATE when reauth token exists
|
||||
public token?: string;
|
||||
|
||||
constructor() {
|
||||
if (instance) {
|
||||
@@ -16,17 +18,29 @@ class ApiImpl {
|
||||
instance = this;
|
||||
}
|
||||
|
||||
public logIn = async (email: string, password: string): Promise<User> => {
|
||||
public hasAuth = () => this.token !== undefined;
|
||||
|
||||
//FIXME: Currently returns Auth token, switch to reauth token
|
||||
public logIn = async (email: string, password: string): Promise<[User, string]> => {
|
||||
const { user, token } = await (await this.post('login', { email, password })).json();
|
||||
this.token = token;
|
||||
|
||||
return user;
|
||||
return [user, token];
|
||||
};
|
||||
|
||||
public logOut = async (): Promise<boolean> => {
|
||||
this.token = undefined;
|
||||
return await (await this.postAuth('logout')).json();
|
||||
};
|
||||
|
||||
public posts = async (page?: number): Promise<PostListNonAuth | PostListAuth> => {
|
||||
const url = `posts?p=${page ?? 0}&l=9`;
|
||||
|
||||
if (this.token) return await (await this.getAuth(url)).json();
|
||||
|
||||
return await (await this.get(url)).json();
|
||||
};
|
||||
|
||||
private post = async (
|
||||
endpoint: string,
|
||||
body: Record<string, unknown> | undefined = undefined,
|
||||
@@ -50,12 +64,32 @@ class ApiImpl {
|
||||
const response = await fetch(`${BASE}${endpoint}`, {
|
||||
mode: 'cors',
|
||||
method: 'post',
|
||||
headers: { token: this.token, ...headers },
|
||||
headers: { token: this.token ?? '', ...headers },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
if (response.ok) return response;
|
||||
throw await response.json();
|
||||
};
|
||||
|
||||
private get = async (endpoint: string, headers: HeadersInit | undefined = undefined) => {
|
||||
const response = await fetch(`${BASE}${endpoint}`, {
|
||||
mode: 'cors',
|
||||
method: 'get',
|
||||
headers,
|
||||
});
|
||||
if (response.ok) return response;
|
||||
throw await response.json();
|
||||
};
|
||||
|
||||
private getAuth = async (endpoint: string, headers: HeadersInit | undefined = undefined) => {
|
||||
const response = await fetch(`${BASE}${endpoint}`, {
|
||||
mode: 'cors',
|
||||
method: 'get',
|
||||
headers: { token: this.token ?? '', ...headers },
|
||||
});
|
||||
if (response.ok) return response;
|
||||
throw await response.json();
|
||||
};
|
||||
}
|
||||
|
||||
const Api = new ApiImpl();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Box, Button, TextField, Typography } from '@mui/material';
|
||||
import { useForm } from '@tanstack/react-form';
|
||||
import { useRouter } from '@tanstack/react-router';
|
||||
import { FC, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Api from '../../../api/Api';
|
||||
@@ -12,6 +13,7 @@ const Login: FC = () => {
|
||||
const setUser = useGuestBookStore((state) => state.setUser);
|
||||
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
|
||||
const form = useForm({
|
||||
defaultValues: {
|
||||
@@ -20,7 +22,9 @@ const Login: FC = () => {
|
||||
},
|
||||
onSubmit: async ({ value }) => {
|
||||
try {
|
||||
setUser(await Api.logIn(value.email, value.password));
|
||||
const [user, token] = await Api.logIn(value.email, value.password);
|
||||
setUser(user, token);
|
||||
router.invalidate();
|
||||
//eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (error: any) {
|
||||
setError(error);
|
||||
@@ -42,7 +46,7 @@ const Login: FC = () => {
|
||||
}}
|
||||
noValidate
|
||||
>
|
||||
<Box sx={{ display: 'grid', gap: 1, padding: 1, minWidth: '100px' }}>
|
||||
<Box sx={{ display: 'grid', gap: 2, padding: 1 }}>
|
||||
<form.Field
|
||||
name="email"
|
||||
validators={{
|
||||
@@ -67,6 +71,9 @@ const Login: FC = () => {
|
||||
required
|
||||
error={field.state.meta.isTouched && field.state.meta.errors.length > 0}
|
||||
helperText={field.state.meta.isTouched ? field.state.meta.errors.join(',') : ''}
|
||||
type="email"
|
||||
autoComplete="username"
|
||||
inputMode="email"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
@@ -96,6 +103,8 @@ const Login: FC = () => {
|
||||
required
|
||||
error={field.state.meta.isTouched && field.state.meta.errors.length > 0}
|
||||
helperText={field.state.meta.isTouched ? field.state.meta.errors.join(',') : ''}
|
||||
type="password"
|
||||
autoComplete="password"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
@@ -111,7 +120,7 @@ const Login: FC = () => {
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
{error && <Typography color="error.main">{t(...handleError(error, 'login'))}</Typography>}
|
||||
{error && <Typography color="error.main">{handleError(error, 'login', t)}</Typography>}
|
||||
</Box>
|
||||
</form>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
import { AccountCircle } from '@mui/icons-material';
|
||||
import { AppBar, Avatar, IconButton, Menu, MenuItem, Toolbar, Typography, useScrollTrigger } from '@mui/material';
|
||||
import {
|
||||
AppBar,
|
||||
Avatar,
|
||||
Box,
|
||||
CircularProgress,
|
||||
IconButton,
|
||||
Menu,
|
||||
MenuItem,
|
||||
Link as MUILink,
|
||||
Toolbar,
|
||||
useScrollTrigger,
|
||||
} from '@mui/material';
|
||||
import { Link, useRouter, useRouterState } from '@tanstack/react-router';
|
||||
import { cloneElement, FC, ReactElement, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Api from '../../api/Api';
|
||||
@@ -24,6 +36,8 @@ const Header: FC = () => {
|
||||
const setUser = useGuestBookStore((state) => state.setUser);
|
||||
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
const isLoading = useRouterState({ select: (s) => s.status === 'pending' });
|
||||
|
||||
const handleMenu = (event: React.MouseEvent<HTMLElement>) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
@@ -35,11 +49,15 @@ const Header: FC = () => {
|
||||
|
||||
return (
|
||||
<ElevationScroll>
|
||||
<>
|
||||
<AppBar>
|
||||
<Toolbar>
|
||||
<Typography variant="h6" component="div" sx={{ flexGrow: 1 }}>
|
||||
<Box sx={{ flexGrow: 1, alignItems: 'center', display: 'flex', gap: 1 }}>
|
||||
<MUILink component={Link} to="/" color="#FFF" variant="h6" underline="none">
|
||||
{t('GuestBook')}
|
||||
</Typography>
|
||||
</MUILink>
|
||||
{isLoading && <CircularProgress size={16} thickness={10} sx={{ color: 'white' }} />}
|
||||
</Box>
|
||||
{user ? (
|
||||
<IconButton onClick={handleMenu} sx={{ p: 0 }}>
|
||||
<Avatar alt={user.username} src={`storage/${user.image}`} />
|
||||
@@ -63,6 +81,11 @@ const Header: FC = () => {
|
||||
}}
|
||||
open={Boolean(anchorEl)}
|
||||
onClose={handleClose}
|
||||
sx={{
|
||||
'& .MuiMenu-paper': {
|
||||
minWidth: '240px',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{user ? (
|
||||
[
|
||||
@@ -73,7 +96,8 @@ const Header: FC = () => {
|
||||
key="logout"
|
||||
onClick={() => {
|
||||
Api.logOut();
|
||||
setUser(undefined);
|
||||
setUser();
|
||||
router.invalidate();
|
||||
}}
|
||||
>
|
||||
{t('Log out')}
|
||||
@@ -85,6 +109,8 @@ const Header: FC = () => {
|
||||
</Menu>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
<Toolbar />
|
||||
</>
|
||||
</ElevationScroll>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { Avatar, Card, CardContent, CardHeader, Link as MUILink, Typography } from '@mui/material';
|
||||
import { Link } from '@tanstack/react-router';
|
||||
import { FC } from 'react';
|
||||
import { PostAuth, PostNonAuth } from '../../types/Post';
|
||||
|
||||
interface Props {
|
||||
post: PostNonAuth | PostAuth;
|
||||
}
|
||||
|
||||
const Post: FC<Props> = ({ post }) => {
|
||||
return (
|
||||
<Card sx={{ display: 'flex', flexDirection: 'column', flexGrow: 1 }}>
|
||||
<CardHeader
|
||||
avatar={
|
||||
<MUILink component={Link} to="/" color="#FFF" variant="h6" underline="none">
|
||||
<Avatar alt={post.user.username} src={`storage/${post.user.image}`} />
|
||||
</MUILink>
|
||||
}
|
||||
title={post.user.username}
|
||||
subheader={new Date(post.postedAt.date).toLocaleString(navigator.languages[0] ?? 'de-DE', {
|
||||
timeZone: post.postedAt.timezone,
|
||||
weekday: 'short',
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
hour12: false,
|
||||
minute: '2-digit',
|
||||
})}
|
||||
/>
|
||||
<CardContent>
|
||||
<Typography>{post.content}</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default Post;
|
||||
@@ -10,6 +10,12 @@ import { routeTree } from './routeTree.gen';
|
||||
// Import i18n
|
||||
import './i18n';
|
||||
|
||||
// Font
|
||||
import '@fontsource/roboto/300.css';
|
||||
import '@fontsource/roboto/400.css';
|
||||
import '@fontsource/roboto/500.css';
|
||||
import '@fontsource/roboto/700.css';
|
||||
|
||||
// Query Client
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
|
||||
@@ -2,15 +2,23 @@ import { Toolbar } from '@mui/material';
|
||||
import { QueryClient } from '@tanstack/react-query';
|
||||
import { createRootRouteWithContext, Outlet } from '@tanstack/react-router';
|
||||
import { TanStackRouterDevtools } from '@tanstack/router-devtools';
|
||||
import Api from '../api/Api';
|
||||
import Header from '../components/Header/Header';
|
||||
import useGuestBookStore from '../store/store';
|
||||
|
||||
export const Route = createRootRouteWithContext<{ queryClient: QueryClient }>()({
|
||||
component: () => (
|
||||
component: () => {
|
||||
//FIXME: REAUTH HERE
|
||||
const token = useGuestBookStore((state) => state.token);
|
||||
Api.token = token;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<Toolbar />
|
||||
<Outlet />
|
||||
{process.env.NODE_ENV === 'development' && <TanStackRouterDevtools />}
|
||||
</>
|
||||
),
|
||||
);
|
||||
},
|
||||
});
|
||||
@@ -1,9 +1,57 @@
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
import { Grid, Pagination, PaginationItem, Snackbar } from '@mui/material';
|
||||
import { queryOptions, useSuspenseQuery } from '@tanstack/react-query';
|
||||
import { createFileRoute, Link } from '@tanstack/react-router';
|
||||
import Api from '../api/Api';
|
||||
import Post from '../components/Post/Post';
|
||||
|
||||
const postsQueryOptions = (page?: number) =>
|
||||
queryOptions({
|
||||
queryKey: ['posts', { page: page ?? 0, hasAuth: Api.hasAuth() }],
|
||||
queryFn: () => Api.posts(page),
|
||||
});
|
||||
|
||||
export const Route = createFileRoute('/')({
|
||||
loaderDeps: ({ search: { page } }) => ({ page }),
|
||||
loader: ({ context: { queryClient }, deps: { page } }) => queryClient.ensureQueryData(postsQueryOptions(page)),
|
||||
validateSearch: (search: Record<string, unknown>): { page?: number } => {
|
||||
return {
|
||||
page: search?.page !== undefined ? Number(search?.page ?? 0) : undefined,
|
||||
};
|
||||
},
|
||||
component: Home,
|
||||
});
|
||||
|
||||
function Home() {
|
||||
return <></>;
|
||||
const { page } = Route.useSearch();
|
||||
const { data: postsQuery, isFetching } = useSuspenseQuery(postsQueryOptions(page));
|
||||
|
||||
return (
|
||||
<>
|
||||
<Snackbar open={isFetching} message="Updating" />
|
||||
<Grid container spacing={2}>
|
||||
{postsQuery.data.map((post) => (
|
||||
<Grid item xs={12} md={6} lg={4} sx={{ display: 'flex' }}>
|
||||
<Post key={post.id} post={post} />
|
||||
</Grid>
|
||||
))}
|
||||
<Grid item xs={12} sx={{ display: 'flex', justifyContent: 'center' }}>
|
||||
<Pagination
|
||||
page={(page ?? 0) + 1}
|
||||
count={postsQuery.pages}
|
||||
color="primary"
|
||||
renderItem={(item) => (
|
||||
<PaginationItem
|
||||
{...item}
|
||||
component={Link}
|
||||
to="/"
|
||||
search={{ page: item.page ? item.page - 1 : undefined }}
|
||||
//eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
onClick={(e) => item.onClick(e as any)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,18 +1,28 @@
|
||||
import type {} from '@redux-devtools/extension'; // required for devtools typing
|
||||
import type {} from '@redux-devtools/extension';
|
||||
import { create } from 'zustand';
|
||||
import { devtools } from 'zustand/middleware';
|
||||
import { devtools, persist } from 'zustand/middleware';
|
||||
import { User } from '../types/User';
|
||||
|
||||
interface GuestBookState {
|
||||
user: User | undefined;
|
||||
setUser: (user: User | undefined) => void;
|
||||
token: string | undefined;
|
||||
setUser: (user?: User, token?: string) => void;
|
||||
}
|
||||
|
||||
const useGuestBookStore = create<GuestBookState>()(
|
||||
devtools((set) => ({
|
||||
devtools(
|
||||
persist(
|
||||
(set) => ({
|
||||
user: undefined,
|
||||
setUser: (user: User | undefined) => set(() => ({ user })),
|
||||
}))
|
||||
// FIXME: Currentlay auth token, switch this to be reauth token
|
||||
token: undefined,
|
||||
setUser: (user?: User, token?: string) => set(() => ({ user, token })),
|
||||
}),
|
||||
{
|
||||
name: 'guestbook-storage',
|
||||
}
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
export default useGuestBookStore;
|
||||
@@ -0,0 +1,29 @@
|
||||
import { Timestamp } from './Timestamp';
|
||||
import { User } from './User';
|
||||
|
||||
export interface PostNonAuth {
|
||||
id: number;
|
||||
user: {
|
||||
username: string;
|
||||
image: string;
|
||||
};
|
||||
content: string;
|
||||
postedAt: Timestamp;
|
||||
}
|
||||
|
||||
export interface PostListNonAuth {
|
||||
pages: number;
|
||||
data: PostNonAuth[];
|
||||
}
|
||||
|
||||
export interface PostAuth {
|
||||
id: number;
|
||||
user: User;
|
||||
content: string;
|
||||
postedAt: Timestamp;
|
||||
}
|
||||
|
||||
export interface PostListAuth {
|
||||
pages: number;
|
||||
data: PostNonAuth[];
|
||||
}
|
||||
@@ -1,25 +1,32 @@
|
||||
import { TFunction } from 'i18next';
|
||||
|
||||
/**
|
||||
* Transform error into i18next spreadable input
|
||||
* Return translated error
|
||||
* @param error Error object
|
||||
* @param context Optional context for translation
|
||||
* @returns Array to be spread into i18next `t` function
|
||||
* @param context Optional context
|
||||
* @param t Optional translation function, defautls to pass through
|
||||
* @returns Translated error or inputs if t as unspecified
|
||||
*/
|
||||
//eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const handleError = (error: any, context?: string): [string, { context?: string }] => {
|
||||
if (!error) return ['', {}];
|
||||
const handleError = (
|
||||
error: any,
|
||||
context?: string,
|
||||
t: TFunction<'translation', undefined> | ((..._in: any) => any) = (..._in: any) => _in
|
||||
): string => {
|
||||
if (!error) return t('', {});
|
||||
|
||||
if (error.code) {
|
||||
switch (error.code) {
|
||||
case 'NotFound':
|
||||
return [error.code, { context: `${error.entity}:${context}` }];
|
||||
return t(error.code, { context: `${error.entity}:${context}` });
|
||||
case 'Unauthorized':
|
||||
return [error.code, { context }];
|
||||
return t(error.code, { context });
|
||||
default:
|
||||
return ['Unknown', { context }];
|
||||
return t('Unknown', { context });
|
||||
}
|
||||
}
|
||||
|
||||
return [error?.message ?? 'Unknown', { context }];
|
||||
return t(error?.message ?? 'Unknown', { context });
|
||||
};
|
||||
|
||||
export default handleError;
|
||||
Reference in new issue
Block a user