Merge pull request 'feat: update user registration to use username instead of name, add user profile model and migration, and implement gender enum' (#4) from feat/user-profile-and-username into dev

Reviewed-on: #4
This commit is contained in:
pangestu 2026-08-03 14:49:32 +08:00
commit b8e807d30b
22 changed files with 301 additions and 358 deletions

View File

@ -12,11 +12,6 @@ class CreateNewUser implements CreatesNewUsers
{
use PasswordValidationRules, ProfileValidationRules;
/**
* Validate and create a newly registered user.
*
* @param array<string, string> $input
*/
public function create(array $input): User
{
Validator::make($input, [
@ -25,7 +20,7 @@ public function create(array $input): User
])->validate();
return User::create([
'name' => $input['name'],
'username' => $input['username'],
'email' => $input['email'],
'password' => $input['password'],
]);

View File

@ -3,39 +3,32 @@
namespace App\Concerns;
use App\Models\User;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Validation\Rule;
trait ProfileValidationRules
{
/**
* Get the validation rules used to validate user profiles.
*
* @return array<string, array<int, ValidationRule|array<mixed>|string>>
*/
protected function profileRules(?int $userId = null): array
{
return [
'name' => $this->nameRules(),
'username' => $this->usernameRules($userId),
'email' => $this->emailRules($userId),
];
}
/**
* Get the validation rules used to validate user names.
*
* @return array<int, ValidationRule|array<mixed>|string>
*/
protected function nameRules(): array
protected function usernameRules(?int $userId = null): array
{
return ['required', 'string', 'max:255'];
return [
'required',
'string',
'max:20',
'min:3',
'alpha_dash',
$userId === null
? Rule::unique(User::class)
: Rule::unique(User::class)->ignore($userId),
];
}
/**
* Get the validation rules used to validate user emails.
*
* @return array<int, ValidationRule|array<mixed>|string>
*/
protected function emailRules(?int $userId = null): array
{
return [

17
app/Enums/Gender.php Normal file
View File

@ -0,0 +1,17 @@
<?php
namespace App\Enums;
enum Gender: string
{
case Male = 'male';
case Female = 'female';
public function label(): string
{
return match ($this) {
self::Male => 'Laki-laki',
self::Female => 'Perempuan',
};
}
}

View File

@ -2,49 +2,40 @@
namespace App\Models;
// use Illuminate\Contracts\Auth\MustVerifyEmail;
use Database\Factories\UserFactory;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Attributes\Guarded;
use Illuminate\Database\Eloquent\Attributes\Hidden;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Carbon;
use Laravel\Fortify\Contracts\PasskeyUser;
use Laravel\Fortify\PasskeyAuthenticatable;
use Laravel\Fortify\TwoFactorAuthenticatable;
/**
* @property int $id
* @property string $name
* @property string $email
* @property Carbon|null $email_verified_at
* @property string $password
* @property string|null $two_factor_secret
* @property string|null $two_factor_recovery_codes
* @property Carbon|null $two_factor_confirmed_at
* @property string|null $remember_token
* @property Carbon|null $created_at
* @property Carbon|null $updated_at
*/
#[Fillable(['name', 'email', 'password'])]
#[Hidden(['password', 'two_factor_secret', 'two_factor_recovery_codes', 'remember_token'])]
class User extends Authenticatable implements PasskeyUser
#[Hidden(['password'])]
#[Guarded(['id', 'last_login_at'])]
class User extends Authenticatable
{
/** @use HasFactory<UserFactory> */
use HasFactory, Notifiable, PasskeyAuthenticatable, TwoFactorAuthenticatable;
use HasFactory, Notifiable, SoftDeletes;
/**
* Get the attributes that should be cast.
*
* @return array<string, string>
*/
protected function casts(): array
{
return [
'email_verified_at' => 'datetime',
'is_active' => 'boolean',
'last_login_at' => 'datetime',
'password' => 'hashed',
'two_factor_confirmed_at' => 'datetime',
];
}
protected function fullName(): Attribute
{
return Attribute::get(function () {
return $this->profile?->full_name ?? $this->username;
});
}
public function profile(): HasOne
{
return $this->hasOne(UserProfile::class);
}
}

View File

@ -0,0 +1,28 @@
<?php
namespace App\Models;
use App\Enums\Gender;
use Illuminate\Database\Eloquent\Attributes\Guarded;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
#[Guarded(['id'])]
class UserProfile extends Model
{
use HasFactory;
protected function casts(): array
{
return [
'gender' => Gender::class,
'birth_date' => 'date',
];
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}

View File

@ -2,7 +2,6 @@
namespace App\Providers;
use App\Actions\Fortify\CreateNewUser;
use App\Actions\Fortify\ResetUserPassword;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Http\Request;
@ -16,17 +15,11 @@
class FortifyServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*/
public function register(): void
{
//
}
/**
* Bootstrap any application services.
*/
public function boot(): void
{
$this->configureActions();
@ -34,18 +27,11 @@ public function boot(): void
$this->configureRateLimiting();
}
/**
* Configure Fortify actions.
*/
private function configureActions(): void
{
Fortify::resetUserPasswordsUsing(ResetUserPassword::class);
Fortify::createUsersUsing(CreateNewUser::class);
}
/**
* Configure Fortify views.
*/
private function configureViews(): void
{
Fortify::loginView(fn (Request $request) => Inertia::render('auth/login', [
@ -67,18 +53,11 @@ private function configureViews(): void
'status' => $request->session()->get('status'),
]));
Fortify::registerView(fn () => Inertia::render('auth/register', [
'passwordRules' => Password::defaults()->toPasswordRulesString(),
]));
Fortify::twoFactorChallengeView(fn () => Inertia::render('auth/two-factor-challenge'));
Fortify::confirmPasswordView(fn () => Inertia::render('auth/confirm-password'));
}
/**
* Configure rate limiting.
*/
private function configureRateLimiting(): void
{
RateLimiter::for('two-factor', function (Request $request) {
@ -90,11 +69,5 @@ private function configureRateLimiting(): void
return Limit::perMinute(5)->by($throttleKey);
});
RateLimiter::for('passkeys', function (Request $request) {
return Limit::perMinute(10)->by(
($request->input('credential.id') ?: $request->session()->getId()).'|'.$request->ip(),
);
});
}
}

View File

@ -45,7 +45,7 @@
|
*/
'username' => 'email',
'username' => 'username',
'email' => 'email',
@ -117,7 +117,6 @@
'limiters' => [
'login' => 'login',
'two-factor' => 'two-factor',
'passkeys' => 'passkeys',
],
/*
@ -133,22 +132,6 @@
'views' => true,
/*
|--------------------------------------------------------------------------
| Passkeys
|--------------------------------------------------------------------------
|
| These settings configure Fortify's passkey (WebAuthn) support.
|
*/
'passkeys' => [
'relying_party_id' => parse_url(config('app.url'), PHP_URL_HOST),
'allowed_origins' => [config('app.url')],
'user_handle_secret' => env('PASSKEYS_USER_HANDLE_SECRET', config('app.key')),
'timeout' => 60000,
],
/*
|--------------------------------------------------------------------------
| Features
@ -161,16 +144,11 @@
*/
'features' => [
Features::registration(),
Features::resetPasswords(),
Features::emailVerification(),
Features::twoFactorAuthentication([
'confirm' => true,
'confirmPassword' => true,
// 'window' => 0
]),
Features::passkeys([
'confirmPassword' => true,
]),
],

View File

@ -5,40 +5,28 @@
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
/**
* @extends Factory<User>
*/
class UserFactory extends Factory
{
/**
* The current password being used by the factory.
*/
protected static ?string $password;
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
return [
'name' => fake()->name(),
'username' => fake()->unique()->userName(),
'email' => fake()->unique()->safeEmail(),
'email_verified_at' => now(),
'password' => static::$password ??= Hash::make('password'),
'remember_token' => Str::random(10),
'is_active' => true,
'last_login_at' => null,
'two_factor_secret' => null,
'two_factor_recovery_codes' => null,
'two_factor_confirmed_at' => null,
];
}
/**
* Indicate that the model's email address should be unverified.
*/
public function unverified(): static
{
return $this->state(fn (array $attributes) => [
@ -46,9 +34,6 @@ public function unverified(): static
]);
}
/**
* Indicate that the model has two-factor authentication configured.
*/
public function withTwoFactor(): static
{
return $this->state(fn (array $attributes) => [

View File

@ -6,19 +6,17 @@
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('username', 20)->unique();
$table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable();
$table->string('password');
$table->rememberToken();
$table->boolean('is_active')->default(true);
$table->timestamp('last_login_at')->nullable();
$table->timestamps();
$table->softDeletes();
});
Schema::create('password_reset_tokens', function (Blueprint $table) {
@ -37,9 +35,6 @@ public function up(): void
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('users');

View File

@ -0,0 +1,30 @@
<?php
use App\Enums\Gender;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('user_profiles', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->string('full_name', 150);
$table->string('phone_number', 20)->nullable();
$table->text('address')->nullable();
$table->enum('gender', array_values(Gender::cases()))->nullable();
$table->string('birth_place', 100)->nullable();
$table->date('birth_date')->nullable();
$table->timestamps();
$table->softDeletes();
});
}
public function down(): void
{
Schema::dropIfExists('user_profiles');
}
};

View File

@ -2,7 +2,6 @@
namespace Database\Seeders;
use App\Models\User;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
@ -10,16 +9,10 @@ class DatabaseSeeder extends Seeder
{
use WithoutModelEvents;
/**
* Seed the application's database.
*/
public function run(): void
{
// User::factory(10)->create();
User::factory()->create([
'name' => 'Test User',
'email' => 'test@example.com',
$this->call([
UserSeeder::class,
]);
}
}

View File

@ -0,0 +1,78 @@
<?php
namespace Database\Seeders;
use App\Models\User;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\Hash;
class UserSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
$users = [
[
'username' => 'pangestu',
'email' => 'pangestu@student.itmpwk.ac.id',
'profile' => [
'full_name' => 'Yoga Pangestu',
'phone_number' => '082121495806',
'address' => 'Jl. Merdeka No. 1, Purwakarta',
'gender' => 'male',
'birth_place' => 'Subang',
'birth_date' => '2005-03-13',
],
],
[
'username' => 'doni',
'email' => 'doni@student.itmpwk.ac.id',
'profile' => [
'full_name' => 'Doni Setiawan Ramadhan',
'phone_number' => '085793462823',
'address' => 'Jl. Merdeka No. 1, Purwakarta',
'gender' => 'male',
'birth_place' => 'Purwakarta',
'birth_date' => '2004-10-30',
],
],
[
'username' => 'asep',
'email' => 'asep@student.itmpwk.ac.id',
'profile' => [
'full_name' => 'Asep Saepudin',
'phone_number' => '081546505033',
'address' => 'Jl. Merdeka No. 1, Purwakarta',
'gender' => 'male',
'birth_place' => 'Purwakarta',
'birth_date' => '2005-09-24',
],
],
[
'username' => 'komoala',
'email' => 'komoala@student.itmpwk.ac.id',
'profile' => [
'full_name' => 'Komala Dewi',
'phone_number' => '085819894938',
'address' => 'Jl. Merdeka No. 1, Purwakarta',
'gender' => 'female',
'birth_place' => 'Purwakarta',
'birth_date' => '2005-05-04',
],
],
];
foreach ($users as $userData) {
$profile = $userData['profile'];
unset($userData['profile']);
$userData['password'] = Hash::make('Minimal8@');
$user = User::create($userData);
$user->profile()->create($profile);
}
}
}

View File

@ -1,4 +1,4 @@
import AuthLayoutTemplate from '@/layouts/auth/auth-simple-layout';
import AuthLayoutTemplate from '@/layouts/auth/auth-split-layout';
export default function AuthLayout({
title = '',

View File

@ -13,7 +13,14 @@ export default function AuthSplitLayout({
return (
<div className="relative grid h-dvh flex-col items-center justify-center px-8 sm:px-0 lg:max-w-none lg:grid-cols-2 lg:px-0">
<div className="relative hidden h-full flex-col bg-muted p-10 text-white lg:flex dark:border-r">
<div className="absolute inset-0 bg-zinc-900" />
<div
className="absolute inset-0 bg-cover bg-center bg-no-repeat"
style={{
backgroundImage:
"url('https://images.pexels.com/photos/267507/pexels-photo-267507.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=2')",
}}
/>
<div className="absolute inset-0 bg-black/60" />
<Link
href={home()}
className="relative z-20 flex items-center text-lg font-medium"

View File

@ -1,10 +1,5 @@
import { Form, Head } from '@inertiajs/react';
import {
index as confirmOptions,
store as confirmStore,
} from '@/actions/Laravel/Passkeys/Http/Controllers/PasskeyConfirmationController';
import InputError from '@/components/input-error';
import PasskeyVerify from '@/components/passkey-verify';
import PasswordInput from '@/components/password-input';
import { Button } from '@/components/ui/button';
import { Label } from '@/components/ui/label';
@ -14,17 +9,7 @@ import { store } from '@/routes/password/confirm';
export default function ConfirmPassword() {
return (
<>
<Head title="Confirm password" />
<PasskeyVerify
routes={{
options: confirmOptions(),
submit: confirmStore(),
}}
label="Confirm with passkey"
loadingLabel="Confirming..."
separator="Or confirm with password"
/>
<Head title="Konfirmasi password" />
<Form {...store.form()} resetOnSuccess={['password']}>
{({ processing, errors }) => (
@ -49,7 +34,7 @@ export default function ConfirmPassword() {
data-test="confirm-password-button"
>
{processing && <Spinner />}
Confirm password
Konfirmasi password
</Button>
</div>
</div>
@ -60,7 +45,7 @@ export default function ConfirmPassword() {
}
ConfirmPassword.layout = {
title: 'Confirm password',
title: 'Konfirmasi password',
description:
'This is a secure area of the application. Please confirm your password before continuing.',
'Ini adalah area aman dari aplikasi. Silakan konfirmasi password Anda sebelum melanjutkan.',
};

View File

@ -1,4 +1,3 @@
// Components
import { Form, Head } from '@inertiajs/react';
import { LoaderCircle } from 'lucide-react';
import InputError from '@/components/input-error';
@ -12,7 +11,7 @@ import { email } from '@/routes/password';
export default function ForgotPassword({ status }: { status?: string }) {
return (
<>
<Head title="Forgot password" />
<Head title="Lupa password" />
{status && (
<div className="mb-4 text-center text-sm font-medium text-green-600">
@ -25,14 +24,14 @@ export default function ForgotPassword({ status }: { status?: string }) {
{({ processing, errors }) => (
<>
<div className="grid gap-2">
<Label htmlFor="email">Email address</Label>
<Label htmlFor="email">Alamat email</Label>
<Input
id="email"
type="email"
name="email"
autoComplete="off"
autoFocus
placeholder="email@example.com"
placeholder="email@contoh.com"
/>
<InputError message={errors.email} />
@ -47,7 +46,7 @@ export default function ForgotPassword({ status }: { status?: string }) {
{processing && (
<LoaderCircle className="h-4 w-4 animate-spin" />
)}
Email password reset link
Kirim link reset password
</Button>
</div>
</>
@ -55,8 +54,8 @@ export default function ForgotPassword({ status }: { status?: string }) {
</Form>
<div className="space-x-1 text-center text-sm text-muted-foreground">
<span>Or, return to</span>
<TextLink href={login()}>log in</TextLink>
<span>Atau kembali ke</span>
<TextLink href={login()}>masuk</TextLink>
</div>
</div>
</>
@ -64,6 +63,6 @@ export default function ForgotPassword({ status }: { status?: string }) {
}
ForgotPassword.layout = {
title: 'Forgot password',
description: 'Enter your email to receive a password reset link',
title: 'Lupa password',
description: 'Masukkan email Anda untuk menerima link reset password',
};

View File

@ -1,16 +1,15 @@
import { Form, Head } from '@inertiajs/react';
import InputError from '@/components/input-error';
import PasskeyVerify from '@/components/passkey-verify';
import PasswordInput from '@/components/password-input';
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 { Separator } from '@/components/ui/separator';
import { Spinner } from '@/components/ui/spinner';
import { register } from '@/routes';
import { store } from '@/routes/login';
import { request } from '@/routes/password';
import { Form, Head } from '@inertiajs/react';
type Props = {
status?: string;
@ -20,9 +19,7 @@ type Props = {
export default function Login({ status, canResetPassword }: Props) {
return (
<>
<Head title="Log in" />
<PasskeyVerify />
<Head title="Masuk" />
<Form
{...store.form()}
@ -33,30 +30,28 @@ export default function Login({ status, canResetPassword }: Props) {
<>
<div className="grid gap-6">
<div className="grid gap-2">
<Label htmlFor="email">Email address</Label>
<Label htmlFor="username">Email/Username{' '}<span className="text-destructive">*</span></Label>
<Input
id="email"
type="email"
name="email"
id="username"
type="text"
name="username"
required
autoFocus
tabIndex={1}
autoComplete="email"
placeholder="email@example.com"
placeholder="Masukkan username Anda"
/>
<InputError message={errors.email} />
<InputError message={errors.username} />
</div>
<div className="grid gap-2">
<div className="flex items-center">
<Label htmlFor="password">Password</Label>
<Label htmlFor="password">Kata Sandi{' '}<span className="text-destructive">*</span></Label>
{canResetPassword && (
<TextLink
href={request()}
className="ml-auto text-sm"
tabIndex={5}
>
Forgot your password?
Lupa kata sandi?
</TextLink>
)}
</div>
@ -65,8 +60,7 @@ export default function Login({ status, canResetPassword }: Props) {
name="password"
required
tabIndex={2}
autoComplete="current-password"
placeholder="Password"
placeholder="Kata Sandi"
/>
<InputError message={errors.password} />
</div>
@ -77,7 +71,9 @@ export default function Login({ status, canResetPassword }: Props) {
name="remember"
tabIndex={3}
/>
<Label htmlFor="remember">Remember me</Label>
<Label htmlFor="remember">
Ingat saya
</Label>
</div>
<Button
@ -88,15 +84,45 @@ export default function Login({ status, canResetPassword }: Props) {
data-test="login-button"
>
{processing && <Spinner />}
Log in
Masuk
</Button>
</div>
<div className="text-center text-sm text-muted-foreground">
Don't have an account?{' '}
<TextLink href={register()} tabIndex={5}>
Sign up
</TextLink>
<div className="relative my-2">
<div className="absolute inset-0 flex items-center">
<Separator className="w-full" />
</div>
<div className="relative flex justify-center text-xs uppercase">
<span className="bg-background px-2 text-muted-foreground">
Atau
</span>
</div>
</div>
<Button
type="button"
variant="outline"
className="w-full"
>
<svg className="h-4 w-4" viewBox="0 0 24 24">
<path
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92a5.06 5.06 0 0 1-2.2 3.32v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.1z"
fill="#4285F4"
/>
<path
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
fill="#34A853"
/>
<path
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
fill="#FBBC05"
/>
<path
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
fill="#EA4335"
/>
</svg>
Masuk dengan Google
</Button>
</div>
</>
)}
@ -112,6 +138,6 @@ export default function Login({ status, canResetPassword }: Props) {
}
Login.layout = {
title: 'Log in to your account',
description: 'Enter your email and password below to log in',
title: 'Masuk ke akun Anda',
description: 'Masukkan username dan password Anda di bawah ini untuk masuk',
};

View File

@ -1,120 +0,0 @@
import { Form, Head } from '@inertiajs/react';
import InputError from '@/components/input-error';
import PasswordInput from '@/components/password-input';
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 { Spinner } from '@/components/ui/spinner';
import { login } from '@/routes';
import { store } from '@/routes/register';
type Props = {
passwordRules: string;
};
export default function Register({ passwordRules }: Props) {
return (
<>
<Head title="Register" />
<Form
{...store.form()}
resetOnSuccess={['password', 'password_confirmation']}
disableWhileProcessing
className="flex flex-col gap-6"
>
{({ processing, errors }) => (
<>
<div className="grid gap-6">
<div className="grid gap-2">
<Label htmlFor="name">Name</Label>
<Input
id="name"
type="text"
required
autoFocus
tabIndex={1}
autoComplete="name"
name="name"
placeholder="Full name"
/>
<InputError
message={errors.name}
className="mt-2"
/>
</div>
<div className="grid gap-2">
<Label htmlFor="email">Email address</Label>
<Input
id="email"
type="email"
required
tabIndex={2}
autoComplete="email"
name="email"
placeholder="email@example.com"
/>
<InputError message={errors.email} />
</div>
<div className="grid gap-2">
<Label htmlFor="password">Password</Label>
<PasswordInput
id="password"
required
tabIndex={3}
autoComplete="new-password"
name="password"
placeholder="Password"
passwordrules={passwordRules}
/>
<InputError message={errors.password} />
</div>
<div className="grid gap-2">
<Label htmlFor="password_confirmation">
Confirm password
</Label>
<PasswordInput
id="password_confirmation"
required
tabIndex={4}
autoComplete="new-password"
name="password_confirmation"
placeholder="Confirm password"
passwordrules={passwordRules}
/>
<InputError
message={errors.password_confirmation}
/>
</div>
<Button
type="submit"
className="mt-2 w-full"
tabIndex={5}
data-test="register-user-button"
>
{processing && <Spinner />}
Create account
</Button>
</div>
<div className="text-center text-sm text-muted-foreground">
Already have an account?{' '}
<TextLink href={login()} tabIndex={6}>
Log in
</TextLink>
</div>
</>
)}
</Form>
</>
);
}
Register.layout = {
title: 'Create an account',
description: 'Enter your details below to create your account',
};

View File

@ -43,14 +43,14 @@ export default function ResetPassword({ token, email, passwordRules }: Props) {
</div>
<div className="grid gap-2">
<Label htmlFor="password">Password</Label>
<Label htmlFor="password">Password baru</Label>
<PasswordInput
id="password"
name="password"
autoComplete="new-password"
className="mt-1 block w-full"
autoFocus
placeholder="Password"
placeholder="Password baru"
passwordrules={passwordRules}
/>
<InputError message={errors.password} />
@ -58,14 +58,14 @@ export default function ResetPassword({ token, email, passwordRules }: Props) {
<div className="grid gap-2">
<Label htmlFor="password_confirmation">
Confirm password
Konfirmasi password
</Label>
<PasswordInput
id="password_confirmation"
name="password_confirmation"
autoComplete="new-password"
className="mt-1 block w-full"
placeholder="Confirm password"
placeholder="Konfirmasi password"
passwordrules={passwordRules}
/>
<InputError
@ -92,5 +92,5 @@ export default function ResetPassword({ token, email, passwordRules }: Props) {
ResetPassword.layout = {
title: 'Reset password',
description: 'Please enter your new password below',
description: 'Masukkan password baru Anda di bawah ini',
};

View File

@ -23,18 +23,18 @@ export default function TwoFactorChallenge() {
}>(() => {
if (showRecoveryInput) {
return {
title: 'Recovery code',
title: 'Kode pemulihan',
description:
'Please confirm access to your account by entering one of your emergency recovery codes.',
toggleText: 'login using an authentication code',
'Silakan konfirmasi akses ke akun Anda dengan memasukkan salah satu kode pemulihan darurat Anda.',
toggleText: 'masuk menggunakan kode autentikasi',
};
}
return {
title: 'Authentication code',
title: 'Kode autentikasi',
description:
'Enter the authentication code provided by your authenticator application.',
toggleText: 'login using a recovery code',
'Masukkan kode autentikasi yang diberikan oleh aplikasi autentikator Anda.',
toggleText: 'masuk menggunakan kode pemulihan',
};
}, [showRecoveryInput]);
@ -51,7 +51,7 @@ export default function TwoFactorChallenge() {
return (
<>
<Head title="Two-factor authentication" />
<Head title="Autentikasi dua faktor" />
<div className="space-y-6">
<Form
@ -67,7 +67,7 @@ export default function TwoFactorChallenge() {
<Input
name="recovery_code"
type="text"
placeholder="Enter recovery code"
placeholder="Masukkan kode pemulihan"
autoFocus={showRecoveryInput}
required
/>
@ -109,11 +109,11 @@ export default function TwoFactorChallenge() {
className="w-full"
disabled={processing}
>
Continue
Lanjutkan
</Button>
<div className="text-center text-sm text-muted-foreground">
<span>or you can </span>
<span>atau Anda bisa </span>
<button
type="button"
className="cursor-pointer text-foreground underline decoration-neutral-300 underline-offset-4 transition-colors duration-300 ease-out hover:decoration-current! dark:decoration-neutral-500"

View File

@ -1,4 +1,3 @@
// Components
import { Form, Head } from '@inertiajs/react';
import TextLink from '@/components/text-link';
import { Button } from '@/components/ui/button';
@ -9,12 +8,12 @@ import { send } from '@/routes/verification';
export default function VerifyEmail({ status }: { status?: string }) {
return (
<>
<Head title="Email verification" />
<Head title="Verifikasi email" />
{status === 'verification-link-sent' && (
<div className="mb-4 text-center text-sm font-medium text-green-600">
A new verification link has been sent to the email address
you provided during registration.
Tautan verifikasi baru telah dikirim ke alamat email
yang Anda berikan saat pendaftaran.
</div>
)}
@ -23,14 +22,14 @@ export default function VerifyEmail({ status }: { status?: string }) {
<>
<Button disabled={processing} variant="secondary">
{processing && <Spinner />}
Resend verification email
Kirim ulang email verifikasi
</Button>
<TextLink
href={logout()}
className="mx-auto block text-sm"
>
Log out
Keluar
</TextLink>
</>
)}
@ -40,7 +39,7 @@ export default function VerifyEmail({ status }: { status?: string }) {
}
VerifyEmail.layout = {
title: 'Email verification',
title: 'Verifikasi email',
description:
'Please verify your email address by clicking on the link we just emailed to you.',
'Silakan verifikasi alamat email Anda dengan mengklik tautan yang baru saja kami kirimkan.',
};

View File

@ -1,6 +1,5 @@
import { Head, Link, usePage } from '@inertiajs/react';
import { dashboard, login } from '@/routes';
import { register } from '@/routes';
export default function Welcome() {
const { auth } = usePage().props;
@ -19,20 +18,12 @@ export default function Welcome() {
Dashboard
</Link>
) : (
<>
<Link
href={login()}
className="inline-block rounded-sm border border-transparent px-5 py-1.5 text-sm leading-normal text-[#1b1b18] hover:border-[#19140035] dark:text-[#EDEDEC] dark:hover:border-[#3E3E3A]"
>
Log in
</Link>
<Link
href={register()}
className="inline-block rounded-sm border border-[#19140035] px-5 py-1.5 text-sm leading-normal text-[#1b1b18] hover:border-[#1915014a] dark:border-[#3E3E3A] dark:text-[#EDEDEC] dark:hover:border-[#62605b]"
>
Register
</Link>
</>
<Link
href={login()}
className="inline-block rounded-sm border border-transparent px-5 py-1.5 text-sm leading-normal text-[#1b1b18] hover:border-[#19140035] dark:text-[#EDEDEC] dark:hover:border-[#3E3E3A]"
>
Masuk
</Link>
)}
</nav>
</header>