From 5e6670a0d3e84887b5edd9fb9a6ce6c6ba0a96ab Mon Sep 17 00:00:00 2001 From: Kaishiyoku Date: Fri, 8 Mar 2024 12:48:13 +0100 Subject: [PATCH 01/79] updated Composer dependencies --- composer.lock | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/composer.lock b/composer.lock index d8c78419..f28e2899 100644 --- a/composer.lock +++ b/composer.lock @@ -8355,16 +8355,16 @@ }, { "name": "jean85/pretty-package-versions", - "version": "2.0.5", + "version": "2.0.6", "source": { "type": "git", "url": "https://github.com/Jean85/pretty-package-versions.git", - "reference": "ae547e455a3d8babd07b96966b17d7fd21d9c6af" + "reference": "f9fdd29ad8e6d024f52678b570e5593759b550b4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Jean85/pretty-package-versions/zipball/ae547e455a3d8babd07b96966b17d7fd21d9c6af", - "reference": "ae547e455a3d8babd07b96966b17d7fd21d9c6af", + "url": "https://api.github.com/repos/Jean85/pretty-package-versions/zipball/f9fdd29ad8e6d024f52678b570e5593759b550b4", + "reference": "f9fdd29ad8e6d024f52678b570e5593759b550b4", "shasum": "" }, "require": { @@ -8372,9 +8372,9 @@ "php": "^7.1|^8.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^2.17", + "friendsofphp/php-cs-fixer": "^3.2", "jean85/composer-provided-replaced-stub-package": "^1.0", - "phpstan/phpstan": "^0.12.66", + "phpstan/phpstan": "^1.4", "phpunit/phpunit": "^7.5|^8.5|^9.4", "vimeo/psalm": "^4.3" }, @@ -8408,9 +8408,9 @@ ], "support": { "issues": "https://github.com/Jean85/pretty-package-versions/issues", - "source": "https://github.com/Jean85/pretty-package-versions/tree/2.0.5" + "source": "https://github.com/Jean85/pretty-package-versions/tree/2.0.6" }, - "time": "2021-10-08T21:21:46+00:00" + "time": "2024-03-08T09:58:59+00:00" }, { "name": "larastan/larastan", From c405664bcf47ed40c821664ea940d1f89083c1c7 Mon Sep 17 00:00:00 2001 From: Kaishiyoku Date: Fri, 8 Mar 2024 12:48:16 +0100 Subject: [PATCH 02/79] updated npm dependencies --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index b3b1e84c..6118f339 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2310,9 +2310,9 @@ "dev": true }, "node_modules/electron-to-chromium": { - "version": "1.4.696", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.696.tgz", - "integrity": "sha512-SOr0bHP52OvYg2chCsz/0+FUSMGFm8L8HKwPpx3cbwRY24EOemVJtbgTm+IFO8LzhcnPy+hXmTq7ZcZ8uUuaYg==", + "version": "1.4.698", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.698.tgz", + "integrity": "sha512-f9iZD1t3CLy1AS6vzM5EKGa6p9pRcOeEFXRFbaG2Ta+Oe7MkfRQ3fsvPYidzHe1h4i0JvIvpcY55C+B6BZNGtQ==", "dev": true }, "node_modules/emoji-regex": { From cf7f58489e6d2dc0d23ac5a215c9358f95659bbe Mon Sep 17 00:00:00 2001 From: Kaishiyoku Date: Fri, 8 Mar 2024 19:43:34 +0100 Subject: [PATCH 03/79] added basic layout and router skeleton --- .../Controllers/Api/CategoryController.php | 87 +++++++++++++++++++ app/Http/Kernel.php | 2 +- package-lock.json | 42 +++++++++ package.json | 1 + .../js/React/Core/AuthenticatedLayout.tsx | 18 ++++ resources/js/React/Core/Breadcrumbs.tsx | 31 +++++++ resources/js/React/Core/router.tsx | 28 ++++++ resources/js/React/Hooks/useBreadcrumbs.ts | 10 +++ .../Pages/Categories/CategoriesIndex.tsx | 16 ++++ .../Categories/Loaders/categoriesLoader.ts | 8 ++ resources/js/React/app.tsx | 17 ++++ resources/js/React/request.ts | 36 ++++++++ resources/js/React/types/Breadcrumb.ts | 7 ++ resources/js/bootstrap.ts | 1 + resources/views/app_react.blade.php | 23 +++++ routes/api.php | 5 ++ routes/web.php | 8 ++ 17 files changed, 339 insertions(+), 1 deletion(-) create mode 100644 app/Http/Controllers/Api/CategoryController.php create mode 100644 resources/js/React/Core/AuthenticatedLayout.tsx create mode 100644 resources/js/React/Core/Breadcrumbs.tsx create mode 100644 resources/js/React/Core/router.tsx create mode 100644 resources/js/React/Hooks/useBreadcrumbs.ts create mode 100644 resources/js/React/Pages/Categories/CategoriesIndex.tsx create mode 100644 resources/js/React/Pages/Categories/Loaders/categoriesLoader.ts create mode 100644 resources/js/React/app.tsx create mode 100644 resources/js/React/request.ts create mode 100644 resources/js/React/types/Breadcrumb.ts create mode 100644 resources/views/app_react.blade.php diff --git a/app/Http/Controllers/Api/CategoryController.php b/app/Http/Controllers/Api/CategoryController.php new file mode 100644 index 00000000..30fedd79 --- /dev/null +++ b/app/Http/Controllers/Api/CategoryController.php @@ -0,0 +1,87 @@ +authorizeResource(Category::class); + } + + /** + * Display a listing of the resource. + */ + public function index(): JsonResponse + { + return response()->json([ + 'categories' => Auth::user()->categories()->withCount('feeds')->get(), + 'canCreate' => Auth::user()->can('create', Category::class), + ]); + } + + /** + * Show the form for creating a new resource. + */ + public function create(): Response + { + return Inertia::render('Categories/Create', [ + 'category' => new Category(), + ]); + } + + /** + * Store a newly created resource in storage. + */ + public function store(StoreCategoryRequest $request): RedirectResponse + { + $validated = $request->validated(); + + Auth::user()->categories()->save(new Category($validated)); + + return redirect()->route('categories.index'); + } + + /** + * Show the form for editing the specified resource. + */ + public function edit(Category $category): Response + { + return Inertia::render('Categories/Edit', [ + 'category' => $category, + 'canDelete' => Auth::user()->can('delete', $category), + ]); + } + + /** + * Update the specified resource in storage. + */ + public function update(UpdateCategoryRequest $request, Category $category): RedirectResponse + { + $validated = $request->validated(); + + $category->update($validated); + + return redirect()->route('categories.index'); + } + + /** + * Remove the specified resource from storage. + */ + public function destroy(Category $category): RedirectResponse + { + $category->delete(); + + return redirect()->route('categories.index'); + } +} diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index 4eec4488..41a1e3ff 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -45,7 +45,7 @@ class Kernel extends HttpKernel ], 'api' => [ - // \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class, + \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class, \Illuminate\Routing\Middleware\ThrottleRequests::class.':api', \Illuminate\Routing\Middleware\SubstituteBindings::class, ], diff --git a/package-lock.json b/package-lock.json index 6118f339..746711a7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -41,6 +41,7 @@ "react": "^18.2.0", "react-blurhash": "^0.3.0", "react-dom": "^18.2.0", + "react-router-dom": "^6.22.3", "slug": "^9.0.0", "tailwindcss": "^3.3.1", "typescript": "^5.2.2", @@ -1122,6 +1123,15 @@ "node": ">=14" } }, + "node_modules/@remix-run/router": { + "version": "1.15.3", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.15.3.tgz", + "integrity": "sha512-Oy8rmScVrVxWZVOpEF57ovlnhpZ8CCPlnIIumVcV9nFdiSIrus99+Lw78ekXyGvVDlIsFJbSfmSovJUhCWYV3w==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@tailwindcss/forms": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.7.tgz", @@ -5110,6 +5120,38 @@ "node": ">=0.10.0" } }, + "node_modules/react-router": { + "version": "6.22.3", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.22.3.tgz", + "integrity": "sha512-dr2eb3Mj5zK2YISHK++foM9w4eBnO23eKnZEDs7c880P6oKbrjz/Svg9+nxqtHQK+oMW4OtjZca0RqPglXxguQ==", + "dev": true, + "dependencies": { + "@remix-run/router": "1.15.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.22.3", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.22.3.tgz", + "integrity": "sha512-7ZILI7HjcE+p31oQvwbokjk6OA/bnFxrhJ19n82Ex9Ph8fNAq+Hm/7KchpMGlTgWhUxRHMMCut+vEtNpWpowKw==", + "dev": true, + "dependencies": { + "@remix-run/router": "1.15.3", + "react-router": "6.22.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", diff --git a/package.json b/package.json index 29a47387..88378b77 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "react": "^18.2.0", "react-blurhash": "^0.3.0", "react-dom": "^18.2.0", + "react-router-dom": "^6.22.3", "slug": "^9.0.0", "tailwindcss": "^3.3.1", "typescript": "^5.2.2", diff --git a/resources/js/React/Core/AuthenticatedLayout.tsx b/resources/js/React/Core/AuthenticatedLayout.tsx new file mode 100644 index 00000000..91896adc --- /dev/null +++ b/resources/js/React/Core/AuthenticatedLayout.tsx @@ -0,0 +1,18 @@ +import {Outlet} from 'react-router-dom'; +import Breadcrumbs from '@/React/Core/Breadcrumbs'; + +const AuthenticatedLayout = () => { + return ( + <> +
+ +
+ +
+ +
+ + ); +}; + +export default AuthenticatedLayout; diff --git a/resources/js/React/Core/Breadcrumbs.tsx b/resources/js/React/Core/Breadcrumbs.tsx new file mode 100644 index 00000000..882665f3 --- /dev/null +++ b/resources/js/React/Core/Breadcrumbs.tsx @@ -0,0 +1,31 @@ +import {Link} from 'react-router-dom'; +import slug from 'slug'; +import {Fragment} from 'react'; +import useBreadcrumbs from '@/React/Hooks/useBreadcrumbs'; +import Breadcrumb from '@/React/types/Breadcrumb'; + +const LinkBreadcrumb = ({breadcrumb}: { breadcrumb: Breadcrumb; }) => ( +
+ {breadcrumb.headline} +
+); + +const TextBreadcrumb = ({breadcrumb}: { breadcrumb: Breadcrumb; }) => ( + + + {breadcrumb.headline} + + +
/
+
+); + +export default function Breadcrumbs() { + const breadcrumbs = useBreadcrumbs(); + + return breadcrumbs.map((breadcrumb, index) => + index === breadcrumbs.length - 1 + ? + : + ); +} diff --git a/resources/js/React/Core/router.tsx b/resources/js/React/Core/router.tsx new file mode 100644 index 00000000..3285acf3 --- /dev/null +++ b/resources/js/React/Core/router.tsx @@ -0,0 +1,28 @@ +import {createBrowserRouter} from 'react-router-dom'; +import categoriesLoader from '@/React/Pages/Categories/Loaders/categoriesLoader'; +import AuthenticatedLayout from '@/React/Core/AuthenticatedLayout'; +import CategoriesIndex from '@/React/Pages/Categories/CategoriesIndex'; + +const router = createBrowserRouter([ + { + path: '/react/', + element: , + handle: { + title: 'Home', + headline: 'Home', + }, + children: [ + { + path: 'categories', + element: , + loader: categoriesLoader, + handle: { + title: 'Categories', + headline: 'Categories', + }, + }, + ], + }, +]); + +export default router; diff --git a/resources/js/React/Hooks/useBreadcrumbs.ts b/resources/js/React/Hooks/useBreadcrumbs.ts new file mode 100644 index 00000000..444ea034 --- /dev/null +++ b/resources/js/React/Hooks/useBreadcrumbs.ts @@ -0,0 +1,10 @@ +import {useMatches} from 'react-router-dom'; +import Breadcrumb from '@/React/types/Breadcrumb'; + +export default function useBreadcrumbs() { + const matches = useMatches(); + + return matches + .filter((match) => !!match.handle) + .map((match) => ({pathname: match.pathname, ...match.handle!})) as Breadcrumb[]; +} diff --git a/resources/js/React/Pages/Categories/CategoriesIndex.tsx b/resources/js/React/Pages/Categories/CategoriesIndex.tsx new file mode 100644 index 00000000..5087b5b1 --- /dev/null +++ b/resources/js/React/Pages/Categories/CategoriesIndex.tsx @@ -0,0 +1,16 @@ +import Category from '@/types/generated/Models/Category'; +import {useLoaderData} from 'react-router-dom'; + +export default function CategoriesIndex() { + const categories = useLoaderData() as Category[]; + + return ( +
+
+ {categories.map((category) => ( +
{category.name}
+ ))} +
+
+ ); +} diff --git a/resources/js/React/Pages/Categories/Loaders/categoriesLoader.ts b/resources/js/React/Pages/Categories/Loaders/categoriesLoader.ts new file mode 100644 index 00000000..0204fad1 --- /dev/null +++ b/resources/js/React/Pages/Categories/Loaders/categoriesLoader.ts @@ -0,0 +1,8 @@ +import Category from '@/types/generated/Models/Category'; +import request from '@/React/request'; + +export default async function categoriesLoader() { + const data = await request('/api/categories').json<{ categories: Category[]; canCreate: boolean; }>(); + + return data.categories; +} diff --git a/resources/js/React/app.tsx b/resources/js/React/app.tsx new file mode 100644 index 00000000..e03f4ae8 --- /dev/null +++ b/resources/js/React/app.tsx @@ -0,0 +1,17 @@ +import {createRoot} from 'react-dom/client'; +import {RouterProvider} from 'react-router-dom'; +import router from '@/React/Core/router'; +import NProgress from 'nprogress'; + +NProgress.configure({ + showSpinner: false, +}); + +const App = () => { + return ( + + ); +}; + +createRoot(document.getElementById('app')!) + .render(); diff --git a/resources/js/React/request.ts b/resources/js/React/request.ts new file mode 100644 index 00000000..ac73d962 --- /dev/null +++ b/resources/js/React/request.ts @@ -0,0 +1,36 @@ +import ky from 'ky'; +import NProgress from 'nprogress'; +import Cookies from 'js-cookie'; + +const request = ky.extend({ + headers: { + Accept: 'application/json', + }, + hooks: { + beforeRequest: [ + (request) => { + NProgress.start(); + + if (window.location.host === new URL(request.url).host) { + request.headers.set('X-XSRF-TOKEN', Cookies.get('XSRF-TOKEN') ?? ''); + } + }, + ], + afterResponse: [ + (request, options, response) => { + NProgress.done(); + + return response; + }, + ], + beforeError: [ + (error) => { + NProgress.done(); + + return error; + }, + ], + }, +}); + +export default request; diff --git a/resources/js/React/types/Breadcrumb.ts b/resources/js/React/types/Breadcrumb.ts new file mode 100644 index 00000000..bdef4cbd --- /dev/null +++ b/resources/js/React/types/Breadcrumb.ts @@ -0,0 +1,7 @@ +type Breadcrumb = { + pathname: string; + title: string; + headline: string; +}; + +export default Breadcrumb; diff --git a/resources/js/bootstrap.ts b/resources/js/bootstrap.ts index 37406182..cfdf308e 100644 --- a/resources/js/bootstrap.ts +++ b/resources/js/bootstrap.ts @@ -1,6 +1,7 @@ import ky from 'ky'; import NProgress from 'nprogress'; import Cookies from 'js-cookie'; +import 'nprogress/nprogress.css'; window.ky = ky.extend({ headers: { diff --git a/resources/views/app_react.blade.php b/resources/views/app_react.blade.php new file mode 100644 index 00000000..b4ec5e4b --- /dev/null +++ b/resources/views/app_react.blade.php @@ -0,0 +1,23 @@ + + + + + + + + {{ config('app.name', 'Laravel') }} + + @include('partials.favicon') + + + + + + + @viteReactRefresh + @vite(['resources/js/React/app.tsx', 'resources/js/bootstrap.ts', 'resources/css/app.css']) + + +
+ + diff --git a/routes/api.php b/routes/api.php index 889937e1..c3c4b020 100644 --- a/routes/api.php +++ b/routes/api.php @@ -1,5 +1,6 @@ get('/user', function (Request $request) { return $request->user(); }); + +Route::middleware('auth:sanctum')->group(function () { + Route::get('/categories', [CategoryController::class, 'index'])->name('categories.index'); +}); diff --git a/routes/web.php b/routes/web.php index 4c573530..c575f10c 100644 --- a/routes/web.php +++ b/routes/web.php @@ -32,6 +32,14 @@ ]); }); +Route::any('/react', function () { + return view('app_react'); +}); + +Route::any('/react/{any}', function () { + return view('app_react'); +}); + Route::middleware('auth')->group(function () { Route::get('/dashboard', DashboardController::class)->name('dashboard'); From 778114422c79169905bff7edc54580723671d970 Mon Sep 17 00:00:00 2001 From: Kaishiyoku Date: Fri, 8 Mar 2024 20:43:13 +0100 Subject: [PATCH 04/79] refactored code --- resources/js/React/Core/Breadcrumbs.tsx | 53 ++++++++++++++++++++----- 1 file changed, 42 insertions(+), 11 deletions(-) diff --git a/resources/js/React/Core/Breadcrumbs.tsx b/resources/js/React/Core/Breadcrumbs.tsx index 882665f3..494b9893 100644 --- a/resources/js/React/Core/Breadcrumbs.tsx +++ b/resources/js/React/Core/Breadcrumbs.tsx @@ -1,31 +1,62 @@ import {Link} from 'react-router-dom'; import slug from 'slug'; -import {Fragment} from 'react'; +import {Fragment, useEffect, useRef} from 'react'; import useBreadcrumbs from '@/React/Hooks/useBreadcrumbs'; import Breadcrumb from '@/React/types/Breadcrumb'; const LinkBreadcrumb = ({breadcrumb}: { breadcrumb: Breadcrumb; }) => ( -
+
  • {breadcrumb.headline} -
  • + ); const TextBreadcrumb = ({breadcrumb}: { breadcrumb: Breadcrumb; }) => ( - - {breadcrumb.headline} - +
  • + + {breadcrumb.headline} + +
  • -
    /
    +
  • + +
  • ); export default function Breadcrumbs() { const breadcrumbs = useBreadcrumbs(); + const breadcrumbsRef = useRef(null); - return breadcrumbs.map((breadcrumb, index) => - index === breadcrumbs.length - 1 - ? - : + useEffect(() => { + setTimeout(() => { + breadcrumbsRef.current?.scrollTo({ + top: 0, + left: breadcrumbsRef.current.getBoundingClientRect().right, + behavior: 'smooth', + }); + }, 250); + }, []); + + return ( +
    + +
    ); } From c3e162535e62266c60359517f6c23c9236def428 Mon Sep 17 00:00:00 2001 From: Kaishiyoku Date: Fri, 8 Mar 2024 20:52:54 +0100 Subject: [PATCH 05/79] set page title properly --- resources/js/React/Core/AuthenticatedLayout.tsx | 11 ++++++++++- resources/js/React/types/MatchWithHandle.tsx | 10 ++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 resources/js/React/types/MatchWithHandle.tsx diff --git a/resources/js/React/Core/AuthenticatedLayout.tsx b/resources/js/React/Core/AuthenticatedLayout.tsx index 91896adc..686eff01 100644 --- a/resources/js/React/Core/AuthenticatedLayout.tsx +++ b/resources/js/React/Core/AuthenticatedLayout.tsx @@ -1,7 +1,16 @@ -import {Outlet} from 'react-router-dom'; +import {Outlet, useLocation, useMatches} from 'react-router-dom'; import Breadcrumbs from '@/React/Core/Breadcrumbs'; +import {useEffect} from 'react'; +import MatchWithHandle from '@/React/types/MatchWithHandle'; const AuthenticatedLayout = () => { + const location = useLocation(); + const match = useMatches().find((match) => match.pathname === location.pathname) as MatchWithHandle; + + useEffect(() => { + document.querySelector('title')!.textContent = match?.handle.title; + }, [location]); + return ( <>
    diff --git a/resources/js/React/types/MatchWithHandle.tsx b/resources/js/React/types/MatchWithHandle.tsx new file mode 100644 index 00000000..49652cee --- /dev/null +++ b/resources/js/React/types/MatchWithHandle.tsx @@ -0,0 +1,10 @@ +import {UIMatch} from 'react-router-dom'; + +type MatchWithHandle = UIMatch & { + handle: { + headline: string; + title: string; + }; +}; + +export default MatchWithHandle; From 0561fd21714d192a8ab1ff1adecf4889a0e30636 Mon Sep 17 00:00:00 2001 From: Kaishiyoku Date: Sat, 9 Mar 2024 12:56:24 +0100 Subject: [PATCH 06/79] optimized category creation page --- .../Controllers/Api/CategoryController.php | 4 +- lang/de.json | 3 +- resources/js/Components/InputError.tsx | 2 +- resources/js/Components/Modal/Modal.tsx | 9 ++- .../js/React/Core/AuthenticatedLayout.tsx | 4 +- resources/js/React/Core/Breadcrumbs.tsx | 55 +++++++++++-------- resources/js/React/Core/router.tsx | 45 +++++++++++++-- resources/js/React/Hooks/useBreadcrumbs.ts | 9 ++- resources/js/React/Hooks/usePageModal.ts | 31 +++++++++++ .../Pages/Categories/CategoriesCreate.tsx | 46 ++++++++++++++++ .../Pages/Categories/CategoriesIndex.tsx | 15 ++++- resources/js/React/Utils/wait.ts | 5 ++ resources/js/React/app.tsx | 11 +++- resources/js/React/types/RouteHandle.ts | 7 +++ resources/js/React/types/ValidationErrors.ts | 3 + routes/api.php | 1 + routes/web.php | 4 ++ 17 files changed, 215 insertions(+), 39 deletions(-) create mode 100644 resources/js/React/Hooks/usePageModal.ts create mode 100644 resources/js/React/Pages/Categories/CategoriesCreate.tsx create mode 100644 resources/js/React/Utils/wait.ts create mode 100644 resources/js/React/types/RouteHandle.ts create mode 100644 resources/js/React/types/ValidationErrors.ts diff --git a/app/Http/Controllers/Api/CategoryController.php b/app/Http/Controllers/Api/CategoryController.php index 30fedd79..0c436af8 100644 --- a/app/Http/Controllers/Api/CategoryController.php +++ b/app/Http/Controllers/Api/CategoryController.php @@ -43,13 +43,13 @@ public function create(): Response /** * Store a newly created resource in storage. */ - public function store(StoreCategoryRequest $request): RedirectResponse + public function store(StoreCategoryRequest $request): JsonResponse { $validated = $request->validated(); Auth::user()->categories()->save(new Category($validated)); - return redirect()->route('categories.index'); + return response()->json(); } /** diff --git a/lang/de.json b/lang/de.json index aff21ba3..836f2c89 100644 --- a/lang/de.json +++ b/lang/de.json @@ -111,5 +111,6 @@ "Users": "Benutzer", "Delete user": "Benutzer löschen", "Do you really want to delete the user “:name”?": "Wollen Sie den Benutzer „:name” wirklich löschen?", - "Manage users": "Benutzer verwalten" + "Manage users": "Benutzer verwalten", + "Home": "Startseite" } diff --git a/resources/js/Components/InputError.tsx b/resources/js/Components/InputError.tsx index 4cc418d6..2318aed1 100644 --- a/resources/js/Components/InputError.tsx +++ b/resources/js/Components/InputError.tsx @@ -1,7 +1,7 @@ export default function InputError({message, className = ''}: { message?: string; className?: string; }) { return message ? ( -

    +

    {message}

    ) diff --git a/resources/js/Components/Modal/Modal.tsx b/resources/js/Components/Modal/Modal.tsx index ca302f4a..7f20e57b 100644 --- a/resources/js/Components/Modal/Modal.tsx +++ b/resources/js/Components/Modal/Modal.tsx @@ -4,12 +4,14 @@ import {Dialog, Transition} from '@headlessui/react'; const Modal = ( { children, + appear = false, show = false, maxWidth = '2xl', closeable = true, onClose = () => {}, }: { children: ReactNode; + appear?: boolean; show?: boolean; maxWidth?: 'sm' | 'md' | 'lg' | 'xl' | '2xl'; closeable?: boolean; @@ -35,7 +37,7 @@ const Modal = ( }[maxWidth]; return ( - + { return (
    -

    +

    {children}

    @@ -100,9 +102,12 @@ const ModalFooter = ({children}: { children: ReactNode; }) => { ); }; +const modalLeaveDuration = 200; + export { Modal, ModalHeader, ModalBody, ModalFooter, + modalLeaveDuration, }; diff --git a/resources/js/React/Core/AuthenticatedLayout.tsx b/resources/js/React/Core/AuthenticatedLayout.tsx index 686eff01..27fa399e 100644 --- a/resources/js/React/Core/AuthenticatedLayout.tsx +++ b/resources/js/React/Core/AuthenticatedLayout.tsx @@ -2,13 +2,15 @@ import {Outlet, useLocation, useMatches} from 'react-router-dom'; import Breadcrumbs from '@/React/Core/Breadcrumbs'; import {useEffect} from 'react'; import MatchWithHandle from '@/React/types/MatchWithHandle'; +import {useLaravelReactI18n} from 'laravel-react-i18n'; const AuthenticatedLayout = () => { + const {t} = useLaravelReactI18n(); const location = useLocation(); const match = useMatches().find((match) => match.pathname === location.pathname) as MatchWithHandle; useEffect(() => { - document.querySelector('title')!.textContent = match?.handle.title; + document.querySelector('title')!.textContent = t(match?.handle.title); }, [location]); return ( diff --git a/resources/js/React/Core/Breadcrumbs.tsx b/resources/js/React/Core/Breadcrumbs.tsx index 494b9893..6ebfdb5b 100644 --- a/resources/js/React/Core/Breadcrumbs.tsx +++ b/resources/js/React/Core/Breadcrumbs.tsx @@ -3,34 +3,43 @@ import slug from 'slug'; import {Fragment, useEffect, useRef} from 'react'; import useBreadcrumbs from '@/React/Hooks/useBreadcrumbs'; import Breadcrumb from '@/React/types/Breadcrumb'; +import {useLaravelReactI18n} from 'laravel-react-i18n'; -const LinkBreadcrumb = ({breadcrumb}: { breadcrumb: Breadcrumb; }) => ( -
  • - {breadcrumb.headline} -
  • -); +const LinkBreadcrumb = ({breadcrumb}: { breadcrumb: Breadcrumb; }) => { + const {t} = useLaravelReactI18n(); -const TextBreadcrumb = ({breadcrumb}: { breadcrumb: Breadcrumb; }) => ( - + return (
  • - - {breadcrumb.headline} - + {t(breadcrumb.headline)}
  • + ); +}; -
  • - -
  • -
    -); +const TextBreadcrumb = ({breadcrumb}: { breadcrumb: Breadcrumb; }) => { + const {t} = useLaravelReactI18n(); + + return ( + +
  • + + {t(breadcrumb.headline)} + +
  • + +
  • + +
  • +
    + ); +}; export default function Breadcrumbs() { const breadcrumbs = useBreadcrumbs(); diff --git a/resources/js/React/Core/router.tsx b/resources/js/React/Core/router.tsx index 3285acf3..41b128bd 100644 --- a/resources/js/React/Core/router.tsx +++ b/resources/js/React/Core/router.tsx @@ -1,16 +1,25 @@ -import {createBrowserRouter} from 'react-router-dom'; +import {createBrowserRouter, redirect} from 'react-router-dom'; import categoriesLoader from '@/React/Pages/Categories/Loaders/categoriesLoader'; import AuthenticatedLayout from '@/React/Core/AuthenticatedLayout'; import CategoriesIndex from '@/React/Pages/Categories/CategoriesIndex'; +import CategoriesCreate from '@/React/Pages/Categories/CategoriesCreate'; +import request from '@/React/request'; +import {HTTPError} from 'ky'; +import ValidationErrors from '@/React/types/ValidationErrors'; +import Breadcrumb from '@/React/types/Breadcrumb'; +import RouteHandle from '@/React/types/RouteHandle'; + +const Error = () =>
    An error occurred.
    ; const router = createBrowserRouter([ { - path: '/react/', + path: '/react', element: , + errorElement: , handle: { title: 'Home', headline: 'Home', - }, + } as RouteHandle, children: [ { path: 'categories', @@ -19,7 +28,35 @@ const router = createBrowserRouter([ handle: { title: 'Categories', headline: 'Categories', - }, + } as RouteHandle, + children: [ + { + path: 'create', + element: , + action: async ({params, request: req}) => { + const formData = await req.formData(); + + try { + await request.post('/api/categories', {body: formData}); + } catch (exception) { + const errorResponse = (exception as HTTPError).response; + + if (errorResponse.status !== 422) { + throw exception; + } + + return (await errorResponse.json() as { errors: ValidationErrors; }).errors; + } + + return null; + }, + handle: { + hide: true, + title: 'Add category', + headline: 'Add category', + } as RouteHandle, + }, + ], }, ], }, diff --git a/resources/js/React/Hooks/useBreadcrumbs.ts b/resources/js/React/Hooks/useBreadcrumbs.ts index 444ea034..d5bcb7a3 100644 --- a/resources/js/React/Hooks/useBreadcrumbs.ts +++ b/resources/js/React/Hooks/useBreadcrumbs.ts @@ -1,10 +1,15 @@ import {useMatches} from 'react-router-dom'; import Breadcrumb from '@/React/types/Breadcrumb'; +import RouteHandle from '@/React/types/RouteHandle'; export default function useBreadcrumbs() { const matches = useMatches(); return matches - .filter((match) => !!match.handle) - .map((match) => ({pathname: match.pathname, ...match.handle!})) as Breadcrumb[]; + .filter((match) => !!match.handle && !(match.handle as RouteHandle).hide) + .map((match) => { + const handle = match.handle as RouteHandle; + + return {pathname: match.pathname, title: handle.title, headline: handle.headline} as Breadcrumb; + }) as Breadcrumb[]; } diff --git a/resources/js/React/Hooks/usePageModal.ts b/resources/js/React/Hooks/usePageModal.ts new file mode 100644 index 00000000..c87f292e --- /dev/null +++ b/resources/js/React/Hooks/usePageModal.ts @@ -0,0 +1,31 @@ +import {useNavigate} from 'react-router-dom'; +import {useEffect, useState} from 'react'; +import ValidationErrors from '@/React/types/ValidationErrors'; +import {modalLeaveDuration} from '@/Components/Modal/Modal'; +import wait from '@/React/Utils/wait'; + +export default function usePageModal(errors: ValidationErrors | null, to: string) { + const [show, setShow] = useState(true); + const navigate = useNavigate(); + + useEffect(() => { + if (errors !== null) { + return; + } + + setShow(false); + }, [errors]); + + useEffect(() => { + if (!show) { + void wait(modalLeaveDuration).then(() => navigate(to)); + } + }, [show]); + + const handleClose = () => setShow(false); + + return { + show, + handleClose, + }; +} diff --git a/resources/js/React/Pages/Categories/CategoriesCreate.tsx b/resources/js/React/Pages/Categories/CategoriesCreate.tsx new file mode 100644 index 00000000..d541339b --- /dev/null +++ b/resources/js/React/Pages/Categories/CategoriesCreate.tsx @@ -0,0 +1,46 @@ +import {Modal, ModalBody, ModalHeader} from '@/Components/Modal/Modal'; +import {Form, useActionData} from 'react-router-dom'; +import TextInput from '@/Components/TextInput'; +import usePageModal from '@/React/Hooks/usePageModal'; +import ValidationErrors from '@/React/types/ValidationErrors'; +import InputError from '@/Components/InputError'; +import {PrimaryButton} from '@/Components/Button'; +import InputLabel from '@/Components/InputLabel'; +import React from 'react'; +import {useLaravelReactI18n} from 'laravel-react-i18n'; + +type CategoriesCreateValidationErrors = ValidationErrors & { name?: string; } | null; + +export default function CategoriesCreate() { + const {t} = useLaravelReactI18n(); + const errors = useActionData() as CategoriesCreateValidationErrors; + const {show, handleClose} = usePageModal(errors, '/react/categories'); + + return ( + + + {t('Add category')} + + + +
    +
    + + + +
    + + + {t('Save')} + +
    +
    +
    + ); +} diff --git a/resources/js/React/Pages/Categories/CategoriesIndex.tsx b/resources/js/React/Pages/Categories/CategoriesIndex.tsx index 5087b5b1..6919f1c3 100644 --- a/resources/js/React/Pages/Categories/CategoriesIndex.tsx +++ b/resources/js/React/Pages/Categories/CategoriesIndex.tsx @@ -1,16 +1,27 @@ import Category from '@/types/generated/Models/Category'; -import {useLoaderData} from 'react-router-dom'; +import {Link, Outlet, useLoaderData} from 'react-router-dom'; +import {useLaravelReactI18n} from 'laravel-react-i18n'; +import Actions from '@/Components/Actions'; export default function CategoriesIndex() { const categories = useLoaderData() as Category[]; + const {t} = useLaravelReactI18n(); return (
    -
    + + + {t('Add category')} + + + +
    {categories.map((category) => (
    {category.name}
    ))}
    + +
    ); } diff --git a/resources/js/React/Utils/wait.ts b/resources/js/React/Utils/wait.ts new file mode 100644 index 00000000..f50cf4b4 --- /dev/null +++ b/resources/js/React/Utils/wait.ts @@ -0,0 +1,5 @@ +export default function wait(duration: number): Promise { + return new Promise((resolve) => { + setTimeout(resolve, duration); + }); +} diff --git a/resources/js/React/app.tsx b/resources/js/React/app.tsx index e03f4ae8..35190782 100644 --- a/resources/js/React/app.tsx +++ b/resources/js/React/app.tsx @@ -2,6 +2,9 @@ import {createRoot} from 'react-dom/client'; import {RouterProvider} from 'react-router-dom'; import router from '@/React/Core/router'; import NProgress from 'nprogress'; +import {LaravelReactI18nProvider} from 'laravel-react-i18n'; +import getBrowserLocale from '@/Utils/getBrowserLocale'; +import AppWithLoadedTranslations from '@/Components/AppWithLoadedTranslations'; NProgress.configure({ showSpinner: false, @@ -9,7 +12,13 @@ NProgress.configure({ const App = () => { return ( - + + + ); }; diff --git a/resources/js/React/types/RouteHandle.ts b/resources/js/React/types/RouteHandle.ts new file mode 100644 index 00000000..87d34629 --- /dev/null +++ b/resources/js/React/types/RouteHandle.ts @@ -0,0 +1,7 @@ +type RouteHandle = { + hide?: boolean; + title: string; + headline: string; +}; + +export default RouteHandle; diff --git a/resources/js/React/types/ValidationErrors.ts b/resources/js/React/types/ValidationErrors.ts new file mode 100644 index 00000000..fb577df0 --- /dev/null +++ b/resources/js/React/types/ValidationErrors.ts @@ -0,0 +1,3 @@ +type ValidationErrors = { [key: string]: string[]; }; + +export default ValidationErrors; diff --git a/routes/api.php b/routes/api.php index c3c4b020..de555332 100644 --- a/routes/api.php +++ b/routes/api.php @@ -21,4 +21,5 @@ Route::middleware('auth:sanctum')->group(function () { Route::get('/categories', [CategoryController::class, 'index'])->name('categories.index'); + Route::post('/categories', [CategoryController::class, 'store'])->name('categories.store'); }); diff --git a/routes/web.php b/routes/web.php index c575f10c..36f7a3cf 100644 --- a/routes/web.php +++ b/routes/web.php @@ -40,6 +40,10 @@ return view('app_react'); }); +Route::any('/react/categories/create', function () { + return view('app_react'); +}); + Route::middleware('auth')->group(function () { Route::get('/dashboard', DashboardController::class)->name('dashboard'); From 2b3db08e8c424e06f164e527d0eb4ceb0f2072f5 Mon Sep 17 00:00:00 2001 From: Kaishiyoku Date: Sat, 9 Mar 2024 19:14:20 +0100 Subject: [PATCH 07/79] added the ability to edit categories --- .../Controllers/Api/CategoryController.php | 8 +-- resources/js/Components/LinkStack.tsx | 12 +++-- resources/js/Components/Modal/Modal.tsx | 4 +- .../js/React/Core/AuthenticatedLayout.tsx | 10 ++-- resources/js/React/Core/router.tsx | 36 +++++++++++-- resources/js/React/Hooks/usePageModal.ts | 7 ++- .../React/Pages/Categories/CategoriesEdit.tsx | 50 +++++++++++++++++++ .../Pages/Categories/CategoriesIndex.tsx | 42 +++++++++++++--- .../Categories/Loaders/categoryLoader.ts | 11 ++++ routes/api.php | 3 +- routes/web.php | 4 ++ 11 files changed, 159 insertions(+), 28 deletions(-) create mode 100644 resources/js/React/Pages/Categories/CategoriesEdit.tsx create mode 100644 resources/js/React/Pages/Categories/Loaders/categoryLoader.ts diff --git a/app/Http/Controllers/Api/CategoryController.php b/app/Http/Controllers/Api/CategoryController.php index 0c436af8..13d23ec0 100644 --- a/app/Http/Controllers/Api/CategoryController.php +++ b/app/Http/Controllers/Api/CategoryController.php @@ -55,9 +55,9 @@ public function store(StoreCategoryRequest $request): JsonResponse /** * Show the form for editing the specified resource. */ - public function edit(Category $category): Response + public function edit(Category $category): JsonResponse { - return Inertia::render('Categories/Edit', [ + return response()->json([ 'category' => $category, 'canDelete' => Auth::user()->can('delete', $category), ]); @@ -66,13 +66,13 @@ public function edit(Category $category): Response /** * Update the specified resource in storage. */ - public function update(UpdateCategoryRequest $request, Category $category): RedirectResponse + public function update(UpdateCategoryRequest $request, Category $category): JsonResponse { $validated = $request->validated(); $category->update($validated); - return redirect()->route('categories.index'); + return response()->json(); } /** diff --git a/resources/js/Components/LinkStack.tsx b/resources/js/Components/LinkStack.tsx index 63926deb..3419d7f0 100644 --- a/resources/js/Components/LinkStack.tsx +++ b/resources/js/Components/LinkStack.tsx @@ -1,6 +1,7 @@ import clsx from 'clsx'; import {ReactNode} from 'react'; import Card from '@/Components/Card'; +import {Link} from 'react-router-dom'; const LinkStack = ({children}: { children: ReactNode; }) => { return ( @@ -12,19 +13,22 @@ const LinkStack = ({children}: { children: ReactNode; }) => { const Item = ( { - href, + to, children, className = '', }: { - href: string; + to: string; children: ReactNode; className?: string; } ) => { return ( - + {children} - + ); }; diff --git a/resources/js/Components/Modal/Modal.tsx b/resources/js/Components/Modal/Modal.tsx index 7f20e57b..0ee36344 100644 --- a/resources/js/Components/Modal/Modal.tsx +++ b/resources/js/Components/Modal/Modal.tsx @@ -22,7 +22,7 @@ const Modal = ( document.body.style.overflowY = show ? 'hidden' : ''; }, [show]); - const close = () => { + const handleClose = () => { if (closeable) { onClose(); } @@ -42,7 +42,7 @@ const Modal = ( as="div" id="modal" className="fixed inset-0 flex max-h-full px-4 py-6 sm:px-0 items-center z-50 transform transition-all backdrop-blur" - onClose={close} + onClose={handleClose} > { }, [location]); return ( - <> +
    + + Categories + +
    - +
    ); }; diff --git a/resources/js/React/Core/router.tsx b/resources/js/React/Core/router.tsx index 41b128bd..2ea5e048 100644 --- a/resources/js/React/Core/router.tsx +++ b/resources/js/React/Core/router.tsx @@ -1,4 +1,4 @@ -import {createBrowserRouter, redirect} from 'react-router-dom'; +import {createBrowserRouter} from 'react-router-dom'; import categoriesLoader from '@/React/Pages/Categories/Loaders/categoriesLoader'; import AuthenticatedLayout from '@/React/Core/AuthenticatedLayout'; import CategoriesIndex from '@/React/Pages/Categories/CategoriesIndex'; @@ -6,8 +6,9 @@ import CategoriesCreate from '@/React/Pages/Categories/CategoriesCreate'; import request from '@/React/request'; import {HTTPError} from 'ky'; import ValidationErrors from '@/React/types/ValidationErrors'; -import Breadcrumb from '@/React/types/Breadcrumb'; import RouteHandle from '@/React/types/RouteHandle'; +import CategoriesEdit from '@/React/Pages/Categories/CategoriesEdit'; +import categoryLoader from '@/React/Pages/Categories/Loaders/categoryLoader'; const Error = () =>
    An error occurred.
    ; @@ -33,11 +34,11 @@ const router = createBrowserRouter([ { path: 'create', element: , - action: async ({params, request: req}) => { + action: async ({request: req}) => { const formData = await req.formData(); try { - await request.post('/api/categories', {body: formData}); + await request.post('/api/categories', {json: Object.fromEntries(formData)}); } catch (exception) { const errorResponse = (exception as HTTPError).response; @@ -56,6 +57,33 @@ const router = createBrowserRouter([ headline: 'Add category', } as RouteHandle, }, + { + path: ':categoryId/edit', + element: , + loader: categoryLoader('/edit'), + action: async ({params, request: req}) => { + const formData = await req.formData(); + + try { + await request.put(`/api/categories/${params.categoryId}`, {json: Object.fromEntries(formData)}); + } catch (exception) { + const errorResponse = (exception as HTTPError).response; + + if (errorResponse.status !== 422) { + throw exception; + } + + return (await errorResponse.json() as { errors: ValidationErrors; }).errors; + } + + return null; + }, + handle: { + hide: true, + title: 'Edit category', + headline: 'Edit category', + } as RouteHandle, + }, ], }, ], diff --git a/resources/js/React/Hooks/usePageModal.ts b/resources/js/React/Hooks/usePageModal.ts index c87f292e..78de01a1 100644 --- a/resources/js/React/Hooks/usePageModal.ts +++ b/resources/js/React/Hooks/usePageModal.ts @@ -1,4 +1,4 @@ -import {useNavigate} from 'react-router-dom'; +import {useLocation, useNavigate} from 'react-router-dom'; import {useEffect, useState} from 'react'; import ValidationErrors from '@/React/types/ValidationErrors'; import {modalLeaveDuration} from '@/Components/Modal/Modal'; @@ -7,6 +7,7 @@ import wait from '@/React/Utils/wait'; export default function usePageModal(errors: ValidationErrors | null, to: string) { const [show, setShow] = useState(true); const navigate = useNavigate(); + const location = useLocation(); useEffect(() => { if (errors !== null) { @@ -22,6 +23,10 @@ export default function usePageModal(errors: ValidationErrors | null, to: string } }, [show]); + useEffect(() => { + setShow(true); + }, [location]); + const handleClose = () => setShow(false); return { diff --git a/resources/js/React/Pages/Categories/CategoriesEdit.tsx b/resources/js/React/Pages/Categories/CategoriesEdit.tsx new file mode 100644 index 00000000..0f98d5a0 --- /dev/null +++ b/resources/js/React/Pages/Categories/CategoriesEdit.tsx @@ -0,0 +1,50 @@ +import {Modal, ModalBody, ModalHeader} from '@/Components/Modal/Modal'; +import {Form, useActionData, useLoaderData, useParams} from 'react-router-dom'; +import TextInput from '@/Components/TextInput'; +import usePageModal from '@/React/Hooks/usePageModal'; +import ValidationErrors from '@/React/types/ValidationErrors'; +import InputError from '@/Components/InputError'; +import {PrimaryButton} from '@/Components/Button'; +import InputLabel from '@/Components/InputLabel'; +import React from 'react'; +import {useLaravelReactI18n} from 'laravel-react-i18n'; +import Category from '@/types/generated/Models/Category'; + +type CategoriesCreateValidationErrors = ValidationErrors & { name?: string; } | null; + +export default function CategoriesEdit() { + const {t} = useLaravelReactI18n(); + const {categoryId} = useParams(); + const category = useLoaderData() as Category; + const errors = useActionData() as CategoriesCreateValidationErrors; + const {show, handleClose} = usePageModal(errors, '/react/categories'); + + return ( + + + {t('Edit category')} + + + +
    +
    + + + +
    + + + {t('Save')} + +
    +
    +
    + ); +} diff --git a/resources/js/React/Pages/Categories/CategoriesIndex.tsx b/resources/js/React/Pages/Categories/CategoriesIndex.tsx index 6919f1c3..b6f1f6a2 100644 --- a/resources/js/React/Pages/Categories/CategoriesIndex.tsx +++ b/resources/js/React/Pages/Categories/CategoriesIndex.tsx @@ -1,11 +1,15 @@ -import Category from '@/types/generated/Models/Category'; import {Link, Outlet, useLoaderData} from 'react-router-dom'; import {useLaravelReactI18n} from 'laravel-react-i18n'; import Actions from '@/Components/Actions'; +import LinkStack from '@/Components/LinkStack'; +import {isEmpty} from 'ramda'; +import EmptyState from '@/Components/EmptyState'; +import TagOutlineIcon from '@/Icons/TagOutlineIcon'; +import CategoryWithFeedsCount from '@/types/generated/Models/CategoryWithFeedsCount'; export default function CategoriesIndex() { - const categories = useLoaderData() as Category[]; - const {t} = useLaravelReactI18n(); + const categories = useLoaderData() as CategoryWithFeedsCount[]; + const {t, tChoice} = useLaravelReactI18n(); return (
    @@ -15,11 +19,33 @@ export default function CategoriesIndex() { -
    - {categories.map((category) => ( -
    {category.name}
    - ))} -
    + {isEmpty(categories) + ? ( + + ) + : ( + + {categories.map((category) => ( + +
    + {category.name} +
    + +
    + {tChoice('category.feeds_count', category.feeds_count)} +
    +
    + ))} +
    + )}
    diff --git a/resources/js/React/Pages/Categories/Loaders/categoryLoader.ts b/resources/js/React/Pages/Categories/Loaders/categoryLoader.ts new file mode 100644 index 00000000..1fa722e5 --- /dev/null +++ b/resources/js/React/Pages/Categories/Loaders/categoryLoader.ts @@ -0,0 +1,11 @@ +import Category from '@/types/generated/Models/Category'; +import request from '@/React/request'; +import {Params} from 'react-router-dom'; + +const categoryLoader = (suffix?: string) => async ({params}: { params: Params; }) => { + const data = await request(`/api/categories/${params.categoryId}${suffix}`).json<{ category: Category; canDelete: boolean; }>(); + + return data.category; +}; + +export default categoryLoader; diff --git a/routes/api.php b/routes/api.php index de555332..0cf2f85d 100644 --- a/routes/api.php +++ b/routes/api.php @@ -20,6 +20,5 @@ }); Route::middleware('auth:sanctum')->group(function () { - Route::get('/categories', [CategoryController::class, 'index'])->name('categories.index'); - Route::post('/categories', [CategoryController::class, 'store'])->name('categories.store'); + Route::resource('categories', CategoryController::class); }); diff --git a/routes/web.php b/routes/web.php index 36f7a3cf..4d7f6c8e 100644 --- a/routes/web.php +++ b/routes/web.php @@ -44,6 +44,10 @@ return view('app_react'); }); +Route::any('/react/categories/{category}/edit', function () { + return view('app_react'); +}); + Route::middleware('auth')->group(function () { Route::get('/dashboard', DashboardController::class)->name('dashboard'); From ec07387e84f814ee46c136924fc5a3cff64232bb Mon Sep 17 00:00:00 2001 From: Kaishiyoku Date: Sat, 9 Mar 2024 20:54:23 +0100 Subject: [PATCH 08/79] adjusted label layout --- resources/js/Components/InputLabel.tsx | 2 +- resources/js/React/Pages/Categories/CategoriesEdit.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/resources/js/Components/InputLabel.tsx b/resources/js/Components/InputLabel.tsx index ef1e1d3d..6ad42090 100644 --- a/resources/js/Components/InputLabel.tsx +++ b/resources/js/Components/InputLabel.tsx @@ -16,7 +16,7 @@ export default function InputLabel( return ( diff --git a/resources/js/React/Pages/Categories/CategoriesEdit.tsx b/resources/js/React/Pages/Categories/CategoriesEdit.tsx index 0f98d5a0..7c771b8d 100644 --- a/resources/js/React/Pages/Categories/CategoriesEdit.tsx +++ b/resources/js/React/Pages/Categories/CategoriesEdit.tsx @@ -26,8 +26,8 @@ export default function CategoriesEdit() { -
    -
    + +
    Date: Sat, 9 Mar 2024 21:01:00 +0100 Subject: [PATCH 09/79] added close button to modals --- resources/js/Components/Modal/Modal.tsx | 8 ++++++++ resources/js/React/Pages/Categories/CategoriesCreate.tsx | 6 +++--- resources/js/React/Pages/Categories/CategoriesEdit.tsx | 2 +- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/resources/js/Components/Modal/Modal.tsx b/resources/js/Components/Modal/Modal.tsx index 0ee36344..76df663c 100644 --- a/resources/js/Components/Modal/Modal.tsx +++ b/resources/js/Components/Modal/Modal.tsx @@ -1,5 +1,7 @@ import {Fragment, ReactNode, useEffect} from 'react'; import {Dialog, Transition} from '@headlessui/react'; +import {HeadlessButton} from '@/Components/Button'; +import XMarkOutlineIcon from '@/Icons/XMarkOutlineIcon'; const Modal = ( { @@ -68,6 +70,12 @@ const Modal = ( + {closeable && ( + + + + )} + {children} diff --git a/resources/js/React/Pages/Categories/CategoriesCreate.tsx b/resources/js/React/Pages/Categories/CategoriesCreate.tsx index d541339b..cd828b49 100644 --- a/resources/js/React/Pages/Categories/CategoriesCreate.tsx +++ b/resources/js/React/Pages/Categories/CategoriesCreate.tsx @@ -23,13 +23,13 @@ export default function CategoriesCreate() { - -
    + +
    diff --git a/resources/js/React/Pages/Categories/CategoriesEdit.tsx b/resources/js/React/Pages/Categories/CategoriesEdit.tsx index 7c771b8d..9b9cdcfb 100644 --- a/resources/js/React/Pages/Categories/CategoriesEdit.tsx +++ b/resources/js/React/Pages/Categories/CategoriesEdit.tsx @@ -33,7 +33,7 @@ export default function CategoriesEdit() { id="name" name="name" defaultValue={category.name} - className="block w-full max-w-xl" + className="block w-full" required isFocused /> From 1bde3143ffbc0b63b1f93cbca58fad0e2277f3dd Mon Sep 17 00:00:00 2001 From: Kaishiyoku Date: Sat, 9 Mar 2024 21:06:21 +0100 Subject: [PATCH 10/79] added navigation dropdown --- resources/js/Components/Dropdown.tsx | 20 +++++++++--- .../js/React/Core/AuthenticatedLayout.tsx | 31 +++++++++++++++---- 2 files changed, 41 insertions(+), 10 deletions(-) diff --git a/resources/js/Components/Dropdown.tsx b/resources/js/Components/Dropdown.tsx index 1af68454..27b484cc 100644 --- a/resources/js/Components/Dropdown.tsx +++ b/resources/js/Components/Dropdown.tsx @@ -1,8 +1,17 @@ -import {useState, createContext, useContext, Fragment, PropsWithChildren, Dispatch, SetStateAction} from 'react'; -import {InertiaLinkProps, Link} from '@inertiajs/react'; +import { + useState, + createContext, + useContext, + Fragment, + PropsWithChildren, + Dispatch, + SetStateAction, + ReactNode +} from 'react'; import {Transition} from '@headlessui/react'; import noop from '@/Utils/noop'; import clsx from 'clsx'; +import {Link} from 'react-router-dom'; type DropDownContextType = { open: boolean; @@ -86,10 +95,13 @@ const Content = ({align = 'right', width = 48, contentClasses = 'p-2 bg-white/80 ); }; -const DropdownLink = ({active = false, className = '', children, ...props}: InertiaLinkProps & { active?: boolean; }) => { +const DropdownLink = ({to, active = false, className = '', children}: { to: string; active?: boolean; className?: string; children: ReactNode; }) => { + const {setOpen} = useContext(DropDownContext); + return ( setOpen(false)} className={clsx( 'block w-full text-left px-4 py-2 text-sm leading-5 rounded-lg focus:outline-none transition duration-150 ease-in-out', { diff --git a/resources/js/React/Core/AuthenticatedLayout.tsx b/resources/js/React/Core/AuthenticatedLayout.tsx index 0c83b053..72faee88 100644 --- a/resources/js/React/Core/AuthenticatedLayout.tsx +++ b/resources/js/React/Core/AuthenticatedLayout.tsx @@ -1,8 +1,10 @@ -import {Link, Outlet, useLocation, useMatches} from 'react-router-dom'; +import {Outlet, useLocation, useMatches} from 'react-router-dom'; import Breadcrumbs from '@/React/Core/Breadcrumbs'; import {useEffect} from 'react'; import MatchWithHandle from '@/React/types/MatchWithHandle'; import {useLaravelReactI18n} from 'laravel-react-i18n'; +import Dropdown from '@/Components/Dropdown'; +import DropdownArrowIcon from '@/Icons/DropdownArrowIcon'; const AuthenticatedLayout = () => { const {t} = useLaravelReactI18n(); @@ -15,15 +17,32 @@ const AuthenticatedLayout = () => { return (
    -
    +
    + + + + + + + + + + + {t('Categories')} + + +
    - - Categories - -
    From 3570ad9b510c5858b5a9ed048424ee8b794bbbe4 Mon Sep 17 00:00:00 2001 From: Kaishiyoku Date: Sat, 9 Mar 2024 21:12:33 +0100 Subject: [PATCH 11/79] get user data for layout --- resources/js/React/Core/AuthenticatedLayout.tsx | 6 ++++-- resources/js/React/Core/router.tsx | 2 ++ resources/js/React/Pages/Loaders/layoutLoader.ts | 8 ++++++++ 3 files changed, 14 insertions(+), 2 deletions(-) create mode 100644 resources/js/React/Pages/Loaders/layoutLoader.ts diff --git a/resources/js/React/Core/AuthenticatedLayout.tsx b/resources/js/React/Core/AuthenticatedLayout.tsx index 72faee88..22f01f5a 100644 --- a/resources/js/React/Core/AuthenticatedLayout.tsx +++ b/resources/js/React/Core/AuthenticatedLayout.tsx @@ -1,12 +1,14 @@ -import {Outlet, useLocation, useMatches} from 'react-router-dom'; +import {Outlet, useLoaderData, useLocation, useMatches} from 'react-router-dom'; import Breadcrumbs from '@/React/Core/Breadcrumbs'; import {useEffect} from 'react'; import MatchWithHandle from '@/React/types/MatchWithHandle'; import {useLaravelReactI18n} from 'laravel-react-i18n'; import Dropdown from '@/Components/Dropdown'; import DropdownArrowIcon from '@/Icons/DropdownArrowIcon'; +import User from '@/types/generated/Models/User'; const AuthenticatedLayout = () => { + const user = useLoaderData() as User; const {t} = useLaravelReactI18n(); const location = useLocation(); const match = useMatches().find((match) => match.pathname === location.pathname) as MatchWithHandle; @@ -27,7 +29,7 @@ const AuthenticatedLayout = () => { type="button" className="inline-flex items-center space-x-2 text-gray-500 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700/50 dark:hover:text-white px-3 py-2 rounded-md text-sm transition" > - USER + {user.name} diff --git a/resources/js/React/Core/router.tsx b/resources/js/React/Core/router.tsx index 2ea5e048..2509432a 100644 --- a/resources/js/React/Core/router.tsx +++ b/resources/js/React/Core/router.tsx @@ -9,6 +9,7 @@ import ValidationErrors from '@/React/types/ValidationErrors'; import RouteHandle from '@/React/types/RouteHandle'; import CategoriesEdit from '@/React/Pages/Categories/CategoriesEdit'; import categoryLoader from '@/React/Pages/Categories/Loaders/categoryLoader'; +import layoutLoader from '@/React/Pages/Loaders/layoutLoader'; const Error = () =>
    An error occurred.
    ; @@ -17,6 +18,7 @@ const router = createBrowserRouter([ path: '/react', element: , errorElement: , + loader: layoutLoader, handle: { title: 'Home', headline: 'Home', diff --git a/resources/js/React/Pages/Loaders/layoutLoader.ts b/resources/js/React/Pages/Loaders/layoutLoader.ts new file mode 100644 index 00000000..1040234e --- /dev/null +++ b/resources/js/React/Pages/Loaders/layoutLoader.ts @@ -0,0 +1,8 @@ +import request from '@/React/request'; +import User from '@/types/generated/Models/User'; + +const layoutLoader = async () => { + return await request('/api/user').json(); +}; + +export default layoutLoader; From c90932c6cdda0bb8fcf0887032015e9b3773b8c8 Mon Sep 17 00:00:00 2001 From: Kaishiyoku Date: Sat, 9 Mar 2024 21:15:51 +0100 Subject: [PATCH 12/79] adjusted progress bar color --- resources/css/app.css | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/resources/css/app.css b/resources/css/app.css index c3aacebf..43c113fa 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -6,6 +6,17 @@ body { margin-bottom: 0 !important; } +#nprogress .bar { + background: #7c3aed !important; +} +#progress .peg { + box-shadow: 0 0 10px #7c3aed, 0 0 5px #7c3aed; +} +#nprogress .spinner-icon { + border-top-color: #7c3aed; + border-left-color: #7c3aed; +} + @layer base { /** * Miscellaneous From bb3cb00c349b221eb99db7f0e84734c299a4f180 Mon Sep 17 00:00:00 2001 From: Kaishiyoku Date: Sat, 9 Mar 2024 21:26:06 +0100 Subject: [PATCH 13/79] open category forms in a pane instead of a modal --- resources/js/Components/Modal/Pane.tsx | 111 ++++++++++++++++++ .../Pages/Categories/CategoriesCreate.tsx | 13 +- .../React/Pages/Categories/CategoriesEdit.tsx | 13 +- 3 files changed, 125 insertions(+), 12 deletions(-) create mode 100644 resources/js/Components/Modal/Pane.tsx diff --git a/resources/js/Components/Modal/Pane.tsx b/resources/js/Components/Modal/Pane.tsx new file mode 100644 index 00000000..a0e41054 --- /dev/null +++ b/resources/js/Components/Modal/Pane.tsx @@ -0,0 +1,111 @@ +import {Fragment, ReactNode, useEffect} from 'react'; +import {Dialog, Transition} from '@headlessui/react'; +import {HeadlessButton} from '@/Components/Button'; +import XMarkOutlineIcon from '@/Icons/XMarkOutlineIcon'; + +const Pane = ( + { + children, + appear = false, + show = false, + closeable = true, + onClose = () => {}, + }: { + children: ReactNode; + appear?: boolean; + show?: boolean; + closeable?: boolean; + onClose?: () => void; + } +) => { + useEffect(() => { + document.body.style.overflowY = show ? 'hidden' : ''; + }, [show]); + + const handleClose = () => { + if (closeable) { + onClose(); + } + }; + + return ( + + + +
    + + + + + {closeable && ( + + + + )} + + {children} + + +
    +
    + ); +}; + +const PaneHeader = ({children}: { children: ReactNode; }) => { + return ( +
    +

    + {children} +

    +
    + ); +}; + +const PaneBody = ({children}: { children: ReactNode; }) => { + return ( +
    + {children} +
    + ); +}; + +const PaneFooter = ({children}: { children: ReactNode; }) => { + return ( +
    + {children} +
    + ); +}; + +const paneLeaveDuration = 200; + +export { + Pane, + PaneHeader, + PaneBody, + PaneFooter, + paneLeaveDuration, +}; diff --git a/resources/js/React/Pages/Categories/CategoriesCreate.tsx b/resources/js/React/Pages/Categories/CategoriesCreate.tsx index cd828b49..104950e8 100644 --- a/resources/js/React/Pages/Categories/CategoriesCreate.tsx +++ b/resources/js/React/Pages/Categories/CategoriesCreate.tsx @@ -8,6 +8,7 @@ import {PrimaryButton} from '@/Components/Button'; import InputLabel from '@/Components/InputLabel'; import React from 'react'; import {useLaravelReactI18n} from 'laravel-react-i18n'; +import {Pane, PaneBody, PaneHeader} from '@/Components/Modal/Pane'; type CategoriesCreateValidationErrors = ValidationErrors & { name?: string; } | null; @@ -17,12 +18,12 @@ export default function CategoriesCreate() { const {show, handleClose} = usePageModal(errors, '/react/categories'); return ( - - + + {t('Add category')} - + - +
    @@ -40,7 +41,7 @@ export default function CategoriesCreate() { {t('Save')} - - + + ); } diff --git a/resources/js/React/Pages/Categories/CategoriesEdit.tsx b/resources/js/React/Pages/Categories/CategoriesEdit.tsx index 9b9cdcfb..b64e31f5 100644 --- a/resources/js/React/Pages/Categories/CategoriesEdit.tsx +++ b/resources/js/React/Pages/Categories/CategoriesEdit.tsx @@ -9,6 +9,7 @@ import InputLabel from '@/Components/InputLabel'; import React from 'react'; import {useLaravelReactI18n} from 'laravel-react-i18n'; import Category from '@/types/generated/Models/Category'; +import {Pane, PaneBody, PaneHeader} from '@/Components/Modal/Pane'; type CategoriesCreateValidationErrors = ValidationErrors & { name?: string; } | null; @@ -20,12 +21,12 @@ export default function CategoriesEdit() { const {show, handleClose} = usePageModal(errors, '/react/categories'); return ( - - + + {t('Edit category')} - + - +
    @@ -44,7 +45,7 @@ export default function CategoriesEdit() { {t('Save')} - - + + ); } From b16f013fc8f9c0703fd90eeb6c82e2b5e9a16303 Mon Sep 17 00:00:00 2001 From: Kaishiyoku Date: Sat, 9 Mar 2024 21:53:48 +0100 Subject: [PATCH 14/79] refactored code --- .../Core/AuthenticatedLayout.tsx | 6 ++--- .../js/{React => V2}/Core/Breadcrumbs.tsx | 4 ++-- resources/js/{React => V2}/Core/router.tsx | 22 +++++++++---------- .../js/{React => V2}/Hooks/useBreadcrumbs.ts | 4 ++-- .../js/{React => V2}/Hooks/usePageModal.ts | 4 ++-- .../Pages/Categories/CategoriesCreate.tsx | 8 +++---- .../Pages/Categories/CategoriesEdit.tsx | 8 +++---- .../Pages/Categories/CategoriesIndex.tsx | 4 ++-- .../Categories/Loaders/categoriesLoader.ts | 2 +- .../Categories/Loaders/categoryLoader.ts | 2 +- .../Pages/Loaders/layoutLoader.ts | 2 +- resources/js/{React => V2}/Utils/wait.ts | 0 resources/js/{React => V2}/app.tsx | 2 +- resources/js/{React => V2}/request.ts | 0 .../js/{React => V2}/types/Breadcrumb.ts | 0 .../{React => V2}/types/MatchWithHandle.tsx | 0 .../js/{React => V2}/types/RouteHandle.ts | 0 .../{React => V2}/types/ValidationErrors.ts | 0 resources/views/app_react.blade.php | 2 +- routes/web.php | 18 ++++----------- 20 files changed, 39 insertions(+), 49 deletions(-) rename resources/js/{React => V2}/Core/AuthenticatedLayout.tsx (92%) rename resources/js/{React => V2}/Core/Breadcrumbs.tsx (95%) rename resources/js/{React => V2}/Core/router.tsx (82%) rename resources/js/{React => V2}/Hooks/useBreadcrumbs.ts (81%) rename resources/js/{React => V2}/Hooks/usePageModal.ts (88%) rename resources/js/{React => V2}/Pages/Categories/CategoriesCreate.tsx (84%) rename resources/js/{React => V2}/Pages/Categories/CategoriesEdit.tsx (85%) rename resources/js/{React => V2}/Pages/Categories/CategoriesIndex.tsx (92%) rename resources/js/{React => V2}/Pages/Categories/Loaders/categoriesLoader.ts (86%) rename resources/js/{React => V2}/Pages/Categories/Loaders/categoryLoader.ts (90%) rename resources/js/{React => V2}/Pages/Loaders/layoutLoader.ts (81%) rename resources/js/{React => V2}/Utils/wait.ts (100%) rename resources/js/{React => V2}/app.tsx (94%) rename resources/js/{React => V2}/request.ts (100%) rename resources/js/{React => V2}/types/Breadcrumb.ts (100%) rename resources/js/{React => V2}/types/MatchWithHandle.tsx (100%) rename resources/js/{React => V2}/types/RouteHandle.ts (100%) rename resources/js/{React => V2}/types/ValidationErrors.ts (100%) diff --git a/resources/js/React/Core/AuthenticatedLayout.tsx b/resources/js/V2/Core/AuthenticatedLayout.tsx similarity index 92% rename from resources/js/React/Core/AuthenticatedLayout.tsx rename to resources/js/V2/Core/AuthenticatedLayout.tsx index 22f01f5a..73d305b0 100644 --- a/resources/js/React/Core/AuthenticatedLayout.tsx +++ b/resources/js/V2/Core/AuthenticatedLayout.tsx @@ -1,7 +1,7 @@ import {Outlet, useLoaderData, useLocation, useMatches} from 'react-router-dom'; -import Breadcrumbs from '@/React/Core/Breadcrumbs'; +import Breadcrumbs from '@/V2/Core/Breadcrumbs'; import {useEffect} from 'react'; -import MatchWithHandle from '@/React/types/MatchWithHandle'; +import MatchWithHandle from '@/V2/types/MatchWithHandle'; import {useLaravelReactI18n} from 'laravel-react-i18n'; import Dropdown from '@/Components/Dropdown'; import DropdownArrowIcon from '@/Icons/DropdownArrowIcon'; @@ -37,7 +37,7 @@ const AuthenticatedLayout = () => { - + {t('Categories')} diff --git a/resources/js/React/Core/Breadcrumbs.tsx b/resources/js/V2/Core/Breadcrumbs.tsx similarity index 95% rename from resources/js/React/Core/Breadcrumbs.tsx rename to resources/js/V2/Core/Breadcrumbs.tsx index 6ebfdb5b..7da40537 100644 --- a/resources/js/React/Core/Breadcrumbs.tsx +++ b/resources/js/V2/Core/Breadcrumbs.tsx @@ -1,8 +1,8 @@ import {Link} from 'react-router-dom'; import slug from 'slug'; import {Fragment, useEffect, useRef} from 'react'; -import useBreadcrumbs from '@/React/Hooks/useBreadcrumbs'; -import Breadcrumb from '@/React/types/Breadcrumb'; +import useBreadcrumbs from '@/V2/Hooks/useBreadcrumbs'; +import Breadcrumb from '@/V2/types/Breadcrumb'; import {useLaravelReactI18n} from 'laravel-react-i18n'; const LinkBreadcrumb = ({breadcrumb}: { breadcrumb: Breadcrumb; }) => { diff --git a/resources/js/React/Core/router.tsx b/resources/js/V2/Core/router.tsx similarity index 82% rename from resources/js/React/Core/router.tsx rename to resources/js/V2/Core/router.tsx index 2509432a..12fd6434 100644 --- a/resources/js/React/Core/router.tsx +++ b/resources/js/V2/Core/router.tsx @@ -1,21 +1,21 @@ import {createBrowserRouter} from 'react-router-dom'; -import categoriesLoader from '@/React/Pages/Categories/Loaders/categoriesLoader'; -import AuthenticatedLayout from '@/React/Core/AuthenticatedLayout'; -import CategoriesIndex from '@/React/Pages/Categories/CategoriesIndex'; -import CategoriesCreate from '@/React/Pages/Categories/CategoriesCreate'; -import request from '@/React/request'; +import categoriesLoader from '@/V2/Pages/Categories/Loaders/categoriesLoader'; +import AuthenticatedLayout from '@/V2/Core/AuthenticatedLayout'; +import CategoriesIndex from '@/V2/Pages/Categories/CategoriesIndex'; +import CategoriesCreate from '@/V2/Pages/Categories/CategoriesCreate'; +import request from '@/V2/request'; import {HTTPError} from 'ky'; -import ValidationErrors from '@/React/types/ValidationErrors'; -import RouteHandle from '@/React/types/RouteHandle'; -import CategoriesEdit from '@/React/Pages/Categories/CategoriesEdit'; -import categoryLoader from '@/React/Pages/Categories/Loaders/categoryLoader'; -import layoutLoader from '@/React/Pages/Loaders/layoutLoader'; +import ValidationErrors from '@/V2/types/ValidationErrors'; +import RouteHandle from '@/V2/types/RouteHandle'; +import CategoriesEdit from '@/V2/Pages/Categories/CategoriesEdit'; +import categoryLoader from '@/V2/Pages/Categories/Loaders/categoryLoader'; +import layoutLoader from '@/V2/Pages/Loaders/layoutLoader'; const Error = () =>
    An error occurred.
    ; const router = createBrowserRouter([ { - path: '/react', + path: '/v2', element: , errorElement: , loader: layoutLoader, diff --git a/resources/js/React/Hooks/useBreadcrumbs.ts b/resources/js/V2/Hooks/useBreadcrumbs.ts similarity index 81% rename from resources/js/React/Hooks/useBreadcrumbs.ts rename to resources/js/V2/Hooks/useBreadcrumbs.ts index d5bcb7a3..34561f38 100644 --- a/resources/js/React/Hooks/useBreadcrumbs.ts +++ b/resources/js/V2/Hooks/useBreadcrumbs.ts @@ -1,6 +1,6 @@ import {useMatches} from 'react-router-dom'; -import Breadcrumb from '@/React/types/Breadcrumb'; -import RouteHandle from '@/React/types/RouteHandle'; +import Breadcrumb from '@/V2/types/Breadcrumb'; +import RouteHandle from '@/V2/types/RouteHandle'; export default function useBreadcrumbs() { const matches = useMatches(); diff --git a/resources/js/React/Hooks/usePageModal.ts b/resources/js/V2/Hooks/usePageModal.ts similarity index 88% rename from resources/js/React/Hooks/usePageModal.ts rename to resources/js/V2/Hooks/usePageModal.ts index 78de01a1..320e7ceb 100644 --- a/resources/js/React/Hooks/usePageModal.ts +++ b/resources/js/V2/Hooks/usePageModal.ts @@ -1,8 +1,8 @@ import {useLocation, useNavigate} from 'react-router-dom'; import {useEffect, useState} from 'react'; -import ValidationErrors from '@/React/types/ValidationErrors'; +import ValidationErrors from '@/V2/types/ValidationErrors'; import {modalLeaveDuration} from '@/Components/Modal/Modal'; -import wait from '@/React/Utils/wait'; +import wait from '@/V2/Utils/wait'; export default function usePageModal(errors: ValidationErrors | null, to: string) { const [show, setShow] = useState(true); diff --git a/resources/js/React/Pages/Categories/CategoriesCreate.tsx b/resources/js/V2/Pages/Categories/CategoriesCreate.tsx similarity index 84% rename from resources/js/React/Pages/Categories/CategoriesCreate.tsx rename to resources/js/V2/Pages/Categories/CategoriesCreate.tsx index 104950e8..7fbb82da 100644 --- a/resources/js/React/Pages/Categories/CategoriesCreate.tsx +++ b/resources/js/V2/Pages/Categories/CategoriesCreate.tsx @@ -1,8 +1,8 @@ import {Modal, ModalBody, ModalHeader} from '@/Components/Modal/Modal'; import {Form, useActionData} from 'react-router-dom'; import TextInput from '@/Components/TextInput'; -import usePageModal from '@/React/Hooks/usePageModal'; -import ValidationErrors from '@/React/types/ValidationErrors'; +import usePageModal from '@/V2/Hooks/usePageModal'; +import ValidationErrors from '@/V2/types/ValidationErrors'; import InputError from '@/Components/InputError'; import {PrimaryButton} from '@/Components/Button'; import InputLabel from '@/Components/InputLabel'; @@ -15,7 +15,7 @@ type CategoriesCreateValidationErrors = ValidationErrors & { name?: string; } | export default function CategoriesCreate() { const {t} = useLaravelReactI18n(); const errors = useActionData() as CategoriesCreateValidationErrors; - const {show, handleClose} = usePageModal(errors, '/react/categories'); + const {show, handleClose} = usePageModal(errors, '/v2/categories'); return ( @@ -24,7 +24,7 @@ export default function CategoriesCreate() { -
    +
    @@ -27,7 +27,7 @@ export default function CategoriesEdit() { - +
    - + {t('Add category')} @@ -32,7 +32,7 @@ export default function CategoriesIndex() { {categories.map((category) => (
    diff --git a/resources/js/React/Pages/Categories/Loaders/categoriesLoader.ts b/resources/js/V2/Pages/Categories/Loaders/categoriesLoader.ts similarity index 86% rename from resources/js/React/Pages/Categories/Loaders/categoriesLoader.ts rename to resources/js/V2/Pages/Categories/Loaders/categoriesLoader.ts index 0204fad1..fb87785b 100644 --- a/resources/js/React/Pages/Categories/Loaders/categoriesLoader.ts +++ b/resources/js/V2/Pages/Categories/Loaders/categoriesLoader.ts @@ -1,5 +1,5 @@ import Category from '@/types/generated/Models/Category'; -import request from '@/React/request'; +import request from '@/V2/request'; export default async function categoriesLoader() { const data = await request('/api/categories').json<{ categories: Category[]; canCreate: boolean; }>(); diff --git a/resources/js/React/Pages/Categories/Loaders/categoryLoader.ts b/resources/js/V2/Pages/Categories/Loaders/categoryLoader.ts similarity index 90% rename from resources/js/React/Pages/Categories/Loaders/categoryLoader.ts rename to resources/js/V2/Pages/Categories/Loaders/categoryLoader.ts index 1fa722e5..7a3dbf6f 100644 --- a/resources/js/React/Pages/Categories/Loaders/categoryLoader.ts +++ b/resources/js/V2/Pages/Categories/Loaders/categoryLoader.ts @@ -1,5 +1,5 @@ import Category from '@/types/generated/Models/Category'; -import request from '@/React/request'; +import request from '@/V2/request'; import {Params} from 'react-router-dom'; const categoryLoader = (suffix?: string) => async ({params}: { params: Params; }) => { diff --git a/resources/js/React/Pages/Loaders/layoutLoader.ts b/resources/js/V2/Pages/Loaders/layoutLoader.ts similarity index 81% rename from resources/js/React/Pages/Loaders/layoutLoader.ts rename to resources/js/V2/Pages/Loaders/layoutLoader.ts index 1040234e..82826443 100644 --- a/resources/js/React/Pages/Loaders/layoutLoader.ts +++ b/resources/js/V2/Pages/Loaders/layoutLoader.ts @@ -1,4 +1,4 @@ -import request from '@/React/request'; +import request from '@/V2/request'; import User from '@/types/generated/Models/User'; const layoutLoader = async () => { diff --git a/resources/js/React/Utils/wait.ts b/resources/js/V2/Utils/wait.ts similarity index 100% rename from resources/js/React/Utils/wait.ts rename to resources/js/V2/Utils/wait.ts diff --git a/resources/js/React/app.tsx b/resources/js/V2/app.tsx similarity index 94% rename from resources/js/React/app.tsx rename to resources/js/V2/app.tsx index 35190782..b5b0cb90 100644 --- a/resources/js/React/app.tsx +++ b/resources/js/V2/app.tsx @@ -1,6 +1,6 @@ import {createRoot} from 'react-dom/client'; import {RouterProvider} from 'react-router-dom'; -import router from '@/React/Core/router'; +import router from '@/V2/Core/router'; import NProgress from 'nprogress'; import {LaravelReactI18nProvider} from 'laravel-react-i18n'; import getBrowserLocale from '@/Utils/getBrowserLocale'; diff --git a/resources/js/React/request.ts b/resources/js/V2/request.ts similarity index 100% rename from resources/js/React/request.ts rename to resources/js/V2/request.ts diff --git a/resources/js/React/types/Breadcrumb.ts b/resources/js/V2/types/Breadcrumb.ts similarity index 100% rename from resources/js/React/types/Breadcrumb.ts rename to resources/js/V2/types/Breadcrumb.ts diff --git a/resources/js/React/types/MatchWithHandle.tsx b/resources/js/V2/types/MatchWithHandle.tsx similarity index 100% rename from resources/js/React/types/MatchWithHandle.tsx rename to resources/js/V2/types/MatchWithHandle.tsx diff --git a/resources/js/React/types/RouteHandle.ts b/resources/js/V2/types/RouteHandle.ts similarity index 100% rename from resources/js/React/types/RouteHandle.ts rename to resources/js/V2/types/RouteHandle.ts diff --git a/resources/js/React/types/ValidationErrors.ts b/resources/js/V2/types/ValidationErrors.ts similarity index 100% rename from resources/js/React/types/ValidationErrors.ts rename to resources/js/V2/types/ValidationErrors.ts diff --git a/resources/views/app_react.blade.php b/resources/views/app_react.blade.php index b4ec5e4b..0b1b41f0 100644 --- a/resources/views/app_react.blade.php +++ b/resources/views/app_react.blade.php @@ -15,7 +15,7 @@ @viteReactRefresh - @vite(['resources/js/React/app.tsx', 'resources/js/bootstrap.ts', 'resources/css/app.css']) + @vite(['resources/js/V2/app.tsx', 'resources/js/bootstrap.ts', 'resources/css/app.css'])
    diff --git a/routes/web.php b/routes/web.php index 4d7f6c8e..80ed1425 100644 --- a/routes/web.php +++ b/routes/web.php @@ -32,20 +32,10 @@ ]); }); -Route::any('/react', function () { - return view('app_react'); -}); - -Route::any('/react/{any}', function () { - return view('app_react'); -}); - -Route::any('/react/categories/create', function () { - return view('app_react'); -}); - -Route::any('/react/categories/{category}/edit', function () { - return view('app_react'); +Route::group(['prefix' => 'v2'], function () { + Route::any('{all?}', function() { + return view('app_react'); + })->where(['all' => '.*']); }); Route::middleware('auth')->group(function () { From c8f011d4ffab703656ae8ea408f332f95e8b8a2a Mon Sep 17 00:00:00 2001 From: Kaishiyoku Date: Sun, 10 Mar 2024 09:33:08 +0100 Subject: [PATCH 15/79] refactored code --- resources/js/V2/Core/ErrorPage.tsx | 11 ++++ .../Router/Actions/updateCategoryAction.ts | 24 +++++++++ .../Router}/Loaders/categoriesLoader.ts | 7 ++- .../Router}/Loaders/categoryLoader.ts | 4 +- .../Router}/Loaders/layoutLoader.ts | 3 +- resources/js/V2/Core/{ => Router}/router.tsx | 52 ++++--------------- resources/js/V2/app.tsx | 2 +- resources/js/V2/types/RouteHandle.ts | 3 +- 8 files changed, 57 insertions(+), 49 deletions(-) create mode 100644 resources/js/V2/Core/ErrorPage.tsx create mode 100644 resources/js/V2/Core/Router/Actions/updateCategoryAction.ts rename resources/js/V2/{Pages/Categories => Core/Router}/Loaders/categoriesLoader.ts (60%) rename resources/js/V2/{Pages/Categories => Core/Router}/Loaders/categoryLoader.ts (67%) rename resources/js/V2/{Pages => Core/Router}/Loaders/layoutLoader.ts (61%) rename resources/js/V2/Core/{ => Router}/router.tsx (53%) diff --git a/resources/js/V2/Core/ErrorPage.tsx b/resources/js/V2/Core/ErrorPage.tsx new file mode 100644 index 00000000..194647d4 --- /dev/null +++ b/resources/js/V2/Core/ErrorPage.tsx @@ -0,0 +1,11 @@ +import {isRouteErrorResponse, useRouteError} from 'react-router-dom'; + +export default function ErrorPage() { + const error = useRouteError(); + + if (isRouteErrorResponse(error)) { + return
    {error.status} {error.statusText}.
    ; + } + + return
    Unknown error occurred.
    ; +} diff --git a/resources/js/V2/Core/Router/Actions/updateCategoryAction.ts b/resources/js/V2/Core/Router/Actions/updateCategoryAction.ts new file mode 100644 index 00000000..adc7c088 --- /dev/null +++ b/resources/js/V2/Core/Router/Actions/updateCategoryAction.ts @@ -0,0 +1,24 @@ +import request from '@/V2/request'; +import {HTTPError} from 'ky'; +import ValidationErrors from '@/V2/types/ValidationErrors'; +import {ActionFunction} from '@remix-run/router/utils'; + +const updateCategoryAction: ActionFunction = async ({params, request: req}) => { + const formData = await req.formData(); + + try { + await request.put(`/api/categories/${params.categoryId}`, {json: Object.fromEntries(formData)}); + } catch (exception) { + const errorResponse = (exception as HTTPError).response; + + if (errorResponse.status !== 422) { + throw exception; + } + + return (await errorResponse.json() as { errors: ValidationErrors; }).errors; + } + + return null; +}; + +export default updateCategoryAction; diff --git a/resources/js/V2/Pages/Categories/Loaders/categoriesLoader.ts b/resources/js/V2/Core/Router/Loaders/categoriesLoader.ts similarity index 60% rename from resources/js/V2/Pages/Categories/Loaders/categoriesLoader.ts rename to resources/js/V2/Core/Router/Loaders/categoriesLoader.ts index fb87785b..63bd8ac3 100644 --- a/resources/js/V2/Pages/Categories/Loaders/categoriesLoader.ts +++ b/resources/js/V2/Core/Router/Loaders/categoriesLoader.ts @@ -1,8 +1,11 @@ import Category from '@/types/generated/Models/Category'; import request from '@/V2/request'; +import {LoaderFunction} from '@remix-run/router/utils'; -export default async function categoriesLoader() { +const categoriesLoader: LoaderFunction = async () => { const data = await request('/api/categories').json<{ categories: Category[]; canCreate: boolean; }>(); return data.categories; -} +}; + +export default categoriesLoader; diff --git a/resources/js/V2/Pages/Categories/Loaders/categoryLoader.ts b/resources/js/V2/Core/Router/Loaders/categoryLoader.ts similarity index 67% rename from resources/js/V2/Pages/Categories/Loaders/categoryLoader.ts rename to resources/js/V2/Core/Router/Loaders/categoryLoader.ts index 7a3dbf6f..11553d47 100644 --- a/resources/js/V2/Pages/Categories/Loaders/categoryLoader.ts +++ b/resources/js/V2/Core/Router/Loaders/categoryLoader.ts @@ -1,8 +1,8 @@ import Category from '@/types/generated/Models/Category'; import request from '@/V2/request'; -import {Params} from 'react-router-dom'; +import {LoaderFunction} from '@remix-run/router/utils'; -const categoryLoader = (suffix?: string) => async ({params}: { params: Params; }) => { +const categoryLoader = (suffix?: string): LoaderFunction => async ({params}) => { const data = await request(`/api/categories/${params.categoryId}${suffix}`).json<{ category: Category; canDelete: boolean; }>(); return data.category; diff --git a/resources/js/V2/Pages/Loaders/layoutLoader.ts b/resources/js/V2/Core/Router/Loaders/layoutLoader.ts similarity index 61% rename from resources/js/V2/Pages/Loaders/layoutLoader.ts rename to resources/js/V2/Core/Router/Loaders/layoutLoader.ts index 82826443..2fbe035f 100644 --- a/resources/js/V2/Pages/Loaders/layoutLoader.ts +++ b/resources/js/V2/Core/Router/Loaders/layoutLoader.ts @@ -1,7 +1,8 @@ import request from '@/V2/request'; import User from '@/types/generated/Models/User'; +import {LoaderFunction} from '@remix-run/router/utils'; -const layoutLoader = async () => { +const layoutLoader: LoaderFunction = async () => { return await request('/api/user').json(); }; diff --git a/resources/js/V2/Core/router.tsx b/resources/js/V2/Core/Router/router.tsx similarity index 53% rename from resources/js/V2/Core/router.tsx rename to resources/js/V2/Core/Router/router.tsx index 12fd6434..bc939a73 100644 --- a/resources/js/V2/Core/router.tsx +++ b/resources/js/V2/Core/Router/router.tsx @@ -1,5 +1,5 @@ import {createBrowserRouter} from 'react-router-dom'; -import categoriesLoader from '@/V2/Pages/Categories/Loaders/categoriesLoader'; +import categoriesLoader from '@/V2/Core/Router/Loaders/categoriesLoader'; import AuthenticatedLayout from '@/V2/Core/AuthenticatedLayout'; import CategoriesIndex from '@/V2/Pages/Categories/CategoriesIndex'; import CategoriesCreate from '@/V2/Pages/Categories/CategoriesCreate'; @@ -8,30 +8,24 @@ import {HTTPError} from 'ky'; import ValidationErrors from '@/V2/types/ValidationErrors'; import RouteHandle from '@/V2/types/RouteHandle'; import CategoriesEdit from '@/V2/Pages/Categories/CategoriesEdit'; -import categoryLoader from '@/V2/Pages/Categories/Loaders/categoryLoader'; -import layoutLoader from '@/V2/Pages/Loaders/layoutLoader'; - -const Error = () =>
    An error occurred.
    ; +import categoryLoader from '@/V2/Core/Router/Loaders/categoryLoader'; +import layoutLoader from '@/V2/Core/Router/Loaders/layoutLoader'; +import updateCategoryAction from '@/V2/Core/Router/Actions/updateCategoryAction'; +import ErrorPage from '@/V2/Core/ErrorPage'; const router = createBrowserRouter([ { path: '/v2', element: , - errorElement: , + errorElement: , loader: layoutLoader, - handle: { - title: 'Home', - headline: 'Home', - } as RouteHandle, + handle: {titleKey: 'Home'} as RouteHandle, children: [ { path: 'categories', element: , loader: categoriesLoader, - handle: { - title: 'Categories', - headline: 'Categories', - } as RouteHandle, + handle: {titleKey: 'Categories'} as RouteHandle, children: [ { path: 'create', @@ -53,38 +47,14 @@ const router = createBrowserRouter([ return null; }, - handle: { - hide: true, - title: 'Add category', - headline: 'Add category', - } as RouteHandle, + handle: {hide: true, titleKey: 'Add category'} as RouteHandle, }, { path: ':categoryId/edit', element: , loader: categoryLoader('/edit'), - action: async ({params, request: req}) => { - const formData = await req.formData(); - - try { - await request.put(`/api/categories/${params.categoryId}`, {json: Object.fromEntries(formData)}); - } catch (exception) { - const errorResponse = (exception as HTTPError).response; - - if (errorResponse.status !== 422) { - throw exception; - } - - return (await errorResponse.json() as { errors: ValidationErrors; }).errors; - } - - return null; - }, - handle: { - hide: true, - title: 'Edit category', - headline: 'Edit category', - } as RouteHandle, + action: updateCategoryAction, + handle: {hide: true, titleKey: 'Edit category'} as RouteHandle, }, ], }, diff --git a/resources/js/V2/app.tsx b/resources/js/V2/app.tsx index b5b0cb90..3b74ba98 100644 --- a/resources/js/V2/app.tsx +++ b/resources/js/V2/app.tsx @@ -1,6 +1,6 @@ import {createRoot} from 'react-dom/client'; import {RouterProvider} from 'react-router-dom'; -import router from '@/V2/Core/router'; +import router from '@/V2/Core/Router/router'; import NProgress from 'nprogress'; import {LaravelReactI18nProvider} from 'laravel-react-i18n'; import getBrowserLocale from '@/Utils/getBrowserLocale'; diff --git a/resources/js/V2/types/RouteHandle.ts b/resources/js/V2/types/RouteHandle.ts index 87d34629..8a63f68d 100644 --- a/resources/js/V2/types/RouteHandle.ts +++ b/resources/js/V2/types/RouteHandle.ts @@ -1,7 +1,6 @@ type RouteHandle = { hide?: boolean; - title: string; - headline: string; + titleKey: string; }; export default RouteHandle; From c6af55b1030151fb85f1c98617cc7ff0645f0978 Mon Sep 17 00:00:00 2001 From: Kaishiyoku Date: Sun, 10 Mar 2024 12:40:11 +0100 Subject: [PATCH 16/79] added logout and authentication checks to application --- .../Auth/AuthenticatedSessionController.php | 5 ++-- resources/js/Components/Button.tsx | 4 +-- resources/js/Components/Dropdown.tsx | 24 ++++++++++++++--- resources/js/Pages/Welcome.tsx | 4 +-- resources/js/V2/Contexts/AuthContext.ts | 7 +++++ resources/js/V2/Core/AuthProvider.tsx | 9 +++++++ resources/js/V2/Core/AuthenticatedLayout.tsx | 27 ++++++++++++++++--- resources/js/V2/Core/Breadcrumbs.tsx | 8 +++--- .../js/V2/Core/Router/Loaders/layoutLoader.ts | 15 ++++++++++- resources/js/V2/Core/Router/router.tsx | 5 ++-- resources/js/V2/Hooks/useAuth.ts | 6 +++++ resources/js/V2/Hooks/useBreadcrumbs.ts | 2 +- .../V2/Pages/Categories/CategoriesCreate.tsx | 4 +-- .../js/V2/Pages/Categories/CategoriesEdit.tsx | 4 +-- .../V2/Pages/Categories/CategoriesIndex.tsx | 4 +-- resources/js/V2/types/AuthContextType.ts | 8 ++++++ resources/js/V2/types/Breadcrumb.ts | 3 +-- routes/api.php | 7 +++-- routes/web.php | 3 ++- 19 files changed, 115 insertions(+), 34 deletions(-) create mode 100644 resources/js/V2/Contexts/AuthContext.ts create mode 100644 resources/js/V2/Core/AuthProvider.tsx create mode 100644 resources/js/V2/Hooks/useAuth.ts create mode 100644 resources/js/V2/types/AuthContextType.ts diff --git a/app/Http/Controllers/Auth/AuthenticatedSessionController.php b/app/Http/Controllers/Auth/AuthenticatedSessionController.php index 2fddead1..bb400066 100644 --- a/app/Http/Controllers/Auth/AuthenticatedSessionController.php +++ b/app/Http/Controllers/Auth/AuthenticatedSessionController.php @@ -5,6 +5,7 @@ use App\Http\Controllers\Controller; use App\Http\Requests\Auth\LoginRequest; use App\Providers\RouteServiceProvider; +use Illuminate\Http\JsonResponse; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; @@ -40,7 +41,7 @@ public function store(LoginRequest $request): RedirectResponse /** * Destroy an authenticated session. */ - public function destroy(Request $request): RedirectResponse + public function destroy(Request $request): JsonResponse { Auth::guard('web')->logout(); @@ -48,6 +49,6 @@ public function destroy(Request $request): RedirectResponse $request->session()->regenerateToken(); - return redirect('/'); + return response()->json(); } } diff --git a/resources/js/Components/Button.tsx b/resources/js/Components/Button.tsx index 73bdb8ef..759b59e0 100644 --- a/resources/js/Components/Button.tsx +++ b/resources/js/Components/Button.tsx @@ -77,9 +77,9 @@ const Button = ( type={type} className={clsx( 'inline-flex space-x-2 items-center text-left transition ease-in disabled:opacity-50 disabled:saturate-50 disabled:cursor-not-allowed', - 'text-sm tracking-widest font-semibold', + 'text-sm', { - 'rounded-lg px-5 py-2.5 shadow dark:shadow-black/25 focus:ring-0 focus:ring-black dark:focus:ring-white focus:shadow-none active:shadow-none': variant !== ButtonVariant.Headless, + 'font-semibold tracking-widest rounded-lg px-5 py-2.5 shadow dark:shadow-black/25 focus:ring-0 focus:ring-black dark:focus:ring-white focus:shadow-none active:shadow-none': variant !== ButtonVariant.Headless, 'shadow-md shadow-black/20 dark:shadow-black/25 text-violet-100 bg-violet-500 hover:bg-violet-700 disabled:hover:bg-violet-500': variant === ButtonVariant.Primary, 'text-gray-600 dark:text-gray-400 dark:hover:text-gray-100 bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600 disabled:hover:text-gray-600 dark:disabled:text-gray-500 disabled:hover:bg-white dark:disabled:hover:bg-gray-800': variant === ButtonVariant.Secondary, 'text-gray-600 dark:text-gray-400 dark:hover:text-gray-100 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 disabled:hover:text-gray-600 dark:disabled:text-gray-500 disabled:hover:bg-white dark:disabled:hover:bg-gray-800': variant === ButtonVariant.Tertiary, diff --git a/resources/js/Components/Dropdown.tsx b/resources/js/Components/Dropdown.tsx index 27b484cc..857dd617 100644 --- a/resources/js/Components/Dropdown.tsx +++ b/resources/js/Components/Dropdown.tsx @@ -1,17 +1,18 @@ import { - useState, createContext, - useContext, + Dispatch, Fragment, PropsWithChildren, - Dispatch, + ReactNode, SetStateAction, - ReactNode + useContext, + useState } from 'react'; import {Transition} from '@headlessui/react'; import noop from '@/Utils/noop'; import clsx from 'clsx'; import {Link} from 'react-router-dom'; +import {HeadlessButton} from '@/Components/Button'; type DropDownContextType = { open: boolean; @@ -116,11 +117,26 @@ const DropdownLink = ({to, active = false, className = '', children}: { to: stri ); }; +const DropdownButton = ({onClick, className = '', children}: { onClick: () => void; className?: string; children: ReactNode; }) => { + return ( + + {children} + + ) +}; + const Spacer = () =>
    ; Dropdown.Trigger = Trigger; Dropdown.Content = Content; Dropdown.Link = DropdownLink; +Dropdown.Button = DropdownButton; Dropdown.Spacer = Spacer; export default Dropdown; diff --git a/resources/js/Pages/Welcome.tsx b/resources/js/Pages/Welcome.tsx index 2ed65380..42ae22a0 100644 --- a/resources/js/Pages/Welcome.tsx +++ b/resources/js/Pages/Welcome.tsx @@ -14,9 +14,9 @@ export default function Welcome(props: WelcomeProps) {
    {props.auth.user ? ( - + {t('Dashboard')} - + ) : ( <> diff --git a/resources/js/V2/Contexts/AuthContext.ts b/resources/js/V2/Contexts/AuthContext.ts new file mode 100644 index 00000000..24de08cf --- /dev/null +++ b/resources/js/V2/Contexts/AuthContext.ts @@ -0,0 +1,7 @@ +import {createContext} from 'react'; +import AuthContextType from '@/V2/types/AuthContextType'; +import noop from '@/Utils/noop'; + +const AuthContext = createContext({user: null, setUser: noop}); + +export default AuthContext; diff --git a/resources/js/V2/Core/AuthProvider.tsx b/resources/js/V2/Core/AuthProvider.tsx new file mode 100644 index 00000000..e3f75094 --- /dev/null +++ b/resources/js/V2/Core/AuthProvider.tsx @@ -0,0 +1,9 @@ +import {ReactElement, useState} from 'react'; +import User from '@/types/generated/Models/User'; +import AuthContext from '@/V2/Contexts/AuthContext'; + +export default function AuthProvider({children}: { children: ReactElement; }) { + const [user, setUser] = useState(null); + + return {children}; +} diff --git a/resources/js/V2/Core/AuthenticatedLayout.tsx b/resources/js/V2/Core/AuthenticatedLayout.tsx index 73d305b0..f87b8535 100644 --- a/resources/js/V2/Core/AuthenticatedLayout.tsx +++ b/resources/js/V2/Core/AuthenticatedLayout.tsx @@ -5,20 +5,37 @@ import MatchWithHandle from '@/V2/types/MatchWithHandle'; import {useLaravelReactI18n} from 'laravel-react-i18n'; import Dropdown from '@/Components/Dropdown'; import DropdownArrowIcon from '@/Icons/DropdownArrowIcon'; +import useAuth from '@/V2/Hooks/useAuth'; import User from '@/types/generated/Models/User'; +import request from '@/V2/request'; const AuthenticatedLayout = () => { - const user = useLoaderData() as User; + const {user, setUser} = useAuth(); + const userData = useLoaderData() as User; const {t} = useLaravelReactI18n(); const location = useLocation(); const match = useMatches().find((match) => match.pathname === location.pathname) as MatchWithHandle; + useEffect(() => { + setUser(userData); + }, []); + useEffect(() => { document.querySelector('title')!.textContent = t(match?.handle.title); }, [location]); + const handleLogout = async () => { + await request.delete('/api/logout'); + + window.location.href = '/'; + }; + + if (!user) { + return null; + } + return ( -
    +
    @@ -37,9 +54,13 @@ const AuthenticatedLayout = () => { - + {t('Categories')} + + + {t('Logout')} +
    diff --git a/resources/js/V2/Core/Breadcrumbs.tsx b/resources/js/V2/Core/Breadcrumbs.tsx index 7da40537..0ce4346e 100644 --- a/resources/js/V2/Core/Breadcrumbs.tsx +++ b/resources/js/V2/Core/Breadcrumbs.tsx @@ -10,7 +10,7 @@ const LinkBreadcrumb = ({breadcrumb}: { breadcrumb: Breadcrumb; }) => { return (
  • - {t(breadcrumb.headline)} + {t(breadcrumb.titleKey)}
  • ); }; @@ -22,7 +22,7 @@ const TextBreadcrumb = ({breadcrumb}: { breadcrumb: Breadcrumb; }) => {
  • - {t(breadcrumb.headline)} + {t(breadcrumb.titleKey)}
  • @@ -61,8 +61,8 @@ export default function Breadcrumbs() {
      {breadcrumbs.map((breadcrumb, index) => index === breadcrumbs.length - 1 - ? - : + ? + : )}
    diff --git a/resources/js/V2/Core/Router/Loaders/layoutLoader.ts b/resources/js/V2/Core/Router/Loaders/layoutLoader.ts index 2fbe035f..0a9f6004 100644 --- a/resources/js/V2/Core/Router/Loaders/layoutLoader.ts +++ b/resources/js/V2/Core/Router/Loaders/layoutLoader.ts @@ -1,9 +1,22 @@ import request from '@/V2/request'; import User from '@/types/generated/Models/User'; import {LoaderFunction} from '@remix-run/router/utils'; +import {HTTPError} from 'ky'; const layoutLoader: LoaderFunction = async () => { - return await request('/api/user').json(); + try { + return await request('/api/user').json(); + } catch (error) { + const errorResponse = (error as HTTPError).response; + + if (errorResponse.status === 401) { + window.location.href = '/login'; + + return null; + } + + throw error; + } }; export default layoutLoader; diff --git a/resources/js/V2/Core/Router/router.tsx b/resources/js/V2/Core/Router/router.tsx index bc939a73..a4ca8e90 100644 --- a/resources/js/V2/Core/Router/router.tsx +++ b/resources/js/V2/Core/Router/router.tsx @@ -12,11 +12,12 @@ import categoryLoader from '@/V2/Core/Router/Loaders/categoryLoader'; import layoutLoader from '@/V2/Core/Router/Loaders/layoutLoader'; import updateCategoryAction from '@/V2/Core/Router/Actions/updateCategoryAction'; import ErrorPage from '@/V2/Core/ErrorPage'; +import AuthProvider from '@/V2/Core/AuthProvider'; const router = createBrowserRouter([ { - path: '/v2', - element: , + path: '/app', + element: , errorElement: , loader: layoutLoader, handle: {titleKey: 'Home'} as RouteHandle, diff --git a/resources/js/V2/Hooks/useAuth.ts b/resources/js/V2/Hooks/useAuth.ts new file mode 100644 index 00000000..e1d6e0cd --- /dev/null +++ b/resources/js/V2/Hooks/useAuth.ts @@ -0,0 +1,6 @@ +import {useContext} from 'react'; +import AuthContext from '@/V2/Contexts/AuthContext'; + +export default function useAuth() { + return useContext(AuthContext); +} diff --git a/resources/js/V2/Hooks/useBreadcrumbs.ts b/resources/js/V2/Hooks/useBreadcrumbs.ts index 34561f38..2a8512e4 100644 --- a/resources/js/V2/Hooks/useBreadcrumbs.ts +++ b/resources/js/V2/Hooks/useBreadcrumbs.ts @@ -10,6 +10,6 @@ export default function useBreadcrumbs() { .map((match) => { const handle = match.handle as RouteHandle; - return {pathname: match.pathname, title: handle.title, headline: handle.headline} as Breadcrumb; + return {pathname: match.pathname, titleKey: handle.titleKey} as Breadcrumb; }) as Breadcrumb[]; } diff --git a/resources/js/V2/Pages/Categories/CategoriesCreate.tsx b/resources/js/V2/Pages/Categories/CategoriesCreate.tsx index 7fbb82da..502b73e7 100644 --- a/resources/js/V2/Pages/Categories/CategoriesCreate.tsx +++ b/resources/js/V2/Pages/Categories/CategoriesCreate.tsx @@ -15,7 +15,7 @@ type CategoriesCreateValidationErrors = ValidationErrors & { name?: string; } | export default function CategoriesCreate() { const {t} = useLaravelReactI18n(); const errors = useActionData() as CategoriesCreateValidationErrors; - const {show, handleClose} = usePageModal(errors, '/v2/categories'); + const {show, handleClose} = usePageModal(errors, '/app/categories'); return ( @@ -24,7 +24,7 @@ export default function CategoriesCreate() { - +
    @@ -27,7 +27,7 @@ export default function CategoriesEdit() { - +
    - + {t('Add category')} @@ -32,7 +32,7 @@ export default function CategoriesIndex() { {categories.map((category) => (
    diff --git a/resources/js/V2/types/AuthContextType.ts b/resources/js/V2/types/AuthContextType.ts new file mode 100644 index 00000000..36a18406 --- /dev/null +++ b/resources/js/V2/types/AuthContextType.ts @@ -0,0 +1,8 @@ +import User from '@/types/generated/Models/User'; + +type AuthContextType = { + user: User | null; + setUser: (user: User | null) => void; +} + +export default AuthContextType; diff --git a/resources/js/V2/types/Breadcrumb.ts b/resources/js/V2/types/Breadcrumb.ts index bdef4cbd..5d3df70b 100644 --- a/resources/js/V2/types/Breadcrumb.ts +++ b/resources/js/V2/types/Breadcrumb.ts @@ -1,7 +1,6 @@ type Breadcrumb = { pathname: string; - title: string; - headline: string; + titleKey: string; }; export default Breadcrumb; diff --git a/routes/api.php b/routes/api.php index 0cf2f85d..5e86438e 100644 --- a/routes/api.php +++ b/routes/api.php @@ -1,6 +1,7 @@ get('/user', function (Request $request) { - return $request->user(); -}); - Route::middleware('auth:sanctum')->group(function () { + Route::get('/user', fn (Request $request) => $request->user()); + Route::delete('/logout', [AuthenticatedSessionController::class, 'destroy'])->name('logout'); Route::resource('categories', CategoryController::class); }); diff --git a/routes/web.php b/routes/web.php index 80ed1425..43839bd0 100644 --- a/routes/web.php +++ b/routes/web.php @@ -10,6 +10,7 @@ use App\Http\Controllers\ProfileController; use App\Http\Controllers\ToggleFeedItemController; use Illuminate\Support\Facades\Route; +use Illuminate\Http\Request; use Inertia\Inertia; /* @@ -32,7 +33,7 @@ ]); }); -Route::group(['prefix' => 'v2'], function () { +Route::group(['prefix' => 'app'], function () { Route::any('{all?}', function() { return view('app_react'); })->where(['all' => '.*']); From 4c197d6b4eb2e6b2201e95608a2f7c2f4604452b Mon Sep 17 00:00:00 2001 From: Kaishiyoku Date: Sun, 10 Mar 2024 12:44:15 +0100 Subject: [PATCH 17/79] added checks if you can create a new category --- .../js/V2/Core/Router/Loaders/categoriesLoader.ts | 6 ++---- resources/js/V2/Pages/Categories/CategoriesEdit.tsx | 1 - resources/js/V2/Pages/Categories/CategoriesIndex.tsx | 12 +++++++----- resources/js/V2/types/CategoriesLoaderType.ts | 7 +++++++ 4 files changed, 16 insertions(+), 10 deletions(-) create mode 100644 resources/js/V2/types/CategoriesLoaderType.ts diff --git a/resources/js/V2/Core/Router/Loaders/categoriesLoader.ts b/resources/js/V2/Core/Router/Loaders/categoriesLoader.ts index 63bd8ac3..4fccde75 100644 --- a/resources/js/V2/Core/Router/Loaders/categoriesLoader.ts +++ b/resources/js/V2/Core/Router/Loaders/categoriesLoader.ts @@ -1,11 +1,9 @@ -import Category from '@/types/generated/Models/Category'; import request from '@/V2/request'; import {LoaderFunction} from '@remix-run/router/utils'; +import CategoriesLoaderType from '@/V2/types/CategoriesLoaderType'; const categoriesLoader: LoaderFunction = async () => { - const data = await request('/api/categories').json<{ categories: Category[]; canCreate: boolean; }>(); - - return data.categories; + return await request('/api/categories').json(); }; export default categoriesLoader; diff --git a/resources/js/V2/Pages/Categories/CategoriesEdit.tsx b/resources/js/V2/Pages/Categories/CategoriesEdit.tsx index 127b90e8..7332d256 100644 --- a/resources/js/V2/Pages/Categories/CategoriesEdit.tsx +++ b/resources/js/V2/Pages/Categories/CategoriesEdit.tsx @@ -1,4 +1,3 @@ -import {Modal, ModalBody, ModalHeader} from '@/Components/Modal/Modal'; import {Form, useActionData, useLoaderData, useParams} from 'react-router-dom'; import TextInput from '@/Components/TextInput'; import usePageModal from '@/V2/Hooks/usePageModal'; diff --git a/resources/js/V2/Pages/Categories/CategoriesIndex.tsx b/resources/js/V2/Pages/Categories/CategoriesIndex.tsx index c220c0ce..a8f0c63a 100644 --- a/resources/js/V2/Pages/Categories/CategoriesIndex.tsx +++ b/resources/js/V2/Pages/Categories/CategoriesIndex.tsx @@ -5,18 +5,20 @@ import LinkStack from '@/Components/LinkStack'; import {isEmpty} from 'ramda'; import EmptyState from '@/Components/EmptyState'; import TagOutlineIcon from '@/Icons/TagOutlineIcon'; -import CategoryWithFeedsCount from '@/types/generated/Models/CategoryWithFeedsCount'; +import CategoriesLoaderType from '@/V2/types/CategoriesLoaderType'; export default function CategoriesIndex() { - const categories = useLoaderData() as CategoryWithFeedsCount[]; + const {categories, canCreate} = useLoaderData() as CategoriesLoaderType; const {t, tChoice} = useLaravelReactI18n(); return (
    - - {t('Add category')} - + {canCreate && ( + + {t('Add category')} + + )} {isEmpty(categories) diff --git a/resources/js/V2/types/CategoriesLoaderType.ts b/resources/js/V2/types/CategoriesLoaderType.ts new file mode 100644 index 00000000..b627174a --- /dev/null +++ b/resources/js/V2/types/CategoriesLoaderType.ts @@ -0,0 +1,7 @@ +import CategoryWithFeedsCount from '@/types/generated/Models/CategoryWithFeedsCount'; + +type CategoriesLoaderType = { + categories: CategoryWithFeedsCount[]; + canCreate: boolean; +} +export default CategoriesLoaderType; From c58bbe076c1de4bae8643c442ff48f54eb96177a Mon Sep 17 00:00:00 2001 From: Kaishiyoku Date: Sun, 10 Mar 2024 14:13:34 +0100 Subject: [PATCH 18/79] added the ability to delete categories --- .../Controllers/Api/CategoryController.php | 5 ++- resources/css/app.css | 3 ++ resources/js/Components/Actions.tsx | 6 ++-- resources/js/Components/Button.tsx | 15 ++++++-- resources/js/Components/Modal/Pane.tsx | 2 +- resources/js/V2/Core/AuthenticatedLayout.tsx | 2 +- .../Router/Actions/updateCategoryAction.ts | 6 ++++ .../V2/Core/Router/Loaders/categoryLoader.ts | 6 ++-- .../js/V2/Pages/Categories/CategoriesEdit.tsx | 36 ++++++++++++++++--- resources/js/V2/types/CategoryLoaderType.ts | 8 +++++ resources/js/V2/types/MatchWithHandle.tsx | 6 ++-- 11 files changed, 72 insertions(+), 23 deletions(-) create mode 100644 resources/js/V2/types/CategoryLoaderType.ts diff --git a/app/Http/Controllers/Api/CategoryController.php b/app/Http/Controllers/Api/CategoryController.php index 13d23ec0..66086715 100644 --- a/app/Http/Controllers/Api/CategoryController.php +++ b/app/Http/Controllers/Api/CategoryController.php @@ -7,7 +7,6 @@ use App\Http\Requests\UpdateCategoryRequest; use App\Models\Category; use Illuminate\Http\JsonResponse; -use Illuminate\Http\RedirectResponse; use Illuminate\Support\Facades\Auth; use Inertia\Inertia; use Inertia\Response; @@ -78,10 +77,10 @@ public function update(UpdateCategoryRequest $request, Category $category): Json /** * Remove the specified resource from storage. */ - public function destroy(Category $category): RedirectResponse + public function destroy(Category $category): JsonResponse { $category->delete(); - return redirect()->route('categories.index'); + return response()->json(); } } diff --git a/resources/css/app.css b/resources/css/app.css index 43c113fa..cd5e65cd 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -92,6 +92,9 @@ body { .link-light { @apply font-semibold text-violet-200 hover:text-white focus:outline focus:outline-2 focus:rounded-sm focus:outline-white/50 transition; } + .link-danger { + @apply font-semibold text-pink-600 hover:text-pink-900 dark:text-pink-400 dark:hover:text-white focus:outline focus:outline-2 focus:rounded-sm focus:outline-pink-500 transition; + } /** * Buttons diff --git a/resources/js/Components/Actions.tsx b/resources/js/Components/Actions.tsx index 2115de17..b0dd5d3e 100644 --- a/resources/js/Components/Actions.tsx +++ b/resources/js/Components/Actions.tsx @@ -21,7 +21,7 @@ const MobileActions = ({children}: { children: ReactNode; }) => { }); return ( -
    +
    setShow(true)} @@ -77,14 +77,14 @@ const MobileActions = ({children}: { children: ReactNode; }) => { ); }; -export default function Actions({className = '', children}: { className?: string; children: ReactNode; }) { +export default function Actions({footer = false, className = '', children}: { footer?: boolean; className?: string; children: ReactNode; }) { if (!children) { return null; } return ( <> -
    +
    {children}
    diff --git a/resources/js/Components/Button.tsx b/resources/js/Components/Button.tsx index 759b59e0..73757079 100644 --- a/resources/js/Components/Button.tsx +++ b/resources/js/Components/Button.tsx @@ -19,6 +19,8 @@ type ButtonProps = { className?: string; disabled?: boolean; onClick?: (event: React.MouseEvent) => void; + name?: string; + value?: string | number; children?: ReactNode; }; @@ -38,6 +40,8 @@ const Button = ( className = '', disabled = false, onClick = noop, + name, + value, children, confirm = false, confirmTitle, @@ -49,6 +53,8 @@ const Button = ( const handleOnClick = (event: React.MouseEvent) => { if (confirm) { + event.preventDefault(); + setShowConfirmModal(true); return; @@ -75,11 +81,12 @@ const Button = (
    diff --git a/resources/js/V2/Core/AuthenticatedLayout.tsx b/resources/js/V2/Core/AuthenticatedLayout.tsx index 1ea21859..c0c69950 100644 --- a/resources/js/V2/Core/AuthenticatedLayout.tsx +++ b/resources/js/V2/Core/AuthenticatedLayout.tsx @@ -58,6 +58,10 @@ const AuthenticatedLayout = () => { {t('Categories')} + + {t('Feeds')} + + {t('Logout')} diff --git a/resources/js/V2/Core/ErrorPage.tsx b/resources/js/V2/Core/ErrorPage.tsx index 194647d4..10b5b72b 100644 --- a/resources/js/V2/Core/ErrorPage.tsx +++ b/resources/js/V2/Core/ErrorPage.tsx @@ -1,11 +1,16 @@ -import {isRouteErrorResponse, useRouteError} from 'react-router-dom'; +import {isRouteErrorResponse, useAsyncError, useRouteError} from 'react-router-dom'; export default function ErrorPage() { const error = useRouteError(); + const asyncError = useAsyncError(); + + if (asyncError) { + return
    Async error occurred: {JSON.stringify({asyncError})}
    ; + } if (isRouteErrorResponse(error)) { return
    {error.status} {error.statusText}.
    ; } - return
    Unknown error occurred.
    ; + return
    Unknown error occurred: {JSON.stringify(error)}
    ; } diff --git a/resources/js/V2/Core/Router/Actions/createFeedAction.ts b/resources/js/V2/Core/Router/Actions/createFeedAction.ts new file mode 100644 index 00000000..6a6ae9ff --- /dev/null +++ b/resources/js/V2/Core/Router/Actions/createFeedAction.ts @@ -0,0 +1,15 @@ +import request from '@/V2/request'; +import {ActionFunction} from '@remix-run/router/utils'; +import handleRequestValidationError from '@/V2/Core/Router/Helpers/handleRequestValidationError'; + +const createFeedAction: ActionFunction = async ({request: req}) => { + const formData = await req.formData(); + + if (!formData.get('is_purgeable')) { + formData.append('is_purgeable', '0'); + } + + return await handleRequestValidationError(() => request.post('/api/feeds', {json: Object.fromEntries(formData)})); +}; + +export default createFeedAction; diff --git a/resources/js/V2/Core/Router/Loaders/createFeedLoader.ts b/resources/js/V2/Core/Router/Loaders/createFeedLoader.ts new file mode 100644 index 00000000..867c57df --- /dev/null +++ b/resources/js/V2/Core/Router/Loaders/createFeedLoader.ts @@ -0,0 +1,9 @@ +import request from '@/V2/request'; +import {LoaderFunction} from '@remix-run/router/utils'; +import CreateFeedLoaderType from '@/V2/types/CreateFeedLoaderType'; + +const createFeedLoader: LoaderFunction = async () => { + return await request('/api/feeds/create').json(); +}; + +export default createFeedLoader; diff --git a/resources/js/V2/Core/Router/Loaders/feedsLoader.ts b/resources/js/V2/Core/Router/Loaders/feedsLoader.ts new file mode 100644 index 00000000..8d668bed --- /dev/null +++ b/resources/js/V2/Core/Router/Loaders/feedsLoader.ts @@ -0,0 +1,9 @@ +import request from '@/V2/request'; +import {LoaderFunction} from '@remix-run/router/utils'; +import FeedsLoaderType from '@/V2/types/FeedsLoaderType'; + +const feedsLoader: LoaderFunction = async () => { + return await request('/api/feeds').json(); +}; + +export default feedsLoader; diff --git a/resources/js/V2/Core/Router/router.tsx b/resources/js/V2/Core/Router/router.tsx index 9af935c7..cd07c56c 100644 --- a/resources/js/V2/Core/Router/router.tsx +++ b/resources/js/V2/Core/Router/router.tsx @@ -11,6 +11,11 @@ import updateCategoryAction from '@/V2/Core/Router/Actions/updateCategoryAction' import ErrorPage from '@/V2/Core/ErrorPage'; import AuthProvider from '@/V2/Core/AuthProvider'; import createCategoryAction from '@/V2/Core/Router/Actions/createCategoryAction'; +import FeedsIndexPage from '@/V2/Pages/Feeds/FeedsIndexPage'; +import feedsLoader from '@/V2/Core/Router/Loaders/feedsLoader'; +import CreateFeedPage from '@/V2/Pages/Feeds/CreateFeedPage'; +import createFeedLoader from '@/V2/Core/Router/Loaders/createFeedLoader'; +import createFeedAction from '@/V2/Core/Router/Actions/createFeedAction'; const router = createBrowserRouter([ { @@ -41,6 +46,21 @@ const router = createBrowserRouter([ }, ], }, + { + path: 'feeds', + element: , + loader: feedsLoader, + handle: {titleKey: 'Feeds'} as RouteHandle, + children: [ + { + path: 'create', + element: , + loader: createFeedLoader, + action: createFeedAction, + handle: {hide: true, titleKey: 'Add feed'} as RouteHandle, + }, + ], + }, ], }, ]); diff --git a/resources/js/V2/Pages/Feeds/CreateFeedPage.tsx b/resources/js/V2/Pages/Feeds/CreateFeedPage.tsx new file mode 100644 index 00000000..8ece7ad2 --- /dev/null +++ b/resources/js/V2/Pages/Feeds/CreateFeedPage.tsx @@ -0,0 +1,220 @@ +import {Form, useActionData, useLoaderData} from 'react-router-dom'; +import TextInput from '@/Components/TextInput'; +import usePageModal from '@/V2/Hooks/usePageModal'; +import ValidationErrors from '@/V2/types/ValidationErrors'; +import InputError from '@/Components/InputError'; +import {PrimaryButton, SecondaryButton} from '@/Components/Button'; +import InputLabel from '@/Components/InputLabel'; +import React, {useImperativeHandle, useRef, useState} from 'react'; +import {useLaravelReactI18n} from 'laravel-react-i18n'; +import {Pane, PaneBody, PaneHeader} from '@/Components/Modal/Pane'; +import Select from '@/Components/Select'; +import Checkbox from '@/Components/Checkbox'; +import useAuth from '@/V2/Hooks/useAuth'; +import CreateFeedLoaderType from '@/V2/types/CreateFeedLoaderType'; +import request from '@/V2/request'; +import DiscoveredFeed from '@/types/DiscoveredFeed'; +import LinkStack from '@/Components/LinkStack'; + +type FeedsCreateValidationErrors = ValidationErrors & { + name?: string; + category_id?: string; + language?: string; + feed_url?: string; + site_url?: string; + favicon_url?: string; +} | null; + +export default function CreateFeedPage() { + const {t, tChoice} = useLaravelReactI18n(); + const {user} = useAuth(); + const {categories} = useLoaderData() as CreateFeedLoaderType; + const errors = useActionData() as FeedsCreateValidationErrors; + const {show, handleClose} = usePageModal(errors, '/app/feeds'); + const [isDiscoverFeedProcessing, setIsDiscoverFeedProcessing] = useState(false); + const [searchUrl, setSearchUrl] = useState(''); + const [discoveredFeedUrls, setDiscoveredFeedUrls] = useState([]); + const nameRef = useRef(null); + const languageRef = useRef(null); + const feedUrlRef = useRef(null); + const siteUrlRef = useRef(null); + const faviconUrlRef = useRef(null); + + const discoverFeedUrls = (searchUrl: string) => { + setDiscoveredFeedUrls([]); + setIsDiscoverFeedProcessing(true); + + request.post('/api/discover-feed-urls', {json: {feed_url: searchUrl}}) + .json() + .then((data) => { + setDiscoveredFeedUrls(data); + }) + .catch((error) => { + console.error(error); + }) + .finally(() => setIsDiscoverFeedProcessing(false)); + }; + + const selectDiscoveredFeedUrl = (feedUrl: string) => () => { + setIsDiscoverFeedProcessing(true); + + request.post('/api/discover-feed', {json: {feed_url: feedUrl}}) + .json() + .then((responseData) => { + nameRef.current!.value = responseData.name; + languageRef.current!.value = responseData.language; + feedUrlRef.current!.value = responseData.feed_url; + siteUrlRef.current!.value = responseData.site_url; + faviconUrlRef.current!.value = responseData.favicon_url; + + setSearchUrl(''); + setDiscoveredFeedUrls([]); + }) + .catch((error) => { + console.error(error); + }) + .finally(() => setIsDiscoverFeedProcessing(false)); + }; + + return ( + + + {t('Add feed')} + + + +
    +
    + setSearchUrl(e.target.value)} + isFocused + /> + + discoverFeedUrls(searchUrl)} + disabled={isDiscoverFeedProcessing || searchUrl.length < 5} + > + {t('Search')} + +
    + + {discoveredFeedUrls.length > 0 && ( + + {discoveredFeedUrls.map((discoveredFeedUrl) => ( + + ))} + + )} + +
    + + + +
    + +
    + + + + + +
    + +
    + + + + + +
    + +
    + + + + + +
    + +
    + + + + + +
    + +
    + + + + + +
    + +
    + +
    + {t('Save')} @@ -60,8 +237,8 @@ export default function EditFeedPage() { {canDelete && ( group(function () { Route::get('/user', fn (Request $request) => $request->user()); Route::delete('/logout', [AuthenticatedSessionController::class, 'destroy'])->name('logout'); + Route::resource('categories', CategoryController::class); Route::resource('feeds', FeedController::class); From e69b437a194d2fcf9736fffe5badf67179a5f17f Mon Sep 17 00:00:00 2001 From: Kaishiyoku Date: Mon, 11 Mar 2024 22:33:51 +0100 Subject: [PATCH 26/79] refactored code --- .../{updateCategoryAction.ts => editCategoryAction.ts} | 4 ++-- .../Actions/{updateFeedAction.ts => editFeedAction.ts} | 4 ++-- resources/js/V2/Core/Router/router.tsx | 8 ++++---- resources/js/V2/Pages/Categories/CreateCategoryPage.tsx | 4 ++-- resources/js/V2/Pages/Categories/EditCategoryPage.tsx | 4 ++-- resources/js/V2/Pages/Feeds/CreateFeedPage.tsx | 7 ++++--- resources/js/V2/Pages/Feeds/EditFeedPage.tsx | 1 + 7 files changed, 17 insertions(+), 15 deletions(-) rename resources/js/V2/Core/Router/Actions/{updateCategoryAction.ts => editCategoryAction.ts} (81%) rename resources/js/V2/Core/Router/Actions/{updateFeedAction.ts => editFeedAction.ts} (84%) diff --git a/resources/js/V2/Core/Router/Actions/updateCategoryAction.ts b/resources/js/V2/Core/Router/Actions/editCategoryAction.ts similarity index 81% rename from resources/js/V2/Core/Router/Actions/updateCategoryAction.ts rename to resources/js/V2/Core/Router/Actions/editCategoryAction.ts index c506b830..59f1b3fe 100644 --- a/resources/js/V2/Core/Router/Actions/updateCategoryAction.ts +++ b/resources/js/V2/Core/Router/Actions/editCategoryAction.ts @@ -2,7 +2,7 @@ import request from '@/V2/request'; import {ActionFunction} from '@remix-run/router/utils'; import handleRequestValidationError from '@/V2/Core/Router/Helpers/handleRequestValidationError'; -const updateCategoryAction: ActionFunction = async ({params, request: req}) => { +const editCategoryAction: ActionFunction = async ({params, request: req}) => { const formData = await req.formData(); if (formData.get('intent') === 'delete') { @@ -14,4 +14,4 @@ const updateCategoryAction: ActionFunction = async ({params, request: req}) => { return await handleRequestValidationError(() => request.put(`/api/categories/${params.categoryId}`, {json: Object.fromEntries(formData)})); }; -export default updateCategoryAction; +export default editCategoryAction; diff --git a/resources/js/V2/Core/Router/Actions/updateFeedAction.ts b/resources/js/V2/Core/Router/Actions/editFeedAction.ts similarity index 84% rename from resources/js/V2/Core/Router/Actions/updateFeedAction.ts rename to resources/js/V2/Core/Router/Actions/editFeedAction.ts index 762fac43..9e9b542f 100644 --- a/resources/js/V2/Core/Router/Actions/updateFeedAction.ts +++ b/resources/js/V2/Core/Router/Actions/editFeedAction.ts @@ -2,7 +2,7 @@ import request from '@/V2/request'; import {ActionFunction} from '@remix-run/router/utils'; import handleRequestValidationError from '@/V2/Core/Router/Helpers/handleRequestValidationError'; -const updateFeedAction: ActionFunction = async ({params, request: req}) => { +const editFeedAction: ActionFunction = async ({params, request: req}) => { const formData = await req.formData(); if (!formData.get('is_purgeable')) { @@ -18,4 +18,4 @@ const updateFeedAction: ActionFunction = async ({params, request: req}) => { return await handleRequestValidationError(() => request.put(`/api/feeds/${params.feedId}`, {json: Object.fromEntries(formData)})); }; -export default updateFeedAction; +export default editFeedAction; diff --git a/resources/js/V2/Core/Router/router.tsx b/resources/js/V2/Core/Router/router.tsx index 1eda898a..a60df6b7 100644 --- a/resources/js/V2/Core/Router/router.tsx +++ b/resources/js/V2/Core/Router/router.tsx @@ -7,7 +7,7 @@ import RouteHandle from '@/V2/types/RouteHandle'; import EditCategoryPage from '@/V2/Pages/Categories/EditCategoryPage'; import editCategoryLoader from '@/V2/Core/Router/Loaders/editCategoryLoader'; import layoutLoader from '@/V2/Core/Router/Loaders/layoutLoader'; -import updateCategoryAction from '@/V2/Core/Router/Actions/updateCategoryAction'; +import editCategoryAction from '@/V2/Core/Router/Actions/editCategoryAction'; import ErrorPage from '@/V2/Core/ErrorPage'; import AuthProvider from '@/V2/Core/AuthProvider'; import createCategoryAction from '@/V2/Core/Router/Actions/createCategoryAction'; @@ -18,7 +18,7 @@ import createFeedLoader from '@/V2/Core/Router/Loaders/createFeedLoader'; import createFeedAction from '@/V2/Core/Router/Actions/createFeedAction'; import EditFeedPage from '@/V2/Pages/Feeds/EditFeedPage'; import editFeedLoader from '@/V2/Core/Router/Loaders/editFeedLoader'; -import updateFeedAction from '@/V2/Core/Router/Actions/updateFeedAction'; +import editFeedAction from '@/V2/Core/Router/Actions/editFeedAction'; const router = createBrowserRouter([ { @@ -44,7 +44,7 @@ const router = createBrowserRouter([ path: ':categoryId/edit', element: , loader: editCategoryLoader, - action: updateCategoryAction, + action: editCategoryAction, handle: {hide: true, titleKey: 'Edit category'} as RouteHandle, }, ], @@ -66,7 +66,7 @@ const router = createBrowserRouter([ path: ':feedId/edit', element: , loader: editFeedLoader, - action: updateFeedAction, + action: editFeedAction, handle: {hide: true, titleKey: 'Edit feed'} as RouteHandle, }, ], diff --git a/resources/js/V2/Pages/Categories/CreateCategoryPage.tsx b/resources/js/V2/Pages/Categories/CreateCategoryPage.tsx index 1393666c..248c098b 100644 --- a/resources/js/V2/Pages/Categories/CreateCategoryPage.tsx +++ b/resources/js/V2/Pages/Categories/CreateCategoryPage.tsx @@ -9,11 +9,11 @@ import React from 'react'; import {useLaravelReactI18n} from 'laravel-react-i18n'; import {Pane, PaneBody, PaneHeader} from '@/Components/Modal/Pane'; -type CategoriesCreateValidationErrors = ValidationErrors & { name?: string; } | null; +type CreateCategoryValidationErrors = ValidationErrors & { name?: string; } | null; export default function CreateCategoryPage() { const {t} = useLaravelReactI18n(); - const errors = useActionData() as CategoriesCreateValidationErrors; + const errors = useActionData() as CreateCategoryValidationErrors; const {show, handleClose} = usePageModal(errors, '/app/categories'); return ( diff --git a/resources/js/V2/Pages/Categories/EditCategoryPage.tsx b/resources/js/V2/Pages/Categories/EditCategoryPage.tsx index c402f397..bcc9e331 100644 --- a/resources/js/V2/Pages/Categories/EditCategoryPage.tsx +++ b/resources/js/V2/Pages/Categories/EditCategoryPage.tsx @@ -11,13 +11,13 @@ import {Pane, PaneBody, PaneFooter, PaneHeader} from '@/Components/Modal/Pane'; import EditCategoryLoaderType from '@/V2/types/EditCategoryLoaderType'; import Actions from '@/Components/Actions'; -type CategoriesCreateValidationErrors = ValidationErrors & { name?: string; } | null; +type EditCategoryValidationErrors = ValidationErrors & { name?: string; } | null; export default function EditCategoryPage() { const {t} = useLaravelReactI18n(); const {categoryId} = useParams(); const {category, canDelete} = useLoaderData() as EditCategoryLoaderType; - const errors = useActionData() as CategoriesCreateValidationErrors; + const errors = useActionData() as EditCategoryValidationErrors; const {show, handleClose} = usePageModal(errors, '/app/categories'); const submit = useSubmit(); diff --git a/resources/js/V2/Pages/Feeds/CreateFeedPage.tsx b/resources/js/V2/Pages/Feeds/CreateFeedPage.tsx index 8ece7ad2..8d105d38 100644 --- a/resources/js/V2/Pages/Feeds/CreateFeedPage.tsx +++ b/resources/js/V2/Pages/Feeds/CreateFeedPage.tsx @@ -5,7 +5,7 @@ import ValidationErrors from '@/V2/types/ValidationErrors'; import InputError from '@/Components/InputError'; import {PrimaryButton, SecondaryButton} from '@/Components/Button'; import InputLabel from '@/Components/InputLabel'; -import React, {useImperativeHandle, useRef, useState} from 'react'; +import React, {useRef, useState} from 'react'; import {useLaravelReactI18n} from 'laravel-react-i18n'; import {Pane, PaneBody, PaneHeader} from '@/Components/Modal/Pane'; import Select from '@/Components/Select'; @@ -16,7 +16,7 @@ import request from '@/V2/request'; import DiscoveredFeed from '@/types/DiscoveredFeed'; import LinkStack from '@/Components/LinkStack'; -type FeedsCreateValidationErrors = ValidationErrors & { +type CreateFeedValidationErrors = ValidationErrors & { name?: string; category_id?: string; language?: string; @@ -29,11 +29,12 @@ export default function CreateFeedPage() { const {t, tChoice} = useLaravelReactI18n(); const {user} = useAuth(); const {categories} = useLoaderData() as CreateFeedLoaderType; - const errors = useActionData() as FeedsCreateValidationErrors; + const errors = useActionData() as CreateFeedValidationErrors; const {show, handleClose} = usePageModal(errors, '/app/feeds'); const [isDiscoverFeedProcessing, setIsDiscoverFeedProcessing] = useState(false); const [searchUrl, setSearchUrl] = useState(''); const [discoveredFeedUrls, setDiscoveredFeedUrls] = useState([]); + const nameRef = useRef(null); const languageRef = useRef(null); const feedUrlRef = useRef(null); diff --git a/resources/js/V2/Pages/Feeds/EditFeedPage.tsx b/resources/js/V2/Pages/Feeds/EditFeedPage.tsx index 335cd949..120cf97d 100644 --- a/resources/js/V2/Pages/Feeds/EditFeedPage.tsx +++ b/resources/js/V2/Pages/Feeds/EditFeedPage.tsx @@ -37,6 +37,7 @@ export default function EditFeedPage() { const [isDiscoverFeedProcessing, setIsDiscoverFeedProcessing] = useState(false); const [searchUrl, setSearchUrl] = useState(''); const [discoveredFeedUrls, setDiscoveredFeedUrls] = useState([]); + const nameRef = useRef(null); const languageRef = useRef(null); const feedUrlRef = useRef(null); From ebca2ee1f75fa72bf7a21e8719dc6ac0c09b16cf Mon Sep 17 00:00:00 2001 From: Kaishiyoku Date: Mon, 11 Mar 2024 22:41:54 +0100 Subject: [PATCH 27/79] refactored code --- .../Pages/Categories/CreateCategoryPage.tsx | 29 +++------------ .../V2/Pages/Categories/EditCategoryPage.tsx | 35 +++++-------------- .../Categories/Partials/CategoryForm.tsx | 35 +++++++++++++++++++ .../types/CreateCategoryValidationErrors.ts | 7 ++++ .../V2/types/EditCategoryValidationErrors.ts | 7 ++++ 5 files changed, 62 insertions(+), 51 deletions(-) create mode 100644 resources/js/V2/Pages/Categories/Partials/CategoryForm.tsx create mode 100644 resources/js/V2/types/CreateCategoryValidationErrors.ts create mode 100644 resources/js/V2/types/EditCategoryValidationErrors.ts diff --git a/resources/js/V2/Pages/Categories/CreateCategoryPage.tsx b/resources/js/V2/Pages/Categories/CreateCategoryPage.tsx index 248c098b..168f5cdb 100644 --- a/resources/js/V2/Pages/Categories/CreateCategoryPage.tsx +++ b/resources/js/V2/Pages/Categories/CreateCategoryPage.tsx @@ -1,15 +1,10 @@ -import {Form, useActionData} from 'react-router-dom'; -import TextInput from '@/Components/TextInput'; +import {useActionData} from 'react-router-dom'; import usePageModal from '@/V2/Hooks/usePageModal'; -import ValidationErrors from '@/V2/types/ValidationErrors'; -import InputError from '@/Components/InputError'; -import {PrimaryButton} from '@/Components/Button'; -import InputLabel from '@/Components/InputLabel'; import React from 'react'; import {useLaravelReactI18n} from 'laravel-react-i18n'; import {Pane, PaneBody, PaneHeader} from '@/Components/Modal/Pane'; - -type CreateCategoryValidationErrors = ValidationErrors & { name?: string; } | null; +import CreateCategoryValidationErrors from '@/V2/types/CreateCategoryValidationErrors'; +import CategoryForm from '@/V2/Pages/Categories/Partials/CategoryForm'; export default function CreateCategoryPage() { const {t} = useLaravelReactI18n(); @@ -23,23 +18,7 @@ export default function CreateCategoryPage() { - -
    - - - -
    - - - {t('Save')} - - +
    ); diff --git a/resources/js/V2/Pages/Categories/EditCategoryPage.tsx b/resources/js/V2/Pages/Categories/EditCategoryPage.tsx index bcc9e331..f4af6969 100644 --- a/resources/js/V2/Pages/Categories/EditCategoryPage.tsx +++ b/resources/js/V2/Pages/Categories/EditCategoryPage.tsx @@ -1,17 +1,13 @@ -import {Form, useActionData, useLoaderData, useParams, useSubmit} from 'react-router-dom'; -import TextInput from '@/Components/TextInput'; +import {useActionData, useLoaderData, useParams, useSubmit} from 'react-router-dom'; import usePageModal from '@/V2/Hooks/usePageModal'; -import ValidationErrors from '@/V2/types/ValidationErrors'; -import InputError from '@/Components/InputError'; -import {HeadlessButton, PrimaryButton} from '@/Components/Button'; -import InputLabel from '@/Components/InputLabel'; +import {HeadlessButton} from '@/Components/Button'; import React from 'react'; import {useLaravelReactI18n} from 'laravel-react-i18n'; import {Pane, PaneBody, PaneFooter, PaneHeader} from '@/Components/Modal/Pane'; import EditCategoryLoaderType from '@/V2/types/EditCategoryLoaderType'; import Actions from '@/Components/Actions'; - -type EditCategoryValidationErrors = ValidationErrors & { name?: string; } | null; +import CategoryForm from '@/V2/Pages/Categories/Partials/CategoryForm'; +import EditCategoryValidationErrors from '@/V2/types/EditCategoryValidationErrors'; export default function EditCategoryPage() { const {t} = useLaravelReactI18n(); @@ -35,24 +31,11 @@ export default function EditCategoryPage() { -
    -
    - - - -
    - - - {t('Save')} - -
    +
    diff --git a/resources/js/V2/Pages/Categories/Partials/CategoryForm.tsx b/resources/js/V2/Pages/Categories/Partials/CategoryForm.tsx new file mode 100644 index 00000000..9aa39390 --- /dev/null +++ b/resources/js/V2/Pages/Categories/Partials/CategoryForm.tsx @@ -0,0 +1,35 @@ +import InputLabel from '@/Components/InputLabel'; +import TextInput from '@/Components/TextInput'; +import InputError from '@/Components/InputError'; +import {PrimaryButton} from '@/Components/Button'; +import React from 'react'; +import CreateCategoryValidationErrors from '@/V2/types/CreateCategoryValidationErrors'; +import {useLaravelReactI18n} from 'laravel-react-i18n'; +import {Form} from 'react-router-dom'; +import EditCategoryValidationErrors from '@/V2/types/EditCategoryValidationErrors'; +import Category from '@/types/generated/Models/Category'; + +export default function CategoryForm({action, category = null, errors}: { action: string; category: Category | null; errors: CreateCategoryValidationErrors | EditCategoryValidationErrors; }) { + const {t} = useLaravelReactI18n(); + + return ( +
    +
    + + + +
    + + + {t('Save')} + +
    + ); +} diff --git a/resources/js/V2/types/CreateCategoryValidationErrors.ts b/resources/js/V2/types/CreateCategoryValidationErrors.ts new file mode 100644 index 00000000..a2a495b6 --- /dev/null +++ b/resources/js/V2/types/CreateCategoryValidationErrors.ts @@ -0,0 +1,7 @@ +import ValidationErrors from '@/V2/types/ValidationErrors'; + +type CreateCategoryValidationErrors = ValidationErrors & { + name?: string; +} | null; + +export default CreateCategoryValidationErrors; diff --git a/resources/js/V2/types/EditCategoryValidationErrors.ts b/resources/js/V2/types/EditCategoryValidationErrors.ts new file mode 100644 index 00000000..1867a051 --- /dev/null +++ b/resources/js/V2/types/EditCategoryValidationErrors.ts @@ -0,0 +1,7 @@ +import ValidationErrors from '@/V2/types/ValidationErrors'; + +type EditCategoryValidationErrors = ValidationErrors & { + name?: string; +} | null; + +export default EditCategoryValidationErrors; From 929c77dcaaf530d4906066ae630505514708a05e Mon Sep 17 00:00:00 2001 From: Kaishiyoku Date: Mon, 11 Mar 2024 22:52:30 +0100 Subject: [PATCH 28/79] refactored code --- .../Pages/Categories/CreateCategoryPage.tsx | 5 +- .../Categories/Partials/CategoryForm.tsx | 2 +- .../js/V2/Pages/Feeds/CreateFeedPage.tsx | 210 +---------------- resources/js/V2/Pages/Feeds/EditFeedPage.tsx | 218 +----------------- .../js/V2/Pages/Feeds/Partials/FeedForm.tsx | 209 +++++++++++++++++ .../js/V2/types/CreateFeedValidationErrors.ts | 12 + .../js/V2/types/EditFeedValidationErrors.ts | 12 + 7 files changed, 260 insertions(+), 408 deletions(-) create mode 100644 resources/js/V2/Pages/Feeds/Partials/FeedForm.tsx create mode 100644 resources/js/V2/types/CreateFeedValidationErrors.ts create mode 100644 resources/js/V2/types/EditFeedValidationErrors.ts diff --git a/resources/js/V2/Pages/Categories/CreateCategoryPage.tsx b/resources/js/V2/Pages/Categories/CreateCategoryPage.tsx index 168f5cdb..c1e6adc2 100644 --- a/resources/js/V2/Pages/Categories/CreateCategoryPage.tsx +++ b/resources/js/V2/Pages/Categories/CreateCategoryPage.tsx @@ -18,7 +18,10 @@ export default function CreateCategoryPage() { - + ); diff --git a/resources/js/V2/Pages/Categories/Partials/CategoryForm.tsx b/resources/js/V2/Pages/Categories/Partials/CategoryForm.tsx index 9aa39390..42c9f6d4 100644 --- a/resources/js/V2/Pages/Categories/Partials/CategoryForm.tsx +++ b/resources/js/V2/Pages/Categories/Partials/CategoryForm.tsx @@ -9,7 +9,7 @@ import {Form} from 'react-router-dom'; import EditCategoryValidationErrors from '@/V2/types/EditCategoryValidationErrors'; import Category from '@/types/generated/Models/Category'; -export default function CategoryForm({action, category = null, errors}: { action: string; category: Category | null; errors: CreateCategoryValidationErrors | EditCategoryValidationErrors; }) { +export default function CategoryForm({action, category = null, errors}: { action: string; category?: Category | null; errors: CreateCategoryValidationErrors | EditCategoryValidationErrors; }) { const {t} = useLaravelReactI18n(); return ( diff --git a/resources/js/V2/Pages/Feeds/CreateFeedPage.tsx b/resources/js/V2/Pages/Feeds/CreateFeedPage.tsx index 8d105d38..28f06eea 100644 --- a/resources/js/V2/Pages/Feeds/CreateFeedPage.tsx +++ b/resources/js/V2/Pages/Feeds/CreateFeedPage.tsx @@ -1,81 +1,17 @@ -import {Form, useActionData, useLoaderData} from 'react-router-dom'; -import TextInput from '@/Components/TextInput'; +import {useActionData, useLoaderData} from 'react-router-dom'; import usePageModal from '@/V2/Hooks/usePageModal'; -import ValidationErrors from '@/V2/types/ValidationErrors'; -import InputError from '@/Components/InputError'; -import {PrimaryButton, SecondaryButton} from '@/Components/Button'; -import InputLabel from '@/Components/InputLabel'; -import React, {useRef, useState} from 'react'; +import React from 'react'; import {useLaravelReactI18n} from 'laravel-react-i18n'; import {Pane, PaneBody, PaneHeader} from '@/Components/Modal/Pane'; -import Select from '@/Components/Select'; -import Checkbox from '@/Components/Checkbox'; -import useAuth from '@/V2/Hooks/useAuth'; import CreateFeedLoaderType from '@/V2/types/CreateFeedLoaderType'; -import request from '@/V2/request'; -import DiscoveredFeed from '@/types/DiscoveredFeed'; -import LinkStack from '@/Components/LinkStack'; - -type CreateFeedValidationErrors = ValidationErrors & { - name?: string; - category_id?: string; - language?: string; - feed_url?: string; - site_url?: string; - favicon_url?: string; -} | null; +import CreateFeedValidationErrors from '@/V2/types/CreateFeedValidationErrors'; +import FeedForm from '@/V2/Pages/Feeds/Partials/FeedForm'; export default function CreateFeedPage() { - const {t, tChoice} = useLaravelReactI18n(); - const {user} = useAuth(); + const {t} = useLaravelReactI18n(); const {categories} = useLoaderData() as CreateFeedLoaderType; const errors = useActionData() as CreateFeedValidationErrors; const {show, handleClose} = usePageModal(errors, '/app/feeds'); - const [isDiscoverFeedProcessing, setIsDiscoverFeedProcessing] = useState(false); - const [searchUrl, setSearchUrl] = useState(''); - const [discoveredFeedUrls, setDiscoveredFeedUrls] = useState([]); - - const nameRef = useRef(null); - const languageRef = useRef(null); - const feedUrlRef = useRef(null); - const siteUrlRef = useRef(null); - const faviconUrlRef = useRef(null); - - const discoverFeedUrls = (searchUrl: string) => { - setDiscoveredFeedUrls([]); - setIsDiscoverFeedProcessing(true); - - request.post('/api/discover-feed-urls', {json: {feed_url: searchUrl}}) - .json() - .then((data) => { - setDiscoveredFeedUrls(data); - }) - .catch((error) => { - console.error(error); - }) - .finally(() => setIsDiscoverFeedProcessing(false)); - }; - - const selectDiscoveredFeedUrl = (feedUrl: string) => () => { - setIsDiscoverFeedProcessing(true); - - request.post('/api/discover-feed', {json: {feed_url: feedUrl}}) - .json() - .then((responseData) => { - nameRef.current!.value = responseData.name; - languageRef.current!.value = responseData.language; - feedUrlRef.current!.value = responseData.feed_url; - siteUrlRef.current!.value = responseData.site_url; - faviconUrlRef.current!.value = responseData.favicon_url; - - setSearchUrl(''); - setDiscoveredFeedUrls([]); - }) - .catch((error) => { - console.error(error); - }) - .finally(() => setIsDiscoverFeedProcessing(false)); - }; return ( @@ -84,137 +20,11 @@ export default function CreateFeedPage() { -
    -
    - setSearchUrl(e.target.value)} - isFocused - /> - - discoverFeedUrls(searchUrl)} - disabled={isDiscoverFeedProcessing || searchUrl.length < 5} - > - {t('Search')} - -
    - - {discoveredFeedUrls.length > 0 && ( - - {discoveredFeedUrls.map((discoveredFeedUrl) => ( - - ))} - - )} - -
    - - - -
    - -
    - - - - - -
    - -
    - - - - - -
    - -
    - - - - - -
    - -
    - - - - - -
    - -
    - - - - - -
    - -
    - -
    - - - {t('Save')} - -
    +
    diff --git a/resources/js/V2/Pages/Feeds/Partials/FeedForm.tsx b/resources/js/V2/Pages/Feeds/Partials/FeedForm.tsx new file mode 100644 index 00000000..b41b0d1b --- /dev/null +++ b/resources/js/V2/Pages/Feeds/Partials/FeedForm.tsx @@ -0,0 +1,209 @@ +import InputLabel from '@/Components/InputLabel'; +import TextInput from '@/Components/TextInput'; +import InputError from '@/Components/InputError'; +import {PrimaryButton, SecondaryButton} from '@/Components/Button'; +import React, {useRef, useState} from 'react'; +import {useLaravelReactI18n} from 'laravel-react-i18n'; +import {Form} from 'react-router-dom'; +import Feed from '@/types/generated/Models/Feed'; +import CreateFeedValidationErrors from '@/V2/types/CreateFeedValidationErrors'; +import EditFeedValidationErrors from '@/V2/types/EditFeedValidationErrors'; +import LinkStack from '@/Components/LinkStack'; +import Select from '@/Components/Select'; +import Checkbox from '@/Components/Checkbox'; +import request from '@/V2/request'; +import DiscoveredFeed from '@/types/DiscoveredFeed'; +import {SelectNumberOption} from '@/types/SelectOption'; +import useAuth from '@/V2/Hooks/useAuth'; + +export default function FeedForm({action, feed = null, categories, errors}: { action: string; feed?: Feed | null; categories: SelectNumberOption[]; errors: CreateFeedValidationErrors | EditFeedValidationErrors; }) { + const {t, tChoice} = useLaravelReactI18n(); + + const {user} = useAuth(); + + const [searchUrl, setSearchUrl] = useState(''); + const [discoveredFeedUrls, setDiscoveredFeedUrls] = useState([]); + const [isDiscoverFeedProcessing, setIsDiscoverFeedProcessing] = useState(false); + + const nameRef = useRef(null); + const languageRef = useRef(null); + const feedUrlRef = useRef(null); + const siteUrlRef = useRef(null); + const faviconUrlRef = useRef(null); + + const discoverFeedUrls = (searchUrl: string) => { + setDiscoveredFeedUrls([]); + setIsDiscoverFeedProcessing(true); + + request.post('/api/discover-feed-urls', {json: {feed_url: searchUrl}}) + .json() + .then((data) => { + setDiscoveredFeedUrls(data); + }) + .catch((error) => { + console.error(error); + }) + .finally(() => setIsDiscoverFeedProcessing(false)); + }; + + const selectDiscoveredFeedUrl = (feedUrl: string) => () => { + setIsDiscoverFeedProcessing(true); + + request.post('/api/discover-feed', {json: {feed_url: feedUrl}}) + .json() + .then((responseData) => { + nameRef.current!.value = responseData.name; + languageRef.current!.value = responseData.language; + feedUrlRef.current!.value = responseData.feed_url; + siteUrlRef.current!.value = responseData.site_url; + faviconUrlRef.current!.value = responseData.favicon_url; + + setSearchUrl(''); + setDiscoveredFeedUrls([]); + }) + .catch((error) => { + console.error(error); + }) + .finally(() => setIsDiscoverFeedProcessing(false)); + }; + + return ( +
    +
    + setSearchUrl(e.target.value)} + isFocused + /> + + discoverFeedUrls(searchUrl)} + disabled={isDiscoverFeedProcessing || searchUrl.length < 5} + > + {t('Search')} + +
    + + {discoveredFeedUrls.length > 0 && ( + + {discoveredFeedUrls.map((discoveredFeedUrl) => ( + + ))} + + )} + +
    + + + +
    + +
    + + + ) => setData('category_id', parseInt(event.currentTarget.value, 10))} - disabled={isDiscoverFeedProcessing} - required - /> - - -
    - -
    - - - setData('language', e.target.value)} - disabled={isDiscoverFeedProcessing} - required - /> - - -
    -
    - -
    -
    - - - setData('feed_url', e.target.value)} - disabled={isDiscoverFeedProcessing} - required - /> - - -
    - -
    - - - setData('site_url', e.target.value)} - disabled={isDiscoverFeedProcessing} - required - /> - - -
    - -
    - - - setData('favicon_url', e.target.value)} - disabled={isDiscoverFeedProcessing} - /> - - -
    -
    - -
    - -
    - -
    - - {t('Save')} - -
    - - - ); -} diff --git a/resources/js/V2/Pages/Home.tsx b/resources/js/Pages/Home.tsx similarity index 96% rename from resources/js/V2/Pages/Home.tsx rename to resources/js/Pages/Home.tsx index 6afd58e2..83949006 100644 --- a/resources/js/V2/Pages/Home.tsx +++ b/resources/js/Pages/Home.tsx @@ -1,14 +1,14 @@ import {useFetcher, useLoaderData, useNavigate, useSearchParams} from 'react-router-dom'; -import FeedItemsLoaderType from '@/V2/types/FeedItemsLoaderType'; +import FeedItemsLoaderType from '@/types/FeedItemsLoaderType'; import {useLaravelReactI18n} from 'laravel-react-i18n'; import {HeadlessButton, TertiaryButton} from '@/Components/Button'; import {useContext, useEffect, useState} from 'react'; import {isEmpty, length} from 'ramda'; import NewspaperSolidIcon from '@/Icons/NewspaperSolidIcon'; import EmptyState from '@/Components/EmptyState'; -import request from '@/V2/request'; +import request from '@/Core/request'; import FeedItemCard from '@/Components/FeedItemCard'; -import TotalNumberOfFeedItemsContext from '@/V2/Contexts/TotalNumberOfFeedItemsContext'; +import TotalNumberOfFeedItemsContext from '@/Contexts/TotalNumberOfFeedItemsContext'; import FeedFilterDropdown from '@/Components/FeedFilterDropdown'; import EyeOutlineIcon from '@/Icons/EyeOutlineIcon'; diff --git a/resources/js/Pages/Profile/Edit.tsx b/resources/js/Pages/Profile/Edit.tsx deleted file mode 100644 index 5237333a..00000000 --- a/resources/js/Pages/Profile/Edit.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout'; -import DeleteUserForm from './Partials/DeleteUserForm'; -import UpdatePasswordForm from './Partials/UpdatePasswordForm'; -import UpdateProfileInformationForm from './Partials/UpdateProfileInformationForm'; -import {Head} from '@inertiajs/react'; -import Header from '@/Components/Page/Header'; -import {BasePageProps} from '@/types'; - -export default function Edit({auth, errors, mustVerifyEmail, status}: BasePageProps & { mustVerifyEmail: boolean; status: string; }) { - return ( - Profile
    } - errors={errors} - > - - -
    - - - - - -
    -
    - ); -} diff --git a/resources/js/Pages/Profile/Partials/DeleteUserForm.tsx b/resources/js/Pages/Profile/Partials/DeleteUserForm.tsx index ac9906e0..5787e2c8 100644 --- a/resources/js/Pages/Profile/Partials/DeleteUserForm.tsx +++ b/resources/js/Pages/Profile/Partials/DeleteUserForm.tsx @@ -7,7 +7,7 @@ import {DangerButton, SecondaryButton} from '@/Components/Button'; import {useLaravelReactI18n} from 'laravel-react-i18n'; import Card from '@/Components/Card'; import {Form, useActionData} from 'react-router-dom'; -import UpdatePasswordValidationErrors from '@/V2/types/UpdatePasswordValidationErrors'; +import UpdatePasswordValidationErrors from '@/types/UpdatePasswordValidationErrors'; export default function DeleteUserForm() { const {t} = useLaravelReactI18n(); diff --git a/resources/js/Pages/Profile/Partials/UpdatePasswordForm.tsx b/resources/js/Pages/Profile/Partials/UpdatePasswordForm.tsx index e9260548..b9ae160d 100644 --- a/resources/js/Pages/Profile/Partials/UpdatePasswordForm.tsx +++ b/resources/js/Pages/Profile/Partials/UpdatePasswordForm.tsx @@ -6,7 +6,7 @@ import TextInput from '@/Components/TextInput'; import {PrimaryButton} from '@/Components/Button'; import Card from '@/Components/Card'; import {Form, useActionData} from 'react-router-dom'; -import UpdatePasswordValidationErrors from '@/V2/types/UpdatePasswordValidationErrors'; +import UpdatePasswordValidationErrors from '@/types/UpdatePasswordValidationErrors'; export default function UpdatePasswordForm() { const {t} = useLaravelReactI18n(); diff --git a/resources/js/Pages/Profile/Partials/UpdateProfileInformationForm.tsx b/resources/js/Pages/Profile/Partials/UpdateProfileInformationForm.tsx index fb385500..32d20af8 100644 --- a/resources/js/Pages/Profile/Partials/UpdateProfileInformationForm.tsx +++ b/resources/js/Pages/Profile/Partials/UpdateProfileInformationForm.tsx @@ -7,7 +7,7 @@ import React from 'react'; import Card from '@/Components/Card'; import User from '@/types/generated/Models/User'; import {Form, useActionData} from 'react-router-dom'; -import UpdateProfileValidationErrors from '@/V2/types/UpdateProfileValidationErrors'; +import UpdateProfileValidationErrors from '@/types/UpdateProfileValidationErrors'; export default function UpdateProfileInformation({mustVerifyEmail, status, user}: { mustVerifyEmail: boolean; status: string; user: User; }) { const {t} = useLaravelReactI18n(); diff --git a/resources/js/V2/Pages/ProfilePage.tsx b/resources/js/Pages/ProfilePage.tsx similarity index 92% rename from resources/js/V2/Pages/ProfilePage.tsx rename to resources/js/Pages/ProfilePage.tsx index 8d36d1aa..2b2f7df7 100644 --- a/resources/js/V2/Pages/ProfilePage.tsx +++ b/resources/js/Pages/ProfilePage.tsx @@ -1,6 +1,6 @@ import {useLoaderData} from 'react-router-dom'; import UpdateProfileInformation from '@/Pages/Profile/Partials/UpdateProfileInformationForm'; -import ProfileLoaderType from '@/V2/types/ProfileLoaderType'; +import ProfileLoaderType from '@/types/ProfileLoaderType'; import UpdatePasswordForm from '@/Pages/Profile/Partials/UpdatePasswordForm'; import DeleteUserForm from '@/Pages/Profile/Partials/DeleteUserForm'; diff --git a/resources/js/V2/Utils/wait.ts b/resources/js/Utils/wait.ts similarity index 100% rename from resources/js/V2/Utils/wait.ts rename to resources/js/Utils/wait.ts diff --git a/resources/js/V2/Contexts/TotalNumberOfFeedItemsContext.ts b/resources/js/V2/Contexts/TotalNumberOfFeedItemsContext.ts deleted file mode 100644 index 94e49a04..00000000 --- a/resources/js/V2/Contexts/TotalNumberOfFeedItemsContext.ts +++ /dev/null @@ -1,10 +0,0 @@ -import {createContext} from 'react'; -import TotalNumberOfFeedItemsContextType from '@/V2/types/TotalNumberOfFeedItemsContextType'; -import noop from '@/Utils/noop'; - -const TotalNumberOfFeedItemsContext = createContext({ - totalNumberOfFeedItems: 0, - setTotalNumberOfFeedItems: noop, -}); - -export default TotalNumberOfFeedItemsContext; diff --git a/resources/js/V2/app.tsx b/resources/js/V2/app.tsx deleted file mode 100644 index ee89308b..00000000 --- a/resources/js/V2/app.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import {createRoot} from 'react-dom/client'; -import NProgress from 'nprogress'; -import {LaravelReactI18nProvider} from 'laravel-react-i18n'; -import getBrowserLocale from '@/Utils/getBrowserLocale'; -import AppWithLoadedTranslations from '@/Components/AppWithLoadedTranslations'; -import AppWithToasts from '@/V2/AppWithToasts'; - -NProgress.configure({ - showSpinner: false, -}); - -const App = () => { - return ( - - - - ); -}; - -createRoot(document.getElementById('app')!) - .render(); diff --git a/resources/js/V2/types/Breadcrumb.ts b/resources/js/V2/types/Breadcrumb.ts deleted file mode 100644 index 5d3df70b..00000000 --- a/resources/js/V2/types/Breadcrumb.ts +++ /dev/null @@ -1,6 +0,0 @@ -type Breadcrumb = { - pathname: string; - titleKey: string; -}; - -export default Breadcrumb; diff --git a/resources/js/app.tsx b/resources/js/app.tsx index ce37aa55..26443657 100644 --- a/resources/js/app.tsx +++ b/resources/js/app.tsx @@ -1,30 +1,25 @@ -import './bootstrap'; -import '../css/app.css'; - import {createRoot} from 'react-dom/client'; -import {createInertiaApp} from '@inertiajs/react'; -import {resolvePageComponent} from 'laravel-vite-plugin/inertia-helpers'; +import NProgress from 'nprogress'; import {LaravelReactI18nProvider} from 'laravel-react-i18n'; import getBrowserLocale from '@/Utils/getBrowserLocale'; import AppWithLoadedTranslations from '@/Components/AppWithLoadedTranslations'; +import AppWithToasts from '@/AppWithToasts'; -window.appName = window.document.getElementsByTagName('title')[0]?.innerText || 'Laravel'; - -void createInertiaApp({ - title: (title: string): string => `${title} - ${window.appName}`, - resolve: (name: string) => resolvePageComponent(`./Pages/${name}.tsx`, import.meta.glob('./Pages/**/*.tsx')), - setup({el, App, props}) { - createRoot(el).render( - - - - ); - }, - progress: { - color: '#7c3aed', - }, +NProgress.configure({ + showSpinner: false, }); + +const App = () => { + return ( + + + + ); +}; + +createRoot(document.getElementById('app')!) + .render(); diff --git a/resources/js/V2/types/AuthContextType.ts b/resources/js/types/AuthContextType.ts similarity index 100% rename from resources/js/V2/types/AuthContextType.ts rename to resources/js/types/AuthContextType.ts diff --git a/resources/js/types/Breadcrumb.ts b/resources/js/types/Breadcrumb.ts index 0cc283ff..5d3df70b 100644 --- a/resources/js/types/Breadcrumb.ts +++ b/resources/js/types/Breadcrumb.ts @@ -1,6 +1,6 @@ type Breadcrumb = { - title: string; - url: string | null; + pathname: string; + titleKey: string; }; export default Breadcrumb; diff --git a/resources/js/V2/types/CategoriesLoaderType.ts b/resources/js/types/CategoriesLoaderType.ts similarity index 100% rename from resources/js/V2/types/CategoriesLoaderType.ts rename to resources/js/types/CategoriesLoaderType.ts diff --git a/resources/js/V2/types/CreateCategoryValidationErrors.ts b/resources/js/types/CreateCategoryValidationErrors.ts similarity index 69% rename from resources/js/V2/types/CreateCategoryValidationErrors.ts rename to resources/js/types/CreateCategoryValidationErrors.ts index a2a495b6..620e5363 100644 --- a/resources/js/V2/types/CreateCategoryValidationErrors.ts +++ b/resources/js/types/CreateCategoryValidationErrors.ts @@ -1,4 +1,4 @@ -import ValidationErrors from '@/V2/types/ValidationErrors'; +import ValidationErrors from '@/types/ValidationErrors'; type CreateCategoryValidationErrors = ValidationErrors & { name?: string; diff --git a/resources/js/V2/types/CreateFeedLoaderType.ts b/resources/js/types/CreateFeedLoaderType.ts similarity index 100% rename from resources/js/V2/types/CreateFeedLoaderType.ts rename to resources/js/types/CreateFeedLoaderType.ts diff --git a/resources/js/V2/types/CreateFeedValidationErrors.ts b/resources/js/types/CreateFeedValidationErrors.ts similarity index 80% rename from resources/js/V2/types/CreateFeedValidationErrors.ts rename to resources/js/types/CreateFeedValidationErrors.ts index 4390c3c6..93b7144c 100644 --- a/resources/js/V2/types/CreateFeedValidationErrors.ts +++ b/resources/js/types/CreateFeedValidationErrors.ts @@ -1,4 +1,4 @@ -import ValidationErrors from '@/V2/types/ValidationErrors'; +import ValidationErrors from '@/types/ValidationErrors'; type CreateFeedValidationErrors = ValidationErrors & { name?: string; diff --git a/resources/js/V2/types/EditCategoryLoaderType.ts b/resources/js/types/EditCategoryLoaderType.ts similarity index 100% rename from resources/js/V2/types/EditCategoryLoaderType.ts rename to resources/js/types/EditCategoryLoaderType.ts diff --git a/resources/js/V2/types/EditCategoryValidationErrors.ts b/resources/js/types/EditCategoryValidationErrors.ts similarity index 68% rename from resources/js/V2/types/EditCategoryValidationErrors.ts rename to resources/js/types/EditCategoryValidationErrors.ts index 1867a051..cb689f4d 100644 --- a/resources/js/V2/types/EditCategoryValidationErrors.ts +++ b/resources/js/types/EditCategoryValidationErrors.ts @@ -1,4 +1,4 @@ -import ValidationErrors from '@/V2/types/ValidationErrors'; +import ValidationErrors from '@/types/ValidationErrors'; type EditCategoryValidationErrors = ValidationErrors & { name?: string; diff --git a/resources/js/V2/types/EditFeddLoaderType.ts b/resources/js/types/EditFeddLoaderType.ts similarity index 100% rename from resources/js/V2/types/EditFeddLoaderType.ts rename to resources/js/types/EditFeddLoaderType.ts diff --git a/resources/js/V2/types/EditFeedValidationErrors.ts b/resources/js/types/EditFeedValidationErrors.ts similarity index 80% rename from resources/js/V2/types/EditFeedValidationErrors.ts rename to resources/js/types/EditFeedValidationErrors.ts index 89281422..5f2597ba 100644 --- a/resources/js/V2/types/EditFeedValidationErrors.ts +++ b/resources/js/types/EditFeedValidationErrors.ts @@ -1,4 +1,4 @@ -import ValidationErrors from '@/V2/types/ValidationErrors'; +import ValidationErrors from '@/types/ValidationErrors'; type EditFeedValidationErrors = ValidationErrors & { name?: string; diff --git a/resources/js/V2/types/FeedItemsLoaderType.ts b/resources/js/types/FeedItemsLoaderType.ts similarity index 100% rename from resources/js/V2/types/FeedItemsLoaderType.ts rename to resources/js/types/FeedItemsLoaderType.ts diff --git a/resources/js/V2/types/FeedsLoaderType.ts b/resources/js/types/FeedsLoaderType.ts similarity index 100% rename from resources/js/V2/types/FeedsLoaderType.ts rename to resources/js/types/FeedsLoaderType.ts diff --git a/resources/js/V2/types/HomeLocationState.ts b/resources/js/types/HomeLocationState.ts similarity index 100% rename from resources/js/V2/types/HomeLocationState.ts rename to resources/js/types/HomeLocationState.ts diff --git a/resources/js/V2/types/MatchWithHandle.tsx b/resources/js/types/MatchWithHandle.tsx similarity index 73% rename from resources/js/V2/types/MatchWithHandle.tsx rename to resources/js/types/MatchWithHandle.tsx index 3f6e539a..ab9cd18f 100644 --- a/resources/js/V2/types/MatchWithHandle.tsx +++ b/resources/js/types/MatchWithHandle.tsx @@ -1,5 +1,5 @@ import {UIMatch} from 'react-router-dom'; -import RouteHandle from '@/V2/types/RouteHandle'; +import RouteHandle from '@/types/RouteHandle'; type MatchWithHandle = UIMatch & { handle: RouteHandle; diff --git a/resources/js/V2/types/ProfileLoaderType.ts b/resources/js/types/ProfileLoaderType.ts similarity index 100% rename from resources/js/V2/types/ProfileLoaderType.ts rename to resources/js/types/ProfileLoaderType.ts diff --git a/resources/js/V2/types/RouteHandle.ts b/resources/js/types/RouteHandle.ts similarity index 100% rename from resources/js/V2/types/RouteHandle.ts rename to resources/js/types/RouteHandle.ts diff --git a/resources/js/V2/types/TotalNumberOfFeedItemsContextType.ts b/resources/js/types/TotalNumberOfFeedItemsContextType.ts similarity index 100% rename from resources/js/V2/types/TotalNumberOfFeedItemsContextType.ts rename to resources/js/types/TotalNumberOfFeedItemsContextType.ts diff --git a/resources/js/V2/types/UpdatePasswordValidationErrors.ts b/resources/js/types/UpdatePasswordValidationErrors.ts similarity index 77% rename from resources/js/V2/types/UpdatePasswordValidationErrors.ts rename to resources/js/types/UpdatePasswordValidationErrors.ts index 9dee9ada..bdf7e1c0 100644 --- a/resources/js/V2/types/UpdatePasswordValidationErrors.ts +++ b/resources/js/types/UpdatePasswordValidationErrors.ts @@ -1,4 +1,4 @@ -import ValidationErrors from '@/V2/types/ValidationErrors'; +import ValidationErrors from '@/types/ValidationErrors'; type UpdatePasswordValidationErrors = ValidationErrors & { current_password?: string; diff --git a/resources/js/V2/types/UpdateProfileValidationErrors.ts b/resources/js/types/UpdateProfileValidationErrors.ts similarity index 72% rename from resources/js/V2/types/UpdateProfileValidationErrors.ts rename to resources/js/types/UpdateProfileValidationErrors.ts index 37ea24de..2112636c 100644 --- a/resources/js/V2/types/UpdateProfileValidationErrors.ts +++ b/resources/js/types/UpdateProfileValidationErrors.ts @@ -1,4 +1,4 @@ -import ValidationErrors from '@/V2/types/ValidationErrors'; +import ValidationErrors from '@/types/ValidationErrors'; type UpdateProfileValidationErrors = ValidationErrors & { name?: string; diff --git a/resources/js/V2/types/UsersLoaderType.ts b/resources/js/types/UsersLoaderType.ts similarity index 100% rename from resources/js/V2/types/UsersLoaderType.ts rename to resources/js/types/UsersLoaderType.ts diff --git a/resources/js/V2/types/ValidationErrors.ts b/resources/js/types/ValidationErrors.ts similarity index 100% rename from resources/js/V2/types/ValidationErrors.ts rename to resources/js/types/ValidationErrors.ts diff --git a/resources/views/app.blade.php b/resources/views/app.blade.php index ad1d4bee..636141f8 100644 --- a/resources/views/app.blade.php +++ b/resources/views/app.blade.php @@ -5,7 +5,7 @@ - {{ config('app.name', 'Laravel') }} + {{ config('app.name', 'Laravel') }} @include('partials.favicon') @@ -14,12 +14,10 @@ - @routes @viteReactRefresh - @vite(['resources/js/app.tsx', "resources/js/Pages/{$page['component']}.tsx"]) - @inertiaHead + @vite(['resources/js/app.tsx', 'resources/js/bootstrap.ts', 'resources/css/app.css']) - @inertia +
    diff --git a/resources/views/app_react.blade.php b/resources/views/app_inertia.blade.php similarity index 77% rename from resources/views/app_react.blade.php rename to resources/views/app_inertia.blade.php index 0b1b41f0..4d951662 100644 --- a/resources/views/app_react.blade.php +++ b/resources/views/app_inertia.blade.php @@ -5,7 +5,7 @@ - {{ config('app.name', 'Laravel') }} + {{ config('app.name', 'Laravel') }} @include('partials.favicon') @@ -14,10 +14,12 @@ + @routes @viteReactRefresh - @vite(['resources/js/V2/app.tsx', 'resources/js/bootstrap.ts', 'resources/css/app.css']) + @vite(['resources/js/Inertia/app.tsx', "resources/js/Inertia/Pages/{$page['component']}.tsx"]) + @inertiaHead -
    + @inertia diff --git a/routes/web.php b/routes/web.php index 8ce2b341..4132c447 100644 --- a/routes/web.php +++ b/routes/web.php @@ -25,7 +25,7 @@ Route::get('/', function (Request $request) { if ($request->user()) { - return view('app_react'); + return view('app'); } return Inertia::render('Welcome', [ @@ -37,8 +37,6 @@ }); Route::middleware('auth')->group(function () { - Route::get('/dashboard', DashboardController::class)->name('dashboard'); - Route::middleware('verified')->group(function () { }); @@ -47,5 +45,5 @@ require __DIR__.'/auth.php'; Route::get('{all?}', function() { - return view('app_react'); + return view('app'); })->where(['all' => '.*']); diff --git a/vite.config.mjs b/vite.config.mjs index d63e56f3..71ca4ce4 100644 --- a/vite.config.mjs +++ b/vite.config.mjs @@ -6,7 +6,7 @@ import i18n from 'laravel-react-i18n/vite'; export default defineConfig({ plugins: [ laravel({ - input: 'resources/js/app.tsx', + input: 'resources/js/Inertia/app.tsx', refresh: true, }), react(), From 60da69b56e8ad9e46be8d323ccda7215100d61a6 Mon Sep 17 00:00:00 2001 From: Kaishiyoku Date: Fri, 15 Mar 2024 21:44:41 +0100 Subject: [PATCH 52/79] renamed request function --- resources/js/Components/FeedItemCard.tsx | 4 ++-- resources/js/Core/AuthenticatedLayout.tsx | 4 ++-- .../js/Core/Router/Actions/createCategoryAction.ts | 8 ++++---- resources/js/Core/Router/Actions/createFeedAction.ts | 8 ++++---- .../js/Core/Router/Actions/editCategoryAction.ts | 10 +++++----- resources/js/Core/Router/Actions/editFeedAction.ts | 10 +++++----- resources/js/Core/Router/Actions/profileAction.ts | 12 ++++++------ resources/js/Core/Router/Actions/usersAction.ts | 8 ++++---- resources/js/Core/Router/Loaders/categoriesLoader.ts | 4 ++-- resources/js/Core/Router/Loaders/createFeedLoader.ts | 4 ++-- .../js/Core/Router/Loaders/editCategoryLoader.ts | 4 ++-- resources/js/Core/Router/Loaders/editFeedLoader.ts | 4 ++-- resources/js/Core/Router/Loaders/feedItemsLoader.ts | 8 ++++---- resources/js/Core/Router/Loaders/feedsLoader.ts | 4 ++-- resources/js/Core/Router/Loaders/layoutLoader.ts | 4 ++-- resources/js/Core/Router/Loaders/profileLoader.ts | 4 ++-- resources/js/Core/Router/Loaders/usersLoader.ts | 4 ++-- resources/js/Core/{request.ts => rq.ts} | 4 ++-- resources/js/Pages/Feeds/Partials/FeedForm.tsx | 6 +++--- resources/js/Pages/Home.tsx | 4 ++-- 20 files changed, 59 insertions(+), 59 deletions(-) rename resources/js/Core/{request.ts => rq.ts} (93%) diff --git a/resources/js/Components/FeedItemCard.tsx b/resources/js/Components/FeedItemCard.tsx index 59c09cb4..32a92520 100644 --- a/resources/js/Components/FeedItemCard.tsx +++ b/resources/js/Components/FeedItemCard.tsx @@ -9,7 +9,7 @@ import EyeSlashOutlineIcon from '@/Icons/EyeSlashOutlineIcon'; import formatDateTime from '@/Utils/formatDateTime'; import CalendarDaysSolidIcon from '@/Icons/CalendarDaysSolidIcon'; import FeedItem from '@/types/generated/Models/FeedItem'; -import request from '@/Core/request'; +import rq from '@/Core/rq'; import TotalNumberOfFeedItemsContext from '@/Contexts/TotalNumberOfFeedItemsContext'; export default function FeedItemCard({hueRotationIndex, feedItem}: { hueRotationIndex: number; feedItem: FeedItem; }) { @@ -21,7 +21,7 @@ export default function FeedItemCard({hueRotationIndex, feedItem}: { hueRotation const toggle = () => { setProcessing(true); - void request.put(`/feeds/${internalFeedItem.id}/toggle`) + void rq.put(`/feeds/${internalFeedItem.id}/toggle`) .json() .then((data) => { if (data.read_at) { diff --git a/resources/js/Core/AuthenticatedLayout.tsx b/resources/js/Core/AuthenticatedLayout.tsx index 6f9544be..bc3921ee 100644 --- a/resources/js/Core/AuthenticatedLayout.tsx +++ b/resources/js/Core/AuthenticatedLayout.tsx @@ -7,7 +7,7 @@ import Dropdown from '@/Components/Dropdown'; import DropdownArrowIcon from '@/Icons/DropdownArrowIcon'; import useAuth from '@/Hooks/useAuth'; import User from '@/types/generated/Models/User'; -import request from '@/Core/request'; +import rq from '@/Core/rq'; import {Transition} from '@headlessui/react'; import clsx from 'clsx'; @@ -40,7 +40,7 @@ const AuthenticatedLayout = () => { }, [isNavigationMenuOpen]); const handleLogout = async () => { - await request.delete('/api/logout'); + await rq.delete('/api/logout'); window.location.href = '/'; }; diff --git a/resources/js/Core/Router/Actions/createCategoryAction.ts b/resources/js/Core/Router/Actions/createCategoryAction.ts index c1da5a8f..a00f5914 100644 --- a/resources/js/Core/Router/Actions/createCategoryAction.ts +++ b/resources/js/Core/Router/Actions/createCategoryAction.ts @@ -1,12 +1,12 @@ -import request from '@/Core/request'; +import rq from '@/Core/rq'; import {ActionFunction} from '@remix-run/router/utils'; import handleRequestValidationError from '@/Core/Router/Helpers/handleRequestValidationError'; import toast from 'react-hot-toast'; -const createCategoryAction: ActionFunction = async ({request: req}) => { - const formData = await req.formData(); +const createCategoryAction: ActionFunction = async ({request}) => { + const formData = await request.formData(); - const response = await handleRequestValidationError(() => request.post('/api/categories', {json: Object.fromEntries(formData)})); + const response = await handleRequestValidationError(() => rq.post('/api/categories', {json: Object.fromEntries(formData)})); toast('Category saved.'); diff --git a/resources/js/Core/Router/Actions/createFeedAction.ts b/resources/js/Core/Router/Actions/createFeedAction.ts index fff99fc5..fb33f1ca 100644 --- a/resources/js/Core/Router/Actions/createFeedAction.ts +++ b/resources/js/Core/Router/Actions/createFeedAction.ts @@ -1,16 +1,16 @@ -import request from '@/Core/request'; +import rq from '@/Core/rq'; import {ActionFunction} from '@remix-run/router/utils'; import handleRequestValidationError from '@/Core/Router/Helpers/handleRequestValidationError'; import toast from 'react-hot-toast'; -const createFeedAction: ActionFunction = async ({request: req}) => { - const formData = await req.formData(); +const createFeedAction: ActionFunction = async ({request}) => { + const formData = await request.formData(); if (!formData.get('is_purgeable')) { formData.append('is_purgeable', '0'); } - const response = await handleRequestValidationError(() => request.post('/api/feeds', {json: Object.fromEntries(formData)})); + const response = await handleRequestValidationError(() => rq.post('/api/feeds', {json: Object.fromEntries(formData)})); toast('Feed saved.'); diff --git a/resources/js/Core/Router/Actions/editCategoryAction.ts b/resources/js/Core/Router/Actions/editCategoryAction.ts index 293b882c..2adb23e0 100644 --- a/resources/js/Core/Router/Actions/editCategoryAction.ts +++ b/resources/js/Core/Router/Actions/editCategoryAction.ts @@ -1,20 +1,20 @@ -import request from '@/Core/request'; +import rq from '@/Core/rq'; import {ActionFunction} from '@remix-run/router/utils'; import handleRequestValidationError from '@/Core/Router/Helpers/handleRequestValidationError'; import toast from 'react-hot-toast'; -const editCategoryAction: ActionFunction = async ({params, request: req}) => { - const formData = await req.formData(); +const editCategoryAction: ActionFunction = async ({params, request}) => { + const formData = await request.formData(); if (formData.get('intent') === 'delete') { - await request.delete(`/api/categories/${params.categoryId}`); + await rq.delete(`/api/categories/${params.categoryId}`); toast('Category deleted.'); return null; } - const response = await handleRequestValidationError(() => request.put(`/api/categories/${params.categoryId}`, {json: Object.fromEntries(formData)})); + const response = await handleRequestValidationError(() => rq.put(`/api/categories/${params.categoryId}`, {json: Object.fromEntries(formData)})); toast('Category saved.'); diff --git a/resources/js/Core/Router/Actions/editFeedAction.ts b/resources/js/Core/Router/Actions/editFeedAction.ts index bbd8f5b4..850912ae 100644 --- a/resources/js/Core/Router/Actions/editFeedAction.ts +++ b/resources/js/Core/Router/Actions/editFeedAction.ts @@ -1,24 +1,24 @@ -import request from '@/Core/request'; +import rq from '@/Core/rq'; import {ActionFunction} from '@remix-run/router/utils'; import handleRequestValidationError from '@/Core/Router/Helpers/handleRequestValidationError'; import toast from 'react-hot-toast'; -const editFeedAction: ActionFunction = async ({params, request: req}) => { - const formData = await req.formData(); +const editFeedAction: ActionFunction = async ({params, request}) => { + const formData = await request.formData(); if (!formData.get('is_purgeable')) { formData.append('is_purgeable', '0'); } if (formData.get('intent') === 'delete') { - await request.delete(`/api/feeds/${params.feedId}`); + await rq.delete(`/api/feeds/${params.feedId}`); toast('Feed deleted.'); return null; } - const response = await handleRequestValidationError(() => request.put(`/api/feeds/${params.feedId}`, {json: Object.fromEntries(formData)})); + const response = await handleRequestValidationError(() => rq.put(`/api/feeds/${params.feedId}`, {json: Object.fromEntries(formData)})); toast('Feed saved.'); diff --git a/resources/js/Core/Router/Actions/profileAction.ts b/resources/js/Core/Router/Actions/profileAction.ts index a994061f..32840655 100644 --- a/resources/js/Core/Router/Actions/profileAction.ts +++ b/resources/js/Core/Router/Actions/profileAction.ts @@ -1,14 +1,14 @@ -import request from '@/Core/request'; +import rq from '@/Core/rq'; import {ActionFunction} from '@remix-run/router/utils'; import handleRequestValidationError from '@/Core/Router/Helpers/handleRequestValidationError'; import {redirect} from 'react-router-dom'; import toast from 'react-hot-toast'; -const profileAction: ActionFunction = async ({request: req}) => { - const formData = await req.formData(); +const profileAction: ActionFunction = async ({request}) => { + const formData = await request.formData(); if (formData.get('intent') === 'update-profile') { - const response = await handleRequestValidationError(() => request.patch('/api/profile', {json: Object.fromEntries(formData)}), '/'); + const response = await handleRequestValidationError(() => rq.patch('/api/profile', {json: Object.fromEntries(formData)}), '/'); toast('Profile saved.'); @@ -16,7 +16,7 @@ const profileAction: ActionFunction = async ({request: req}) => { } if (formData.get('intent') === 'update-password') { - const response = await handleRequestValidationError(() => request.put('/api/password', {json: Object.fromEntries(formData)}), '/'); + const response = await handleRequestValidationError(() => rq.put('/api/password', {json: Object.fromEntries(formData)}), '/'); toast('Password updated.'); @@ -24,7 +24,7 @@ const profileAction: ActionFunction = async ({request: req}) => { } if (formData.get('intent') === 'delete') { - const errors = await handleRequestValidationError(() => request.delete('/api/profile', {json: Object.fromEntries(formData)})); + const errors = await handleRequestValidationError(() => rq.delete('/api/profile', {json: Object.fromEntries(formData)})); if (errors) { return errors; diff --git a/resources/js/Core/Router/Actions/usersAction.ts b/resources/js/Core/Router/Actions/usersAction.ts index 690eab71..f8d230cf 100644 --- a/resources/js/Core/Router/Actions/usersAction.ts +++ b/resources/js/Core/Router/Actions/usersAction.ts @@ -1,13 +1,13 @@ -import request from '@/Core/request'; +import rq from '@/Core/rq'; import {ActionFunction} from '@remix-run/router/utils'; import {redirect} from 'react-router-dom'; import toast from 'react-hot-toast'; -const usersAction: ActionFunction = async ({request: req}) => { - const formData = await req.formData(); +const usersAction: ActionFunction = async ({request}) => { + const formData = await request.formData(); if (formData.get('intent') === 'delete') { - await request.delete(`/api/admin/users/${formData.get('userId')}`); + await rq.delete(`/api/admin/users/${formData.get('userId')}`); toast('User deleted.'); } diff --git a/resources/js/Core/Router/Loaders/categoriesLoader.ts b/resources/js/Core/Router/Loaders/categoriesLoader.ts index 0c34aa97..3cb6e357 100644 --- a/resources/js/Core/Router/Loaders/categoriesLoader.ts +++ b/resources/js/Core/Router/Loaders/categoriesLoader.ts @@ -1,9 +1,9 @@ -import request from '@/Core/request'; +import rq from '@/Core/rq'; import {LoaderFunction} from '@remix-run/router/utils'; import CategoriesLoaderType from '@/types/CategoriesLoaderType'; const categoriesLoader: LoaderFunction = async () => { - return await request('/api/categories').json(); + return await rq('/api/categories').json(); }; export default categoriesLoader; diff --git a/resources/js/Core/Router/Loaders/createFeedLoader.ts b/resources/js/Core/Router/Loaders/createFeedLoader.ts index 44efb9ef..f4432ce3 100644 --- a/resources/js/Core/Router/Loaders/createFeedLoader.ts +++ b/resources/js/Core/Router/Loaders/createFeedLoader.ts @@ -1,9 +1,9 @@ -import request from '@/Core/request'; +import rq from '@/Core/rq'; import {LoaderFunction} from '@remix-run/router/utils'; import CreateFeedLoaderType from '@/types/CreateFeedLoaderType'; const createFeedLoader: LoaderFunction = async () => { - return await request('/api/feeds/create').json(); + return await rq('/api/feeds/create').json(); }; export default createFeedLoader; diff --git a/resources/js/Core/Router/Loaders/editCategoryLoader.ts b/resources/js/Core/Router/Loaders/editCategoryLoader.ts index 196d2915..66852c19 100644 --- a/resources/js/Core/Router/Loaders/editCategoryLoader.ts +++ b/resources/js/Core/Router/Loaders/editCategoryLoader.ts @@ -1,9 +1,9 @@ -import request from '@/Core/request'; +import rq from '@/Core/rq'; import {LoaderFunction} from '@remix-run/router/utils'; import EditCategoryLoaderType from '@/types/EditCategoryLoaderType'; const editCategoryLoader: LoaderFunction = async ({params}) => { - return await request(`/api/categories/${params.categoryId}/edit`).json(); + return await rq(`/api/categories/${params.categoryId}/edit`).json(); }; export default editCategoryLoader; diff --git a/resources/js/Core/Router/Loaders/editFeedLoader.ts b/resources/js/Core/Router/Loaders/editFeedLoader.ts index 6e6750ab..ad308f03 100644 --- a/resources/js/Core/Router/Loaders/editFeedLoader.ts +++ b/resources/js/Core/Router/Loaders/editFeedLoader.ts @@ -1,9 +1,9 @@ -import request from '@/Core/request'; +import rq from '@/Core/rq'; import {LoaderFunction} from '@remix-run/router/utils'; import EditFeedLoaderType from '@/types/EditFeddLoaderType'; const editFeedLoader: LoaderFunction = async ({params}) => { - return await request(`/api/feeds/${params.feedId}/edit`).json(); + return await rq(`/api/feeds/${params.feedId}/edit`).json(); }; export default editFeedLoader; diff --git a/resources/js/Core/Router/Loaders/feedItemsLoader.ts b/resources/js/Core/Router/Loaders/feedItemsLoader.ts index 14958022..a5f4868f 100644 --- a/resources/js/Core/Router/Loaders/feedItemsLoader.ts +++ b/resources/js/Core/Router/Loaders/feedItemsLoader.ts @@ -1,9 +1,9 @@ -import request from '@/Core/request'; +import rq from '@/Core/rq'; import {LoaderFunction} from '@remix-run/router/utils'; import FeedItemsLoaderType from '@/types/FeedItemsLoaderType'; -const feedItemsLoader: LoaderFunction = async ({request: req}) => { - const searchParams = new URL(req.url).searchParams; +const feedItemsLoader: LoaderFunction = async ({request}) => { + const searchParams = new URL(request.url).searchParams; const cursor = searchParams.get('cursor'); const feedId = searchParams.get('feed_id'); @@ -17,7 +17,7 @@ const feedItemsLoader: LoaderFunction = async ({request: req}) => { customSearchParams.set('feed_id', feedId); } - return await request(`/api/feed-items${customSearchParams.size > 0 ? `?${customSearchParams.toString()}` : ''}`).json(); + return await rq(`/api/feed-items${customSearchParams.size > 0 ? `?${customSearchParams.toString()}` : ''}`).json(); }; export default feedItemsLoader; diff --git a/resources/js/Core/Router/Loaders/feedsLoader.ts b/resources/js/Core/Router/Loaders/feedsLoader.ts index 73c375e0..97736cd7 100644 --- a/resources/js/Core/Router/Loaders/feedsLoader.ts +++ b/resources/js/Core/Router/Loaders/feedsLoader.ts @@ -1,9 +1,9 @@ -import request from '@/Core/request'; +import rq from '@/Core/rq'; import {LoaderFunction} from '@remix-run/router/utils'; import FeedsLoaderType from '@/types/FeedsLoaderType'; const feedsLoader: LoaderFunction = async () => { - return await request('/api/feeds').json(); + return await rq('/api/feeds').json(); }; export default feedsLoader; diff --git a/resources/js/Core/Router/Loaders/layoutLoader.ts b/resources/js/Core/Router/Loaders/layoutLoader.ts index 75e52478..a32b640c 100644 --- a/resources/js/Core/Router/Loaders/layoutLoader.ts +++ b/resources/js/Core/Router/Loaders/layoutLoader.ts @@ -1,11 +1,11 @@ -import request from '@/Core/request'; +import rq from '@/Core/rq'; import User from '@/types/generated/Models/User'; import {LoaderFunction} from '@remix-run/router/utils'; import {HTTPError} from 'ky'; const layoutLoader: LoaderFunction = async () => { try { - return await request('/api/user').json(); + return await rq('/api/user').json(); } catch (error) { const errorResponse = (error as HTTPError).response; diff --git a/resources/js/Core/Router/Loaders/profileLoader.ts b/resources/js/Core/Router/Loaders/profileLoader.ts index 103f6882..aa4329b0 100644 --- a/resources/js/Core/Router/Loaders/profileLoader.ts +++ b/resources/js/Core/Router/Loaders/profileLoader.ts @@ -1,9 +1,9 @@ -import request from '@/Core/request'; +import rq from '@/Core/rq'; import {LoaderFunction} from '@remix-run/router/utils'; import ProfileLoaderType from '@/types/ProfileLoaderType'; const profileLoader: LoaderFunction = async () => { - return await request('/api/profile').json(); + return await rq('/api/profile').json(); }; export default profileLoader; diff --git a/resources/js/Core/Router/Loaders/usersLoader.ts b/resources/js/Core/Router/Loaders/usersLoader.ts index 068396da..7db1e019 100644 --- a/resources/js/Core/Router/Loaders/usersLoader.ts +++ b/resources/js/Core/Router/Loaders/usersLoader.ts @@ -1,4 +1,4 @@ -import request from '@/Core/request'; +import rq from '@/Core/rq'; import {LoaderFunction} from '@remix-run/router/utils'; import UsersLoaderType from '@/types/UsersLoaderType'; import {redirect} from 'react-router-dom'; @@ -6,7 +6,7 @@ import {HTTPError} from 'ky'; const usersLoader: LoaderFunction = async () => { try { - return await request('/api/admin/users').json(); + return await rq('/api/admin/users').json(); } catch (error) { const errorResponse = (error as HTTPError).response; diff --git a/resources/js/Core/request.ts b/resources/js/Core/rq.ts similarity index 93% rename from resources/js/Core/request.ts rename to resources/js/Core/rq.ts index ac73d962..f0b04bb1 100644 --- a/resources/js/Core/request.ts +++ b/resources/js/Core/rq.ts @@ -2,7 +2,7 @@ import ky from 'ky'; import NProgress from 'nprogress'; import Cookies from 'js-cookie'; -const request = ky.extend({ +const rq = ky.extend({ headers: { Accept: 'application/json', }, @@ -33,4 +33,4 @@ const request = ky.extend({ }, }); -export default request; +export default rq; diff --git a/resources/js/Pages/Feeds/Partials/FeedForm.tsx b/resources/js/Pages/Feeds/Partials/FeedForm.tsx index 6b722111..6fd82451 100644 --- a/resources/js/Pages/Feeds/Partials/FeedForm.tsx +++ b/resources/js/Pages/Feeds/Partials/FeedForm.tsx @@ -11,7 +11,7 @@ import EditFeedValidationErrors from '@/types/EditFeedValidationErrors'; import LinkStack from '@/Components/LinkStack'; import Select from '@/Components/Select'; import Checkbox from '@/Components/Checkbox'; -import request from '@/Core/request'; +import rq from '@/Core/rq'; import DiscoveredFeed from '@/types/DiscoveredFeed'; import {SelectNumberOption} from '@/types/SelectOption'; import useAuth from '@/Hooks/useAuth'; @@ -35,7 +35,7 @@ export default function FeedForm({action, feed = null, categories, errors}: { ac setDiscoveredFeedUrls([]); setIsDiscoverFeedProcessing(true); - request.post('/api/discover-feed-urls', {json: {feed_url: searchUrl}}) + rq.post('/api/discover-feed-urls', {json: {feed_url: searchUrl}}) .json() .then((data) => { setDiscoveredFeedUrls(data); @@ -49,7 +49,7 @@ export default function FeedForm({action, feed = null, categories, errors}: { ac const selectDiscoveredFeedUrl = (feedUrl: string) => () => { setIsDiscoverFeedProcessing(true); - request.post('/api/discover-feed', {json: {feed_url: feedUrl}}) + rq.post('/api/discover-feed', {json: {feed_url: feedUrl}}) .json() .then((responseData) => { nameRef.current!.value = responseData.name; diff --git a/resources/js/Pages/Home.tsx b/resources/js/Pages/Home.tsx index 83949006..b5379125 100644 --- a/resources/js/Pages/Home.tsx +++ b/resources/js/Pages/Home.tsx @@ -6,7 +6,7 @@ import {useContext, useEffect, useState} from 'react'; import {isEmpty, length} from 'ramda'; import NewspaperSolidIcon from '@/Icons/NewspaperSolidIcon'; import EmptyState from '@/Components/EmptyState'; -import request from '@/Core/request'; +import rq from '@/Core/rq'; import FeedItemCard from '@/Components/FeedItemCard'; import TotalNumberOfFeedItemsContext from '@/Contexts/TotalNumberOfFeedItemsContext'; import FeedFilterDropdown from '@/Components/FeedFilterDropdown'; @@ -79,7 +79,7 @@ export default function Home() { }, [fetcher]); const markAllAsRead = async () => { - await request.put('/feeds/mark-all-as-read'); + await rq.put('/feeds/mark-all-as-read'); setTotalNumberOfFeedItems(0); From 03bb8aaab744ffdc2c8f42ad90c39cdbf7fc4b54 Mon Sep 17 00:00:00 2001 From: Kaishiyoku Date: Fri, 15 Mar 2024 21:46:35 +0100 Subject: [PATCH 53/79] fixed ESLint warnings --- resources/js/Components/Dropdown.tsx | 2 +- resources/js/Components/FeedFilterDropdown.tsx | 2 +- resources/js/Components/TextInput.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/resources/js/Components/Dropdown.tsx b/resources/js/Components/Dropdown.tsx index d3581bd5..8cd9d895 100644 --- a/resources/js/Components/Dropdown.tsx +++ b/resources/js/Components/Dropdown.tsx @@ -143,7 +143,7 @@ const DropdownButton = ({onClick, className = '', children}: { onClick: () => vo > {children} - ) + ); }; const Spacer = () =>
    ; diff --git a/resources/js/Components/FeedFilterDropdown.tsx b/resources/js/Components/FeedFilterDropdown.tsx index 08757098..89e4a7f3 100644 --- a/resources/js/Components/FeedFilterDropdown.tsx +++ b/resources/js/Components/FeedFilterDropdown.tsx @@ -1,6 +1,6 @@ import Dropdown from '@/Components/Dropdown'; import {useLaravelReactI18n} from 'laravel-react-i18n'; -import {HeadlessButton, TertiaryButton} from '@/Components/Button'; +import {TertiaryButton} from '@/Components/Button'; import FunnelOutlineIcon from '@/Icons/FunnelOutlineIcon'; import DropdownArrowIcon from '@/Icons/DropdownArrowIcon'; import {OtherProps} from '@/types'; diff --git a/resources/js/Components/TextInput.tsx b/resources/js/Components/TextInput.tsx index 01e2c86d..0476394a 100644 --- a/resources/js/Components/TextInput.tsx +++ b/resources/js/Components/TextInput.tsx @@ -1,4 +1,4 @@ -import {forwardRef, InputHTMLAttributes, Ref, useEffect, useImperativeHandle, useRef} from 'react'; +import {forwardRef, InputHTMLAttributes, useEffect, useImperativeHandle, useRef} from 'react'; import clsx from 'clsx'; type TextInputProps = InputHTMLAttributes & { isFocused?: boolean; }; From 1c4b84106e1b15fd7d93de46f7989091f86e7798 Mon Sep 17 00:00:00 2001 From: Kaishiyoku Date: Sat, 16 Mar 2024 09:44:19 +0100 Subject: [PATCH 54/79] fixed category controller tests --- .../Controllers/Api/CategoryController.php | 12 -- app/Http/Controllers/CategoryController.php | 85 -------------- resources/js/Inertia/app.tsx | 4 +- resources/js/Inertia/test.http | 4 + routes/api.php | 10 +- routes/web.php | 7 -- .../{ => Api}/CategoryControllerTest.php | 107 +++++++++--------- 7 files changed, 63 insertions(+), 166 deletions(-) delete mode 100644 app/Http/Controllers/CategoryController.php create mode 100644 resources/js/Inertia/test.http rename tests/Feature/Http/Controllers/{ => Api}/CategoryControllerTest.php (61%) diff --git a/app/Http/Controllers/Api/CategoryController.php b/app/Http/Controllers/Api/CategoryController.php index 66086715..c29766cf 100644 --- a/app/Http/Controllers/Api/CategoryController.php +++ b/app/Http/Controllers/Api/CategoryController.php @@ -8,8 +8,6 @@ use App\Models\Category; use Illuminate\Http\JsonResponse; use Illuminate\Support\Facades\Auth; -use Inertia\Inertia; -use Inertia\Response; class CategoryController extends Controller { @@ -29,16 +27,6 @@ public function index(): JsonResponse ]); } - /** - * Show the form for creating a new resource. - */ - public function create(): Response - { - return Inertia::render('Categories/Create', [ - 'category' => new Category(), - ]); - } - /** * Store a newly created resource in storage. */ diff --git a/app/Http/Controllers/CategoryController.php b/app/Http/Controllers/CategoryController.php deleted file mode 100644 index 48ae4610..00000000 --- a/app/Http/Controllers/CategoryController.php +++ /dev/null @@ -1,85 +0,0 @@ -authorizeResource(Category::class); - } - - /** - * Display a listing of the resource. - */ - public function index(): Response - { - return Inertia::render('Categories/Index', [ - 'categories' => Auth::user()->categories()->withCount('feeds')->get(), - 'canCreate' => Auth::user()->can('create', Category::class), - ]); - } - - /** - * Show the form for creating a new resource. - */ - public function create(): Response - { - return Inertia::render('Categories/Create', [ - 'category' => new Category(), - ]); - } - - /** - * Store a newly created resource in storage. - */ - public function store(StoreCategoryRequest $request): RedirectResponse - { - $validated = $request->validated(); - - Auth::user()->categories()->save(new Category($validated)); - - return redirect()->route('categories.index'); - } - - /** - * Show the form for editing the specified resource. - */ - public function edit(Category $category): Response - { - return Inertia::render('Categories/Edit', [ - 'category' => $category, - 'canDelete' => Auth::user()->can('delete', $category), - ]); - } - - /** - * Update the specified resource in storage. - */ - public function update(UpdateCategoryRequest $request, Category $category): RedirectResponse - { - $validated = $request->validated(); - - $category->update($validated); - - return redirect()->route('categories.index'); - } - - /** - * Remove the specified resource from storage. - */ - public function destroy(Category $category): RedirectResponse - { - $category->delete(); - - return redirect()->route('categories.index'); - } -} diff --git a/resources/js/Inertia/app.tsx b/resources/js/Inertia/app.tsx index ce37aa55..c848dc09 100644 --- a/resources/js/Inertia/app.tsx +++ b/resources/js/Inertia/app.tsx @@ -1,5 +1,5 @@ -import './bootstrap'; -import '../css/app.css'; +import '../bootstrap'; +import '../../css/app.css'; import {createRoot} from 'react-dom/client'; import {createInertiaApp} from '@inertiajs/react'; diff --git a/resources/js/Inertia/test.http b/resources/js/Inertia/test.http new file mode 100644 index 00000000..3f020523 --- /dev/null +++ b/resources/js/Inertia/test.http @@ -0,0 +1,4 @@ +GET http://localhost:8000/api/categories +Accept: application/json + +### diff --git a/routes/api.php b/routes/api.php index b494fb9b..94453a1c 100644 --- a/routes/api.php +++ b/routes/api.php @@ -7,7 +7,9 @@ use App\Http\Controllers\Api\FeedController; use App\Http\Controllers\Api\FeedDiscovererController; use App\Http\Controllers\Api\FeedUrlDiscovererController; +use App\Http\Controllers\Api\MarkAllUnreadFeedItemsAsReadController; use App\Http\Controllers\Api\ProfileController; +use App\Http\Controllers\Api\ToggleFeedItemController; use App\Http\Controllers\Auth\AuthenticatedSessionController; use Illuminate\Http\Request; use Illuminate\Support\Facades\Route; @@ -23,7 +25,7 @@ | */ -Route::middleware('auth:sanctum')->group(function () { +Route::middleware('auth:sanctum')->as('api.')->group(function () { Route::get('/user', fn (Request $request) => $request->user()); Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit'); @@ -34,10 +36,10 @@ Route::delete('/logout', [AuthenticatedSessionController::class, 'destroy'])->name('logout'); - Route::resource('categories', CategoryController::class); - Route::put('/feeds/mark-all-as-read', \App\Http\Controllers\Api\MarkAllUnreadFeedItemsAsReadController::class)->name('mark-all-as-read'); + Route::resource('categories', CategoryController::class)->except(['create']); + Route::put('/feeds/mark-all-as-read', MarkAllUnreadFeedItemsAsReadController::class)->name('mark-all-as-read'); Route::resource('feeds', FeedController::class); - Route::put('/feeds/{feedItem}/toggle', \App\Http\Controllers\Api\ToggleFeedItemController::class)->name('toggle-feed-item'); + Route::put('/feeds/{feedItem}/toggle', ToggleFeedItemController::class)->name('toggle-feed-item'); Route::post('discover-feed', FeedDiscovererController::class)->name('discover-feed'); Route::post('discover-feed-urls', FeedUrlDiscovererController::class)->name('discover-feed-urls'); diff --git a/routes/web.php b/routes/web.php index 4132c447..a3f02cb6 100644 --- a/routes/web.php +++ b/routes/web.php @@ -1,12 +1,5 @@ actingAs($user = User::factory()->create()); - Category::factory()->for($user)->create(); + $category = Category::factory()->for($user)->create(); - $this->get(route('categories.index')) - ->assertInertia(fn (Assert $page) => $page - ->component('Categories/Index') - ->count('categories', 1) - ->where('canCreate', true) - ); + $this->json('get', route('api.categories.index')) + ->assertJson([ + 'categories' => [$category->toArray()], + 'canCreate' => true, + ]); } public function test_cannot_access_index_as_guest(): void { - $response = $this->get(route('categories.index')); - - $response->assertRedirect('/login'); - } - - public function test_create(): void - { - $this->actingAs(User::factory()->create()); - - $this->get(route('categories.create')) - ->assertInertia(fn (Assert $page) => $page - ->has('category') - ); + $this->json('get', route('api.categories.index')) + ->assertUnauthorized(); } public function test_store(): void @@ -72,9 +59,9 @@ public function test_store(): void // here we test if the unique rule is only applied to a specific user's categories Category::factory()->state(['name' => $expectedName]); - $response = $this->post(route('categories.store'), ['name' => $expectedName]); - - $response->assertRedirect(route('categories.index')); + $this->json('post', route('api.categories.store'), ['name' => $expectedName]) + ->assertOk() + ->assertJson([]); static::assertSame(1, $user->categories()->count()); static::assertSame($user->id, $user->categories()->first()->user_id); static::assertSame($expectedName, $user->categories()->first()->name); @@ -86,11 +73,14 @@ public function test_store_validation_fails_due_to_duplicate_name(): void $expectedName = 'Test'; - $this->get(route('categories.create')); - $response = $this->post(route('categories.store'), ['name' => $expectedName]); - - $response->assertRedirect(route('categories.create')); - $response->assertSessionHasErrors(['name' => 'The Name has already been taken.']); + $this->json('post', route('api.categories.store'), ['name' => $expectedName]) + ->assertUnprocessable() + ->assertJson([ + 'message' => 'The Name has already been taken.', + 'errors' => [ + 'name' => ['The Name has already been taken.'], + ], + ]); static::assertSame(1, $user->categories()->count()); } @@ -98,11 +88,14 @@ public function test_store_validation_fails_due_to_missing_data(): void { $this->actingAs($user = User::factory()->create()); - $this->get(route('categories.create')); - $response = $this->post(route('categories.store'), ['name' => ' ']); - - $response->assertRedirect(route('categories.create')); - $response->assertSessionHasErrors(['name' => 'The Name field is required.']); + $this->json('post', route('api.categories.store'), ['name' => ' ']) + ->assertUnprocessable() + ->assertJson([ + 'message' => 'The Name field is required.', + 'errors' => [ + 'name' => ['The Name field is required.'], + ], + ]); static::assertSame(0, $user->categories()->count()); } @@ -112,11 +105,11 @@ public function test_edit(): void $category = Category::factory()->for($user)->create(); - $this->get(route('categories.edit', $category)) - ->assertInertia(fn (Assert $page) => $page - ->has('category') - ->where('canDelete', true) - ); + $this->json('get', route('api.categories.edit', $category)) + ->assertJson([ + 'category' => $category->toArray(), + 'canDelete' => true, + ]); } public function test_cannot_edit_category_of_another_user(): void @@ -125,7 +118,7 @@ public function test_cannot_edit_category_of_another_user(): void $category = Category::factory()->create(); - $this->get(route('categories.edit', $category)) + $this->json('get', route('api.categories.edit', $category)) ->assertForbidden(); } @@ -140,9 +133,9 @@ public function test_update(): void // here we test if the unique rule is only applied to a specific user's categories Category::factory()->state(['name' => $expectedName]); - $response = $this->put(route('categories.update', $category), ['name' => $expectedName]); - - $response->assertRedirect(route('categories.index')); + $this->json('put', route('api.categories.update', $category), ['name' => $expectedName]) + ->assertOk() + ->assertJson([]); static::assertSame($user->id, $user->categories()->first()->user_id); static::assertSame($expectedName, $user->categories()->first()->name); } @@ -153,7 +146,7 @@ public function test_cannot_update_category_of_another_user(): void $category = Category::factory()->create(); - $this->put(route('categories.update', $category), ['name' => 'Test (updated)']) + $this->json('put', route('api.categories.update', $category), ['name' => 'Test (updated)']) ->assertForbidden(); } @@ -164,12 +157,14 @@ public function test_cannot_update_category_due_to_duplicate_name(): void Category::factory()->state(['name' => 'Test 1'])->for($user)->create(); $category = Category::factory()->state(['name' => 'Test 2'])->for($user)->create(); - $this->get(route('categories.edit', $category)); - - $response = $this->put(route('categories.update', $category), ['name' => 'Test 1']); - - $response->assertRedirect(route('categories.edit', $category)); - $response->assertSessionHasErrors(['name' => 'The Name has already been taken.']); + $this->json('put', route('api.categories.update', $category), ['name' => 'Test 1']) + ->assertUnprocessable() + ->assertJson([ + 'message' => 'The Name has already been taken.', + 'errors' => [ + 'name' => ['The Name has already been taken.'], + ], + ]); } public function test_delete(): void @@ -178,9 +173,9 @@ public function test_delete(): void $category = Category::factory()->for($user)->create(); - $response = $this->delete(route('categories.destroy', $category)); - - $response->assertRedirect(route('categories.index')); + $this->json('delete', route('api.categories.destroy', $category)) + ->assertOk() + ->assertJson([]); static::assertSame(0, $user->categories()->count()); } @@ -190,7 +185,7 @@ public function test_cannot_delete_category_of_another_user(): void $category = Category::factory()->create(); - $this->delete(route('categories.destroy', $category)) + $this->json('delete', route('api.categories.destroy', $category)) ->assertForbidden(); static::assertSame(1, Category::count()); From 5a4e64e261fac2deea749e26b1889948ef890a1b Mon Sep 17 00:00:00 2001 From: Kaishiyoku Date: Sat, 16 Mar 2024 10:14:29 +0100 Subject: [PATCH 55/79] fixed dashboard controller tests --- .../Controllers/Api/DashboardController.php | 6 - app/Http/Controllers/DashboardController.php | 48 ------- routes/api.php | 2 +- .../Api/DashboardControllerTest.php | 126 ++++++++++++++++++ .../Controllers/DashboardControllerTest.php | 108 --------------- 5 files changed, 127 insertions(+), 163 deletions(-) delete mode 100644 app/Http/Controllers/DashboardController.php create mode 100644 tests/Feature/Http/Controllers/Api/DashboardControllerTest.php delete mode 100644 tests/Feature/Http/Controllers/DashboardControllerTest.php diff --git a/app/Http/Controllers/Api/DashboardController.php b/app/Http/Controllers/Api/DashboardController.php index 04f43600..3f0499d2 100644 --- a/app/Http/Controllers/Api/DashboardController.php +++ b/app/Http/Controllers/Api/DashboardController.php @@ -6,7 +6,6 @@ use App\Http\Requests\DashboardRequest; use Illuminate\Database\Eloquent\Builder; use Illuminate\Http\JsonResponse; -use Illuminate\Http\Response; use Illuminate\Routing\ResponseFactory; use Illuminate\Support\Facades\Auth; @@ -33,11 +32,6 @@ public function __invoke(DashboardRequest $request): ResponseFactory|JsonRespons ->cursorPaginate() ->withQueryString(); - // if feed filtering is active and there are no unread feed items go back to dashboard without query strings -// if ($feedId && $feedItems->isEmpty()) { -// return response()->json(new \stdClass()); -// } - return response()->json([ 'selectedFeed' => $feedId ? $unreadFeeds->firstWhere('id', $feedId) : null, 'totalNumberOfFeedItems' => $totalNumberOfFeedItems, diff --git a/app/Http/Controllers/DashboardController.php b/app/Http/Controllers/DashboardController.php deleted file mode 100644 index 0d41fdb3..00000000 --- a/app/Http/Controllers/DashboardController.php +++ /dev/null @@ -1,48 +0,0 @@ -exists('feed_id') ? $request->integer('feed_id') : null; - - $totalNumberOfFeedItems = Auth::user()->feedItems()->unread()->count(); - $unreadFeeds = Auth::user()->feeds() - ->select(['id', 'name']) - ->whereHas('feedItems', fn (Builder $query) => $query->unread()) /** @phpstan-ignore-line */ - ->withCount(['feedItems' => fn (Builder $query) => $query->unread()]) /** @phpstan-ignore-line */ - ->get(); - - $feedItems = Auth::user()->feedItems() - ->unread() - ->when($feedId, fn (Builder $query) => $query->where('feed_id', $feedId)) /** @phpstan-ignore-line */ - ->with('feed') - ->cursorPaginate() - ->withQueryString(); - - // if feed filtering is active and there are no unread feed items go back to dashboard without query strings - if ($feedId && $feedItems->isEmpty()) { - return redirect()->route('dashboard'); - } - - return Inertia::render('Dashboard', [ - 'selectedFeed' => $feedId ? $unreadFeeds->firstWhere('id', $feedId) : null, - 'totalNumberOfFeedItems' => $totalNumberOfFeedItems, - 'unreadFeeds' => $unreadFeeds, - 'feedItems' => $feedItems, - 'currentCursor' => $request->query('cursor'), - ]); - } -} diff --git a/routes/api.php b/routes/api.php index 94453a1c..9dc7b876 100644 --- a/routes/api.php +++ b/routes/api.php @@ -44,7 +44,7 @@ Route::post('discover-feed', FeedDiscovererController::class)->name('discover-feed'); Route::post('discover-feed-urls', FeedUrlDiscovererController::class)->name('discover-feed-urls'); - Route::get('feed-items', DashboardController::class); + Route::get('feed-items', DashboardController::class)->name('dashboard'); Route::middleware('administrate')->prefix('admin')->as('admin.')->group(function () { Route::resource('users', UserController::class)->only(['index', 'destroy']); diff --git a/tests/Feature/Http/Controllers/Api/DashboardControllerTest.php b/tests/Feature/Http/Controllers/Api/DashboardControllerTest.php new file mode 100644 index 00000000..313ac212 --- /dev/null +++ b/tests/Feature/Http/Controllers/Api/DashboardControllerTest.php @@ -0,0 +1,126 @@ +actingAs($user = User::factory()->create()); + + $feedWithUnreadFeedItems = Feed::factory()->for($user)->has(FeedItem::factory()->unread()->count(15))->create(); + $anotherFeedWithUnreadFeedItems = Feed::factory()->for($user)->has(FeedItem::factory()->unread()->count(5))->create(); + $feedWithoutUnreadFeedItems = Feed::factory()->for($user)->has(FeedItem::factory()->read()->count(6))->create(); + + // visit dashboard without any query parameters + $this->json('get', route('api.dashboard')) + ->assertJsonStructure([ + 'selectedFeed', + 'totalNumberOfFeedItems', + 'unreadFeeds', + 'feedItems' => [ + 'data', + 'path', + 'per_page', + 'next_cursor', + 'next_page_url', + 'prev_cursor', + 'prev_page_url', + ], + 'currentCursor', + ]) + ->assertJsonPath('selectedFeed', null) + ->assertJsonPath('totalNumberOfFeedItems', 20) + ->assertJsonCount(2, 'unreadFeeds') + ->assertJsonCount(15, 'feedItems.data') + ->assertJsonPath('currentCursor', null); + + // visit dashboard with cursor query parameter + $feedItems = Auth::user()->feedItems() + ->unread() + ->cursorPaginate() + ->withQueryString(); + + $cursor = Arr::get($feedItems->toArray(), 'next_cursor'); + + $this->json('get', route('api.dashboard').'?'.Arr::query(['cursor' => $cursor])) + ->assertJsonStructure([ + 'selectedFeed', + 'totalNumberOfFeedItems', + 'unreadFeeds', + 'feedItems' => [ + 'data', + 'path', + 'per_page', + 'next_cursor', + 'next_page_url', + 'prev_cursor', + 'prev_page_url', + ], + 'currentCursor', + ]) + ->assertJsonPath('selectedFeed', null) + ->assertJsonPath('totalNumberOfFeedItems', 20) + ->assertJsonCount(2, 'unreadFeeds') + ->assertJsonCount(5, 'feedItems.data') + ->assertJsonPath('currentCursor', $cursor); + + // visit dashboard with selected feed + $this->json('get', route('api.dashboard').'?'.Arr::query(['feed_id' => $feedWithUnreadFeedItems->id])) + ->assertJsonStructure([ + 'selectedFeed' => [ + 'id', + 'name', + 'feed_items_count', + ], + 'totalNumberOfFeedItems', + 'unreadFeeds', + 'feedItems' => [ + 'data', + 'path', + 'per_page', + 'next_cursor', + 'next_page_url', + 'prev_cursor', + 'prev_page_url', + ], + 'currentCursor', + ]) + ->assertJsonPath('selectedFeed', [...$feedWithUnreadFeedItems->only(['id', 'name']), 'feed_items_count' => 15]) + ->assertJsonPath('totalNumberOfFeedItems', 20) + ->assertJsonCount(2, 'unreadFeeds') + ->assertJsonCount(15, 'feedItems.data') + ->assertJsonPath('currentCursor', null); + + // visit dashboard with selected feed which has no unread feed items + $this->get(route('api.dashboard').'?'.Arr::query(['feed_id' => $feedWithoutUnreadFeedItems->id])) + ->assertJsonStructure([ + 'selectedFeed', + 'totalNumberOfFeedItems', + 'unreadFeeds', + 'feedItems', + 'currentCursor', + ]) + ->assertJsonPath('selectedFeed', null) + ->assertJsonPath('totalNumberOfFeedItems', 20) + ->assertJsonCount(2, 'unreadFeeds') + ->assertJsonCount(0, 'feedItems.data') + ->assertJsonPath('currentCursor', null); + } + + public function test_cannot_access_as_guest(): void + { + $this->json('get', route('api.dashboard')) + ->assertUnauthorized(); + } +} diff --git a/tests/Feature/Http/Controllers/DashboardControllerTest.php b/tests/Feature/Http/Controllers/DashboardControllerTest.php deleted file mode 100644 index cb10094d..00000000 --- a/tests/Feature/Http/Controllers/DashboardControllerTest.php +++ /dev/null @@ -1,108 +0,0 @@ -actingAs($user = User::factory()->create()); - - $feedWithUnreadFeedItems = Feed::factory()->for($user)->has(FeedItem::factory()->unread()->count(15))->create(); - Feed::factory()->for($user)->has(FeedItem::factory()->unread()->count(5))->create(); - $feedWithoutUnreadFeedItems = Feed::factory()->for($user)->has(FeedItem::factory()->read()->count(3))->create(); - - // visit dashboard without any query parameters - $this->get(route('dashboard')) - ->assertInertia(fn (Assert $page) => $page - ->component('Dashboard') - ->where('selectedFeed', null) - ->where('totalNumberOfFeedItems', 20) - ->count('unreadFeeds', 2) - ->has('feedItems', fn (Assert $page) => $page - ->count('data', 15) - ->has('data') - ->has('path') - ->has('per_page') - ->has('next_cursor') - ->has('next_page_url') - ->has('prev_cursor') - ->has('prev_page_url') - ) - ->where('currentCursor', null) - ); - - // visit dashboard with cursor query parameter - $feedItems = Auth::user()->feedItems() - ->unread() - ->cursorPaginate() - ->withQueryString(); - - $cursor = Arr::get($feedItems->toArray(), 'next_cursor'); - - $this->get(route('dashboard').'?'.Arr::query(['cursor' => $cursor])) - ->assertInertia(fn (Assert $page) => $page - ->component('Dashboard') - ->where('selectedFeed', null) - ->where('totalNumberOfFeedItems', 20) - ->count('unreadFeeds', 2) - ->has('feedItems', fn (Assert $page) => $page - ->count('data', 5) - ->has('data') - ->has('path') - ->has('per_page') - ->has('next_cursor') - ->has('next_page_url') - ->has('prev_cursor') - ->has('prev_page_url') - ) - ->where('currentCursor', $cursor) - ); - - // visit dashboard with selected feed - $this->get(route('dashboard').'?'.Arr::query(['feed_id' => $feedWithUnreadFeedItems->id])) - ->assertInertia(fn (Assert $page) => $page - ->component('Dashboard') - ->where('selectedFeed', [ - 'id' => $feedWithUnreadFeedItems->id, - 'name' => $feedWithUnreadFeedItems->name, - 'feed_items_count' => 15, - ]) - ->where('totalNumberOfFeedItems', 20) - ->count('unreadFeeds', 2) - ->has('feedItems', fn (Assert $page) => $page - ->count('data', 15) - ->has('data') - ->has('path') - ->has('per_page') - ->has('next_cursor') - ->has('next_page_url') - ->has('prev_cursor') - ->has('prev_page_url') - ) - ->where('currentCursor', null) - ); - - // visit dashboard with selected feed which has no unread feed items - $this->get(route('dashboard').'?'.Arr::query(['feed_id' => $feedWithoutUnreadFeedItems->id])) - ->assertRedirect(route('dashboard')); - } - - public function test_cannot_access_as_guest(): void - { - $response = $this->get(route('dashboard')); - - $response->assertRedirect('/login'); - } -} From 5466cfa7bde2673cea3566ab14a4f90b52c983e9 Mon Sep 17 00:00:00 2001 From: Kaishiyoku Date: Sat, 16 Mar 2024 11:25:40 +0100 Subject: [PATCH 56/79] fixed feed controller tests --- app/Http/Controllers/Api/FeedController.php | 5 +- app/Http/Controllers/FeedController.php | 95 ------------------- app/Models/Feed.php | 1 + .../{ => Api}/FeedControllerTest.php | 80 ++++++++-------- 4 files changed, 44 insertions(+), 137 deletions(-) delete mode 100644 app/Http/Controllers/FeedController.php rename tests/Feature/Http/Controllers/{ => Api}/FeedControllerTest.php (73%) diff --git a/app/Http/Controllers/Api/FeedController.php b/app/Http/Controllers/Api/FeedController.php index 5db6b4ac..7d307e1d 100644 --- a/app/Http/Controllers/Api/FeedController.php +++ b/app/Http/Controllers/Api/FeedController.php @@ -7,7 +7,6 @@ use App\Http\Requests\UpdateFeedRequest; use App\Models\Feed; use Illuminate\Http\JsonResponse; -use Illuminate\Http\RedirectResponse; use Illuminate\Support\Arr; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Auth; @@ -43,7 +42,7 @@ public function create(): JsonResponse /** * Store a newly created resource in storage. */ - public function store(StoreFeedRequest $request): RedirectResponse + public function store(StoreFeedRequest $request): JsonResponse { $validated = $request->validated(); @@ -52,7 +51,7 @@ public function store(StoreFeedRequest $request): RedirectResponse Auth::user()->feeds()->save($feed); - return redirect()->route('feeds.index'); + return response()->json(); } /** diff --git a/app/Http/Controllers/FeedController.php b/app/Http/Controllers/FeedController.php deleted file mode 100644 index 1ec3bc3a..00000000 --- a/app/Http/Controllers/FeedController.php +++ /dev/null @@ -1,95 +0,0 @@ -authorizeResource(Feed::class); - } - - /** - * Display a listing of the resource. - */ - public function index(): Response - { - return Inertia::render('Feeds/Index', [ - 'feeds' => Auth::user()->feeds()->with('category')->withCount('feedItems')->get(), - 'canCreate' => Auth::user()->can('create', Feed::class), - ]); - } - - /** - * Show the form for creating a new resource. - */ - public function create(): Response - { - return Inertia::render('Feeds/Create', [ - 'categories' => Auth::user()->categories()->pluck('name', 'id')->map(fn (string $name, int $id) => ['value' => $id, 'name' => $name])->values(), - 'feed' => new Feed(), - ]); - } - - /** - * Store a newly created resource in storage. - */ - public function store(StoreFeedRequest $request): RedirectResponse - { - $validated = $request->validated(); - - $feed = new Feed($validated); - $feed->category_id = Arr::get($validated, 'category_id'); - - Auth::user()->feeds()->save($feed); - - return redirect()->route('feeds.index'); - } - - /** - * Show the form for editing the specified resource. - */ - public function edit(Feed $feed): Response - { - return Inertia::render('Feeds/Edit', [ - 'categories' => Auth::user()->categories()->pluck('name', 'id')->map(fn (string $name, int $id) => ['value' => $id, 'name' => $name])->values(), - 'feed' => $feed, - 'canDelete' => Auth::user()->can('delete', $feed), - ]); - } - - /** - * Update the specified resource in storage. - */ - public function update(UpdateFeedRequest $request, Feed $feed): RedirectResponse - { - $validated = $request->validated(); - - $feed = $feed->fill($validated); - $feed->category_id = Arr::get($validated, 'category_id'); - - $feed->save(); - - return redirect()->route('feeds.index'); - } - - /** - * Remove the specified resource from storage. - */ - public function destroy(Feed $feed): RedirectResponse - { - $feed->feedItems()->delete(); - $feed->delete(); - - return redirect()->route('feeds.index'); - } -} diff --git a/app/Models/Feed.php b/app/Models/Feed.php index 3b0eadaf..eb5b5b66 100644 --- a/app/Models/Feed.php +++ b/app/Models/Feed.php @@ -73,6 +73,7 @@ class Feed extends Model */ protected $casts = [ 'is_purgeable' => 'bool', + 'last_checked_at' => 'datetime', ]; public function user(): BelongsTo diff --git a/tests/Feature/Http/Controllers/FeedControllerTest.php b/tests/Feature/Http/Controllers/Api/FeedControllerTest.php similarity index 73% rename from tests/Feature/Http/Controllers/FeedControllerTest.php rename to tests/Feature/Http/Controllers/Api/FeedControllerTest.php index cc5dc234..f2814fb0 100644 --- a/tests/Feature/Http/Controllers/FeedControllerTest.php +++ b/tests/Feature/Http/Controllers/Api/FeedControllerTest.php @@ -1,15 +1,14 @@ actingAs($user = User::factory()->create()); - Feed::factory()->for($user)->create(); + $feed = Feed::factory()->for($user)->create(); - $this->get(route('feeds.index')) - ->assertInertia(fn (Assert $page) => $page - ->component('Feeds/Index') - ->count('feeds', 1) - ->where('canCreate', true) - ); + $this->json('get', route('api.feeds.index')) + ->assertJson([ + 'feeds' => [$feed->toArray()], + 'canCreate' => true, + ]); } public function test_cannot_access_index_as_guest(): void { - $response = $this->get(route('feeds.index')); - - $response->assertRedirect('/login'); + $this->json('get', route('api.feeds.index')) + ->assertUnauthorized(); } public function test_create(): void { $this->actingAs(User::factory()->create()); - $this->get(route('feeds.create')) - ->assertInertia(fn (Assert $page) => $page - ->has('feed') - ); + $this->json('get', route('api.feeds.create')) + ->assertJsonStructure([ + 'categories', + ]); } public function test_store(): void @@ -77,7 +74,7 @@ public function test_store(): void $language = 'en'; $isPurgeable = true; - $response = $this->post(route('feeds.store'), [ + $this->json('post', route('api.feeds.store'), [ 'category_id' => $category->id, 'feed_url' => $feedUrl, 'site_url' => $siteUrl, @@ -85,9 +82,8 @@ public function test_store(): void 'name' => $name, 'language' => $language, 'is_purgeable' => $isPurgeable, - ]); + ])->assertJson([]); - $response->assertRedirect(route('feeds.index')); static::assertSame(1, $user->feeds()->count()); static::assertSame($user->id, $user->feeds()->first()->user_id); static::assertSame($category->id, $user->feeds()->first()->category_id); @@ -103,11 +99,19 @@ public function test_store_validation_fails_due_to_missing_data(): void { $this->actingAs($user = User::factory()->create()); - $this->get(route('feeds.create')); - $response = $this->post(route('feeds.store'), ['name' => ' ']); - - $response->assertRedirect(route('feeds.create')); - $response->assertSessionHasErrors(['name' => 'The Name field is required.']); + $this->json('post', route('api.feeds.store'), ['name' => ' ']) + ->assertUnprocessable() + ->assertJson([ + 'message' => 'The Category field is required. (and 5 more errors)', + 'errors' => [ + 'category_id' => ['The Category field is required.'], + 'feed_url' => ['The Feed URL field is required.'], + 'site_url' => ['The Site URL field is required.'], + 'name' => ['The Name field is required.'], + 'language' => ['The Language field is required.'], + 'is_purgeable' => ['The Purgeable field is required.'], + ], + ]); static::assertSame(0, $user->feeds()->count()); } @@ -117,11 +121,11 @@ public function test_edit(): void $feed = Feed::factory()->for($user)->create(); - $this->get(route('feeds.edit', $feed)) - ->assertInertia(fn (Assert $page) => $page - ->has('feed') - ->where('canDelete', true) - ); + $this->json('get', route('api.feeds.edit', $feed)) + ->assertJson([ + 'feed' => $feed->toArray(), + 'canDelete' => true, + ]); } public function test_cannot_edit_feed_of_another_user(): void @@ -130,7 +134,7 @@ public function test_cannot_edit_feed_of_another_user(): void $feed = Feed::factory()->create(); - $this->get(route('feeds.edit', $feed)) + $this->json('get', route('api.feeds.edit', $feed)) ->assertForbidden(); } @@ -148,7 +152,7 @@ public function test_update(): void $language = 'de'; $isPurgeable = false; - $response = $this->put(route('feeds.update', $feed), [ + $this->json('put', route('api.feeds.update', $feed), [ 'category_id' => $category->id, 'feed_url' => $feedUrl, 'site_url' => $siteUrl, @@ -156,9 +160,8 @@ public function test_update(): void 'name' => $name, 'language' => $language, 'is_purgeable' => $isPurgeable, - ]); + ])->assertJson([]); - $response->assertRedirect(route('feeds.index')); static::assertSame($user->id, $user->feeds()->first()->user_id); static::assertSame($category->id, $user->feeds()->first()->category_id); static::assertSame($feedUrl, $user->feeds()->first()->feed_url); @@ -175,7 +178,7 @@ public function test_cannot_update_feed_of_another_user(): void $feed = Feed::factory()->create(); - $this->put(route('feeds.update', $feed), ['name' => 'Test (updated)']) + $this->json('put', route('api.feeds.update', $feed), ['name' => 'Test (updated)']) ->assertForbidden(); } @@ -185,9 +188,8 @@ public function test_delete(): void $feed = Feed::factory()->for($user)->hasFeedItems(10)->create(); - $response = $this->delete(route('feeds.destroy', $feed)); - - $response->assertRedirect(route('feeds.index')); + $this->json('delete', route('api.feeds.destroy', $feed)) + ->assertJson([]); static::assertSame(0, $user->feeds()->count()); static::assertSame(0, $user->feedItems()->count()); } @@ -198,7 +200,7 @@ public function test_cannot_delete_feed_of_another_user(): void $feed = Feed::factory()->hasFeedItems(10)->create(); - $this->delete(route('feeds.destroy', $feed)) + $this->json('delete', route('api.feeds.destroy', $feed)) ->assertForbidden(); static::assertSame(1, Feed::count()); From 1dbbb37b441d0e823f8a50f41ec9ed49561559aa Mon Sep 17 00:00:00 2001 From: Kaishiyoku Date: Sat, 16 Mar 2024 11:27:24 +0100 Subject: [PATCH 57/79] fixed ToggleFeedItemController tests --- .../Controllers/ToggleFeedItemController.php | 24 ------------------- .../ToggleFeedItemControllerTest.php | 8 +++---- 2 files changed, 4 insertions(+), 28 deletions(-) delete mode 100644 app/Http/Controllers/ToggleFeedItemController.php rename tests/Feature/Http/Controllers/{ => Api}/ToggleFeedItemControllerTest.php (84%) diff --git a/app/Http/Controllers/ToggleFeedItemController.php b/app/Http/Controllers/ToggleFeedItemController.php deleted file mode 100644 index ab042454..00000000 --- a/app/Http/Controllers/ToggleFeedItemController.php +++ /dev/null @@ -1,24 +0,0 @@ -authorize('update', $feedItem); - - $feedItem->update([ - 'read_at' => $feedItem->read_at ? null : now(), - ]); - - return response()->json($feedItem); - } -} diff --git a/tests/Feature/Http/Controllers/ToggleFeedItemControllerTest.php b/tests/Feature/Http/Controllers/Api/ToggleFeedItemControllerTest.php similarity index 84% rename from tests/Feature/Http/Controllers/ToggleFeedItemControllerTest.php rename to tests/Feature/Http/Controllers/Api/ToggleFeedItemControllerTest.php index bdf34f0e..bd6bd834 100644 --- a/tests/Feature/Http/Controllers/ToggleFeedItemControllerTest.php +++ b/tests/Feature/Http/Controllers/Api/ToggleFeedItemControllerTest.php @@ -1,6 +1,6 @@ recycle($user)->create(); $feedItem = FeedItem::factory()->recycle($feed)->state(['read_at' => null])->create(); - $this->put(route('toggle-feed-item', $feedItem)) + $this->json('put', route('api.toggle-feed-item', $feedItem)) ->assertOk() ->assertJsonPath('read_at', now()->micro(0)->toJSON()); } @@ -35,7 +35,7 @@ public function test_marks_feed_item_as_unread(): void $feed = Feed::factory()->recycle($user)->create(); $feedItem = FeedItem::factory()->recycle($feed)->state(['read_at' => now()])->create(); - $this->put(route('toggle-feed-item', $feedItem)) + $this->json('put', route('api.toggle-feed-item', $feedItem)) ->assertOk() ->assertJsonPath('read_at', null); } @@ -47,7 +47,7 @@ public function test_cannot_toggle_feed_item_of_another_user(): void $feed = Feed::factory()->create(); $feedItem = FeedItem::factory()->recycle($feed)->create(); - $this->put(route('toggle-feed-item', $feedItem)) + $this->json('put', route('api.toggle-feed-item', $feedItem)) ->assertForbidden(); } } From 8d8303132fcfcd6555377855c1be8017efcc0f71 Mon Sep 17 00:00:00 2001 From: Kaishiyoku Date: Sat, 16 Mar 2024 11:28:58 +0100 Subject: [PATCH 58/79] fixed MarkAllUnreadFeedItemsAsReadController tests --- ...MarkAllUnreadFeedItemsAsReadController.php | 23 ------------------- ...AllUnreadFeedItemsAsReadControllerTest.php | 8 +++---- 2 files changed, 4 insertions(+), 27 deletions(-) delete mode 100644 app/Http/Controllers/MarkAllUnreadFeedItemsAsReadController.php rename tests/Feature/Http/Controllers/{ => Api}/MarkAllUnreadFeedItemsAsReadControllerTest.php (85%) diff --git a/app/Http/Controllers/MarkAllUnreadFeedItemsAsReadController.php b/app/Http/Controllers/MarkAllUnreadFeedItemsAsReadController.php deleted file mode 100644 index 003b0117..00000000 --- a/app/Http/Controllers/MarkAllUnreadFeedItemsAsReadController.php +++ /dev/null @@ -1,23 +0,0 @@ -feedItems()->unread()->update([ - 'read_at' => $now, - ]); - } -} diff --git a/tests/Feature/Http/Controllers/MarkAllUnreadFeedItemsAsReadControllerTest.php b/tests/Feature/Http/Controllers/Api/MarkAllUnreadFeedItemsAsReadControllerTest.php similarity index 85% rename from tests/Feature/Http/Controllers/MarkAllUnreadFeedItemsAsReadControllerTest.php rename to tests/Feature/Http/Controllers/Api/MarkAllUnreadFeedItemsAsReadControllerTest.php index 4a2c209b..2052a659 100644 --- a/tests/Feature/Http/Controllers/MarkAllUnreadFeedItemsAsReadControllerTest.php +++ b/tests/Feature/Http/Controllers/Api/MarkAllUnreadFeedItemsAsReadControllerTest.php @@ -1,6 +1,6 @@ feedItems()->unread()->count()); static::assertSame(100, $user->feedItems()->whereNotNull('read_at')->count()); - $this->put(route('mark-all-as-read')) + $this->json('put', route('api.mark-all-as-read')) ->assertOk(); static::assertSame(0, $user->feedItems()->unread()->count()); @@ -36,7 +36,7 @@ public function test_marks_all_unread_feed_items_as_read_for_specific_user(): vo public function test_cannot_access_as_guest(): void { - $this->put(route('mark-all-as-read')) - ->assertRedirect('/login'); + $this->json('put', route('api.mark-all-as-read')) + ->assertUnauthorized(); } } From 0e8823c2927dc5387264fad1481cc0a80af0d402 Mon Sep 17 00:00:00 2001 From: Kaishiyoku Date: Sat, 16 Mar 2024 11:31:00 +0100 Subject: [PATCH 59/79] fixed FeedUrlDiscovererController tests --- .../FeedUrlDiscovererController.php | 24 ------------------- .../FeedUrlDiscovererControllerTest.php | 4 ++-- 2 files changed, 2 insertions(+), 26 deletions(-) delete mode 100644 app/Http/Controllers/FeedUrlDiscovererController.php rename tests/Feature/Http/Controllers/{ => Api}/FeedUrlDiscovererControllerTest.php (73%) diff --git a/app/Http/Controllers/FeedUrlDiscovererController.php b/app/Http/Controllers/FeedUrlDiscovererController.php deleted file mode 100644 index ba79c711..00000000 --- a/app/Http/Controllers/FeedUrlDiscovererController.php +++ /dev/null @@ -1,24 +0,0 @@ -validated(); - - return response()->json($heraRssCrawler->discoverFeedUrls(Arr::get($validated, 'feed_url'))); - } -} diff --git a/tests/Feature/Http/Controllers/FeedUrlDiscovererControllerTest.php b/tests/Feature/Http/Controllers/Api/FeedUrlDiscovererControllerTest.php similarity index 73% rename from tests/Feature/Http/Controllers/FeedUrlDiscovererControllerTest.php rename to tests/Feature/Http/Controllers/Api/FeedUrlDiscovererControllerTest.php index 63e540db..0405f814 100644 --- a/tests/Feature/Http/Controllers/FeedUrlDiscovererControllerTest.php +++ b/tests/Feature/Http/Controllers/Api/FeedUrlDiscovererControllerTest.php @@ -1,6 +1,6 @@ actingAs(User::factory()->create()); - $this->post(route('discover-feed-urls'), ['feed_url' => 'https://tailwindcss.com']) + $this->json('post', route('api.discover-feed-urls'), ['feed_url' => 'https://tailwindcss.com']) ->assertJsonIsArray() ->assertJsonCount(2); } From 146ae606be717b521ddf490fd6dc0b32021c5bd8 Mon Sep 17 00:00:00 2001 From: Kaishiyoku Date: Sat, 16 Mar 2024 11:33:44 +0100 Subject: [PATCH 60/79] fixed FeedDiscovererController tests --- .../Controllers/FeedDiscovererController.php | 46 ------------------- .../FeedDiscovererControllerTest.php | 14 +++--- 2 files changed, 7 insertions(+), 53 deletions(-) delete mode 100644 app/Http/Controllers/FeedDiscovererController.php rename tests/Feature/Http/Controllers/{ => Api}/FeedDiscovererControllerTest.php (69%) diff --git a/app/Http/Controllers/FeedDiscovererController.php b/app/Http/Controllers/FeedDiscovererController.php deleted file mode 100644 index 366742e7..00000000 --- a/app/Http/Controllers/FeedDiscovererController.php +++ /dev/null @@ -1,46 +0,0 @@ -validated(); - - try { - $discoveredFeedUrls = $heraRssCrawler->discoverFeedUrls(Arr::get($validated, 'feed_url')); - - if ($discoveredFeedUrls->isEmpty()) { - abort(404, 'No feeds found.'); - } - - $feedMetadata = $heraRssCrawler->parseFeed($discoveredFeedUrls->first()); - - return response()->json([ - 'feed_url' => $feedMetadata->getFeedUrl() ?? $feedMetadata->getUrl(), - 'site_url' => $feedMetadata->getUrl(), - 'favicon_url' => $heraRssCrawler->discoverFavicon($feedMetadata->getUrl()) ?? '', - 'name' => $feedMetadata->getTitle(), - 'language' => $feedMetadata->getLanguage() ?? '', - ]); - } catch (ConnectException) { - abort(422, 'The given URL is invalid.'); - } catch (ClientException) { - abort(422, 'The given URL could not be resolved.'); - } - } -} diff --git a/tests/Feature/Http/Controllers/FeedDiscovererControllerTest.php b/tests/Feature/Http/Controllers/Api/FeedDiscovererControllerTest.php similarity index 69% rename from tests/Feature/Http/Controllers/FeedDiscovererControllerTest.php rename to tests/Feature/Http/Controllers/Api/FeedDiscovererControllerTest.php index 11451bde..524cf7dd 100644 --- a/tests/Feature/Http/Controllers/FeedDiscovererControllerTest.php +++ b/tests/Feature/Http/Controllers/Api/FeedDiscovererControllerTest.php @@ -1,6 +1,6 @@ actingAs(User::factory()->create()); - $this->post(route('discover-feed'), ['feed_url' => 'https://tailwindcss.com/feeds/feed.xml']) + $this->json('post', route('api.discover-feed'), ['feed_url' => 'https://tailwindcss.com/feeds/feed.xml']) ->assertJsonIsObject() ->assertJsonStructure([ 'feed_url', @@ -29,7 +29,7 @@ public function test_no_feed_found(): void { $this->actingAs(User::factory()->create()); - $response = $this->post(route('discover-feed'), ['feed_url' => 'https://blurha.sh/']); + $response = $this->json('post', route('api.discover-feed'), ['feed_url' => 'https://blurha.sh/']); $response->assertNotFound(); static::assertSame('No feeds found.', $response->exception->getMessage()); @@ -37,15 +37,15 @@ public function test_no_feed_found(): void public function test_cannot_access_as_guest(): void { - $this->post(route('discover-feed')) - ->assertRedirect('/login'); + $this->json('post', route('api.discover-feed')) + ->assertUnauthorized(); } public function test_connect_exception(): void { $this->actingAs(User::factory()->create()); - $response = $this->post(route('discover-feed'), ['feed_url' => 'https://test.dev']); + $response = $this->json('post', route('api.discover-feed'), ['feed_url' => 'https://test.dev']); $response->assertUnprocessable(); static::assertSame('The given URL is invalid.', $response->exception->getMessage()); @@ -55,7 +55,7 @@ public function test_client_exception(): void { $this->actingAs(User::factory()->create()); - $response = $this->post(route('discover-feed'), ['feed_url' => 'https://tailwindcss.com/random-page']); + $response = $this->json('post', route('api.discover-feed'), ['feed_url' => 'https://tailwindcss.com/random-page']); $response->assertUnprocessable(); static::assertSame('The given URL could not be resolved.', $response->exception->getMessage()); From b42bba32ec71cb01c41b9142d3c82fd350e4c179 Mon Sep 17 00:00:00 2001 From: Kaishiyoku Date: Sat, 16 Mar 2024 13:39:30 +0100 Subject: [PATCH 61/79] fixed UserWithFeedsCountAndUnreadFeedItemsCount tests --- .../TypeScriptModelGenerator/Nodes/InheritedType.php | 2 +- config/type-script-model-generator.php | 2 +- resources/js/Pages/Admin/Users/UsersIndexPage.tsx | 4 ++-- resources/js/types/UsersLoaderType.ts | 4 ++-- resources/js/types/generated/Models/Feed.ts | 2 +- .../Models/UserWithFeedsAndUnreadFeedItemsCount.ts | 9 --------- .../Models/UserWithFeedsCountAndUnreadFeedItemsCount.ts | 9 +++++++++ .../Console/Commands/GenerateModelsToTypeScriptTest.php | 1 + 8 files changed, 17 insertions(+), 16 deletions(-) delete mode 100644 resources/js/types/generated/Models/UserWithFeedsAndUnreadFeedItemsCount.ts create mode 100644 resources/js/types/generated/Models/UserWithFeedsCountAndUnreadFeedItemsCount.ts diff --git a/app/Services/TypeScriptModelGenerator/Nodes/InheritedType.php b/app/Services/TypeScriptModelGenerator/Nodes/InheritedType.php index 1dc67813..3776b5f2 100644 --- a/app/Services/TypeScriptModelGenerator/Nodes/InheritedType.php +++ b/app/Services/TypeScriptModelGenerator/Nodes/InheritedType.php @@ -92,6 +92,6 @@ public function toString(): string ->replace('{{ model }}', $modelName) ->replace('{{ importPath }}', "{$importDirectory}/{$modelName}") ->replace('{{ name }}', $this->name) - ->replace('{{ fields }}', $this->additionalFields->map(fn (TypeProperty $typeProperty) => $typeProperty->toString())->join("\n")); + ->replace('{{ fields }}', $this->additionalFields->map(fn (TypeProperty $typeProperty) => $typeProperty->toString())->join("\n ")); } } diff --git a/config/type-script-model-generator.php b/config/type-script-model-generator.php index 35fe1473..22f0de19 100644 --- a/config/type-script-model-generator.php +++ b/config/type-script-model-generator.php @@ -41,7 +41,7 @@ ], ], [ - 'name' => 'UserWithFeedsAndUnreadFeedItemsCount', + 'name' => 'UserWithFeedsCountAndUnreadFeedItemsCount', 'model' => \App\Models\User::class, 'additional_fields' => [ [ diff --git a/resources/js/Pages/Admin/Users/UsersIndexPage.tsx b/resources/js/Pages/Admin/Users/UsersIndexPage.tsx index 05e1b014..2c49b233 100644 --- a/resources/js/Pages/Admin/Users/UsersIndexPage.tsx +++ b/resources/js/Pages/Admin/Users/UsersIndexPage.tsx @@ -5,7 +5,7 @@ import {useLaravelReactI18n} from 'laravel-react-i18n'; import formatDateTime from '@/Utils/formatDateTime'; import {DangerButton} from '@/Components/Button'; import useAuth from '@/Hooks/useAuth'; -import UserWithFeedsAndUnreadFeedItemsCount from '@/types/generated/Models/UserWithFeedsAndUnreadFeedItemsCount'; +import UserWithFeedsCountAndUnreadFeedItemsCount from '@/types/generated/Models/UserWithFeedsCountAndUnreadFeedItemsCount'; export default function UsersIndexPage() { const {t} = useLaravelReactI18n(); @@ -13,7 +13,7 @@ export default function UsersIndexPage() { const {user: authUser} = useAuth(); const {users} = useLoaderData() as UsersLoaderType; - const handleDelete = (user: UserWithFeedsAndUnreadFeedItemsCount) => () => { + const handleDelete = (user: UserWithFeedsCountAndUnreadFeedItemsCount) => () => { fetcher.submit({intent: 'delete', userId: user.id}, {method: 'delete', action: '/admin/users', fetcherKey: 'delete'}); }; diff --git a/resources/js/types/UsersLoaderType.ts b/resources/js/types/UsersLoaderType.ts index fa388b79..d2b12763 100644 --- a/resources/js/types/UsersLoaderType.ts +++ b/resources/js/types/UsersLoaderType.ts @@ -1,6 +1,6 @@ -import UserWithFeedsAndUnreadFeedItemsCount from '@/types/generated/Models/UserWithFeedsAndUnreadFeedItemsCount'; +import UserWithFeedsCountAndUnreadFeedItemsCount from '@/types/generated/Models/UserWithFeedsCountAndUnreadFeedItemsCount'; type UsersLoaderType = { - users: UserWithFeedsAndUnreadFeedItemsCount[]; + users: UserWithFeedsCountAndUnreadFeedItemsCount[]; } export default UsersLoaderType; diff --git a/resources/js/types/generated/Models/Feed.ts b/resources/js/types/generated/Models/Feed.ts index 0632fb04..54acb462 100644 --- a/resources/js/types/generated/Models/Feed.ts +++ b/resources/js/types/generated/Models/Feed.ts @@ -13,7 +13,7 @@ type Feed = { name: string; language: string; is_purgeable: boolean /** cast attribute */; - last_checked_at: string | null; + last_checked_at: string /** cast attribute */; last_failed_at: string | null; created_at: string | null; updated_at: string | null; diff --git a/resources/js/types/generated/Models/UserWithFeedsAndUnreadFeedItemsCount.ts b/resources/js/types/generated/Models/UserWithFeedsAndUnreadFeedItemsCount.ts deleted file mode 100644 index 2d1822d9..00000000 --- a/resources/js/types/generated/Models/UserWithFeedsAndUnreadFeedItemsCount.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* this file has been automatically generated */ -import User from '@/types/generated/Models/User'; - -type UserWithFeedsAndUnreadFeedItemsCount = User & { - feeds_count: number; -unread_feed_items_count: number; -}; - -export default UserWithFeedsAndUnreadFeedItemsCount; diff --git a/resources/js/types/generated/Models/UserWithFeedsCountAndUnreadFeedItemsCount.ts b/resources/js/types/generated/Models/UserWithFeedsCountAndUnreadFeedItemsCount.ts new file mode 100644 index 00000000..c5254ea5 --- /dev/null +++ b/resources/js/types/generated/Models/UserWithFeedsCountAndUnreadFeedItemsCount.ts @@ -0,0 +1,9 @@ +/* this file has been automatically generated */ +import User from '@/types/generated/Models/User'; + +type UserWithFeedsCountAndUnreadFeedItemsCount = User & { + feeds_count: number; + unread_feed_items_count: number; +}; + +export default UserWithFeedsCountAndUnreadFeedItemsCount; diff --git a/tests/Feature/Console/Commands/GenerateModelsToTypeScriptTest.php b/tests/Feature/Console/Commands/GenerateModelsToTypeScriptTest.php index eaea5ff8..99655606 100644 --- a/tests/Feature/Console/Commands/GenerateModelsToTypeScriptTest.php +++ b/tests/Feature/Console/Commands/GenerateModelsToTypeScriptTest.php @@ -18,5 +18,6 @@ 'ShortFeed.ts', 'ShortFeedWithFeedItemsCount.ts', 'User.ts', + 'UserWithFeedsCountAndUnreadFeedItemsCount.ts', ]); }); From 4f9ab034aa1ae34a43fba3113be7a36338cc3406 Mon Sep 17 00:00:00 2001 From: Kaishiyoku Date: Sat, 16 Mar 2024 13:41:00 +0100 Subject: [PATCH 62/79] adjusted feed model casts --- app/Models/Feed.php | 1 + resources/js/types/generated/Models/Feed.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/app/Models/Feed.php b/app/Models/Feed.php index eb5b5b66..b3a8ddc6 100644 --- a/app/Models/Feed.php +++ b/app/Models/Feed.php @@ -74,6 +74,7 @@ class Feed extends Model protected $casts = [ 'is_purgeable' => 'bool', 'last_checked_at' => 'datetime', + 'last_failed_at' => 'datetime', ]; public function user(): BelongsTo diff --git a/resources/js/types/generated/Models/Feed.ts b/resources/js/types/generated/Models/Feed.ts index 54acb462..3c9d304d 100644 --- a/resources/js/types/generated/Models/Feed.ts +++ b/resources/js/types/generated/Models/Feed.ts @@ -14,7 +14,7 @@ type Feed = { language: string; is_purgeable: boolean /** cast attribute */; last_checked_at: string /** cast attribute */; - last_failed_at: string | null; + last_failed_at: string /** cast attribute */; created_at: string | null; updated_at: string | null; user: User; From 8e85c08b3d5057dbaaf67cb1516137a4e85a4486 Mon Sep 17 00:00:00 2001 From: Kaishiyoku Date: Sat, 16 Mar 2024 13:54:09 +0100 Subject: [PATCH 63/79] fixed ProfileController tests --- app/Http/Controllers/ProfileController.php | 63 ---------------------- tests/Feature/ProfileTest.php | 58 ++++++-------------- tests/TestCase.php | 1 - 3 files changed, 17 insertions(+), 105 deletions(-) delete mode 100644 app/Http/Controllers/ProfileController.php diff --git a/app/Http/Controllers/ProfileController.php b/app/Http/Controllers/ProfileController.php deleted file mode 100644 index 681b3885..00000000 --- a/app/Http/Controllers/ProfileController.php +++ /dev/null @@ -1,63 +0,0 @@ - $request->user() instanceof MustVerifyEmail, - 'status' => session('status'), - ]); - } - - /** - * Update the user's profile information. - */ - public function update(ProfileUpdateRequest $request): RedirectResponse - { - $request->user()->fill($request->validated()); - - if ($request->user()->isDirty('email')) { - $request->user()->email_verified_at = null; - } - - $request->user()->save(); - - return Redirect::route('profile.edit'); - } - - /** - * Delete the user's account. - */ - public function destroy(Request $request): RedirectResponse - { - $request->validate([ - 'password' => ['required', 'current-password'], - ]); - - $user = $request->user(); - - Auth::logout(); - - $user->delete(); - - $request->session()->invalidate(); - $request->session()->regenerateToken(); - - return Redirect::to('/'); - } -} diff --git a/tests/Feature/ProfileTest.php b/tests/Feature/ProfileTest.php index 49886c3e..24a06567 100644 --- a/tests/Feature/ProfileTest.php +++ b/tests/Feature/ProfileTest.php @@ -10,31 +10,17 @@ class ProfileTest extends TestCase { use RefreshDatabase; - public function test_profile_page_is_displayed(): void - { - $user = User::factory()->create(); - - $response = $this - ->actingAs($user) - ->get('/profile'); - - $response->assertOk(); - } - public function test_profile_information_can_be_updated(): void { $user = User::factory()->create(); - $response = $this + $this ->actingAs($user) - ->patch('/profile', [ + ->json('patch', route('api.profile.update'), [ 'name' => 'Test User', 'email' => 'test@example.com', - ]); - - $response - ->assertSessionHasNoErrors() - ->assertRedirect('/profile'); + ]) + ->assertJson([]); $user->refresh(); @@ -47,16 +33,13 @@ public function test_email_verification_status_is_unchanged_when_the_email_addre { $user = User::factory()->create(); - $response = $this + $this ->actingAs($user) - ->patch('/profile', [ + ->json('patch', route('api.profile.update'), [ 'name' => 'Test User', 'email' => $user->email, - ]); - - $response - ->assertSessionHasNoErrors() - ->assertRedirect('/profile'); + ]) + ->assertJson([]); $this->assertNotNull($user->refresh()->email_verified_at); } @@ -65,17 +48,14 @@ public function test_user_can_delete_their_account(): void { $user = User::factory()->create(); - $response = $this + $this ->actingAs($user) - ->delete('/profile', [ + ->json('delete', route('api.profile.destroy'), [ 'password' => 'password', - ]); + ]) + ->assertJson([]); - $response - ->assertSessionHasNoErrors() - ->assertRedirect('/'); - - $this->assertGuest(); + $this->assertGuest('web'); $this->assertNull($user->fresh()); } @@ -83,16 +63,12 @@ public function test_correct_password_must_be_provided_to_delete_account(): void { $user = User::factory()->create(); - $response = $this + $this ->actingAs($user) - ->from('/profile') - ->delete('/profile', [ + ->json('delete', route('api.profile.destroy'), [ 'password' => 'wrong-password', - ]); - - $response - ->assertSessionHasErrors('password') - ->assertRedirect('/profile'); + ]) + ->assertJson([]); $this->assertNotNull($user->fresh()); } diff --git a/tests/TestCase.php b/tests/TestCase.php index b02c506d..3b14486b 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -2,7 +2,6 @@ namespace Tests; -use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\TestCase as BaseTestCase; abstract class TestCase extends BaseTestCase From 12979d080615d5d0213c6fc582273218b0b83d1a Mon Sep 17 00:00:00 2001 From: Kaishiyoku Date: Sat, 16 Mar 2024 14:01:33 +0100 Subject: [PATCH 64/79] fixed UserController tests --- app/Http/Controllers/Admin/UserController.php | 48 ------------------- .../Nodes/TypeTest/it_builds_a_type.snap | 4 +- .../Nodes/TypeTest/it_builds_a_type__2.snap | 10 ++-- .../{ => Api}/Admin/UserControllerTest.php | 30 ++++++------ 4 files changed, 22 insertions(+), 70 deletions(-) delete mode 100644 app/Http/Controllers/Admin/UserController.php rename tests/Feature/Http/Controllers/{ => Api}/Admin/UserControllerTest.php (73%) diff --git a/app/Http/Controllers/Admin/UserController.php b/app/Http/Controllers/Admin/UserController.php deleted file mode 100644 index 45b4d803..00000000 --- a/app/Http/Controllers/Admin/UserController.php +++ /dev/null @@ -1,48 +0,0 @@ -authorizeResource(User::class); - } - - /** - * Display a listing of the resource. - */ - public function index(): Response - { - return Inertia::render('Admin/Users/Index', [ - 'users' => User::orderBy('name') - ->withCount([ - 'feeds', - 'feedItems as unread_feed_items_count' => function (Builder $query) { - $query->whereNull('read_at'); - }, - ]) - ->get(), - ]); - } - - /** - * Remove the specified resource from storage. - */ - public function destroy(User $user): RedirectResponse - { - $user->feedItems()->delete(); - $user->feeds()->delete(); - $user->categories()->delete(); - $user->delete(); - - return redirect()->route('admin.users.index'); - } -} diff --git a/tests/.pest/snapshots/Feature/Services/TypeScriptModelGenerator/Nodes/TypeTest/it_builds_a_type.snap b/tests/.pest/snapshots/Feature/Services/TypeScriptModelGenerator/Nodes/TypeTest/it_builds_a_type.snap index 0632fb04..3c9d304d 100644 --- a/tests/.pest/snapshots/Feature/Services/TypeScriptModelGenerator/Nodes/TypeTest/it_builds_a_type.snap +++ b/tests/.pest/snapshots/Feature/Services/TypeScriptModelGenerator/Nodes/TypeTest/it_builds_a_type.snap @@ -13,8 +13,8 @@ type Feed = { name: string; language: string; is_purgeable: boolean /** cast attribute */; - last_checked_at: string | null; - last_failed_at: string | null; + last_checked_at: string /** cast attribute */; + last_failed_at: string /** cast attribute */; created_at: string | null; updated_at: string | null; user: User; diff --git a/tests/.pest/snapshots/Feature/Services/TypeScriptModelGenerator/Nodes/TypeTest/it_builds_a_type__2.snap b/tests/.pest/snapshots/Feature/Services/TypeScriptModelGenerator/Nodes/TypeTest/it_builds_a_type__2.snap index af2682f3..51b1bded 100644 --- a/tests/.pest/snapshots/Feature/Services/TypeScriptModelGenerator/Nodes/TypeTest/it_builds_a_type__2.snap +++ b/tests/.pest/snapshots/Feature/Services/TypeScriptModelGenerator/Nodes/TypeTest/it_builds_a_type__2.snap @@ -76,19 +76,17 @@ }, { "returnTypes": [ - "string", - "null" + "string" ], - "comment": null, + "comment": "cast attribute", "model": "Feed", "name": "last_checked_at" }, { "returnTypes": [ - "string", - "null" + "string" ], - "comment": null, + "comment": "cast attribute", "model": "Feed", "name": "last_failed_at" }, diff --git a/tests/Feature/Http/Controllers/Admin/UserControllerTest.php b/tests/Feature/Http/Controllers/Api/Admin/UserControllerTest.php similarity index 73% rename from tests/Feature/Http/Controllers/Admin/UserControllerTest.php rename to tests/Feature/Http/Controllers/Api/Admin/UserControllerTest.php index d5aa6181..0a0675fc 100644 --- a/tests/Feature/Http/Controllers/Admin/UserControllerTest.php +++ b/tests/Feature/Http/Controllers/Api/Admin/UserControllerTest.php @@ -1,8 +1,8 @@ count(5)->create(); - $this->get(route('admin.users.index')) - ->assertInertia(fn (Assert $page) => $page - ->component('Admin/Users/Index') - ->count('users', 6) - ); + $this->json('get', route('api.admin.users.index')) + ->assertJsonStructure([ + 'users', + ]) + ->assertJsonCount(6, 'users'); } public function test_cannot_access_index_as_guest(): void { - $this->get(route('admin.users.index')) - ->assertRedirect('/login'); + $this->json('get', route('api.admin.users.index')) + ->assertUnauthorized(); } public function test_cannot_access_index_as_normal_user(): void { - $this->get(route('admin.users.index')) - ->assertRedirect('/login'); + $this->actingAs(User::factory()->create()); + + $this->get(route('api.admin.users.index')) + ->assertForbidden(); } public function test_delete(): void @@ -67,8 +69,8 @@ public function test_delete(): void ->hasFeedItems(10) ->create(); - $this->delete(route('admin.users.destroy', $user)) - ->assertRedirect(route('admin.users.index')); + $this->json('delete', route('api.admin.users.destroy', $user)) + ->assertJson([]); static::assertCount(1, User::get()); static::assertCount(0, Category::get()); @@ -81,7 +83,7 @@ public function test_cannot_delete_own_user(): void { $this->actingAs($user = User::factory()->admin()->create()); - $this->delete(route('admin.users.destroy', $user)) + $this->json('delete', route('api.admin.users.destroy', $user)) ->assertForbidden(); static::assertSame(1, User::count()); From 6d58a5ffa6e355f369a85c2ec7beb7a6a7a44c11 Mon Sep 17 00:00:00 2001 From: Kaishiyoku Date: Sat, 16 Mar 2024 14:06:00 +0100 Subject: [PATCH 65/79] fixed administrate middleware tests --- tests/Feature/Http/Middleware/AdministrateTest.php | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/tests/Feature/Http/Middleware/AdministrateTest.php b/tests/Feature/Http/Middleware/AdministrateTest.php index 71f5f557..adbef8e8 100644 --- a/tests/Feature/Http/Middleware/AdministrateTest.php +++ b/tests/Feature/Http/Middleware/AdministrateTest.php @@ -8,6 +8,7 @@ use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Http\Request; use Illuminate\Support\Facades\Response; +use Symfony\Component\HttpKernel\Exception\HttpException; use Tests\TestCase; class AdministrateTest extends TestCase @@ -27,11 +28,16 @@ public function test_middleware(?UserFactory $userFactory, bool $expectVisited): $visited = false; $middleware = new Administrate(); - $middleware->handle(Request::create('/telescope')->setUserResolver(fn () => $user ?? null), function () use (&$visited) { - $visited = true; - return Response::make(); - }); + try { + $middleware->handle(Request::create('/telescope')->setUserResolver(fn () => $user ?? null), function () use (&$visited) { + $visited = true; + + return Response::make(); + }); + } catch (HttpException) { + // + } static::assertSame($expectVisited, $visited); } From 99eb598d66015970299186af431af23eb9da6445 Mon Sep 17 00:00:00 2001 From: Kaishiyoku Date: Sat, 16 Mar 2024 14:09:18 +0100 Subject: [PATCH 66/79] adjusted administrate middleware --- app/Http/Middleware/Administrate.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/Http/Middleware/Administrate.php b/app/Http/Middleware/Administrate.php index 72493fd3..df625453 100644 --- a/app/Http/Middleware/Administrate.php +++ b/app/Http/Middleware/Administrate.php @@ -23,6 +23,8 @@ public function handle(Request $request, Closure $next): Response abort(Response::HTTP_FORBIDDEN); } + // @codeCoverageIgnoreStart return redirect('/'); + // @codeCoverageIgnoreEnd } } From c65b172a912b066e00151916c4573db072e9d323 Mon Sep 17 00:00:00 2001 From: Kaishiyoku Date: Tue, 19 Mar 2024 20:59:35 +0100 Subject: [PATCH 67/79] fixed code style --- app/Http/Controllers/Api/FeedDiscovererController.php | 1 - app/Http/Controllers/Auth/AuthenticatedSessionController.php | 1 - app/Models/User.php | 1 - routes/web.php | 5 ++--- .../Http/Controllers/Api/Admin/UserControllerTest.php | 1 - 5 files changed, 2 insertions(+), 7 deletions(-) diff --git a/app/Http/Controllers/Api/FeedDiscovererController.php b/app/Http/Controllers/Api/FeedDiscovererController.php index b7a2953e..b608175c 100644 --- a/app/Http/Controllers/Api/FeedDiscovererController.php +++ b/app/Http/Controllers/Api/FeedDiscovererController.php @@ -15,7 +15,6 @@ class FeedDiscovererController extends Controller /** * Handle the incoming request. * - * @return \Illuminate\Http\JsonResponse * * @throws \Exception */ diff --git a/app/Http/Controllers/Auth/AuthenticatedSessionController.php b/app/Http/Controllers/Auth/AuthenticatedSessionController.php index e7601ec5..67ec5a9d 100644 --- a/app/Http/Controllers/Auth/AuthenticatedSessionController.php +++ b/app/Http/Controllers/Auth/AuthenticatedSessionController.php @@ -6,7 +6,6 @@ use App\Http\Requests\Auth\LoginRequest; use App\Providers\RouteServiceProvider; use Illuminate\Http\JsonResponse; -use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Route; diff --git a/app/Models/User.php b/app/Models/User.php index c76dc1f2..40f8f51e 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -10,7 +10,6 @@ use Illuminate\Database\Eloquent\Relations\HasManyThrough; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; -use Illuminate\Support\Str; use Laravel\Sanctum\HasApiTokens; /** diff --git a/routes/web.php b/routes/web.php index a3f02cb6..3e004776 100644 --- a/routes/web.php +++ b/routes/web.php @@ -16,8 +16,7 @@ */ Route::get('/', function (Request $request) { - if ($request->user()) - { + if ($request->user()) { return view('app'); } @@ -37,6 +36,6 @@ require __DIR__.'/auth.php'; -Route::get('{all?}', function() { +Route::get('{all?}', function () { return view('app'); })->where(['all' => '.*']); diff --git a/tests/Feature/Http/Controllers/Api/Admin/UserControllerTest.php b/tests/Feature/Http/Controllers/Api/Admin/UserControllerTest.php index 0a0675fc..4ac5f4ff 100644 --- a/tests/Feature/Http/Controllers/Api/Admin/UserControllerTest.php +++ b/tests/Feature/Http/Controllers/Api/Admin/UserControllerTest.php @@ -9,7 +9,6 @@ use App\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Arr; -use Inertia\Testing\AssertableInertia as Assert; use Tests\TestCase; class UserControllerTest extends TestCase From 4a17b5f984c72b7ef3fa2020d330220928bfcdde Mon Sep 17 00:00:00 2001 From: Kaishiyoku Date: Tue, 19 Mar 2024 21:01:01 +0100 Subject: [PATCH 68/79] fixed code inspection warnings --- app/Http/Controllers/Api/FeedController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Http/Controllers/Api/FeedController.php b/app/Http/Controllers/Api/FeedController.php index 7d307e1d..2e07f783 100644 --- a/app/Http/Controllers/Api/FeedController.php +++ b/app/Http/Controllers/Api/FeedController.php @@ -93,7 +93,7 @@ public function destroy(Feed $feed): JsonResponse } /** - * @return Collection> + * @return Collection */ private function categories(): Collection { From 3c96ae3b541a708ba9344645f11756cea1281307 Mon Sep 17 00:00:00 2001 From: Kaishiyoku Date: Tue, 19 Mar 2024 21:03:39 +0100 Subject: [PATCH 69/79] fixed npm build --- .../js/Components/Breadcrumbs/Breadcrumbs.tsx | 71 ------- resources/js/Layouts/AuthenticatedLayout.tsx | 198 ------------------ 2 files changed, 269 deletions(-) delete mode 100644 resources/js/Components/Breadcrumbs/Breadcrumbs.tsx delete mode 100644 resources/js/Layouts/AuthenticatedLayout.tsx diff --git a/resources/js/Components/Breadcrumbs/Breadcrumbs.tsx b/resources/js/Components/Breadcrumbs/Breadcrumbs.tsx deleted file mode 100644 index f87d3c2c..00000000 --- a/resources/js/Components/Breadcrumbs/Breadcrumbs.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import {Link} from '@inertiajs/react'; -import slug from 'slug'; -import {useEffect, useRef} from 'react'; -import Breadcrumb from '@/types/Breadcrumb'; - -export default function Breadcrumbs({breadcrumbs}: { breadcrumbs?: Breadcrumb[]; }) { - if (!breadcrumbs) { - return null; - } - - const breadcrumbsRef = useRef(null); - - useEffect(() => { - setTimeout(() => { - breadcrumbsRef.current?.scrollTo({ - top: 0, - left: breadcrumbsRef.current.getBoundingClientRect().right, - behavior: 'smooth', - }); - }, 250); - }, []); - - const breadcrumbMapper = (breadcrumb: Breadcrumb, index: number, arr: Breadcrumb[]) => { - const breadcrumbElement = breadcrumb.url - ? ( -
  • - - {breadcrumb.title} - -
  • - ) - : ( -
  • - {breadcrumb.title} -
  • - ); - - if (index === arr.length - 1) { - return breadcrumbElement; - } - - return [ - breadcrumbElement, -
  • - -
  • , - ]; - }; - - return ( -
    - -
    - ); -} diff --git a/resources/js/Layouts/AuthenticatedLayout.tsx b/resources/js/Layouts/AuthenticatedLayout.tsx deleted file mode 100644 index 76f8eafa..00000000 --- a/resources/js/Layouts/AuthenticatedLayout.tsx +++ /dev/null @@ -1,198 +0,0 @@ -import {Fragment, ReactNode, useEffect, useState} from 'react'; -import {Link} from '@inertiajs/react'; -import {useLaravelReactI18n} from 'laravel-react-i18n'; -import ApplicationLogo from '@/Components/ApplicationLogo'; -import Dropdown from '@/Components/Dropdown'; -import NavLink from '@/Components/NavLink'; -import ResponsiveNavLink from '@/Components/ResponsiveNavLink'; -import {Transition} from '@headlessui/react'; -import DropdownArrowIcon from '@/Icons/DropdownArrowIcon'; -import {BasePageProps} from '@/types'; - -export default function Authenticated({auth, header, children}: BasePageProps & { header: ReactNode; children: ReactNode; }) { - const {t} = useLaravelReactI18n(); - const [showingNavigationDropdown, setShowingNavigationDropdown] = useState(false); - - useEffect(() => { - document.body.style.overflowY = showingNavigationDropdown ? 'hidden' : ''; - document.body.style.position = showingNavigationDropdown ? 'fixed' : ''; - document.body.style.width = showingNavigationDropdown ? '100%' : ''; - }, [showingNavigationDropdown]); - - return ( -
    -
    - -
    - - {(header) && ( -
    - {header} -
    - )} - -
    - {children} -
    -
    - ); -} From 91797636665ed791ba064fb4cdeaa70d912abce3 Mon Sep 17 00:00:00 2001 From: Kaishiyoku Date: Tue, 19 Mar 2024 21:07:03 +0100 Subject: [PATCH 70/79] adjusted vite.config.mjs --- vite.config.mjs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/vite.config.mjs b/vite.config.mjs index 71ca4ce4..7334de9b 100644 --- a/vite.config.mjs +++ b/vite.config.mjs @@ -6,7 +6,10 @@ import i18n from 'laravel-react-i18n/vite'; export default defineConfig({ plugins: [ laravel({ - input: 'resources/js/Inertia/app.tsx', + input: [ + 'resources/js/app.tsx', + 'resources/js/Inertia/app.tsx', + ], refresh: true, }), react(), From b727151d96fab8935b496c57849632a013dda9d5 Mon Sep 17 00:00:00 2001 From: Kaishiyoku Date: Tue, 19 Mar 2024 21:09:26 +0100 Subject: [PATCH 71/79] adjusted vite.config.mjs --- resources/js/app.tsx | 2 ++ resources/views/app.blade.php | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/resources/js/app.tsx b/resources/js/app.tsx index 26443657..09fb239a 100644 --- a/resources/js/app.tsx +++ b/resources/js/app.tsx @@ -1,3 +1,5 @@ +import '../bootstrap'; +import '../../css/app.css'; import {createRoot} from 'react-dom/client'; import NProgress from 'nprogress'; import {LaravelReactI18nProvider} from 'laravel-react-i18n'; diff --git a/resources/views/app.blade.php b/resources/views/app.blade.php index 636141f8..dcf68e2a 100644 --- a/resources/views/app.blade.php +++ b/resources/views/app.blade.php @@ -15,7 +15,7 @@ @viteReactRefresh - @vite(['resources/js/app.tsx', 'resources/js/bootstrap.ts', 'resources/css/app.css']) + @vite(['resources/js/app.tsx'])
    From 3d1f117a68282855b6f3f06bc324579f3223bdd8 Mon Sep 17 00:00:00 2001 From: Kaishiyoku Date: Tue, 19 Mar 2024 21:10:21 +0100 Subject: [PATCH 72/79] adjusted app.tsx --- resources/js/app.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/js/app.tsx b/resources/js/app.tsx index 09fb239a..9d3142be 100644 --- a/resources/js/app.tsx +++ b/resources/js/app.tsx @@ -1,5 +1,5 @@ -import '../bootstrap'; -import '../../css/app.css'; +import './bootstrap'; +import '../css/app.css'; import {createRoot} from 'react-dom/client'; import NProgress from 'nprogress'; import {LaravelReactI18nProvider} from 'laravel-react-i18n'; From 0deb7fb793910dba2c1847622b60ea5875553a31 Mon Sep 17 00:00:00 2001 From: Kaishiyoku Date: Tue, 19 Mar 2024 21:13:37 +0100 Subject: [PATCH 73/79] fixed feed item toggle request --- resources/js/Components/FeedItemCard.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/js/Components/FeedItemCard.tsx b/resources/js/Components/FeedItemCard.tsx index 32a92520..47ac215d 100644 --- a/resources/js/Components/FeedItemCard.tsx +++ b/resources/js/Components/FeedItemCard.tsx @@ -21,7 +21,7 @@ export default function FeedItemCard({hueRotationIndex, feedItem}: { hueRotation const toggle = () => { setProcessing(true); - void rq.put(`/feeds/${internalFeedItem.id}/toggle`) + void rq.put(`/api/feeds/${internalFeedItem.id}/toggle`) .json() .then((data) => { if (data.read_at) { From f5f569fbbb04c576002d1a9379f3a7b1a68adce5 Mon Sep 17 00:00:00 2001 From: Kaishiyoku Date: Wed, 20 Mar 2024 19:46:11 +0100 Subject: [PATCH 74/79] fixed "mark all as read" request --- resources/js/Pages/Home.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/resources/js/Pages/Home.tsx b/resources/js/Pages/Home.tsx index b5379125..174d13b8 100644 --- a/resources/js/Pages/Home.tsx +++ b/resources/js/Pages/Home.tsx @@ -79,11 +79,11 @@ export default function Home() { }, [fetcher]); const markAllAsRead = async () => { - await rq.put('/feeds/mark-all-as-read'); - - setTotalNumberOfFeedItems(0); + await rq.put('/api/feeds/mark-all-as-read'); navigate('/'); + + setTotalNumberOfFeedItems(0); }; return ( From bacaa8b76c1b3a3f2e3ba3ad486532ba9e4d5eae Mon Sep 17 00:00:00 2001 From: Kaishiyoku Date: Wed, 20 Mar 2024 19:46:53 +0100 Subject: [PATCH 75/79] fixed "home" link --- resources/js/Core/AuthenticatedLayout.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/js/Core/AuthenticatedLayout.tsx b/resources/js/Core/AuthenticatedLayout.tsx index bc3921ee..057fa754 100644 --- a/resources/js/Core/AuthenticatedLayout.tsx +++ b/resources/js/Core/AuthenticatedLayout.tsx @@ -148,7 +148,7 @@ const AuthenticatedLayout = () => { >
    - + {t('Home')} From 10130c38d06c719a42852b4a49d6e3cf623a1186 Mon Sep 17 00:00:00 2001 From: Kaishiyoku Date: Wed, 20 Mar 2024 19:47:16 +0100 Subject: [PATCH 76/79] added "home" link to navigation dropdown --- resources/js/Core/AuthenticatedLayout.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/resources/js/Core/AuthenticatedLayout.tsx b/resources/js/Core/AuthenticatedLayout.tsx index 057fa754..2aec5520 100644 --- a/resources/js/Core/AuthenticatedLayout.tsx +++ b/resources/js/Core/AuthenticatedLayout.tsx @@ -70,6 +70,10 @@ const AuthenticatedLayout = () => { + + {t('Home')} + + {t('Profile')} From 353ed6e2444660b13b04127046dd8752e84ae3b6 Mon Sep 17 00:00:00 2001 From: Kaishiyoku Date: Wed, 20 Mar 2024 19:48:46 +0100 Subject: [PATCH 77/79] minor layout adjustments --- resources/js/Pages/Home.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/resources/js/Pages/Home.tsx b/resources/js/Pages/Home.tsx index 174d13b8..24a7e68e 100644 --- a/resources/js/Pages/Home.tsx +++ b/resources/js/Pages/Home.tsx @@ -88,9 +88,11 @@ export default function Home() { return (
    -
    - {tChoice('dashboard.unread_articles', totalNumberOfFeedItems)} -
    + {totalNumberOfFeedItems > 0 && ( +
    + {tChoice('dashboard.unread_articles', totalNumberOfFeedItems)} +
    + )}
    {data.feedItems && } From d51789b22619ee7076b9cc46a407bc8a252aeaeb Mon Sep 17 00:00:00 2001 From: Kaishiyoku Date: Wed, 20 Mar 2024 19:52:13 +0100 Subject: [PATCH 78/79] adjsuted mobile navigation menu style --- resources/js/Components/Dropdown.tsx | 2 +- resources/js/Core/AuthenticatedLayout.tsx | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/resources/js/Components/Dropdown.tsx b/resources/js/Components/Dropdown.tsx index 8cd9d895..d1a2d46e 100644 --- a/resources/js/Components/Dropdown.tsx +++ b/resources/js/Components/Dropdown.tsx @@ -146,7 +146,7 @@ const DropdownButton = ({onClick, className = '', children}: { onClick: () => vo ); }; -const Spacer = () =>
    ; +const Spacer = () =>
    ; Dropdown.Trigger = Trigger; Dropdown.Content = Content; diff --git a/resources/js/Core/AuthenticatedLayout.tsx b/resources/js/Core/AuthenticatedLayout.tsx index 2aec5520..57290919 100644 --- a/resources/js/Core/AuthenticatedLayout.tsx +++ b/resources/js/Core/AuthenticatedLayout.tsx @@ -150,8 +150,8 @@ const AuthenticatedLayout = () => { leaveFrom="opacity-100 translate-y-0" leaveTo="opacity-0 -translate-y-8 sm:translate-y-0" > -
    -
    +
    +
    {t('Home')} @@ -185,7 +185,9 @@ const AuthenticatedLayout = () => { )}
    -
    + + +
    {user.name} From 54d24b4a7ba583828dd5c7bd54511de8723d8f7e Mon Sep 17 00:00:00 2001 From: Kaishiyoku Date: Wed, 20 Mar 2024 20:17:30 +0100 Subject: [PATCH 79/79] adjusted tests --- .../Controllers/Api/ProfileController.php | 2 + tests/Feature/Auth/PasswordUpdateTest.php | 37 +++++++++++++++++++ tests/Feature/ProfileTest.php | 14 +++++++ tests/Pest.php | 2 +- 4 files changed, 54 insertions(+), 1 deletion(-) diff --git a/app/Http/Controllers/Api/ProfileController.php b/app/Http/Controllers/Api/ProfileController.php index bbeb482e..73c57741 100644 --- a/app/Http/Controllers/Api/ProfileController.php +++ b/app/Http/Controllers/Api/ProfileController.php @@ -54,9 +54,11 @@ public function destroy(Request $request): JsonResponse $user->delete(); + // @codeCoverageIgnoreStart $request->session()->invalidate(); $request->session()->regenerateToken(); return response()->json(); + // @codeCoverageIgnoreEnd } } diff --git a/tests/Feature/Auth/PasswordUpdateTest.php b/tests/Feature/Auth/PasswordUpdateTest.php index bbf079d2..883f9d8d 100644 --- a/tests/Feature/Auth/PasswordUpdateTest.php +++ b/tests/Feature/Auth/PasswordUpdateTest.php @@ -31,6 +31,24 @@ public function test_password_can_be_updated(): void $this->assertTrue(Hash::check('new-password', $user->refresh()->password)); } + public function test_password_can_be_updated_via_api(): void + { + $user = User::factory()->create(); + + $response = $this + ->actingAs($user) + ->json('put', route('api.password.update'), [ + 'current_password' => 'password', + 'password' => 'new-password', + 'password_confirmation' => 'new-password', + ]); + + $response + ->assertJson([]); + + $this->assertTrue(Hash::check('new-password', $user->refresh()->password)); + } + public function test_correct_password_must_be_provided_to_update_password(): void { $user = User::factory()->create(); @@ -48,4 +66,23 @@ public function test_correct_password_must_be_provided_to_update_password(): voi ->assertSessionHasErrors('current_password') ->assertRedirect('/profile'); } + + public function test_correct_password_must_be_provided_to_update_password_via_api(): void + { + $user = User::factory()->create(); + + $this + ->actingAs($user) + ->json('put', route('api.password.update'), [ + 'current_password' => 'wrong-password', + 'password' => 'new-password', + 'password_confirmation' => 'new-password', + ]) + ->assertJson([ + 'message' => 'The password is incorrect.', + 'errors' => [ + 'current_password' => ['The password is incorrect.'], + ], + ]); + } } diff --git a/tests/Feature/ProfileTest.php b/tests/Feature/ProfileTest.php index 24a06567..c0badf0e 100644 --- a/tests/Feature/ProfileTest.php +++ b/tests/Feature/ProfileTest.php @@ -10,6 +10,20 @@ class ProfileTest extends TestCase { use RefreshDatabase; + public function test_profile_edit_information_is_returned(): void + { + $user = User::factory()->create(); + + $this + ->actingAs($user) + ->json('get', route('api.profile.edit')) + ->assertJson([ + 'mustVerifyEmail' => true, + 'status' => null, + 'user' => $user->toArray(), + ]); + } + public function test_profile_information_can_be_updated(): void { $user = User::factory()->create(); diff --git a/tests/Pest.php b/tests/Pest.php index b9763c6a..b1776a4d 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -13,7 +13,7 @@ uses( Tests\TestCase::class, - // Illuminate\Foundation\Testing\RefreshDatabase::class, + Illuminate\Foundation\Testing\RefreshDatabase::class, )->in('Feature'); /*