diff --git a/app/Http/Controllers/Auth/AuthenticatedSessionController.php b/app/Http/Controllers/Auth/AuthenticatedSessionController.php deleted file mode 100644 index b4a48d9..0000000 --- a/app/Http/Controllers/Auth/AuthenticatedSessionController.php +++ /dev/null @@ -1,51 +0,0 @@ - Route::has('password.request'), - 'status' => $request->session()->get('status'), - ]); - } - - /** - * Handle an incoming authentication request. - */ - public function store(LoginRequest $request): RedirectResponse - { - $request->authenticate(); - - $request->session()->regenerate(); - - return redirect()->intended(route('dashboard', absolute: false)); - } - - /** - * Destroy an authenticated session. - */ - public function destroy(Request $request): RedirectResponse - { - Auth::guard('web')->logout(); - - $request->session()->invalidate(); - $request->session()->regenerateToken(); - - return redirect('/'); - } -} diff --git a/app/Http/Controllers/Auth/ConfirmablePasswordController.php b/app/Http/Controllers/Auth/ConfirmablePasswordController.php deleted file mode 100644 index c729706..0000000 --- a/app/Http/Controllers/Auth/ConfirmablePasswordController.php +++ /dev/null @@ -1,41 +0,0 @@ -validate([ - 'email' => $request->user()->email, - 'password' => $request->password, - ])) { - throw ValidationException::withMessages([ - 'password' => __('auth.password'), - ]); - } - - $request->session()->put('auth.password_confirmed_at', time()); - - return redirect()->intended(route('dashboard', absolute: false)); - } -} diff --git a/app/Http/Controllers/Auth/EmailVerificationNotificationController.php b/app/Http/Controllers/Auth/EmailVerificationNotificationController.php deleted file mode 100644 index f64fa9b..0000000 --- a/app/Http/Controllers/Auth/EmailVerificationNotificationController.php +++ /dev/null @@ -1,24 +0,0 @@ -user()->hasVerifiedEmail()) { - return redirect()->intended(route('dashboard', absolute: false)); - } - - $request->user()->sendEmailVerificationNotification(); - - return back()->with('status', 'verification-link-sent'); - } -} diff --git a/app/Http/Controllers/Auth/EmailVerificationPromptController.php b/app/Http/Controllers/Auth/EmailVerificationPromptController.php deleted file mode 100644 index 672f7cf..0000000 --- a/app/Http/Controllers/Auth/EmailVerificationPromptController.php +++ /dev/null @@ -1,22 +0,0 @@ -user()->hasVerifiedEmail() - ? redirect()->intended(route('dashboard', absolute: false)) - : Inertia::render('auth/verify-email', ['status' => $request->session()->get('status')]); - } -} diff --git a/app/Http/Controllers/Auth/NewPasswordController.php b/app/Http/Controllers/Auth/NewPasswordController.php deleted file mode 100644 index 0b4c6cb..0000000 --- a/app/Http/Controllers/Auth/NewPasswordController.php +++ /dev/null @@ -1,69 +0,0 @@ - $request->email, - 'token' => $request->route('token'), - ]); - } - - /** - * Handle an incoming new password request. - * - * @throws \Illuminate\Validation\ValidationException - */ - public function store(Request $request): RedirectResponse - { - $request->validate([ - 'token' => 'required', - 'email' => 'required|email', - 'password' => ['required', 'confirmed', Rules\Password::defaults()], - ]); - - // Here we will attempt to reset the user's password. If it is successful we - // will update the password on an actual user model and persist it to the - // database. Otherwise we will parse the error and return the response. - $status = Password::reset( - $request->only('email', 'password', 'password_confirmation', 'token'), - function ($user) use ($request) { - $user->forceFill([ - 'password' => Hash::make($request->password), - 'remember_token' => Str::random(60), - ])->save(); - - event(new PasswordReset($user)); - } - ); - - // If the password was successfully reset, we will redirect the user back to - // the application's home authenticated view. If there is an error we can - // redirect them back to where they came from with their error message. - if ($status == Password::PasswordReset) { - return to_route('login')->with('status', __($status)); - } - - throw ValidationException::withMessages([ - 'email' => [__($status)], - ]); - } -} diff --git a/app/Http/Controllers/Auth/PasswordResetLinkController.php b/app/Http/Controllers/Auth/PasswordResetLinkController.php deleted file mode 100644 index 9fcfe49..0000000 --- a/app/Http/Controllers/Auth/PasswordResetLinkController.php +++ /dev/null @@ -1,41 +0,0 @@ - $request->session()->get('status'), - ]); - } - - /** - * Handle an incoming password reset link request. - * - * @throws \Illuminate\Validation\ValidationException - */ - public function store(Request $request): RedirectResponse - { - $request->validate([ - 'email' => 'required|email', - ]); - - Password::sendResetLink( - $request->only('email') - ); - - return back()->with('status', __('A reset link will be sent if the account exists.')); - } -} diff --git a/app/Http/Controllers/Auth/RegisteredUserController.php b/app/Http/Controllers/Auth/RegisteredUserController.php deleted file mode 100644 index 08caeef..0000000 --- a/app/Http/Controllers/Auth/RegisteredUserController.php +++ /dev/null @@ -1,51 +0,0 @@ -validate([ - 'name' => 'required|string|max:255', - 'email' => 'required|string|lowercase|email|max:255|unique:'.User::class, - 'password' => ['required', 'confirmed', Rules\Password::defaults()], - ]); - - $user = User::create([ - 'name' => $request->name, - 'email' => $request->email, - 'password' => Hash::make($request->password), - ]); - - event(new Registered($user)); - - Auth::login($user); - - return redirect()->intended(route('dashboard', absolute: false)); - } -} diff --git a/app/Http/Controllers/Auth/VerifyEmailController.php b/app/Http/Controllers/Auth/VerifyEmailController.php deleted file mode 100644 index a300bfa..0000000 --- a/app/Http/Controllers/Auth/VerifyEmailController.php +++ /dev/null @@ -1,30 +0,0 @@ -user()->hasVerifiedEmail()) { - return redirect()->intended(route('dashboard', absolute: false).'?verified=1'); - } - - if ($request->user()->markEmailAsVerified()) { - /** @var \Illuminate\Contracts\Auth\MustVerifyEmail $user */ - $user = $request->user(); - - event(new Verified($user)); - } - - return redirect()->intended(route('dashboard', absolute: false).'?verified=1'); - } -} diff --git a/app/Http/Controllers/Settings/PasswordController.php b/app/Http/Controllers/Settings/PasswordController.php deleted file mode 100644 index f8d19b9..0000000 --- a/app/Http/Controllers/Settings/PasswordController.php +++ /dev/null @@ -1,39 +0,0 @@ -validate([ - 'current_password' => ['required', 'current_password'], - 'password' => ['required', Password::defaults(), 'confirmed'], - ]); - - $request->user()->update([ - 'password' => Hash::make($validated['password']), - ]); - - return back(); - } -} diff --git a/app/Http/Controllers/Settings/ProfileController.php b/app/Http/Controllers/Settings/ProfileController.php deleted file mode 100644 index a6cb7e1..0000000 --- a/app/Http/Controllers/Settings/ProfileController.php +++ /dev/null @@ -1,63 +0,0 @@ - $request->user() instanceof MustVerifyEmail, - 'status' => $request->session()->get('status'), - ]); - } - - /** - * Update the user's profile settings. - */ - 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 to_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('/'); - } -} diff --git a/app/Http/Middleware/HandleInertiaRequests.php b/app/Http/Middleware/HandleInertiaRequests.php deleted file mode 100644 index 3af9fd4..0000000 --- a/app/Http/Middleware/HandleInertiaRequests.php +++ /dev/null @@ -1,56 +0,0 @@ - - */ - public function share(Request $request): array - { - [$message, $author] = str(Inspiring::quotes()->random())->explode('-'); - - return [ - ...parent::share($request), - 'name' => config('app.name'), - 'quote' => ['message' => trim($message), 'author' => trim($author)], - 'auth' => [ - 'user' => $request->user(), - ], - 'ziggy' => fn (): array => [ - ...(new Ziggy)->toArray(), - 'location' => $request->url(), - ], - 'sidebarOpen' => ! $request->hasCookie('sidebar_state') || $request->cookie('sidebar_state') === 'true', - ]; - } -} diff --git a/app/Http/Requests/Auth/LoginRequest.php b/app/Http/Requests/Auth/LoginRequest.php deleted file mode 100644 index d236bf9..0000000 --- a/app/Http/Requests/Auth/LoginRequest.php +++ /dev/null @@ -1,85 +0,0 @@ -|string> - */ - public function rules(): array - { - return [ - 'email' => ['required', 'string', 'email'], - 'password' => ['required', 'string'], - ]; - } - - /** - * Attempt to authenticate the request's credentials. - * - * @throws \Illuminate\Validation\ValidationException - */ - public function authenticate(): void - { - $this->ensureIsNotRateLimited(); - - if (! Auth::attempt($this->only('email', 'password'), $this->boolean('remember'))) { - RateLimiter::hit($this->throttleKey()); - - throw ValidationException::withMessages([ - 'email' => __('auth.failed'), - ]); - } - - RateLimiter::clear($this->throttleKey()); - } - - /** - * Ensure the login request is not rate limited. - * - * @throws \Illuminate\Validation\ValidationException - */ - public function ensureIsNotRateLimited(): void - { - if (! RateLimiter::tooManyAttempts($this->throttleKey(), 5)) { - return; - } - - event(new Lockout($this)); - - $seconds = RateLimiter::availableIn($this->throttleKey()); - - throw ValidationException::withMessages([ - 'email' => __('auth.throttle', [ - 'seconds' => $seconds, - 'minutes' => ceil($seconds / 60), - ]), - ]); - } - - /** - * Get the rate limiting throttle key for the request. - */ - public function throttleKey(): string - { - return Str::transliterate(Str::lower($this->string('email')).'|'.$this->ip()); - } -} diff --git a/app/Http/Requests/Settings/ProfileUpdateRequest.php b/app/Http/Requests/Settings/ProfileUpdateRequest.php deleted file mode 100644 index 64cf26b..0000000 --- a/app/Http/Requests/Settings/ProfileUpdateRequest.php +++ /dev/null @@ -1,32 +0,0 @@ -|string> - */ - public function rules(): array - { - return [ - 'name' => ['required', 'string', 'max:255'], - - 'email' => [ - 'required', - 'string', - 'lowercase', - 'email', - 'max:255', - Rule::unique(User::class)->ignore($this->user()->id), - ], - ]; - } -} diff --git a/app/Listeners/ValidateArticleListener.php b/app/Listeners/ValidateArticleListener.php index f8aca2e..fa76c3f 100644 --- a/app/Listeners/ValidateArticleListener.php +++ b/app/Listeners/ValidateArticleListener.php @@ -15,12 +15,7 @@ public function handle(NewArticleFetched $event): void { $article = $event->article; - // Skip if already validated if (! is_null($article->validated_at)) { - // Even if validated, don't fire ready-to-publish if already has publication - if ($article->isValid() && !$article->articlePublication()->exists()) { - event(new ArticleReadyToPublish($article)); - } return; } diff --git a/config/inertia.php b/config/inertia.php deleted file mode 100644 index f75e7b8..0000000 --- a/config/inertia.php +++ /dev/null @@ -1,55 +0,0 @@ - [ - 'enabled' => true, - 'url' => 'http://127.0.0.1:13714', - // 'bundle' => base_path('bootstrap/ssr/ssr.mjs'), - - ], - - /* - |-------------------------------------------------------------------------- - | Testing - |-------------------------------------------------------------------------- - | - | The values described here are used to locate Inertia components on the - | filesystem. For instance, when using `assertInertia`, the assertion - | attempts to locate the component as a file relative to the paths. - | - */ - - 'testing' => [ - - 'ensure_pages_exist' => true, - - 'page_paths' => [ - resource_path('js/pages'), - ], - - 'page_extensions' => [ - 'js', - 'jsx', - 'svelte', - 'ts', - 'tsx', - 'vue', - ], - - ], - -]; diff --git a/package.json b/package.json index 20229aa..afdc724 100644 --- a/package.json +++ b/package.json @@ -3,62 +3,10 @@ "type": "module", "scripts": { "build": "vite build", - "build:ssr": "vite build && vite build --ssr", - "dev": "vite", - "format": "prettier --write resources/", - "format:check": "prettier --check resources/", - "lint": "eslint . --fix", - "types": "tsc --noEmit" + "dev": "vite" }, "devDependencies": { - "@eslint/js": "^9.19.0", - "@types/node": "^22.13.5", - "eslint": "^9.17.0", - "eslint-config-prettier": "^10.0.1", - "eslint-plugin-react": "^7.37.3", - "eslint-plugin-react-hooks": "^5.1.0", - "prettier": "^3.4.2", - "prettier-plugin-organize-imports": "^4.1.0", - "prettier-plugin-tailwindcss": "^0.6.11", - "typescript-eslint": "^8.23.0" - }, - "dependencies": { - "@headlessui/react": "^2.2.0", - "@inertiajs/react": "^2.0.0", - "@radix-ui/react-avatar": "^1.1.3", - "@radix-ui/react-checkbox": "^1.1.4", - "@radix-ui/react-collapsible": "^1.1.3", - "@radix-ui/react-dialog": "^1.1.6", - "@radix-ui/react-dropdown-menu": "^2.1.6", - "@radix-ui/react-label": "^2.1.2", - "@radix-ui/react-navigation-menu": "^1.2.5", - "@radix-ui/react-select": "^2.1.6", - "@radix-ui/react-separator": "^1.1.2", - "@radix-ui/react-slot": "^1.1.2", - "@radix-ui/react-toggle": "^1.1.2", - "@radix-ui/react-toggle-group": "^1.1.2", - "@radix-ui/react-tooltip": "^1.1.8", - "@tailwindcss/vite": "^4.0.6", - "@types/react": "^19.0.3", - "@types/react-dom": "^19.0.2", - "@vitejs/plugin-react": "^4.3.4", - "class-variance-authority": "^0.7.1", - "clsx": "^2.1.1", - "concurrently": "^9.0.1", - "globals": "^15.14.0", "laravel-vite-plugin": "^1.0", - "lucide-react": "^0.475.0", - "react": "^19.0.0", - "react-dom": "^19.0.0", - "tailwind-merge": "^3.0.1", - "tailwindcss": "^4.0.0", - "tailwindcss-animate": "^1.0.7", - "typescript": "^5.7.2", "vite": "^6.0" - }, - "optionalDependencies": { - "@rollup/rollup-linux-x64-gnu": "4.9.5", - "@tailwindcss/oxide-linux-x64-gnu": "^4.0.1", - "lightningcss-linux-x64-gnu": "^1.29.1" } -} +} \ No newline at end of file diff --git a/resources/js/app.js b/resources/js/app.js new file mode 100644 index 0000000..951c6db --- /dev/null +++ b/resources/js/app.js @@ -0,0 +1,9 @@ +/** + * Main application JavaScript entry point + * This file is included on all pages using the main Blade layout + */ + +// Import any page-specific modules as needed +// The routing module will be loaded separately only on routing pages + +console.log('Lemmy Poster app loaded'); \ No newline at end of file diff --git a/resources/js/app.tsx b/resources/js/app.tsx deleted file mode 100644 index b8d0c91..0000000 --- a/resources/js/app.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import '../css/app.css'; - -import { createInertiaApp } from '@inertiajs/react'; -import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers'; -import { createRoot } from 'react-dom/client'; -import { initializeTheme } from './hooks/use-appearance'; - -const appName = import.meta.env.VITE_APP_NAME || 'Laravel'; - -createInertiaApp({ - title: (title) => `${title} - ${appName}`, - resolve: (name) => resolvePageComponent(`./pages/${name}.tsx`, import.meta.glob('./pages/**/*.tsx')), - setup({ el, App, props }) { - const root = createRoot(el); - - root.render(); - }, - progress: { - color: '#4B5563', - }, -}); - -// This will set light / dark mode on load... -initializeTheme(); diff --git a/resources/js/components/app-content.tsx b/resources/js/components/app-content.tsx deleted file mode 100644 index e5d04cd..0000000 --- a/resources/js/components/app-content.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { SidebarInset } from '@/components/ui/sidebar'; -import * as React from 'react'; - -interface AppContentProps extends React.ComponentProps<'main'> { - variant?: 'header' | 'sidebar'; -} - -export function AppContent({ variant = 'header', children, ...props }: AppContentProps) { - if (variant === 'sidebar') { - return {children}; - } - - return ( -
- {children} -
- ); -} diff --git a/resources/js/components/app-header.tsx b/resources/js/components/app-header.tsx deleted file mode 100644 index 831f2c3..0000000 --- a/resources/js/components/app-header.tsx +++ /dev/null @@ -1,182 +0,0 @@ -import { Breadcrumbs } from '@/components/breadcrumbs'; -import { Icon } from '@/components/icon'; -import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; -import { Button } from '@/components/ui/button'; -import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'; -import { NavigationMenu, NavigationMenuItem, NavigationMenuList, navigationMenuTriggerStyle } from '@/components/ui/navigation-menu'; -import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from '@/components/ui/sheet'; -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; -import { UserMenuContent } from '@/components/user-menu-content'; -import { useInitials } from '@/hooks/use-initials'; -import { cn } from '@/lib/utils'; -import { type BreadcrumbItem, type NavItem, type SharedData } from '@/types'; -import { Link, usePage } from '@inertiajs/react'; -import { BookOpen, Folder, LayoutGrid, Menu, Search } from 'lucide-react'; -import AppLogo from './app-logo'; -import AppLogoIcon from './app-logo-icon'; - -const mainNavItems: NavItem[] = [ - { - title: 'Dashboard', - href: '/dashboard', - icon: LayoutGrid, - }, -]; - -const rightNavItems: NavItem[] = [ - { - title: 'Repository', - href: 'https://github.com/laravel/react-starter-kit', - icon: Folder, - }, - { - title: 'Documentation', - href: 'https://laravel.com/docs/starter-kits#react', - icon: BookOpen, - }, -]; - -const activeItemStyles = 'text-neutral-900 dark:bg-neutral-800 dark:text-neutral-100'; - -interface AppHeaderProps { - breadcrumbs?: BreadcrumbItem[]; -} - -export function AppHeader({ breadcrumbs = [] }: AppHeaderProps) { - const page = usePage(); - const { auth } = page.props; - const getInitials = useInitials(); - return ( - <> -
-
- {/* Mobile Menu */} -
- - - - - - Navigation Menu - - - -
-
-
- {mainNavItems.map((item) => ( - - {item.icon && } - {item.title} - - ))} -
- -
- {rightNavItems.map((item) => ( - - {item.icon && } - {item.title} - - ))} -
-
-
-
-
-
- - - - - - {/* Desktop Navigation */} -
- - - {mainNavItems.map((item, index) => ( - - - {item.icon && } - {item.title} - - {page.url === item.href && ( -
- )} -
- ))} -
-
-
- -
-
- -
- {rightNavItems.map((item) => ( - - - - - {item.title} - {item.icon && } - - - -

{item.title}

-
-
-
- ))} -
-
- - - - - - - - -
-
-
- {breadcrumbs.length > 1 && ( -
-
- -
-
- )} - - ); -} diff --git a/resources/js/components/app-logo-icon.tsx b/resources/js/components/app-logo-icon.tsx deleted file mode 100644 index 9bd62ad..0000000 --- a/resources/js/components/app-logo-icon.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { SVGAttributes } from 'react'; - -export default function AppLogoIcon(props: SVGAttributes) { - return ( - - - - ); -} diff --git a/resources/js/components/app-logo.tsx b/resources/js/components/app-logo.tsx deleted file mode 100644 index 69bdcb8..0000000 --- a/resources/js/components/app-logo.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import AppLogoIcon from './app-logo-icon'; - -export default function AppLogo() { - return ( - <> -
- -
-
- Laravel Starter Kit -
- - ); -} diff --git a/resources/js/components/app-shell.tsx b/resources/js/components/app-shell.tsx deleted file mode 100644 index 0d5cdb9..0000000 --- a/resources/js/components/app-shell.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { SidebarProvider } from '@/components/ui/sidebar'; -import { SharedData } from '@/types'; -import { usePage } from '@inertiajs/react'; - -interface AppShellProps { - children: React.ReactNode; - variant?: 'header' | 'sidebar'; -} - -export function AppShell({ children, variant = 'header' }: AppShellProps) { - const isOpen = usePage().props.sidebarOpen; - - if (variant === 'header') { - return
{children}
; - } - - return {children}; -} diff --git a/resources/js/components/app-sidebar-header.tsx b/resources/js/components/app-sidebar-header.tsx deleted file mode 100644 index 6a3128b..0000000 --- a/resources/js/components/app-sidebar-header.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { Breadcrumbs } from '@/components/breadcrumbs'; -import { SidebarTrigger } from '@/components/ui/sidebar'; -import { type BreadcrumbItem as BreadcrumbItemType } from '@/types'; - -export function AppSidebarHeader({ breadcrumbs = [] }: { breadcrumbs?: BreadcrumbItemType[] }) { - return ( -
-
- - -
-
- ); -} diff --git a/resources/js/components/app-sidebar.tsx b/resources/js/components/app-sidebar.tsx deleted file mode 100644 index c517672..0000000 --- a/resources/js/components/app-sidebar.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { NavFooter } from '@/components/nav-footer'; -import { NavMain } from '@/components/nav-main'; -import { NavUser } from '@/components/nav-user'; -import { Sidebar, SidebarContent, SidebarFooter, SidebarHeader, SidebarMenu, SidebarMenuButton, SidebarMenuItem } from '@/components/ui/sidebar'; -import { type NavItem } from '@/types'; -import { Link } from '@inertiajs/react'; -import { BookOpen, Folder, LayoutGrid } from 'lucide-react'; -import AppLogo from './app-logo'; - -const mainNavItems: NavItem[] = [ - { - title: 'Dashboard', - href: '/dashboard', - icon: LayoutGrid, - }, -]; - -const footerNavItems: NavItem[] = [ - { - title: 'Repository', - href: 'https://github.com/laravel/react-starter-kit', - icon: Folder, - }, - { - title: 'Documentation', - href: 'https://laravel.com/docs/starter-kits#react', - icon: BookOpen, - }, -]; - -export function AppSidebar() { - return ( - - - - - - - - - - - - - - - - - - - - - - - ); -} diff --git a/resources/js/components/appearance-dropdown.tsx b/resources/js/components/appearance-dropdown.tsx deleted file mode 100644 index 89a4586..0000000 --- a/resources/js/components/appearance-dropdown.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import { Button } from '@/components/ui/button'; -import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'; -import { useAppearance } from '@/hooks/use-appearance'; -import { Monitor, Moon, Sun } from 'lucide-react'; -import { HTMLAttributes } from 'react'; - -export default function AppearanceToggleDropdown({ className = '', ...props }: HTMLAttributes) { - const { appearance, updateAppearance } = useAppearance(); - - const getCurrentIcon = () => { - switch (appearance) { - case 'dark': - return ; - case 'light': - return ; - default: - return ; - } - }; - - return ( -
- - - - - - updateAppearance('light')}> - - - Light - - - updateAppearance('dark')}> - - - Dark - - - updateAppearance('system')}> - - - System - - - - -
- ); -} diff --git a/resources/js/components/appearance-tabs.tsx b/resources/js/components/appearance-tabs.tsx deleted file mode 100644 index 900b0f2..0000000 --- a/resources/js/components/appearance-tabs.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { Appearance, useAppearance } from '@/hooks/use-appearance'; -import { cn } from '@/lib/utils'; -import { LucideIcon, Monitor, Moon, Sun } from 'lucide-react'; -import { HTMLAttributes } from 'react'; - -export default function AppearanceToggleTab({ className = '', ...props }: HTMLAttributes) { - const { appearance, updateAppearance } = useAppearance(); - - const tabs: { value: Appearance; icon: LucideIcon; label: string }[] = [ - { value: 'light', icon: Sun, label: 'Light' }, - { value: 'dark', icon: Moon, label: 'Dark' }, - { value: 'system', icon: Monitor, label: 'System' }, - ]; - - return ( -
- {tabs.map(({ value, icon: Icon, label }) => ( - - ))} -
- ); -} diff --git a/resources/js/components/breadcrumbs.tsx b/resources/js/components/breadcrumbs.tsx deleted file mode 100644 index cb00f91..0000000 --- a/resources/js/components/breadcrumbs.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator } from '@/components/ui/breadcrumb'; -import { type BreadcrumbItem as BreadcrumbItemType } from '@/types'; -import { Link } from '@inertiajs/react'; -import { Fragment } from 'react'; - -export function Breadcrumbs({ breadcrumbs }: { breadcrumbs: BreadcrumbItemType[] }) { - return ( - <> - {breadcrumbs.length > 0 && ( - - - {breadcrumbs.map((item, index) => { - const isLast = index === breadcrumbs.length - 1; - return ( - - - {isLast ? ( - {item.title} - ) : ( - - {item.title} - - )} - - {!isLast && } - - ); - })} - - - )} - - ); -} diff --git a/resources/js/components/delete-user.tsx b/resources/js/components/delete-user.tsx deleted file mode 100644 index e1f8788..0000000 --- a/resources/js/components/delete-user.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import { useForm } from '@inertiajs/react'; -import { FormEventHandler, useRef } from 'react'; - -import InputError from '@/components/input-error'; -import { Button } from '@/components/ui/button'; -import { Input } from '@/components/ui/input'; -import { Label } from '@/components/ui/label'; - -import HeadingSmall from '@/components/heading-small'; - -import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogTitle, DialogTrigger } from '@/components/ui/dialog'; - -export default function DeleteUser() { - const passwordInput = useRef(null); - const { data, setData, delete: destroy, processing, reset, errors, clearErrors } = useForm>({ password: '' }); - - const deleteUser: FormEventHandler = (e) => { - e.preventDefault(); - - destroy(route('profile.destroy'), { - preserveScroll: true, - onSuccess: () => closeModal(), - onError: () => passwordInput.current?.focus(), - onFinish: () => reset(), - }); - }; - - const closeModal = () => { - clearErrors(); - reset(); - }; - - return ( -
- -
-
-

Warning

-

Please proceed with caution, this cannot be undone.

-
- - - - - - - Are you sure you want to delete your account? - - Once your account is deleted, all of its resources and data will also be permanently deleted. Please enter your password - to confirm you would like to permanently delete your account. - -
-
- - - setData('password', e.target.value)} - placeholder="Password" - autoComplete="current-password" - /> - - -
- - - - - - - - - -
-
-
-
-
- ); -} diff --git a/resources/js/components/heading-small.tsx b/resources/js/components/heading-small.tsx deleted file mode 100644 index cbd3658..0000000 --- a/resources/js/components/heading-small.tsx +++ /dev/null @@ -1,8 +0,0 @@ -export default function HeadingSmall({ title, description }: { title: string; description?: string }) { - return ( -
-

{title}

- {description &&

{description}

} -
- ); -} diff --git a/resources/js/components/heading.tsx b/resources/js/components/heading.tsx deleted file mode 100644 index 6384e4b..0000000 --- a/resources/js/components/heading.tsx +++ /dev/null @@ -1,8 +0,0 @@ -export default function Heading({ title, description }: { title: string; description?: string }) { - return ( -
-

{title}

- {description &&

{description}

} -
- ); -} diff --git a/resources/js/components/icon.tsx b/resources/js/components/icon.tsx deleted file mode 100644 index 0f81d9c..0000000 --- a/resources/js/components/icon.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { cn } from '@/lib/utils'; -import { type LucideProps } from 'lucide-react'; -import { type ComponentType } from 'react'; - -interface IconProps extends Omit { - iconNode: ComponentType; -} - -export function Icon({ iconNode: IconComponent, className, ...props }: IconProps) { - return ; -} diff --git a/resources/js/components/input-error.tsx b/resources/js/components/input-error.tsx deleted file mode 100644 index bb48d71..0000000 --- a/resources/js/components/input-error.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { cn } from '@/lib/utils'; -import { type HTMLAttributes } from 'react'; - -export default function InputError({ message, className = '', ...props }: HTMLAttributes & { message?: string }) { - return message ? ( -

- {message} -

- ) : null; -} diff --git a/resources/js/components/nav-footer.tsx b/resources/js/components/nav-footer.tsx deleted file mode 100644 index 13b6737..0000000 --- a/resources/js/components/nav-footer.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { Icon } from '@/components/icon'; -import { SidebarGroup, SidebarGroupContent, SidebarMenu, SidebarMenuButton, SidebarMenuItem } from '@/components/ui/sidebar'; -import { type NavItem } from '@/types'; -import { type ComponentPropsWithoutRef } from 'react'; - -export function NavFooter({ - items, - className, - ...props -}: ComponentPropsWithoutRef & { - items: NavItem[]; -}) { - return ( - - - - {items.map((item) => ( - - - - {item.icon && } - {item.title} - - - - ))} - - - - ); -} diff --git a/resources/js/components/nav-main.tsx b/resources/js/components/nav-main.tsx deleted file mode 100644 index 0dc62c6..0000000 --- a/resources/js/components/nav-main.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { SidebarGroup, SidebarGroupLabel, SidebarMenu, SidebarMenuButton, SidebarMenuItem } from '@/components/ui/sidebar'; -import { type NavItem } from '@/types'; -import { Link, usePage } from '@inertiajs/react'; - -export function NavMain({ items = [] }: { items: NavItem[] }) { - const page = usePage(); - return ( - - Platform - - {items.map((item) => ( - - - - {item.icon && } - {item.title} - - - - ))} - - - ); -} diff --git a/resources/js/components/nav-user.tsx b/resources/js/components/nav-user.tsx deleted file mode 100644 index 386be8f..0000000 --- a/resources/js/components/nav-user.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'; -import { SidebarMenu, SidebarMenuButton, SidebarMenuItem, useSidebar } from '@/components/ui/sidebar'; -import { UserInfo } from '@/components/user-info'; -import { UserMenuContent } from '@/components/user-menu-content'; -import { useIsMobile } from '@/hooks/use-mobile'; -import { type SharedData } from '@/types'; -import { usePage } from '@inertiajs/react'; -import { ChevronsUpDown } from 'lucide-react'; - -export function NavUser() { - const { auth } = usePage().props; - const { state } = useSidebar(); - const isMobile = useIsMobile(); - - return ( - - - - - - - - - - - - - - - - ); -} diff --git a/resources/js/components/text-link.tsx b/resources/js/components/text-link.tsx deleted file mode 100644 index 1c2ddb8..0000000 --- a/resources/js/components/text-link.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { cn } from '@/lib/utils'; -import { Link } from '@inertiajs/react'; -import { ComponentProps } from 'react'; - -type LinkProps = ComponentProps; - -export default function TextLink({ className = '', children, ...props }: LinkProps) { - return ( - - {children} - - ); -} diff --git a/resources/js/components/ui/alert.tsx b/resources/js/components/ui/alert.tsx deleted file mode 100644 index 3b8ee79..0000000 --- a/resources/js/components/ui/alert.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import * as React from "react" -import { cva, type VariantProps } from "class-variance-authority" - -import { cn } from "@/lib/utils" - -const alertVariants = cva( - "relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current", - { - variants: { - variant: { - default: "bg-background text-foreground", - destructive: - "text-destructive-foreground [&>svg]:text-current *:data-[slot=alert-description]:text-destructive-foreground/80", - }, - }, - defaultVariants: { - variant: "default", - }, - } -) - -function Alert({ - className, - variant, - ...props -}: React.ComponentProps<"div"> & VariantProps) { - return ( -
- ) -} - -function AlertTitle({ className, ...props }: React.ComponentProps<"div">) { - return ( -
- ) -} - -function AlertDescription({ - className, - ...props -}: React.ComponentProps<"div">) { - return ( -
- ) -} - -export { Alert, AlertTitle, AlertDescription } diff --git a/resources/js/components/ui/avatar.tsx b/resources/js/components/ui/avatar.tsx deleted file mode 100644 index b7224f0..0000000 --- a/resources/js/components/ui/avatar.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import * as React from "react" -import * as AvatarPrimitive from "@radix-ui/react-avatar" - -import { cn } from "@/lib/utils" - -function Avatar({ - className, - ...props -}: React.ComponentProps) { - return ( - - ) -} - -function AvatarImage({ - className, - ...props -}: React.ComponentProps) { - return ( - - ) -} - -function AvatarFallback({ - className, - ...props -}: React.ComponentProps) { - return ( - - ) -} - -export { Avatar, AvatarImage, AvatarFallback } diff --git a/resources/js/components/ui/badge.tsx b/resources/js/components/ui/badge.tsx deleted file mode 100644 index 268ea77..0000000 --- a/resources/js/components/ui/badge.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import * as React from "react" -import { Slot } from "@radix-ui/react-slot" -import { cva, type VariantProps } from "class-variance-authority" - -import { cn } from "@/lib/utils" - -const badgeVariants = cva( - "inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-auto", - { - variants: { - variant: { - default: - "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90", - secondary: - "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90", - destructive: - "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40", - outline: - "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground", - }, - }, - defaultVariants: { - variant: "default", - }, - } -) - -function Badge({ - className, - variant, - asChild = false, - ...props -}: React.ComponentProps<"span"> & - VariantProps & { asChild?: boolean }) { - const Comp = asChild ? Slot : "span" - - return ( - - ) -} - -export { Badge, badgeVariants } diff --git a/resources/js/components/ui/breadcrumb.tsx b/resources/js/components/ui/breadcrumb.tsx deleted file mode 100644 index eb88f32..0000000 --- a/resources/js/components/ui/breadcrumb.tsx +++ /dev/null @@ -1,109 +0,0 @@ -import * as React from "react" -import { Slot } from "@radix-ui/react-slot" -import { ChevronRight, MoreHorizontal } from "lucide-react" - -import { cn } from "@/lib/utils" - -function Breadcrumb({ ...props }: React.ComponentProps<"nav">) { - return - - - - -
-
{children}
-
-
-
- ); -} diff --git a/resources/js/lib/utils.ts b/resources/js/lib/utils.ts deleted file mode 100644 index dd53ea8..0000000 --- a/resources/js/lib/utils.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { type ClassValue, clsx } from 'clsx'; -import { twMerge } from 'tailwind-merge'; - -export function cn(...inputs: ClassValue[]) { - return twMerge(clsx(inputs)); -} diff --git a/resources/js/pages/auth/confirm-password.tsx b/resources/js/pages/auth/confirm-password.tsx deleted file mode 100644 index bc1ae57..0000000 --- a/resources/js/pages/auth/confirm-password.tsx +++ /dev/null @@ -1,60 +0,0 @@ -// Components -import { Head, useForm } from '@inertiajs/react'; -import { LoaderCircle } from 'lucide-react'; -import { FormEventHandler } from 'react'; - -import InputError from '@/components/input-error'; -import { Button } from '@/components/ui/button'; -import { Input } from '@/components/ui/input'; -import { Label } from '@/components/ui/label'; -import AuthLayout from '@/layouts/auth-layout'; - -export default function ConfirmPassword() { - const { data, setData, post, processing, errors, reset } = useForm>({ - password: '', - }); - - const submit: FormEventHandler = (e) => { - e.preventDefault(); - - post(route('password.confirm'), { - onFinish: () => reset('password'), - }); - }; - - return ( - - - -
-
-
- - setData('password', e.target.value)} - /> - - -
- -
- -
-
-
-
- ); -} diff --git a/resources/js/pages/auth/forgot-password.tsx b/resources/js/pages/auth/forgot-password.tsx deleted file mode 100644 index 86d1b30..0000000 --- a/resources/js/pages/auth/forgot-password.tsx +++ /dev/null @@ -1,63 +0,0 @@ -// Components -import { Head, useForm } from '@inertiajs/react'; -import { LoaderCircle } from 'lucide-react'; -import { FormEventHandler } from 'react'; - -import InputError from '@/components/input-error'; -import TextLink from '@/components/text-link'; -import { Button } from '@/components/ui/button'; -import { Input } from '@/components/ui/input'; -import { Label } from '@/components/ui/label'; -import AuthLayout from '@/layouts/auth-layout'; - -export default function ForgotPassword({ status }: { status?: string }) { - const { data, setData, post, processing, errors } = useForm>({ - email: '', - }); - - const submit: FormEventHandler = (e) => { - e.preventDefault(); - - post(route('password.email')); - }; - - return ( - - - - {status &&
{status}
} - -
-
-
- - setData('email', e.target.value)} - placeholder="email@example.com" - /> - - -
- -
- -
-
- -
- Or, return to - log in -
-
-
- ); -} diff --git a/resources/js/pages/auth/login.tsx b/resources/js/pages/auth/login.tsx deleted file mode 100644 index 28f76f0..0000000 --- a/resources/js/pages/auth/login.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import { Head, useForm } from '@inertiajs/react'; -import { LoaderCircle } from 'lucide-react'; -import { FormEventHandler } from 'react'; - -import InputError from '@/components/input-error'; -import TextLink from '@/components/text-link'; -import { Button } from '@/components/ui/button'; -import { Checkbox } from '@/components/ui/checkbox'; -import { Input } from '@/components/ui/input'; -import { Label } from '@/components/ui/label'; -import AuthLayout from '@/layouts/auth-layout'; - -type LoginForm = { - email: string; - password: string; - remember: boolean; -}; - -interface LoginProps { - status?: string; - canResetPassword: boolean; -} - -export default function Login({ status, canResetPassword }: LoginProps) { - const { data, setData, post, processing, errors, reset } = useForm>({ - email: '', - password: '', - remember: false, - }); - - const submit: FormEventHandler = (e) => { - e.preventDefault(); - post(route('login'), { - onFinish: () => reset('password'), - }); - }; - - return ( - - - -
-
-
- - setData('email', e.target.value)} - placeholder="email@example.com" - /> - -
- -
-
- - {canResetPassword && ( - - Forgot password? - - )} -
- setData('password', e.target.value)} - placeholder="Password" - /> - -
- -
- setData('remember', !data.remember)} - tabIndex={3} - /> - -
- - -
- -
- Don't have an account?{' '} - - Sign up - -
-
- - {status &&
{status}
} -
- ); -} diff --git a/resources/js/pages/auth/register.tsx b/resources/js/pages/auth/register.tsx deleted file mode 100644 index 6b0faa8..0000000 --- a/resources/js/pages/auth/register.tsx +++ /dev/null @@ -1,119 +0,0 @@ -import { Head, useForm } from '@inertiajs/react'; -import { LoaderCircle } from 'lucide-react'; -import { FormEventHandler } from 'react'; - -import InputError from '@/components/input-error'; -import TextLink from '@/components/text-link'; -import { Button } from '@/components/ui/button'; -import { Input } from '@/components/ui/input'; -import { Label } from '@/components/ui/label'; -import AuthLayout from '@/layouts/auth-layout'; - -type RegisterForm = { - name: string; - email: string; - password: string; - password_confirmation: string; -}; - -export default function Register() { - const { data, setData, post, processing, errors, reset } = useForm>({ - name: '', - email: '', - password: '', - password_confirmation: '', - }); - - const submit: FormEventHandler = (e) => { - e.preventDefault(); - post(route('register'), { - onFinish: () => reset('password', 'password_confirmation'), - }); - }; - - return ( - - -
-
-
- - setData('name', e.target.value)} - disabled={processing} - placeholder="Full name" - /> - -
- -
- - setData('email', e.target.value)} - disabled={processing} - placeholder="email@example.com" - /> - -
- -
- - setData('password', e.target.value)} - disabled={processing} - placeholder="Password" - /> - -
- -
- - setData('password_confirmation', e.target.value)} - disabled={processing} - placeholder="Confirm password" - /> - -
- - -
- -
- Already have an account?{' '} - - Log in - -
-
-
- ); -} diff --git a/resources/js/pages/auth/reset-password.tsx b/resources/js/pages/auth/reset-password.tsx deleted file mode 100644 index 8ea5303..0000000 --- a/resources/js/pages/auth/reset-password.tsx +++ /dev/null @@ -1,98 +0,0 @@ -import { Head, useForm } from '@inertiajs/react'; -import { LoaderCircle } from 'lucide-react'; -import { FormEventHandler } from 'react'; - -import InputError from '@/components/input-error'; -import { Button } from '@/components/ui/button'; -import { Input } from '@/components/ui/input'; -import { Label } from '@/components/ui/label'; -import AuthLayout from '@/layouts/auth-layout'; - -interface ResetPasswordProps { - token: string; - email: string; -} - -type ResetPasswordForm = { - token: string; - email: string; - password: string; - password_confirmation: string; -}; - -export default function ResetPassword({ token, email }: ResetPasswordProps) { - const { data, setData, post, processing, errors, reset } = useForm>({ - token: token, - email: email, - password: '', - password_confirmation: '', - }); - - const submit: FormEventHandler = (e) => { - e.preventDefault(); - post(route('password.store'), { - onFinish: () => reset('password', 'password_confirmation'), - }); - }; - - return ( - - - -
-
-
- - setData('email', e.target.value)} - /> - -
- -
- - setData('password', e.target.value)} - placeholder="Password" - /> - -
- -
- - setData('password_confirmation', e.target.value)} - placeholder="Confirm password" - /> - -
- - -
-
-
- ); -} diff --git a/resources/js/pages/auth/verify-email.tsx b/resources/js/pages/auth/verify-email.tsx deleted file mode 100644 index b4f7846..0000000 --- a/resources/js/pages/auth/verify-email.tsx +++ /dev/null @@ -1,41 +0,0 @@ -// Components -import { Head, useForm } from '@inertiajs/react'; -import { LoaderCircle } from 'lucide-react'; -import { FormEventHandler } from 'react'; - -import TextLink from '@/components/text-link'; -import { Button } from '@/components/ui/button'; -import AuthLayout from '@/layouts/auth-layout'; - -export default function VerifyEmail({ status }: { status?: string }) { - const { post, processing } = useForm({}); - - const submit: FormEventHandler = (e) => { - e.preventDefault(); - - post(route('verification.send')); - }; - - return ( - - - - {status === 'verification-link-sent' && ( -
- A new verification link has been sent to the email address you provided during registration. -
- )} - -
- - - - Log out - -
-
- ); -} diff --git a/resources/js/pages/dashboard.tsx b/resources/js/pages/dashboard.tsx deleted file mode 100644 index 3f73f02..0000000 --- a/resources/js/pages/dashboard.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { PlaceholderPattern } from '@/components/ui/placeholder-pattern'; -import AppLayout from '@/layouts/app-layout'; -import { type BreadcrumbItem } from '@/types'; -import { Head } from '@inertiajs/react'; - -const breadcrumbs: BreadcrumbItem[] = [ - { - title: 'Dashboard', - href: '/dashboard', - }, -]; - -export default function Dashboard() { - return ( - - -
-
-
- -
-
- -
-
- -
-
-
- -
-
-
- ); -} diff --git a/resources/js/pages/settings/appearance.tsx b/resources/js/pages/settings/appearance.tsx deleted file mode 100644 index 5099b25..0000000 --- a/resources/js/pages/settings/appearance.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { Head } from '@inertiajs/react'; - -import AppearanceTabs from '@/components/appearance-tabs'; -import HeadingSmall from '@/components/heading-small'; -import { type BreadcrumbItem } from '@/types'; - -import AppLayout from '@/layouts/app-layout'; -import SettingsLayout from '@/layouts/settings/layout'; - -const breadcrumbs: BreadcrumbItem[] = [ - { - title: 'Appearance settings', - href: '/settings/appearance', - }, -]; - -export default function Appearance() { - return ( - - - - -
- - -
-
-
- ); -} diff --git a/resources/js/pages/settings/password.tsx b/resources/js/pages/settings/password.tsx deleted file mode 100644 index 43540bb..0000000 --- a/resources/js/pages/settings/password.tsx +++ /dev/null @@ -1,128 +0,0 @@ -import InputError from '@/components/input-error'; -import AppLayout from '@/layouts/app-layout'; -import SettingsLayout from '@/layouts/settings/layout'; -import { type BreadcrumbItem } from '@/types'; -import { Transition } from '@headlessui/react'; -import { Head, useForm } from '@inertiajs/react'; -import { FormEventHandler, useRef } from 'react'; - -import HeadingSmall from '@/components/heading-small'; -import { Button } from '@/components/ui/button'; -import { Input } from '@/components/ui/input'; -import { Label } from '@/components/ui/label'; - -const breadcrumbs: BreadcrumbItem[] = [ - { - title: 'Password settings', - href: '/settings/password', - }, -]; - -export default function Password() { - const passwordInput = useRef(null); - const currentPasswordInput = useRef(null); - - const { data, setData, errors, put, reset, processing, recentlySuccessful } = useForm({ - current_password: '', - password: '', - password_confirmation: '', - }); - - const updatePassword: FormEventHandler = (e) => { - e.preventDefault(); - - put(route('password.update'), { - preserveScroll: true, - onSuccess: () => reset(), - onError: (errors) => { - if (errors.password) { - reset('password', 'password_confirmation'); - passwordInput.current?.focus(); - } - - if (errors.current_password) { - reset('current_password'); - currentPasswordInput.current?.focus(); - } - }, - }); - }; - - return ( - - - - -
- - -
-
- - - setData('current_password', e.target.value)} - type="password" - className="mt-1 block w-full" - autoComplete="current-password" - placeholder="Current password" - /> - - -
- -
- - - setData('password', e.target.value)} - type="password" - className="mt-1 block w-full" - autoComplete="new-password" - placeholder="New password" - /> - - -
- -
- - - setData('password_confirmation', e.target.value)} - type="password" - className="mt-1 block w-full" - autoComplete="new-password" - placeholder="Confirm password" - /> - - -
- -
- - - -

Saved

-
-
-
-
-
-
- ); -} diff --git a/resources/js/pages/settings/profile.tsx b/resources/js/pages/settings/profile.tsx deleted file mode 100644 index 3aeed3a..0000000 --- a/resources/js/pages/settings/profile.tsx +++ /dev/null @@ -1,127 +0,0 @@ -import { type BreadcrumbItem, type SharedData } from '@/types'; -import { Transition } from '@headlessui/react'; -import { Head, Link, useForm, usePage } from '@inertiajs/react'; -import { FormEventHandler } from 'react'; - -import DeleteUser from '@/components/delete-user'; -import HeadingSmall from '@/components/heading-small'; -import InputError from '@/components/input-error'; -import { Button } from '@/components/ui/button'; -import { Input } from '@/components/ui/input'; -import { Label } from '@/components/ui/label'; -import AppLayout from '@/layouts/app-layout'; -import SettingsLayout from '@/layouts/settings/layout'; - -const breadcrumbs: BreadcrumbItem[] = [ - { - title: 'Profile settings', - href: '/settings/profile', - }, -]; - -type ProfileForm = { - name: string; - email: string; -}; - -export default function Profile({ mustVerifyEmail, status }: { mustVerifyEmail: boolean; status?: string }) { - const { auth } = usePage().props; - - const { data, setData, patch, errors, processing, recentlySuccessful } = useForm>({ - name: auth.user.name, - email: auth.user.email, - }); - - const submit: FormEventHandler = (e) => { - e.preventDefault(); - - patch(route('profile.update'), { - preserveScroll: true, - }); - }; - - return ( - - - - -
- - -
-
- - - setData('name', e.target.value)} - required - autoComplete="name" - placeholder="Full name" - /> - - -
- -
- - - setData('email', e.target.value)} - required - autoComplete="username" - placeholder="Email address" - /> - - -
- - {mustVerifyEmail && auth.user.email_verified_at === null && ( -
-

- Your email address is unverified.{' '} - - Click here to resend the verification email. - -

- - {status === 'verification-link-sent' && ( -
- A new verification link has been sent to your email address. -
- )} -
- )} - -
- - - -

Saved

-
-
-
-
- - -
-
- ); -} diff --git a/resources/js/pages/welcome.tsx b/resources/js/pages/welcome.tsx deleted file mode 100644 index 3f3afc1..0000000 --- a/resources/js/pages/welcome.tsx +++ /dev/null @@ -1,791 +0,0 @@ -import { type SharedData } from '@/types'; -import { Head, Link, usePage } from '@inertiajs/react'; - -export default function Welcome() { - const { auth } = usePage().props; - - return ( - <> - - - - -
-
- -
-
-
-
-

Let's get started

-

- Laravel has an incredibly rich ecosystem. -
- We suggest starting with the following. -

- - -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
-
-
-
- - ); -} diff --git a/resources/js/routing.js b/resources/js/routing.js new file mode 100644 index 0000000..8862f65 --- /dev/null +++ b/resources/js/routing.js @@ -0,0 +1,40 @@ +/** + * Routing page JavaScript functionality + * Handles dynamic keyword input management + */ + +function addKeyword() { + const container = document.getElementById('keywords-container'); + const newGroup = document.createElement('div'); + newGroup.className = 'keyword-input-group flex items-center space-x-2'; + newGroup.innerHTML = ` + + + `; + container.appendChild(newGroup); +} + +function removeKeyword(button) { + const container = document.getElementById('keywords-container'); + const groups = container.querySelectorAll('.keyword-input-group'); + + // Don't remove if it's the last remaining input + if (groups.length > 1) { + button.parentElement.remove(); + } else { + // Clear the input value instead of removing the field + const input = button.parentElement.querySelector('input'); + input.value = ''; + } +} + +// Make functions globally available for onclick handlers +window.addKeyword = addKeyword; +window.removeKeyword = removeKeyword; \ No newline at end of file diff --git a/resources/js/ssr.tsx b/resources/js/ssr.tsx deleted file mode 100644 index c6cd25a..0000000 --- a/resources/js/ssr.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { createInertiaApp } from '@inertiajs/react'; -import createServer from '@inertiajs/react/server'; -import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers'; -import ReactDOMServer from 'react-dom/server'; -import { type RouteName, route } from 'ziggy-js'; - -const appName = import.meta.env.VITE_APP_NAME || 'Laravel'; - -createServer((page) => - createInertiaApp({ - page, - render: ReactDOMServer.renderToString, - title: (title) => `${title} - ${appName}`, - resolve: (name) => resolvePageComponent(`./pages/${name}.tsx`, import.meta.glob('./pages/**/*.tsx')), - setup: ({ App, props }) => { - /* eslint-disable */ - // @ts-expect-error - global.route = (name, params, absolute) => - route(name, params as any, absolute, { - // @ts-expect-error - ...page.props.ziggy, - // @ts-expect-error - location: new URL(page.props.ziggy.location), - }); - /* eslint-enable */ - - return ; - }, - }), -); diff --git a/resources/js/types/global.d.ts b/resources/js/types/global.d.ts deleted file mode 100644 index b3c9b78..0000000 --- a/resources/js/types/global.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -import type { route as routeFn } from 'ziggy-js'; - -declare global { - const route: typeof routeFn; -} diff --git a/resources/js/types/index.d.ts b/resources/js/types/index.d.ts deleted file mode 100644 index 1a82d8e..0000000 --- a/resources/js/types/index.d.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { LucideIcon } from 'lucide-react'; -import type { Config } from 'ziggy-js'; - -export interface Auth { - user: User; -} - -export interface BreadcrumbItem { - title: string; - href: string; -} - -export interface NavGroup { - title: string; - items: NavItem[]; -} - -export interface NavItem { - title: string; - href: string; - icon?: LucideIcon | null; - isActive?: boolean; -} - -export interface SharedData { - name: string; - quote: { message: string; author: string }; - auth: Auth; - ziggy: Config & { location: string }; - sidebarOpen: boolean; - [key: string]: unknown; -} - -export interface User { - id: number; - name: string; - email: string; - avatar?: string; - email_verified_at: string | null; - created_at: string; - updated_at: string; - [key: string]: unknown; // This allows for additional properties... -} diff --git a/resources/js/types/vite-env.d.ts b/resources/js/types/vite-env.d.ts deleted file mode 100644 index 11f02fe..0000000 --- a/resources/js/types/vite-env.d.ts +++ /dev/null @@ -1 +0,0 @@ -/// diff --git a/resources/views/app.blade.php b/resources/views/app.blade.php deleted file mode 100644 index 8218267..0000000 --- a/resources/views/app.blade.php +++ /dev/null @@ -1,50 +0,0 @@ - - ($appearance ?? 'system') == 'dark'])> - - - - - {{-- Inline script to detect system dark mode preference and apply it immediately --}} - - - {{-- Inline style to set the HTML background color based on our theme in app.css --}} - - - {{ config('app.name', 'Laravel') }} - - - - - - - - - @routes - @viteReactRefresh - @vite(['resources/js/app.tsx', "resources/js/pages/{$page['component']}.tsx"]) - @inertiaHead - - - @inertia - - diff --git a/resources/views/layouts/app.blade.php b/resources/views/layouts/app.blade.php index 0859f67..355c64d 100644 --- a/resources/views/layouts/app.blade.php +++ b/resources/views/layouts/app.blade.php @@ -5,6 +5,7 @@ @yield('title', 'Lemmy Poster Admin') + @vite(['resources/css/app.css', 'resources/js/app.js'])
diff --git a/resources/views/pages/routing/create.blade.php b/resources/views/pages/routing/create.blade.php index 2e44ac7..5da8e02 100644 --- a/resources/views/pages/routing/create.blade.php +++ b/resources/views/pages/routing/create.blade.php @@ -155,37 +155,5 @@ class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-
- +@vite('resources/js/routing.js') @endsection \ No newline at end of file diff --git a/routes/auth.php b/routes/auth.php deleted file mode 100644 index 7862ed4..0000000 --- a/routes/auth.php +++ /dev/null @@ -1,56 +0,0 @@ -group(function () { - Route::get('register', [RegisteredUserController::class, 'create']) - ->name('register'); - - Route::post('register', [RegisteredUserController::class, 'store']); - - Route::get('login', [AuthenticatedSessionController::class, 'create']) - ->name('login'); - - Route::post('login', [AuthenticatedSessionController::class, 'store']); - - Route::get('forgot-password', [PasswordResetLinkController::class, 'create']) - ->name('password.request'); - - Route::post('forgot-password', [PasswordResetLinkController::class, 'store']) - ->name('password.email'); - - Route::get('reset-password/{token}', [NewPasswordController::class, 'create']) - ->name('password.reset'); - - Route::post('reset-password', [NewPasswordController::class, 'store']) - ->name('password.store'); -}); - -Route::middleware('auth')->group(function () { - Route::get('verify-email', EmailVerificationPromptController::class) - ->name('verification.notice'); - - Route::get('verify-email/{id}/{hash}', VerifyEmailController::class) - ->middleware(['signed', 'throttle:6,1']) - ->name('verification.verify'); - - Route::post('email/verification-notification', [EmailVerificationNotificationController::class, 'store']) - ->middleware('throttle:6,1') - ->name('verification.send'); - - Route::get('confirm-password', [ConfirmablePasswordController::class, 'show']) - ->name('password.confirm'); - - Route::post('confirm-password', [ConfirmablePasswordController::class, 'store']); - - Route::post('logout', [AuthenticatedSessionController::class, 'destroy']) - ->name('logout'); -}); diff --git a/routes/settings.php b/routes/settings.php deleted file mode 100644 index 9503137..0000000 --- a/routes/settings.php +++ /dev/null @@ -1,21 +0,0 @@ -group(function () { - Route::redirect('settings', 'settings/profile'); - - Route::get('settings/profile', [ProfileController::class, 'edit'])->name('profile.edit'); - Route::patch('settings/profile', [ProfileController::class, 'update'])->name('profile.update'); - Route::delete('settings/profile', [ProfileController::class, 'destroy'])->name('profile.destroy'); - - Route::get('settings/password', [PasswordController::class, 'edit'])->name('password.edit'); - Route::put('settings/password', [PasswordController::class, 'update'])->name('password.update'); - - Route::get('settings/appearance', function () { - return Inertia::render('settings/appearance'); - })->name('appearance'); -}); diff --git a/routes/web.php b/routes/web.php index 25a1f58..4221fd6 100644 --- a/routes/web.php +++ b/routes/web.php @@ -3,20 +3,6 @@ use App\Http\Controllers\ArticlesController; use App\Http\Controllers\LogsController; use Illuminate\Support\Facades\Route; -use Inertia\Inertia; - -Route::get('/', function () { - return Inertia::render('welcome'); -})->name('home'); - -Route::middleware(['auth', 'verified'])->group(function () { - Route::get('dashboard', function () { - return Inertia::render('dashboard'); - })->name('dashboard'); -}); - -require __DIR__.'/settings.php'; -require __DIR__.'/auth.php'; Route::get('/articles', ArticlesController::class)->name('articles'); Route::get('/logs', LogsController::class)->name('logs'); diff --git a/tests/Feature/ArticlePublishingTest.php b/tests/Feature/ArticlePublishingTest.php new file mode 100644 index 0000000..a050053 --- /dev/null +++ b/tests/Feature/ArticlePublishingTest.php @@ -0,0 +1,184 @@ +create(); + $article = Article::factory()->create([ + 'feed_id' => $feed->id, + 'url' => 'https://example.com/article', + 'validated_at' => now(), + 'is_valid' => true, + ]); + + $listener = new PublishArticle(); + $event = new ArticleReadyToPublish($article); + + $listener->handle($event); + + Queue::assertPushed(PublishToLemmyJob::class); + } + + public function test_publish_article_listener_skips_already_published_articles(): void + { + Queue::fake(); + + $feed = Feed::factory()->create(); + $article = Article::factory()->create([ + 'feed_id' => $feed->id, + 'url' => 'https://example.com/article', + 'validated_at' => now(), + 'is_valid' => true, + ]); + + // Create existing publication + ArticlePublication::create([ + 'article_id' => $article->id, + 'post_id' => 'existing-post-id', + 'platform_channel_id' => 1, + 'published_at' => now(), + 'published_by' => 'test-user', + ]); + + $listener = new PublishArticle(); + $event = new ArticleReadyToPublish($article); + + $listener->handle($event); + + Queue::assertNotPushed(PublishToLemmyJob::class); + } + + public function test_publish_to_lemmy_job_calls_publishing_service(): void + { + $feed = Feed::factory()->create(); + $article = Article::factory()->create([ + 'feed_id' => $feed->id, + 'url' => 'https://example.com/article', + 'validated_at' => now(), + 'is_valid' => true, + ]); + + $job = new PublishToLemmyJob($article); + + $this->assertEquals('lemmy-posts', $job->queue); + $this->assertInstanceOf(PublishToLemmyJob::class, $job); + } + + public function test_article_ready_to_publish_event_integration(): void + { + Queue::fake(); + Event::fake(); + + $feed = Feed::factory()->create(); + $article = Article::factory()->create([ + 'feed_id' => $feed->id, + 'url' => 'https://example.com/article', + 'validated_at' => now(), + 'is_valid' => true, + ]); + + event(new ArticleReadyToPublish($article)); + + Event::assertDispatched(ArticleReadyToPublish::class, function (ArticleReadyToPublish $event) use ($article) { + return $event->article->id === $article->id; + }); + } + + public function test_publishing_prevents_duplicate_publications(): void + { + $feed = Feed::factory()->create(); + $article = Article::factory()->create([ + 'feed_id' => $feed->id, + 'url' => 'https://example.com/article', + 'validated_at' => now(), + 'is_valid' => true, + ]); + + ArticlePublication::create([ + 'article_id' => $article->id, + 'post_id' => 'first-post-id', + 'platform_channel_id' => 1, + 'published_at' => now(), + 'published_by' => 'test-user', + ]); + + $this->mock(ArticlePublishingService::class, function ($mock) { + $mock->shouldNotReceive('publishToRoutedChannels'); + }); + + $listener = new PublishArticle(); + $event = new ArticleReadyToPublish($article); + + $listener->handle($event); + + $this->assertEquals(1, ArticlePublication::where('article_id', $article->id)->count()); + } + + public function test_publish_article_listener_has_correct_queue_configuration(): void + { + $listener = new PublishArticle(); + + $this->assertEquals('lemmy-publish', $listener->queue); + $this->assertEquals(300, $listener->delay); + $this->assertEquals(3, $listener->tries); + $this->assertEquals(300, $listener->backoff); + } + + public function test_publish_to_lemmy_job_has_correct_queue_configuration(): void + { + $feed = Feed::factory()->create(); + $article = Article::factory()->create([ + 'feed_id' => $feed->id, + 'url' => 'https://example.com/article', + ]); + + $job = new PublishToLemmyJob($article); + + $this->assertEquals('lemmy-posts', $job->queue); + } + + public function test_multiple_articles_can_be_queued_independently(): void + { + Queue::fake(); + + $feed = Feed::factory()->create(); + $article1 = Article::factory()->create([ + 'feed_id' => $feed->id, + 'url' => 'https://example.com/article1', + 'validated_at' => now(), + 'is_valid' => true, + ]); + $article2 = Article::factory()->create([ + 'feed_id' => $feed->id, + 'url' => 'https://example.com/article2', + 'validated_at' => now(), + 'is_valid' => true, + ]); + + $listener = new PublishArticle(); + + $listener->handle(new ArticleReadyToPublish($article1)); + $listener->handle(new ArticleReadyToPublish($article2)); + + Queue::assertPushed(PublishToLemmyJob::class, 2); + } +} \ No newline at end of file diff --git a/tests/Feature/Auth/AuthenticationTest.php b/tests/Feature/Auth/AuthenticationTest.php deleted file mode 100644 index c59d166..0000000 --- a/tests/Feature/Auth/AuthenticationTest.php +++ /dev/null @@ -1,54 +0,0 @@ -get('/login'); - - $response->assertStatus(200); - } - - public function test_users_can_authenticate_using_the_login_screen() - { - $user = User::factory()->create(); - - $response = $this->post('/login', [ - 'email' => $user->email, - 'password' => 'password', - ]); - - $this->assertAuthenticated(); - $response->assertRedirect(route('dashboard', absolute: false)); - } - - public function test_users_can_not_authenticate_with_invalid_password() - { - $user = User::factory()->create(); - - $this->post('/login', [ - 'email' => $user->email, - 'password' => 'wrong-password', - ]); - - $this->assertGuest(); - } - - public function test_users_can_logout() - { - $user = User::factory()->create(); - - $response = $this->actingAs($user)->post('/logout'); - - $this->assertGuest(); - $response->assertRedirect('/'); - } -} diff --git a/tests/Feature/Auth/EmailVerificationTest.php b/tests/Feature/Auth/EmailVerificationTest.php deleted file mode 100644 index 627fe70..0000000 --- a/tests/Feature/Auth/EmailVerificationTest.php +++ /dev/null @@ -1,58 +0,0 @@ -unverified()->create(); - - $response = $this->actingAs($user)->get('/verify-email'); - - $response->assertStatus(200); - } - - public function test_email_can_be_verified() - { - $user = User::factory()->unverified()->create(); - - Event::fake(); - - $verificationUrl = URL::temporarySignedRoute( - 'verification.verify', - now()->addMinutes(60), - ['id' => $user->id, 'hash' => sha1($user->email)] - ); - - $response = $this->actingAs($user)->get($verificationUrl); - - Event::assertDispatched(Verified::class); - $this->assertTrue($user->fresh()->hasVerifiedEmail()); - $response->assertRedirect(route('dashboard', absolute: false).'?verified=1'); - } - - public function test_email_is_not_verified_with_invalid_hash() - { - $user = User::factory()->unverified()->create(); - - $verificationUrl = URL::temporarySignedRoute( - 'verification.verify', - now()->addMinutes(60), - ['id' => $user->id, 'hash' => sha1('wrong-email')] - ); - - $this->actingAs($user)->get($verificationUrl); - - $this->assertFalse($user->fresh()->hasVerifiedEmail()); - } -} diff --git a/tests/Feature/Auth/PasswordConfirmationTest.php b/tests/Feature/Auth/PasswordConfirmationTest.php deleted file mode 100644 index d2072ff..0000000 --- a/tests/Feature/Auth/PasswordConfirmationTest.php +++ /dev/null @@ -1,44 +0,0 @@ -create(); - - $response = $this->actingAs($user)->get('/confirm-password'); - - $response->assertStatus(200); - } - - public function test_password_can_be_confirmed() - { - $user = User::factory()->create(); - - $response = $this->actingAs($user)->post('/confirm-password', [ - 'password' => 'password', - ]); - - $response->assertRedirect(); - $response->assertSessionHasNoErrors(); - } - - public function test_password_is_not_confirmed_with_invalid_password() - { - $user = User::factory()->create(); - - $response = $this->actingAs($user)->post('/confirm-password', [ - 'password' => 'wrong-password', - ]); - - $response->assertSessionHasErrors(); - } -} diff --git a/tests/Feature/Auth/PasswordResetTest.php b/tests/Feature/Auth/PasswordResetTest.php deleted file mode 100644 index 3c7441f..0000000 --- a/tests/Feature/Auth/PasswordResetTest.php +++ /dev/null @@ -1,73 +0,0 @@ -get('/forgot-password'); - - $response->assertStatus(200); - } - - public function test_reset_password_link_can_be_requested() - { - Notification::fake(); - - $user = User::factory()->create(); - - $this->post('/forgot-password', ['email' => $user->email]); - - Notification::assertSentTo($user, ResetPassword::class); - } - - public function test_reset_password_screen_can_be_rendered() - { - Notification::fake(); - - $user = User::factory()->create(); - - $this->post('/forgot-password', ['email' => $user->email]); - - Notification::assertSentTo($user, ResetPassword::class, function ($notification) { - $response = $this->get('/reset-password/'.$notification->token); - - $response->assertStatus(200); - - return true; - }); - } - - public function test_password_can_be_reset_with_valid_token() - { - Notification::fake(); - - $user = User::factory()->create(); - - $this->post('/forgot-password', ['email' => $user->email]); - - Notification::assertSentTo($user, ResetPassword::class, function ($notification) use ($user) { - $response = $this->post('/reset-password', [ - 'token' => $notification->token, - 'email' => $user->email, - 'password' => 'password', - 'password_confirmation' => 'password', - ]); - - $response - ->assertSessionHasNoErrors() - ->assertRedirect(route('login')); - - return true; - }); - } -} diff --git a/tests/Feature/Auth/RegistrationTest.php b/tests/Feature/Auth/RegistrationTest.php deleted file mode 100644 index d0c3ea2..0000000 --- a/tests/Feature/Auth/RegistrationTest.php +++ /dev/null @@ -1,31 +0,0 @@ -get('/register'); - - $response->assertStatus(200); - } - - public function test_new_users_can_register() - { - $response = $this->post('/register', [ - 'name' => 'Test User', - 'email' => 'test@example.com', - 'password' => 'password', - 'password_confirmation' => 'password', - ]); - - $this->assertAuthenticated(); - $response->assertRedirect(route('dashboard', absolute: false)); - } -} diff --git a/tests/Feature/DashboardTest.php b/tests/Feature/DashboardTest.php deleted file mode 100644 index 8585ade..0000000 --- a/tests/Feature/DashboardTest.php +++ /dev/null @@ -1,24 +0,0 @@ -get('/dashboard')->assertRedirect('/login'); - } - - public function test_authenticated_users_can_visit_the_dashboard() - { - $this->actingAs($user = User::factory()->create()); - - $this->get('/dashboard')->assertOk(); - } -} diff --git a/tests/Feature/NewArticleFetchedEventTest.php b/tests/Feature/NewArticleFetchedEventTest.php new file mode 100644 index 0000000..f1ad8d1 --- /dev/null +++ b/tests/Feature/NewArticleFetchedEventTest.php @@ -0,0 +1,31 @@ +create(); + + $article = Article::create([ + 'url' => 'https://www.google.com', + 'feed_id' => $feed->id, + ]); + + Event::assertDispatched(NewArticleFetched::class, function (NewArticleFetched $event) use ($article) { + return $event->article->id === $article->id; + }); + } +} diff --git a/tests/Feature/Settings/PasswordUpdateTest.php b/tests/Feature/Settings/PasswordUpdateTest.php deleted file mode 100644 index 64e9189..0000000 --- a/tests/Feature/Settings/PasswordUpdateTest.php +++ /dev/null @@ -1,51 +0,0 @@ -create(); - - $response = $this - ->actingAs($user) - ->from('/settings/password') - ->put('/settings/password', [ - 'current_password' => 'password', - 'password' => 'new-password', - 'password_confirmation' => 'new-password', - ]); - - $response - ->assertSessionHasNoErrors() - ->assertRedirect('/settings/password'); - - $this->assertTrue(Hash::check('new-password', $user->refresh()->password)); - } - - public function test_correct_password_must_be_provided_to_update_password() - { - $user = User::factory()->create(); - - $response = $this - ->actingAs($user) - ->from('/settings/password') - ->put('/settings/password', [ - 'current_password' => 'wrong-password', - 'password' => 'new-password', - 'password_confirmation' => 'new-password', - ]); - - $response - ->assertSessionHasErrors('current_password') - ->assertRedirect('/settings/password'); - } -} diff --git a/tests/Feature/Settings/ProfileUpdateTest.php b/tests/Feature/Settings/ProfileUpdateTest.php deleted file mode 100644 index 7d51214..0000000 --- a/tests/Feature/Settings/ProfileUpdateTest.php +++ /dev/null @@ -1,99 +0,0 @@ -create(); - - $response = $this - ->actingAs($user) - ->get('/settings/profile'); - - $response->assertOk(); - } - - public function test_profile_information_can_be_updated() - { - $user = User::factory()->create(); - - $response = $this - ->actingAs($user) - ->patch('/settings/profile', [ - 'name' => 'Test User', - 'email' => 'test@example.com', - ]); - - $response - ->assertSessionHasNoErrors() - ->assertRedirect('/settings/profile'); - - $user->refresh(); - - $this->assertSame('Test User', $user->name); - $this->assertSame('test@example.com', $user->email); - $this->assertNull($user->email_verified_at); - } - - public function test_email_verification_status_is_unchanged_when_the_email_address_is_unchanged() - { - $user = User::factory()->create(); - - $response = $this - ->actingAs($user) - ->patch('/settings/profile', [ - 'name' => 'Test User', - 'email' => $user->email, - ]); - - $response - ->assertSessionHasNoErrors() - ->assertRedirect('/settings/profile'); - - $this->assertNotNull($user->refresh()->email_verified_at); - } - - public function test_user_can_delete_their_account() - { - $user = User::factory()->create(); - - $response = $this - ->actingAs($user) - ->delete('/settings/profile', [ - 'password' => 'password', - ]); - - $response - ->assertSessionHasNoErrors() - ->assertRedirect('/'); - - $this->assertGuest(); - $this->assertNull($user->fresh()); - } - - public function test_correct_password_must_be_provided_to_delete_account() - { - $user = User::factory()->create(); - - $response = $this - ->actingAs($user) - ->from('/settings/profile') - ->delete('/settings/profile', [ - 'password' => 'wrong-password', - ]); - - $response - ->assertSessionHasErrors('password') - ->assertRedirect('/settings/profile'); - - $this->assertNotNull($user->fresh()); - } -} diff --git a/tests/Feature/ValidateArticleListenerTest.php b/tests/Feature/ValidateArticleListenerTest.php new file mode 100644 index 0000000..b94ed3b --- /dev/null +++ b/tests/Feature/ValidateArticleListenerTest.php @@ -0,0 +1,117 @@ +create(); + $article = Article::factory()->create([ + 'feed_id' => $feed->id, + 'url' => 'https://example.com/article', + 'validated_at' => null, + 'is_valid' => null, + ]); + + $listener = new ValidateArticleListener(); + $event = new NewArticleFetched($article); + + $listener->handle($event); + + $article->refresh(); + + if ($article->isValid()) { + Event::assertDispatched(ArticleReadyToPublish::class, function (ArticleReadyToPublish $event) use ($article) { + return $event->article->id === $article->id; + }); + } else { + Event::assertNotDispatched(ArticleReadyToPublish::class); + } + } + + public function test_listener_skips_already_validated_articles(): void + { + Event::fake([ArticleReadyToPublish::class]); + + $feed = Feed::factory()->create(); + $article = Article::factory()->create([ + 'feed_id' => $feed->id, + 'url' => 'https://example.com/article', + 'validated_at' => now(), + 'is_valid' => true, + ]); + + $listener = new ValidateArticleListener(); + $event = new NewArticleFetched($article); + + $listener->handle($event); + + Event::assertNotDispatched(ArticleReadyToPublish::class); + } + + public function test_listener_skips_articles_with_existing_publication(): void + { + Event::fake([ArticleReadyToPublish::class]); + + $feed = Feed::factory()->create(); + $article = Article::factory()->create([ + 'feed_id' => $feed->id, + 'url' => 'https://example.com/article', + 'validated_at' => null, + 'is_valid' => null, + ]); + + ArticlePublication::create([ + 'article_id' => $article->id, + 'post_id' => 'test-post-id', + 'platform_channel_id' => 1, + 'published_at' => now(), + 'published_by' => 'test-user', + ]); + + $listener = new ValidateArticleListener(); + $event = new NewArticleFetched($article); + + $listener->handle($event); + + Event::assertNotDispatched(ArticleReadyToPublish::class); + } + + public function test_listener_calls_validation_service(): void + { + Event::fake([ArticleReadyToPublish::class]); + + $feed = Feed::factory()->create(); + $article = Article::factory()->create([ + 'feed_id' => $feed->id, + 'url' => 'https://example.com/article', + 'validated_at' => null, + 'is_valid' => null, + ]); + + $listener = new ValidateArticleListener(); + $event = new NewArticleFetched($article); + + $listener->handle($event); + + // Verify that the article was processed by ValidationService + $article->refresh(); + $this->assertNotNull($article->validated_at, 'Article should have been validated'); + $this->assertNotNull($article->is_valid, 'Article should have validation result'); + } +} diff --git a/tests/Unit/ExampleTest.php b/tests/Unit/ExampleTest.php deleted file mode 100644 index 2cc6866..0000000 --- a/tests/Unit/ExampleTest.php +++ /dev/null @@ -1,16 +0,0 @@ -assertTrue(true); - } -} diff --git a/vite.config.js b/vite.config.js new file mode 100644 index 0000000..7b6cb94 --- /dev/null +++ b/vite.config.js @@ -0,0 +1,15 @@ +import { defineConfig } from 'vite'; +import laravel from 'laravel-vite-plugin'; + +export default defineConfig({ + plugins: [ + laravel({ + input: [ + 'resources/css/app.css', + 'resources/js/app.js', + 'resources/js/routing.js' + ], + refresh: true, + }), + ], +}); \ No newline at end of file