Skip to content

Commit

Permalink
[1.x] Adds Livewire's Functional API (#318)
Browse files Browse the repository at this point in the history
* Adds Livewire Functional API

* Apply fixes from StyleCI

* Update InstallCommand.php

* formatting

---------

Co-authored-by: StyleCI Bot <bot@styleci.io>
Co-authored-by: Taylor Otwell <taylor@laravel.com>
  • Loading branch information
3 people authored Oct 6, 2023
1 parent 914a8bc commit d623712
Show file tree
Hide file tree
Showing 43 changed files with 875 additions and 14 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
strategy:
fail-fast: true
matrix:
stack: [blade, livewire, react, vue, api]
stack: [blade, livewire, livewire-functional, react, vue, api]
laravel: [10]
args: ["", --pest]
include:
Expand Down
14 changes: 9 additions & 5 deletions src/Console/InstallCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ class InstallCommand extends Command implements PromptsForMissingInput
*
* @var string
*/
protected $signature = 'breeze:install {stack : The development stack that should be installed (blade,livewire,react,vue,api)}
protected $signature = 'breeze:install {stack : The development stack that should be installed (blade,livewire,livewire-functional,react,vue,api)}
{--dark : Indicate that dark mode support should be installed}
{--pest : Indicate that Pest should be installed}
{--ssr : Indicates if Inertia SSR support should be installed}
Expand Down Expand Up @@ -57,9 +57,11 @@ public function handle()
return $this->installBladeStack();
} elseif ($this->argument('stack') === 'livewire') {
return $this->installLivewireStack();
} elseif ($this->argument('stack') === 'livewire-functional') {
return $this->installLivewireStack(true);
}

$this->components->error('Invalid stack. Supported stacks are [blade], [livewire], [react], [vue], and [api].');
$this->components->error('Invalid stack. Supported stacks are [blade], [livewire], [livewire-functional], [react], [vue], and [api].');

return 1;
}
Expand All @@ -75,7 +77,8 @@ protected function installTests()

$stubStack = match ($this->argument('stack')) {
'api' => 'api',
'livewire' => 'livewire',
'livewire' => 'livewire-common',
'livewire-functional' => 'livewire-common',
default => 'default',
};

Expand Down Expand Up @@ -314,7 +317,8 @@ protected function promptForMissingArgumentsUsing()
label: 'Which Breeze stack would you like to install?',
options: [
'blade' => 'Blade with Alpine',
'livewire' => 'Livewire with Alpine',
'livewire' => 'Livewire (Volt Class API) with Alpine',
'livewire-functional' => 'Livewire (Volt Functional API) with Alpine',
'react' => 'React with Inertia',
'vue' => 'Vue with Inertia',
'api' => 'API only',
Expand Down Expand Up @@ -343,7 +347,7 @@ protected function afterPromptingForMissingArguments(InputInterface $input, Outp
'typescript' => 'TypeScript (experimental)',
]
))->each(fn ($option) => $input->setOption($option, true));
} elseif (in_array($stack, ['blade', 'livewire'])) {
} elseif (in_array($stack, ['blade', 'livewire', 'livewire-functional'])) {
$input->setOption('dark', confirm(
label: 'Would you like dark mode support?',
default: false
Expand Down
18 changes: 12 additions & 6 deletions src/Console/InstallsLivewireStack.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ trait InstallsLivewireStack
*
* @return int|null
*/
protected function installLivewireStack()
protected function installLivewireStack($functional = false)
{
// NPM Packages...
$this->updateNodePackages(function ($packages) {
Expand Down Expand Up @@ -44,16 +44,22 @@ protected function installLivewireStack()

// Views...
(new Filesystem)->ensureDirectoryExists(resource_path('views'));
(new Filesystem)->copyDirectory(__DIR__.'/../../stubs/livewire/resources/views', resource_path('views'));
(new Filesystem)->copyDirectory(__DIR__.'/../../stubs/livewire-common/resources/views', resource_path('views'));

// Livewire Components...
(new Filesystem)->ensureDirectoryExists(resource_path('views/livewire'));
(new Filesystem)->copyDirectory(__DIR__.'/../../stubs/'
.($functional ? 'livewire-functional' : 'livewire')
.'/resources/views/livewire', resource_path('views/livewire'));

// Views Components...
(new Filesystem)->ensureDirectoryExists(resource_path('views/components'));
(new Filesystem)->copyDirectory(__DIR__.'/../../stubs/default/resources/views/components', resource_path('views/components'));
(new Filesystem)->copyDirectory(__DIR__.'/../../stubs/livewire/resources/views/components', resource_path('views/components'));
(new Filesystem)->copyDirectory(__DIR__.'/../../stubs/livewire-common/resources/views/components', resource_path('views/components'));

// Views Layouts...
(new Filesystem)->ensureDirectoryExists(resource_path('views/layouts'));
(new Filesystem)->copyDirectory(__DIR__.'/../../stubs/livewire/resources/views/layouts', resource_path('views/layouts'));
(new Filesystem)->copyDirectory(__DIR__.'/../../stubs/livewire-common/resources/views/layouts', resource_path('views/layouts'));

// Components...
(new Filesystem)->ensureDirectoryExists(app_path('View/Components'));
Expand All @@ -74,8 +80,8 @@ protected function installLivewireStack()
}

// Routes...
copy(__DIR__.'/../../stubs/livewire/routes/web.php', base_path('routes/web.php'));
copy(__DIR__.'/../../stubs/livewire/routes/auth.php', base_path('routes/auth.php'));
copy(__DIR__.'/../../stubs/livewire-common/routes/web.php', base_path('routes/web.php'));
copy(__DIR__.'/../../stubs/livewire-common/routes/auth.php', base_path('routes/auth.php'));

// "Dashboard" Route...
$this->replaceInFile('/home', '/dashboard', app_path('Providers/RouteServiceProvider.php'));
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
<?php
$logout = function () {
auth()->guard('web')->logout();
session()->invalidate();
session()->regenerateToken();
$this->redirect('/', navigate: true);
};
?>

<nav x-data="{ open: false }" class="bg-white dark:bg-gray-800 border-b border-gray-100 dark:border-gray-700">
<!-- Primary Navigation Menu -->
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between h-16">
<div class="flex">
<!-- Logo -->
<div class="shrink-0 flex items-center">
<a href="{{ route('dashboard') }}" wire:navigate>
<x-application-logo class="block h-9 w-auto fill-current text-gray-800 dark:text-gray-200" />
</a>
</div>

<!-- Navigation Links -->
<div class="hidden space-x-8 sm:-my-px sm:ml-10 sm:flex">
<x-nav-link :href="route('dashboard')" :active="request()->routeIs('dashboard')" wire:navigate>
{{ __('Dashboard') }}
</x-nav-link>
</div>
</div>

<!-- Settings Dropdown -->
<div class="hidden sm:flex sm:items-center sm:ml-6">
<x-dropdown align="right" width="48">
<x-slot name="trigger">
<button class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-gray-500 dark:text-gray-400 bg-white dark:bg-gray-800 hover:text-gray-700 dark:hover:text-gray-300 focus:outline-none transition ease-in-out duration-150">
<div x-data="{ name: '{{ auth()->user()->name }}' }" x-text="name" x-on:profile-updated.window="name = $event.detail.name"></div>

<div class="ml-1">
<svg class="fill-current h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" />
</svg>
</div>
</button>
</x-slot>

<x-slot name="content">
<x-dropdown-link :href="route('profile')" wire:navigate>
{{ __('Profile') }}
</x-dropdown-link>

<!-- Authentication -->
<button wire:click="logout" class="w-full text-left">
<x-dropdown-link>
{{ __('Log Out') }}
</x-dropdown-link>
</button>
</x-slot>
</x-dropdown>
</div>

<!-- Hamburger -->
<div class="-mr-2 flex items-center sm:hidden">
<button @click="open = ! open" class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 dark:text-gray-500 hover:text-gray-500 dark:hover:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-900 focus:outline-none focus:bg-gray-100 dark:focus:bg-gray-900 focus:text-gray-500 dark:focus:text-gray-400 transition duration-150 ease-in-out">
<svg class="h-6 w-6" stroke="currentColor" fill="none" viewBox="0 0 24 24">
<path :class="{'hidden': open, 'inline-flex': ! open }" class="inline-flex" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
<path :class="{'hidden': ! open, 'inline-flex': open }" class="hidden" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</div>
</div>

<!-- Responsive Navigation Menu -->
<div :class="{'block': open, 'hidden': ! open}" class="hidden sm:hidden">
<div class="pt-2 pb-3 space-y-1">
<x-responsive-nav-link :href="route('dashboard')" :active="request()->routeIs('dashboard')" wire:navigate>
{{ __('Dashboard') }}
</x-responsive-nav-link>
</div>

<!-- Responsive Settings Options -->
<div class="pt-4 pb-1 border-t border-gray-200 dark:border-gray-600">
<div class="px-4">
<div class="font-medium text-base text-gray-800 dark:text-gray-200" x-data="{ name: '{{ auth()->user()->name }}' }" x-text="name" x-on:profile-updated.window="name = $event.detail.name"></div>
<div class="font-medium text-sm text-gray-500">{{ auth()->user()->email }}</div>
</div>

<div class="mt-3 space-y-1">
<x-responsive-nav-link :href="route('profile')" wire:navigate>
{{ __('Profile') }}
</x-responsive-nav-link>

<!-- Authentication -->
<button wire:click="logout" class="w-full text-left">
<x-responsive-nav-link>
{{ __('Log Out') }}
</x-responsive-nav-link>
</button>
</div>
</div>
</div>
</nav>
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<?php
use App\Providers\RouteServiceProvider;
use Illuminate\Validation\ValidationException;
use function Livewire\Volt\layout;
use function Livewire\Volt\rules;
use function Livewire\Volt\state;
layout('layouts.guest');
state(['password' => '']);
rules(['password' => ['required', 'string']]);
$confirmPassword = function () {
$this->validate();
if (! auth()->guard('web')->validate([
'email' => auth()->user()->email,
'password' => $this->password,
])) {
throw ValidationException::withMessages([
'password' => __('auth.password'),
]);
}
session(['auth.password_confirmed_at' => time()]);
$this->redirect(
session('url.intended', RouteServiceProvider::HOME),
navigate: true
);
};
?>

<div>
<div class="mb-4 text-sm text-gray-600 dark:text-gray-400">
{{ __('This is a secure area of the application. Please confirm your password before continuing.') }}
</div>

<form wire:submit="confirmPassword">
<!-- Password -->
<div>
<x-input-label for="password" :value="__('Password')" />

<x-text-input wire:model="password"
id="password"
class="block mt-1 w-full"
type="password"
name="password"
required autocomplete="current-password" />

<x-input-error :messages="$errors->get('password')" class="mt-2" />
</div>

<div class="flex justify-end mt-4">
<x-primary-button>
{{ __('Confirm') }}
</x-primary-button>
</div>
</form>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?php
use Illuminate\Support\Facades\Password;
use function Livewire\Volt\layout;
use function Livewire\Volt\rules;
use function Livewire\Volt\state;
layout('layouts.guest');
state(['email' => '']);
rules(['email' => ['required', 'string', 'email']]);
$sendPasswordResetLink = function () {
$this->validate();
// We will send the password reset link to this user. Once we have attempted
// to send the link, we will examine the response then see the message we
// need to show to the user. Finally, we'll send out a proper response.
$status = Password::sendResetLink(
$this->only('email')
);
if ($status != Password::RESET_LINK_SENT) {
$this->addError('email', __($status));
return;
}
$this->reset('email');
session()->flash('status', __($status));
};
?>

<div>
<div class="mb-4 text-sm text-gray-600 dark:text-gray-400">
{{ __('Forgot your password? No problem. Just let us know your email address and we will email you a password reset link that will allow you to choose a new one.') }}
</div>

<!-- Session Status -->
<x-auth-session-status class="mb-4" :status="session('status')" />

<form wire:submit="sendPasswordResetLink">
<!-- Email Address -->
<div>
<x-input-label for="email" :value="__('Email')" />
<x-text-input wire:model="email" id="email" class="block mt-1 w-full" type="email" name="email" required autofocus />
<x-input-error :messages="$errors->get('email')" class="mt-2" />
</div>

<div class="flex items-center justify-end mt-4">
<x-primary-button>
{{ __('Email Password Reset Link') }}
</x-primary-button>
</div>
</form>
</div>
Loading

0 comments on commit d623712

Please sign in to comment.