diff --git a/app/Actions/Fortify/CreateNewUserWithTeams.php b/app/Actions/Fortify/CreateNewUserWithTeams.php new file mode 100644 index 0000000..9788471 --- /dev/null +++ b/app/Actions/Fortify/CreateNewUserWithTeams.php @@ -0,0 +1,53 @@ + $input + */ + public function create(array $input): User + { + Validator::make($input, [ + 'name' => ['required', 'string', 'max:255'], + 'email' => ['required', 'string', 'email', 'max:255', 'unique:users'], + 'password' => $this->passwordRules(), + 'terms' => Jetstream::hasTermsAndPrivacyPolicyFeature() ? ['accepted', 'required'] : '', + ])->validate(); + + return DB::transaction(function () use ($input) { + return tap(User::create([ + 'name' => $input['name'], + 'email' => $input['email'], + 'password' => Hash::make($input['password']), + ]), function (User $user) { + $this->createTeam($user); + }); + }); + } + + /** + * Create a personal team for the user. + */ + protected function createTeam(User $user): void + { + $user->ownedTeams()->save(Team::forceCreate([ + 'user_id' => $user->id, + 'name' => explode(' ', $user->name, 2)[0]."'s Team", + 'personal_team' => true, + ])); + } +} diff --git a/app/Actions/Jetstream/AddTeamMember.php b/app/Actions/Jetstream/AddTeamMember.php new file mode 100644 index 0000000..c3d91bc --- /dev/null +++ b/app/Actions/Jetstream/AddTeamMember.php @@ -0,0 +1,82 @@ +authorize('addTeamMember', $team); + + $this->validate($team, $email, $role); + + $newTeamMember = Jetstream::findUserByEmailOrFail($email); + + AddingTeamMember::dispatch($team, $newTeamMember); + + $team->users()->attach( + $newTeamMember, + ['role' => $role] + ); + + TeamMemberAdded::dispatch($team, $newTeamMember); + } + + /** + * Validate the add member operation. + */ + protected function validate(Team $team, string $email, ?string $role): void + { + Validator::make([ + 'email' => $email, + 'role' => $role, + ], $this->rules(), [ + 'email.exists' => __('We were unable to find a registered user with this email address.'), + ])->after( + $this->ensureUserIsNotAlreadyOnTeam($team, $email) + )->validateWithBag('addTeamMember'); + } + + /** + * Get the validation rules for adding a team member. + * + * @return array + */ + protected function rules(): array + { + return array_filter([ + 'email' => ['required', 'email', 'exists:users'], + 'role' => Jetstream::hasRoles() + ? ['required', 'string', new Role()] + : null, + ]); + } + + /** + * Ensure that the user is not already on the team. + */ + protected function ensureUserIsNotAlreadyOnTeam(Team $team, string $email): Closure + { + return function ($validator) use ($team, $email) { + $validator->errors()->addIf( + $team->hasUserWithEmail($email), + 'email', + __('This user already belongs to the team.') + ); + }; + } +} diff --git a/app/Actions/Jetstream/CreateTeam.php b/app/Actions/Jetstream/CreateTeam.php new file mode 100644 index 0000000..6e6020c --- /dev/null +++ b/app/Actions/Jetstream/CreateTeam.php @@ -0,0 +1,37 @@ + $input + */ + public function create(User $user, array $input): Team + { + Gate::forUser($user)->authorize('create', Jetstream::newTeamModel()); + + Validator::make($input, [ + 'name' => ['required', 'string', 'max:255'], + ])->validateWithBag('createTeam'); + + AddingTeam::dispatch($user); + + $user->switchTeam($team = $user->ownedTeams()->create([ + 'name' => $input['name'], + 'personal_team' => true, + ])); + + return $team; + } +} diff --git a/app/Actions/Jetstream/DeleteTeam.php b/app/Actions/Jetstream/DeleteTeam.php new file mode 100644 index 0000000..680dc36 --- /dev/null +++ b/app/Actions/Jetstream/DeleteTeam.php @@ -0,0 +1,17 @@ +purge(); + } +} diff --git a/app/Actions/Jetstream/DeleteUserWithTeams.php b/app/Actions/Jetstream/DeleteUserWithTeams.php new file mode 100644 index 0000000..4b051af --- /dev/null +++ b/app/Actions/Jetstream/DeleteUserWithTeams.php @@ -0,0 +1,44 @@ +deleteTeams($user); + $user->deleteProfilePhoto(); + $user->tokens->each->delete(); + $user->delete(); + }); + } + + /** + * Delete the teams and team associations attached to the user. + */ + protected function deleteTeams(User $user): void + { + $user->teams()->detach(); + + $user->ownedTeams->each(function (Team $team) { + $this->deletesTeams->delete($team); + }); + } +} diff --git a/app/Actions/Jetstream/InviteTeamMember.php b/app/Actions/Jetstream/InviteTeamMember.php new file mode 100644 index 0000000..4d74abe --- /dev/null +++ b/app/Actions/Jetstream/InviteTeamMember.php @@ -0,0 +1,88 @@ +authorize('addTeamMember', $team); + + $this->validate($team, $email, $role); + + InvitingTeamMember::dispatch($team, $email, $role); + + $invitation = $team->teamInvitations()->create([ + 'email' => $email, + 'role' => $role, + ]); + + Mail::to($email)->send(new TeamInvitation($invitation)); + } + + /** + * Validate the invite member operation. + */ + protected function validate(Team $team, string $email, ?string $role): void + { + Validator::make([ + 'email' => $email, + 'role' => $role, + ], $this->rules($team), [ + 'email.unique' => __('This user has already been invited to the team.'), + ])->after( + $this->ensureUserIsNotAlreadyOnTeam($team, $email) + )->validateWithBag('addTeamMember'); + } + + /** + * Get the validation rules for inviting a team member. + * + * @return array + */ + protected function rules(Team $team): array + { + return array_filter([ + 'email' => [ + 'required', 'email', + Rule::unique(Jetstream::teamInvitationModel())->where(function (Builder $query) use ($team) { + $query->where('team_id', $team->id); + }), + ], + 'role' => Jetstream::hasRoles() + ? ['required', 'string', new Role()] + : null, + ]); + } + + /** + * Ensure that the user is not already on the team. + */ + protected function ensureUserIsNotAlreadyOnTeam(Team $team, string $email): Closure + { + return function ($validator) use ($team, $email) { + $validator->errors()->addIf( + $team->hasUserWithEmail($email), + 'email', + __('This user already belongs to the team.') + ); + }; + } +} diff --git a/app/Actions/Jetstream/RemoveTeamMember.php b/app/Actions/Jetstream/RemoveTeamMember.php new file mode 100644 index 0000000..2b354ef --- /dev/null +++ b/app/Actions/Jetstream/RemoveTeamMember.php @@ -0,0 +1,51 @@ +authorize($user, $team, $teamMember); + + $this->ensureUserDoesNotOwnTeam($teamMember, $team); + + $team->removeUser($teamMember); + + TeamMemberRemoved::dispatch($team, $teamMember); + } + + /** + * Authorize that the user can remove the team member. + */ + protected function authorize(User $user, Team $team, User $teamMember): void + { + if (!Gate::forUser($user)->check('removeTeamMember', $team) && + $user->id !== $teamMember->id) { + throw new AuthorizationException(); + } + } + + /** + * Ensure that the currently authenticated user does not own the team. + */ + protected function ensureUserDoesNotOwnTeam(User $teamMember, Team $team): void + { + if ($teamMember->id === $team->owner->id) { + throw ValidationException::withMessages([ + 'team' => [__('You may not leave a team that you created.')], + ])->errorBag('removeTeamMember'); + } + } +} diff --git a/app/Actions/Jetstream/UpdateTeamName.php b/app/Actions/Jetstream/UpdateTeamName.php new file mode 100644 index 0000000..e0dadb8 --- /dev/null +++ b/app/Actions/Jetstream/UpdateTeamName.php @@ -0,0 +1,30 @@ + $input + */ + public function update(User $user, Team $team, array $input): void + { + Gate::forUser($user)->authorize('update', $team); + + Validator::make($input, [ + 'name' => ['required', 'string', 'max:255'], + ])->validateWithBag('updateTeamName'); + + $team->forceFill([ + 'name' => $input['name'], + ])->save(); + } +} diff --git a/app/Actions/Socialstream/CreateUserWithTeamsFromProvider.php b/app/Actions/Socialstream/CreateUserWithTeamsFromProvider.php new file mode 100644 index 0000000..0dc5b9e --- /dev/null +++ b/app/Actions/Socialstream/CreateUserWithTeamsFromProvider.php @@ -0,0 +1,62 @@ +createsConnectedAccounts = $createsConnectedAccounts; + } + + /** + * Create a new user from a social provider user. + */ + public function create(string $provider, ProviderUserContract $providerUser): User + { + return DB::transaction(function () use ($provider, $providerUser) { + return tap(User::create([ + 'name' => $providerUser->getName(), + 'email' => $providerUser->getEmail(), + ]), function (User $user) use ($provider, $providerUser) { + $user->markEmailAsVerified(); + + if (Socialstream::hasProviderAvatarsFeature() && $providerUser->getAvatar()) { + $user->setProfilePhotoFromUrl($providerUser->getAvatar()); + } + + $this->createsConnectedAccounts->create($user, $provider, $providerUser); + + $this->createTeam($user); + }); + }); + } + + /** + * Create a personal team for the user. + */ + protected function createTeam(User $user): void + { + $user->ownedTeams()->save(Team::forceCreate([ + 'user_id' => $user->id, + 'name' => explode(' ', $user->name, 2)[0]."'s Team", + 'personal_team' => true, + ])); + } +} diff --git a/app/Http/Controllers/ForgotPasswordController.php b/app/Http/Controllers/ForgotPasswordController.php new file mode 100644 index 0000000..4517a24 --- /dev/null +++ b/app/Http/Controllers/ForgotPasswordController.php @@ -0,0 +1,28 @@ +validate(['email' => 'required|email']); + + $status = Password::broker('admins')->sendResetLink( + $request->only('email') + ); + + return $status === Password::RESET_LINK_SENT + ? back()->with(['status' => __($status)]) + : back()->withErrors(['email' => __($status)]); + } +} diff --git a/app/Http/Controllers/LoginController.php b/app/Http/Controllers/LoginController.php new file mode 100644 index 0000000..3cfdaa5 --- /dev/null +++ b/app/Http/Controllers/LoginController.php @@ -0,0 +1,41 @@ +validate([ + 'email' => ['required', 'email'], + 'password' => ['required'], + ]); + + if (Auth::guard('admin')->attempt($credentials)) { + $request->session()->regenerate(); + + return redirect()->intended('/admin'); + } + + return back()->withErrors([ + 'email' => 'The provided credentials do not match our records.', + ]); + } + + public function logout(Request $request) + { + Auth::guard('admin')->logout(); + $request->session()->invalidate(); + $request->session()->regenerateToken(); + + return redirect('/admin/login'); + } +} diff --git a/app/Http/Controllers/ResetPasswordController.php b/app/Http/Controllers/ResetPasswordController.php new file mode 100644 index 0000000..6919be2 --- /dev/null +++ b/app/Http/Controllers/ResetPasswordController.php @@ -0,0 +1,44 @@ + $token, 'email' => $request->email]); + } + + public function reset(Request $request) + { + $request->validate([ + 'token' => 'required', + 'email' => 'required|email', + 'password' => 'required|min:8|confirmed', + ]); + + $status = Password::broker('admins')->reset( + $request->only('email', 'password', 'password_confirmation', 'token'), + function ($user, $password) { + $user->forceFill([ + 'password' => Hash::make($password), + ])->setRememberToken(Str::random(60)); + + $user->save(); + + event(new PasswordReset($user)); + } + ); + + return $status === Password::PASSWORD_RESET + ? redirect()->route('admin.login')->with('status', __($status)) + : back()->withErrors(['email' => [__($status)]]); + } +} diff --git a/app/Http/Controllers/TeamInvitationController.php b/app/Http/Controllers/TeamInvitationController.php new file mode 100644 index 0000000..d445b7c --- /dev/null +++ b/app/Http/Controllers/TeamInvitationController.php @@ -0,0 +1,49 @@ +validate([ + 'email' => 'required|email', + 'team_id' => 'required|exists:teams,id', + ]); + + $user = User::firstOrCreate(['email' => $request->email], ['password' => bcrypt(Str::random(10))]); + $team = Team::findOrFail($request->team_id); + + $invitationToken = Str::random(32); + $user->invitations()->create([ + 'team_id' => $team->id, + 'token' => $invitationToken, + ]); + + Mail::to($request->email)->send(new TeamInvitation($user, $team, $invitationToken)); + + return response()->json(['message' => 'Invitation sent successfully.']); + } + + public function acceptInvitation(Request $request) + { + $request->validate([ + 'token' => 'required|exists:invitations,token', + ]); + + $invitation = Invitation::where('token', $request->token)->firstOrFail(); + $team = Team::findOrFail($invitation->team_id); + $team->members()->attach($invitation->user_id); + + $invitation->update(['accepted' => true]); + + return response()->json(['message' => 'Invitation accepted successfully.']); + } +} diff --git a/app/Listeners/CreatePersonalTeam.php b/app/Listeners/CreatePersonalTeam.php new file mode 100644 index 0000000..c2e36e3 --- /dev/null +++ b/app/Listeners/CreatePersonalTeam.php @@ -0,0 +1,22 @@ + + + + + {{ __('Create API Token') }} + + + + {{ __('API tokens allow third-party services to authenticate with our application on your behalf.') }} + + + + +
+ + + +
+ + + @if (Laravel\Jetstream\Jetstream::hasPermissions()) +
+ + +
+ @foreach (Laravel\Jetstream\Jetstream::$permissions as $permission) + + @endforeach +
+
+ @endif +
+ + + + {{ __('Created.') }} + + + + {{ __('Create') }} + + +
+ + @if ($this->user->tokens->isNotEmpty()) + + + +
+ + + {{ __('Manage API Tokens') }} + + + + {{ __('You may delete any of your existing tokens if they are no longer needed.') }} + + + + +
+ @foreach ($this->user->tokens->sortBy('name') as $token) +
+
+ {{ $token->name }} +
+ +
+ @if ($token->last_used_at) +
+ {{ __('Last used') }} {{ $token->last_used_at->diffForHumans() }} +
+ @endif + + @if (Laravel\Jetstream\Jetstream::hasPermissions()) + + @endif + + +
+
+ @endforeach +
+
+
+
+ @endif + + + + + {{ __('API Token') }} + + + +
+ {{ __('Please copy your new API token. For your security, it won\'t be shown again.') }} +
+ + +
+ + + + {{ __('Close') }} + + +
+ + + + + {{ __('API Token Permissions') }} + + + +
+ @foreach (Laravel\Jetstream\Jetstream::$permissions as $permission) + + @endforeach +
+
+ + + + {{ __('Cancel') }} + + + + {{ __('Save') }} + + +
+ + + + + {{ __('Delete API Token') }} + + + + {{ __('Are you sure you would like to delete this API token?') }} + + + + + {{ __('Cancel') }} + + + + {{ __('Delete') }} + + + + diff --git a/resources/views/api/index.blade.php b/resources/views/api/index.blade.php new file mode 100644 index 0000000..d15b6a3 --- /dev/null +++ b/resources/views/api/index.blade.php @@ -0,0 +1,13 @@ + + +

+ {{ __('API Tokens') }} +

+
+ +
+
+ @livewire('api.api-token-manager') +
+
+
diff --git a/resources/views/auth/confirm-password.blade.php b/resources/views/auth/confirm-password.blade.php new file mode 100644 index 0000000..0a109b8 --- /dev/null +++ b/resources/views/auth/confirm-password.blade.php @@ -0,0 +1,28 @@ + + + + + + +
+ {{ __('This is a secure area of the application. Please confirm your password before continuing.') }} +
+ + + +
+ @csrf + +
+ + +
+ +
+ + {{ __('Confirm') }} + +
+
+
+
diff --git a/resources/views/auth/forgot-password.blade.php b/resources/views/auth/forgot-password.blade.php new file mode 100644 index 0000000..15235f4 --- /dev/null +++ b/resources/views/auth/forgot-password.blade.php @@ -0,0 +1,34 @@ + + + + + + +
+ {{ __('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.') }} +
+ + @session('status') +
+ {{ $value }} +
+ @endsession + + + +
+ @csrf + +
+ + +
+ +
+ + {{ __('Email Password Reset Link') }} + +
+
+
+
diff --git a/resources/views/auth/login.blade.php b/resources/views/auth/login.blade.php index 8689ce2..8866b11 100644 --- a/resources/views/auth/login.blade.php +++ b/resources/views/auth/login.blade.php @@ -1,52 +1,42 @@ - - - - - - - - - @if (session('status')) -
- {{ session('status') }} -
- @endif - -
- @csrf - -
- - -
- -
- - -
- -
- -
- -
- @if (Route::has('password.request')) - - {{ __('Forgot your password?') }} - - @endif - - - {{ __('Login') }} - -
-
- - @if (JoelButcher\Socialstream\Socialstream::show()) - - @endif -
-
+@extends('layouts.home') + +@section('content') +
+
+
+ {{ __('Please sign in to access the admin panel.') }} +
+ +
+ @csrf + +
+ + +
+ +
+ + +
+ +
+ +
+ +
+ +
+
+
+
+@endsection diff --git a/resources/views/auth/register.blade.php b/resources/views/auth/register.blade.php index 526eeff..c3a252a 100644 --- a/resources/views/auth/register.blade.php +++ b/resources/views/auth/register.blade.php @@ -1,64 +1,47 @@ - - - - - - - - -
- @csrf - -
- - -
- -
- - -
- -
- - -
- -
- - -
+@extends('layouts.home') + +@section('content') +
+
+ + + +
+ +
+ + @csrf + +
+ + +
- @if (Laravel\Jetstream\Jetstream::hasTermsAndPrivacyPolicyFeature())
- -
- + + +
-
- {!! __('I agree to the :terms_of_service and :privacy_policy', [ - 'terms_of_service' => ''.__('Terms of Service').'', - 'privacy_policy' => ''.__('Privacy Policy').'', - ]) !!} -
-
- +
+ +
- @endif -
- - {{ __('Already registered?') }} - +
+ + +
- - {{ __('Register') }} - -
- +
+ + {{ __('Already registered?') }} + - @if (JoelButcher\Socialstream\Socialstream::show()) - - @endif - - + + {{ __('Register') }} + +
+ +
+
+@endsection diff --git a/resources/views/auth/reset-password.blade.php b/resources/views/auth/reset-password.blade.php new file mode 100644 index 0000000..5991e3e --- /dev/null +++ b/resources/views/auth/reset-password.blade.php @@ -0,0 +1,36 @@ + + + + + + + + +
+ @csrf + + + +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + {{ __('Reset Password') }} + +
+
+
+
diff --git a/resources/views/auth/two-factor-challenge.blade.php b/resources/views/auth/two-factor-challenge.blade.php new file mode 100644 index 0000000..57e0347 --- /dev/null +++ b/resources/views/auth/two-factor-challenge.blade.php @@ -0,0 +1,58 @@ + + + + + + +
+
+ {{ __('Please confirm access to your account by entering the authentication code provided by your authenticator application.') }} +
+ +
+ {{ __('Please confirm access to your account by entering one of your emergency recovery codes.') }} +
+ + + +
+ @csrf + +
+ + +
+ +
+ + +
+ +
+ + + + + + {{ __('Log in') }} + +
+
+
+
+
diff --git a/resources/views/auth/verify-email.blade.php b/resources/views/auth/verify-email.blade.php new file mode 100644 index 0000000..0572853 --- /dev/null +++ b/resources/views/auth/verify-email.blade.php @@ -0,0 +1,45 @@ + + + + + + +
+ {{ __('Before continuing, could you verify your email address by clicking on the link we just emailed to you? If you didn\'t receive the email, we will gladly send you another.') }} +
+ + @if (session('status') == 'verification-link-sent') +
+ {{ __('A new verification link has been sent to the email address you provided in your profile settings.') }} +
+ @endif + +
+
+ @csrf + +
+ + {{ __('Resend Verification Email') }} + +
+
+ +
+ + {{ __('Edit Profile') }} + +
+ @csrf + + +
+
+
+
+
diff --git a/resources/views/components/action-message.blade.php b/resources/views/components/action-message.blade.php new file mode 100644 index 0000000..a8a95f9 --- /dev/null +++ b/resources/views/components/action-message.blade.php @@ -0,0 +1,10 @@ +@props(['on']) + +
merge(['class' => 'text-sm text-gray-600']) }}> + {{ $slot->isEmpty() ? 'Saved.' : $slot }} +
diff --git a/resources/views/components/action-section.blade.php b/resources/views/components/action-section.blade.php new file mode 100644 index 0000000..93e2434 --- /dev/null +++ b/resources/views/components/action-section.blade.php @@ -0,0 +1,12 @@ +
merge(['class' => 'md:grid md:grid-cols-3 md:gap-6']) }}> + + {{ $title }} + {{ $description }} + + +
+
+ {{ $content }} +
+
+
diff --git a/resources/views/components/application-logo.blade.php b/resources/views/components/application-logo.blade.php new file mode 100644 index 0000000..b9725e6 --- /dev/null +++ b/resources/views/components/application-logo.blade.php @@ -0,0 +1,5 @@ + + + + + diff --git a/resources/views/components/application-mark.blade.php b/resources/views/components/application-mark.blade.php new file mode 100644 index 0000000..182054e --- /dev/null +++ b/resources/views/components/application-mark.blade.php @@ -0,0 +1,4 @@ + + + + diff --git a/resources/views/components/authentication-card-logo.blade.php b/resources/views/components/authentication-card-logo.blade.php new file mode 100644 index 0000000..0b59654 --- /dev/null +++ b/resources/views/components/authentication-card-logo.blade.php @@ -0,0 +1,6 @@ + + + + + + diff --git a/resources/views/components/authentication-card.blade.php b/resources/views/components/authentication-card.blade.php new file mode 100644 index 0000000..71235cf --- /dev/null +++ b/resources/views/components/authentication-card.blade.php @@ -0,0 +1,9 @@ +
+
+ {{ $logo }} +
+ +
+ {{ $slot }} +
+
diff --git a/resources/views/components/banner.blade.php b/resources/views/components/banner.blade.php new file mode 100644 index 0000000..b45c25d --- /dev/null +++ b/resources/views/components/banner.blade.php @@ -0,0 +1,48 @@ +@props(['style' => session('flash.bannerStyle', 'success'), 'message' => session('flash.banner')]) + +
+
+
+
+ + + + + + + + + + + + + + + + +

+
+ +
+ +
+
+
+
diff --git a/resources/views/components/button.blade.php b/resources/views/components/button.blade.php new file mode 100644 index 0000000..232734c --- /dev/null +++ b/resources/views/components/button.blade.php @@ -0,0 +1,3 @@ + diff --git a/resources/views/components/buttons.blade.php b/resources/views/components/buttons.blade.php new file mode 100644 index 0000000..ce6babc --- /dev/null +++ b/resources/views/components/buttons.blade.php @@ -0,0 +1,10 @@ +
+
+ + + diff --git a/resources/views/components/danger-button.blade.php b/resources/views/components/danger-button.blade.php new file mode 100644 index 0000000..55831ff --- /dev/null +++ b/resources/views/components/danger-button.blade.php @@ -0,0 +1,3 @@ + diff --git a/resources/views/components/dialog-modal.blade.php b/resources/views/components/dialog-modal.blade.php new file mode 100644 index 0000000..00e3581 --- /dev/null +++ b/resources/views/components/dialog-modal.blade.php @@ -0,0 +1,17 @@ +@props(['id' => null, 'maxWidth' => null]) + + +
+
+ {{ $title }} +
+ +
+ {{ $content }} +
+
+ +
+ {{ $footer }} +
+
diff --git a/resources/views/components/dropdown-link.blade.php b/resources/views/components/dropdown-link.blade.php new file mode 100644 index 0000000..e0f8ce1 --- /dev/null +++ b/resources/views/components/dropdown-link.blade.php @@ -0,0 +1 @@ +merge(['class' => 'block w-full px-4 py-2 text-start text-sm leading-5 text-gray-700 hover:bg-gray-100 focus:outline-none focus:bg-gray-100 transition duration-150 ease-in-out']) }}>{{ $slot }} diff --git a/resources/views/components/dropdown.blade.php b/resources/views/components/dropdown.blade.php new file mode 100644 index 0000000..29409e3 --- /dev/null +++ b/resources/views/components/dropdown.blade.php @@ -0,0 +1,47 @@ +@props(['align' => 'right', 'width' => '48', 'contentClasses' => 'py-1 bg-white', 'dropdownClasses' => '']) + +@php +switch ($align) { + case 'left': + $alignmentClasses = 'ltr:origin-top-left rtl:origin-top-right start-0'; + break; + case 'top': + $alignmentClasses = 'origin-top'; + break; + case 'none': + case 'false': + $alignmentClasses = ''; + break; + case 'right': + default: + $alignmentClasses = 'ltr:origin-top-right rtl:origin-top-left end-0'; + break; +} + +switch ($width) { + case '48': + $width = 'w-48'; + break; +} +@endphp + +
+
+ {{ $trigger }} +
+ + +
diff --git a/resources/views/components/footer.blade.php b/resources/views/components/footer.blade.php new file mode 100644 index 0000000..3a73e81 --- /dev/null +++ b/resources/views/components/footer.blade.php @@ -0,0 +1,20 @@ + diff --git a/resources/views/components/form-section.blade.php b/resources/views/components/form-section.blade.php new file mode 100644 index 0000000..9dd4e0e --- /dev/null +++ b/resources/views/components/form-section.blade.php @@ -0,0 +1,24 @@ +@props(['submit']) + +
merge(['class' => 'md:grid md:grid-cols-3 md:gap-6']) }}> + + {{ $title }} + {{ $description }} + + +
+
+
+
+ {{ $form }} +
+
+ + @if (isset($actions)) +
+ {{ $actions }} +
+ @endif +
+
+
diff --git a/resources/views/components/header.blade.php b/resources/views/components/header.blade.php new file mode 100644 index 0000000..6a79b8d --- /dev/null +++ b/resources/views/components/header.blade.php @@ -0,0 +1,27 @@ +
+ +
+@include('components.buttons') +@include('components.buttons') diff --git a/resources/views/components/home-header.blade.php b/resources/views/components/home-header.blade.php new file mode 100644 index 0000000..4ae6481 --- /dev/null +++ b/resources/views/components/home-header.blade.php @@ -0,0 +1,25 @@ +
+ +
diff --git a/resources/views/components/home-navbar.blade.php b/resources/views/components/home-navbar.blade.php new file mode 100644 index 0000000..674d1c3 --- /dev/null +++ b/resources/views/components/home-navbar.blade.php @@ -0,0 +1,78 @@ + + + + + diff --git a/resources/views/components/input-error.blade.php b/resources/views/components/input-error.blade.php new file mode 100644 index 0000000..b5ad968 --- /dev/null +++ b/resources/views/components/input-error.blade.php @@ -0,0 +1,5 @@ +@props(['for']) + +@error($for) +

merge(['class' => 'text-sm text-red-600']) }}>{{ $message }}

+@enderror diff --git a/resources/views/components/input.blade.php b/resources/views/components/input.blade.php new file mode 100644 index 0000000..18c4b31 --- /dev/null +++ b/resources/views/components/input.blade.php @@ -0,0 +1,3 @@ +@props(['disabled' => false]) + +merge(['class' => 'border-gray-300 focus:border-indigo-500 text-gray-700 focus:ring-indigo-500 rounded-md shadow-sm']) !!}> diff --git a/resources/views/components/label.blade.php b/resources/views/components/label.blade.php new file mode 100644 index 0000000..1cc65e2 --- /dev/null +++ b/resources/views/components/label.blade.php @@ -0,0 +1,5 @@ +@props(['value']) + + diff --git a/resources/views/components/manage_section.blade.php b/resources/views/components/manage_section.blade.php new file mode 100644 index 0000000..f2f7f34 --- /dev/null +++ b/resources/views/components/manage_section.blade.php @@ -0,0 +1,24 @@ +
+

Manage Your Genealogy

+

Explore and manage your family tree with ease.

+
+
+
+
Family Tree
+

View and edit your family tree.

+
+
+
+
+
Records
+

Access and manage historical records.

+
+
+
+
+
Settings
+

Configure your account and preferences.

+
+
+
+
diff --git a/resources/views/components/modal.blade.php b/resources/views/components/modal.blade.php new file mode 100644 index 0000000..b1c1e68 --- /dev/null +++ b/resources/views/components/modal.blade.php @@ -0,0 +1,43 @@ +@props(['id', 'maxWidth']) + +@php +$id = $id ?? md5($attributes->wire('model')); + +$maxWidth = [ + 'sm' => 'sm:max-w-sm', + 'md' => 'sm:max-w-md', + 'lg' => 'sm:max-w-lg', + 'xl' => 'sm:max-w-xl', + '2xl' => 'sm:max-w-2xl', +][$maxWidth ?? '2xl']; +@endphp + + diff --git a/resources/views/components/nav-link.blade.php b/resources/views/components/nav-link.blade.php new file mode 100644 index 0000000..5c101a2 --- /dev/null +++ b/resources/views/components/nav-link.blade.php @@ -0,0 +1,11 @@ +@props(['active']) + +@php +$classes = ($active ?? false) + ? 'inline-flex items-center px-1 pt-1 border-b-2 border-indigo-400 text-sm font-medium leading-5 text-gray-900 focus:outline-none focus:border-indigo-700 transition duration-150 ease-in-out' + : 'inline-flex items-center px-1 pt-1 border-b-2 border-transparent text-sm font-medium leading-5 text-gray-500 hover:text-gray-700 hover:border-gray-300 focus:outline-none focus:text-gray-700 focus:border-gray-300 transition duration-150 ease-in-out'; +@endphp + +merge(['class' => $classes]) }}> + {{ $slot }} + diff --git a/resources/views/components/products_section.blade.php b/resources/views/components/products_section.blade.php new file mode 100644 index 0000000..9bc9d3f --- /dev/null +++ b/resources/views/components/products_section.blade.php @@ -0,0 +1,18 @@ +
+
+

Our Products

+

Discover our range of genealogy tools designed to help you explore your ancestry.

+
+ @foreach($products as $product) +
+ {{ $product->name }} +
+

{{ $product->name }}

+

{{ $product->description }}

+ +
+
+ @endforeach +
+
+
diff --git a/resources/views/components/responsive-nav-link.blade.php b/resources/views/components/responsive-nav-link.blade.php new file mode 100644 index 0000000..43b91e7 --- /dev/null +++ b/resources/views/components/responsive-nav-link.blade.php @@ -0,0 +1,11 @@ +@props(['active']) + +@php +$classes = ($active ?? false) + ? 'block w-full ps-3 pe-4 py-2 border-l-4 border-indigo-400 text-start text-base font-medium text-indigo-700 bg-indigo-50 focus:outline-none focus:text-indigo-800 focus:bg-indigo-100 focus:border-indigo-700 transition duration-150 ease-in-out' + : 'block w-full ps-3 pe-4 py-2 border-l-4 border-transparent text-start text-base font-medium text-gray-600 hover:text-gray-800 hover:bg-gray-50 hover:border-gray-300 focus:outline-none focus:text-gray-800 focus:bg-gray-50 focus:border-gray-300 transition duration-150 ease-in-out'; +@endphp + +merge(['class' => $classes]) }}> + {{ $slot }} + diff --git a/resources/views/components/secondary-button.blade.php b/resources/views/components/secondary-button.blade.php new file mode 100644 index 0000000..b32b69f --- /dev/null +++ b/resources/views/components/secondary-button.blade.php @@ -0,0 +1,3 @@ + diff --git a/resources/views/components/section-border.blade.php b/resources/views/components/section-border.blade.php new file mode 100644 index 0000000..414ade6 --- /dev/null +++ b/resources/views/components/section-border.blade.php @@ -0,0 +1,5 @@ + diff --git a/resources/views/components/section-title.blade.php b/resources/views/components/section-title.blade.php new file mode 100644 index 0000000..72e5193 --- /dev/null +++ b/resources/views/components/section-title.blade.php @@ -0,0 +1,13 @@ +
+
+

{{ $title }}

+ +

+ {{ $description }} +

+
+ +
+ {{ $aside ?? '' }} +
+
diff --git a/resources/views/components/switchable-team.blade.php b/resources/views/components/switchable-team.blade.php new file mode 100644 index 0000000..881bd83 --- /dev/null +++ b/resources/views/components/switchable-team.blade.php @@ -0,0 +1,21 @@ +@props(['team', 'component' => 'dropdown-link']) + +
+ @method('PUT') + @csrf + + + + + +
+ @if (Auth::user()->isCurrentTeam($team)) + + + + @endif + +
{{ $team->name }}
+
+
+
diff --git a/resources/views/components/validation-errors.blade.php b/resources/views/components/validation-errors.blade.php index a62b46e..ef753f5 100644 --- a/resources/views/components/validation-errors.blade.php +++ b/resources/views/components/validation-errors.blade.php @@ -1,5 +1,4 @@ -@if ($errors->has('socialstream')) -@elseif ($errors->any()) +@if ($errors->any())
{{ __('Whoops! Something went wrong.') }}
diff --git a/resources/views/components/welcome.blade.php b/resources/views/components/welcome.blade.php new file mode 100644 index 0000000..298e56e --- /dev/null +++ b/resources/views/components/welcome.blade.php @@ -0,0 +1,96 @@ +
+ + +

+ Welcome to your Jetstream application! +

+ +

+ Laravel Jetstream provides a beautiful, robust starting point for your next Laravel application. Laravel is designed + to help you build your application using a development environment that is simple, powerful, and enjoyable. We believe + you should love expressing your creativity through programming, so we have spent time carefully crafting the Laravel + ecosystem to be a breath of fresh air. We hope you love it. +

+
+ +
+
+
+ + + +

+ Documentation +

+
+ +

+ Laravel has wonderful documentation covering every aspect of the framework. Whether you're new to the framework or have previous experience, we recommend reading all of the documentation from beginning to end. +

+ +

+ + Explore the documentation + + + + + +

+
+ +
+
+ + + +

+ Laracasts +

+
+ +

+ Laracasts offers thousands of video tutorials on Laravel, PHP, and JavaScript development. Check them out, see for yourself, and massively level up your development skills in the process. +

+ +

+ + Start watching Laracasts + + + + + +

+
+ +
+
+ + + +

+ Tailwind +

+
+ +

+ Laravel Jetstream is built with Tailwind, an amazing utility first CSS framework that doesn't get in your way. You'll be amazed how easily you can build and maintain fresh, modern designs with this wonderful framework at your fingertips. +

+
+ +
+
+ + + +

+ Authentication +

+
+ +

+ Authentication and registration views are included with Laravel Jetstream, as well as support for user email verification and resetting forgotten passwords. So, you're free to get started with what matters most: building your application. +

+
+
diff --git a/resources/views/components/why_us_section.blade.php b/resources/views/components/why_us_section.blade.php new file mode 100644 index 0000000..82d3001 --- /dev/null +++ b/resources/views/components/why_us_section.blade.php @@ -0,0 +1,17 @@ +
+
+

{{ $title }}

+

{{ $description }}

+
+ @foreach($features as $feature) +
+ {{ $feature['title'] }} +
+

{{ $feature['title'] }}

+

{{ $feature['description'] }}

+
+
+ @endforeach +
+
+