This commit is contained in:
m.zargarov 2024-06-16 00:41:31 +04:00
parent b3b3e2b8ee
commit 67c2e366c5
104 changed files with 4688 additions and 163 deletions

View File

@ -1,13 +1,13 @@
APP_NAME=Laravel
APP_ENV=local
APP_KEY=
APP_KEY=base64:wEWXUAAyjMhebMbczeMSiTGcEfi6YRVy/ZgB+u+cDRE=
APP_DEBUG=true
APP_TIMEZONE=UTC
APP_URL=http://localhost
APP_LOCALE=en
APP_LOCALE=ru
APP_FALLBACK_LOCALE=en
APP_FAKER_LOCALE=en_US
APP_FAKER_LOCALE=ru_RU
APP_MAINTENANCE_DRIVER=file
APP_MAINTENANCE_STORE=database
@ -19,12 +19,12 @@ LOG_STACK=single
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug
DB_CONNECTION=sqlite
# DB_HOST=127.0.0.1
# DB_PORT=3306
# DB_DATABASE=laravel
# DB_USERNAME=root
# DB_PASSWORD=
DB_CONNECTION=pgsql
DB_HOST=127.0.0.1
DB_PORT=5432
DB_DATABASE=edu_diary
DB_USERNAME=postgres
DB_PASSWORD=qwerty123
SESSION_DRIVER=database
SESSION_LIFETIME=120

View File

@ -0,0 +1,47 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Http\Requests\Auth\LoginRequest;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\View\View;
class AuthenticatedSessionController extends Controller
{
/**
* Display the login view.
*/
public function create(): View
{
return view('auth.login');
}
/**
* 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('/');
}
}

View File

@ -0,0 +1,40 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Validation\ValidationException;
use Illuminate\View\View;
class ConfirmablePasswordController extends Controller
{
/**
* Show the confirm password view.
*/
public function show(): View
{
return view('auth.confirm-password');
}
/**
* Confirm the user's password.
*/
public function store(Request $request): RedirectResponse
{
if (! Auth::guard('web')->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));
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class EmailVerificationNotificationController extends Controller
{
/**
* Send a new email verification notification.
*/
public function store(Request $request): RedirectResponse
{
if ($request->user()->hasVerifiedEmail()) {
return redirect()->intended(route('dashboard', absolute: false));
}
$request->user()->sendEmailVerificationNotification();
return back()->with('status', 'verification-link-sent');
}
}

View File

@ -0,0 +1,21 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
class EmailVerificationPromptController extends Controller
{
/**
* Display the email verification prompt.
*/
public function __invoke(Request $request): RedirectResponse|View
{
return $request->user()->hasVerifiedEmail()
? redirect()->intended(route('dashboard', absolute: false))
: view('auth.verify-email');
}
}

View File

@ -0,0 +1,61 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Auth\Events\PasswordReset;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Password;
use Illuminate\Support\Str;
use Illuminate\Validation\Rules;
use Illuminate\View\View;
class NewPasswordController extends Controller
{
/**
* Display the password reset view.
*/
public function create(Request $request): View
{
return view('auth.reset-password', ['request' => $request]);
}
/**
* 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.
return $status == Password::PASSWORD_RESET
? redirect()->route('login')->with('status', __($status))
: back()->withInput($request->only('email'))
->withErrors(['email' => __($status)]);
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\Rules\Password;
class PasswordController extends Controller
{
/**
* Update the user's password.
*/
public function update(Request $request): RedirectResponse
{
$validated = $request->validateWithBag('updatePassword', [
'current_password' => ['required', 'current_password'],
'password' => ['required', Password::defaults(), 'confirmed'],
]);
$request->user()->update([
'password' => Hash::make($validated['password']),
]);
return back()->with('status', 'password-updated');
}
}

View File

@ -0,0 +1,44 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Password;
use Illuminate\View\View;
class PasswordResetLinkController extends Controller
{
/**
* Display the password reset link request view.
*/
public function create(): View
{
return view('auth.forgot-password');
}
/**
* Handle an incoming password reset link request.
*
* @throws \Illuminate\Validation\ValidationException
*/
public function store(Request $request): RedirectResponse
{
$request->validate([
'email' => ['required', 'email'],
]);
// 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(
$request->only('email')
);
return $status == Password::RESET_LINK_SENT
? back()->with('status', __($status))
: back()->withInput($request->only('email'))
->withErrors(['email' => __($status)]);
}
}

View File

@ -0,0 +1,50 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Auth\Events\Registered;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\Rules;
use Illuminate\View\View;
class RegisteredUserController extends Controller
{
/**
* Display the registration view.
*/
public function create(): View
{
return view('auth.register');
}
/**
* Handle an incoming registration request.
*
* @throws \Illuminate\Validation\ValidationException
*/
public function store(Request $request): RedirectResponse
{
$request->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(route('dashboard', absolute: false));
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Auth\Events\Verified;
use Illuminate\Foundation\Auth\EmailVerificationRequest;
use Illuminate\Http\RedirectResponse;
class VerifyEmailController extends Controller
{
/**
* Mark the authenticated user's email address as verified.
*/
public function __invoke(EmailVerificationRequest $request): RedirectResponse
{
if ($request->user()->hasVerifiedEmail()) {
return redirect()->intended(route('dashboard', absolute: false).'?verified=1');
}
if ($request->user()->markEmailAsVerified()) {
event(new Verified($request->user()));
}
return redirect()->intended(route('dashboard', absolute: false).'?verified=1');
}
}

View File

@ -10,18 +10,13 @@ use Illuminate\View\View;
class GradeController extends Controller
{
public function __construct(
protected ServiceInterface $service,
) {
}
/**
* Display a listing of the resource.
*/
public function index(): View
{
return view('grades.index', [
'grades' => $this->service->getAll(),
'grades' => Grade::filter()->paginate(5)->withQueryString(),
]);
}
@ -38,7 +33,7 @@ class GradeController extends Controller
*/
public function store(GradePostRequest $request): RedirectResponse
{
return redirect()->route('grades.show', $this->service->create($request->validated()));
return redirect()->route('grades.show', Grade::create($request->validated()));
}
/**
@ -67,7 +62,7 @@ class GradeController extends Controller
*/
public function update(GradePostRequest $request, Grade $grade): RedirectResponse
{
return redirect()->route('grades.show', $this->service->update($grade, $request->validated()));
return redirect()->route('grades.show', $grade->update($request->validated()));
}
/**
@ -75,7 +70,7 @@ class GradeController extends Controller
*/
public function destroy(Grade $grade): RedirectResponse
{
$this->service->delete($grade);
$grade->delete();
return redirect()->route('grades.index');
}

View File

@ -20,12 +20,14 @@ class GradeSubjectController extends Controller
{
return view('grade-subject.create', [
'grade' => $grade,
'subjects' => $this->service->getAllSubjects(),
'subjects' => Subject::all(),
]);
}
public function store(GradeSubjectPostRequest $request, Grade $grade): RedirectResponse
{
$grade->subjects()->syncWithoutDetaching($request->subject_id);
return redirect()->route('grades.show', $this->service->create($request->validated(), $grade));
}
@ -34,7 +36,7 @@ class GradeSubjectController extends Controller
return view('grade-subject.edit', [
'grade' => $grade,
'updateSubject' => $subject,
'subjects' => $this->service->getAllSubjects(),
'subjects' => Subject::all(),
]);
}

View File

@ -21,7 +21,7 @@ class LessonController extends Controller
public function gradeList(): View
{
return view('grade-lesson.grades-list', [
'grades' => $this->service->getGrades(),
'grades' => Grade::all(),
]);
}
@ -31,7 +31,7 @@ class LessonController extends Controller
public function index(Grade $grade): View
{
return view('grade-lesson.index', [
'lessons' => $this->service->getAll($grade),
'lessons' => $grade->lessons()->filter()->get(),
'grade' => $grade,
'subjects' => $grade->subjects,
]);

View File

@ -0,0 +1,60 @@
<?php
namespace App\Http\Controllers;
use App\Http\Requests\ProfileUpdateRequest;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Redirect;
use Illuminate\View\View;
class ProfileController extends Controller
{
/**
* Display the user's profile form.
*/
public function edit(Request $request): View
{
return view('profile.edit', [
'user' => $request->user(),
]);
}
/**
* Update the user's profile information.
*/
public function update(ProfileUpdateRequest $request): RedirectResponse
{
$request->user()->fill($request->validated());
if ($request->user()->isDirty('email')) {
$request->user()->email_verified_at = null;
}
$request->user()->save();
return Redirect::route('profile.edit')->with('status', 'profile-updated');
}
/**
* Delete the user's account.
*/
public function destroy(Request $request): RedirectResponse
{
$request->validateWithBag('userDeletion', [
'password' => ['required', 'current_password'],
]);
$user = $request->user();
Auth::logout();
$user->delete();
$request->session()->invalidate();
$request->session()->regenerateToken();
return Redirect::to('/');
}
}

View File

@ -0,0 +1,85 @@
<?php
namespace App\Http\Requests\Auth;
use Illuminate\Auth\Events\Lockout;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
class LoginRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\Rule|array|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' => trans('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' => trans('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());
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace App\Http\Requests;
use App\Models\User;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class ProfileUpdateRequest extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\Rule|array|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)],
];
}
}

View File

@ -42,12 +42,6 @@ class ModelServiceProvider extends ServiceProvider
return new TeacherService();
});
$this->app->when(GradeController::class)
->needs(ServiceInterface::class)
->give(function () {
return new GradeService();
});
$this->app->when(SubjectController::class)
->needs(ServiceInterface::class)
->give(function () {

View File

@ -1,32 +0,0 @@
<?php
namespace App\Services;
use App\Models\Grade;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Pagination\LengthAwarePaginator;
class GradeService implements ServiceInterface
{
public function getAll(): LengthAwarePaginator
{
return Grade::filter()->paginate(5)->withQueryString();
}
public function create(array $data): Grade
{
return Grade::create($data);
}
public function update(Model $model, array $data): Grade
{
$model->update($data);
return $model;
}
public function delete($model): void
{
$model->delete();
}
}

View File

@ -0,0 +1,17 @@
<?php
namespace App\View\Components;
use Illuminate\View\Component;
use Illuminate\View\View;
class AppLayout extends Component
{
/**
* Get the view / contents that represents the component.
*/
public function render(): View
{
return view('layouts.app');
}
}

View File

@ -0,0 +1,17 @@
<?php
namespace App\View\Components;
use Illuminate\View\Component;
use Illuminate\View\View;
class GuestLayout extends Component
{
/**
* Get the view / contents that represents the component.
*/
public function render(): View
{
return view('layouts.guest');
}
}

View File

@ -11,7 +11,7 @@ return Application::configure(basePath: dirname(__DIR__))
health: '/up',
)
->withMiddleware(function (Middleware $middleware) {
//
$middleware->redirectGuestsTo('/login');
})
->withExceptions(function (Exceptions $exceptions) {
//

View File

@ -13,6 +13,8 @@
},
"require-dev": {
"fakerphp/faker": "^1.23",
"laravel-lang/lang": "^15.5",
"laravel/breeze": "^2.0",
"laravel/pint": "^1.13",
"laravel/sail": "^1.26",
"mockery/mockery": "^1.6",

1081
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -16,13 +16,11 @@ class StudentFactory extends Factory
*/
public function definition(): array
{
$faker = \Faker\Factory::create('ru_RU');
return [
'name' => $faker->firstNameMale,
'last_name' => $faker->lastNameMale,
'middle_name' => $faker->middleNameMale,
'birthday' => $faker->date(),
'name' => fake()->firstNameMale,
'last_name' => fake()->lastNameMale,
'middle_name' => fake()->middleNameMale,
'birthday' => fake()->date(),
];
}
}

View File

@ -16,13 +16,11 @@ class TeacherFactory extends Factory
*/
public function definition(): array
{
$faker = \Faker\Factory::create('ru_RU');
return [
'name' => $faker->firstNameMale,
'last_name' => $faker->lastNameMale,
'middle_name' => $faker->middleNameMale,
'birthday' => $faker->date(),
'name' => fake()->firstNameMale,
'last_name' => fake()->lastNameMale,
'middle_name' => fake()->middleNameMale,
'birthday' => fake()->date(),
];
}
}

92
lang/en.json Normal file
View File

@ -0,0 +1,92 @@
{
"(and :count more error)": "(and :count more error)",
"(and :count more errors)": "(and :count more error)|(and :count more errors)|(and :count more errors)",
"A fresh verification link has been sent to your email address.": "A fresh verification link has been sent to your email address.",
"A new verification link has been sent to the email address you provided during registration.": "A new verification link has been sent to the email address you provided during registration.",
"A new verification link has been sent to your email address.": "A new verification link has been sent to your email address.",
"All rights reserved.": "All rights reserved.",
"Already registered?": "Already registered?",
"Are you sure you want to delete your account?": "Are you sure you want to delete your account?",
"Before proceeding, please check your email for a verification link.": "Before proceeding, please check your email for a verification link.",
"Cancel": "Cancel",
"Click here to re-send the verification email.": "Click here to re-send the verification email.",
"click here to request another": "click here to request another",
"Confirm": "Confirm",
"Confirm Password": "Confirm Password",
"Current Password": "Current Password",
"Dashboard": "Dashboard",
"Delete Account": "Delete Account",
"Email": "Email",
"email": "The :attribute must be a valid email address.",
"Email Address": "Email Address",
"Email Password Reset Link": "Email Password Reset Link",
"Ensure your account is using a long, random password to stay secure.": "Ensure your account is using a long, random password to stay secure.",
"errors": "errors",
"Forbidden": "Forbidden",
"Forgot Your Password?": "Forgot Your Password?",
"Forgot your password?": "Forgot your password?",
"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.": "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.",
"Go to page :page": "Go to page :page",
"Hello!": "Hello!",
"If you did not create an account, no further action is required.": "If you did not create an account, no further action is required.",
"If you did not receive the email": "If you did not receive the email",
"If you did not request a password reset, no further action is required.": "If you did not request a password reset, no further action is required.",
"If you're having trouble clicking the \":actionText\" button, copy and paste the URL below\ninto your web browser:": "If you're having trouble clicking the \":actionText\" button, copy and paste the URL below\ninto your web browser:",
"Invalid JSON was returned from the route.": "Invalid JSON was returned from the route.",
"length": "length",
"Location": "Location",
"Log in": "Log in",
"Log Out": "Log Out",
"Login": "Login",
"Logout": "Logout",
"Name": "Name",
"name": "name",
"New Password": "New Password",
"Not Found": "Not Found",
"of": "of",
"Once your account is deleted, all of its resources and data will be permanently deleted. Before deleting your account, please download any data or information that you wish to retain.": "Once your account is deleted, all of its resources and data will be permanently deleted. Before deleting your account, please download any data or information that you wish to retain.",
"Once your account is deleted, all of its resources and data will be permanently deleted. Please enter your password to confirm you would like to permanently delete your account.": "Once your account is deleted, all of its resources and data will be permanently deleted. Please enter your password to confirm you would like to permanently delete your account.",
"Page Expired": "Page Expired",
"Pagination Navigation": "Pagination Navigation",
"Password": "Password",
"password": "The password is incorrect.",
"Payment Required": "Payment Required",
"Please click the button below to verify your email address.": "Please click the button below to verify your email address.",
"Please confirm your password before continuing.": "Please confirm your password before continuing.",
"Profile": "Profile",
"Profile Information": "Profile Information",
"Regards": "Regards",
"Register": "Register",
"Remember Me": "Remember Me",
"Remember me": "Remember me",
"Resend Verification Email": "Resend Verification Email",
"Reset Password": "Reset Password",
"Reset Password Notification": "Reset Password Notification",
"results": "results",
"Save": "Save",
"Saved.": "Saved.",
"Send Password Reset Link": "Send Password Reset Link",
"Server Error": "Server Error",
"Service Unavailable": "Service Unavailable",
"Showing": "Showing",
"Thanks for signing up! Before getting started, 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.": "Thanks for signing up! Before getting started, 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.",
"The given data was invalid.": "The given data was invalid.",
"The response is not a streamed response.": "The response is not a streamed response.",
"The response is not a view.": "The response is not a view.",
"This action is unauthorized.": "This action is unauthorized.",
"This is a secure area of the application. Please confirm your password before continuing.": "This is a secure area of the application. Please confirm your password before continuing.",
"This password reset link will expire in :count minutes.": "This password reset link will expire in :count minutes.",
"to": "to",
"Toggle navigation": "Toggle navigation",
"Too Many Requests": "Too Many Requests",
"Unauthorized": "Unauthorized",
"Update Password": "Update Password",
"Update your account's profile information and email address.": "Update your account's profile information and email address.",
"Verify Email Address": "Verify Email Address",
"Verify Your Email Address": "Verify Your Email Address",
"Whoops!": "Whoops!",
"You are logged in!": "You are logged in!",
"You are receiving this email because we received a password reset request for your account.": "You are receiving this email because we received a password reset request for your account.",
"You're logged in!": "You're logged in!",
"Your email address is unverified.": "Your email address is unverified."
}

9
lang/en/auth.php Normal file
View File

@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
return [
'failed' => 'These credentials do not match our records.',
'password' => 'The password is incorrect.',
'throttle' => 'Too many login attempts. Please try again in :seconds seconds.',
];

8
lang/en/pagination.php Normal file
View File

@ -0,0 +1,8 @@
<?php
declare(strict_types=1);
return [
'next' => 'Next &raquo;',
'previous' => '&laquo; Previous',
];

11
lang/en/passwords.php Normal file
View File

@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
return [
'reset' => 'Your password has been reset.',
'sent' => 'We have emailed your password reset link.',
'throttled' => 'Please wait before retrying.',
'token' => 'This password reset token is invalid.',
'user' => 'We can\'t find a user with that email address.',
];

153
lang/en/validation.php Normal file
View File

@ -0,0 +1,153 @@
<?php
declare(strict_types=1);
return [
'accepted' => 'The :attribute must be accepted.',
'accepted_if' => 'The :attribute must be accepted when :other is :value.',
'active_url' => 'The :attribute is not a valid URL.',
'after' => 'The :attribute must be a date after :date.',
'after_or_equal' => 'The :attribute must be a date after or equal to :date.',
'alpha' => 'The :attribute must only contain letters.',
'alpha_dash' => 'The :attribute must only contain letters, numbers, dashes and underscores.',
'alpha_num' => 'The :attribute must only contain letters and numbers.',
'array' => 'The :attribute must be an array.',
'ascii' => 'The :attribute field must only contain single-byte alphanumeric characters and symbols.',
'before' => 'The :attribute must be a date before :date.',
'before_or_equal' => 'The :attribute must be a date before or equal to :date.',
'between' => [
'array' => 'The :attribute must have between :min and :max items.',
'file' => 'The :attribute must be between :min and :max kilobytes.',
'numeric' => 'The :attribute must be between :min and :max.',
'string' => 'The :attribute must be between :min and :max characters.',
],
'boolean' => 'The :attribute field must be true or false.',
'can' => 'The :attribute field contains an unauthorized value.',
'confirmed' => 'The :attribute confirmation does not match.',
'contains' => 'The :attribute field is missing a required value.',
'current_password' => 'The password is incorrect.',
'date' => 'The :attribute is not a valid date.',
'date_equals' => 'The :attribute must be a date equal to :date.',
'date_format' => 'The :attribute does not match the format :format.',
'decimal' => 'The :attribute field must have :decimal decimal places.',
'declined' => 'The :attribute must be declined.',
'declined_if' => 'The :attribute must be declined when :other is :value.',
'different' => 'The :attribute and :other must be different.',
'digits' => 'The :attribute must be :digits digits.',
'digits_between' => 'The :attribute must be between :min and :max digits.',
'dimensions' => 'The :attribute has invalid image dimensions.',
'distinct' => 'The :attribute field has a duplicate value.',
'doesnt_end_with' => 'The :attribute field must not end with one of the following: :values.',
'doesnt_start_with' => 'The :attribute field must not start with one of the following: :values.',
'email' => 'The :attribute must be a valid email address.',
'ends_with' => 'The :attribute must end with one of the following: :values.',
'enum' => 'The :attribute field value is not in the list of allowed values.',
'exists' => 'The :attribute field value does not exist.',
'extensions' => 'The :attribute field must have one of the following extensions: :values.',
'file' => 'The :attribute must be a file.',
'filled' => 'The :attribute field must have a value.',
'gt' => [
'array' => 'The :attribute must have more than :value items.',
'file' => 'The :attribute must be greater than :value kilobytes.',
'numeric' => 'The :attribute must be greater than :value.',
'string' => 'The :attribute must be greater than :value characters.',
],
'gte' => [
'array' => 'The :attribute must have :value items or more.',
'file' => 'The :attribute must be greater than or equal to :value kilobytes.',
'numeric' => 'The :attribute must be greater than or equal to :value.',
'string' => 'The :attribute must be greater than or equal to :value characters.',
],
'hex_color' => 'The :attribute field must be a valid hexadecimal color.',
'image' => 'The :attribute must be an image.',
'in' => 'The :attribute field value is not in the list of allowed values.',
'in_array' => 'The :attribute field does not exist in :other.',
'integer' => 'The :attribute must be an integer.',
'ip' => 'The :attribute must be a valid IP address.',
'ipv4' => 'The :attribute must be a valid IPv4 address.',
'ipv6' => 'The :attribute must be a valid IPv6 address.',
'json' => 'The :attribute must be a valid JSON string.',
'list' => 'The :attribute field must be a list.',
'lowercase' => 'The :attribute field must be lowercase.',
'lt' => [
'array' => 'The :attribute must have less than :value items.',
'file' => 'The :attribute must be less than :value kilobytes.',
'numeric' => 'The :attribute must be less than :value.',
'string' => 'The :attribute must be less than :value characters.',
],
'lte' => [
'array' => 'The :attribute must not have more than :value items.',
'file' => 'The :attribute must be less than or equal to :value kilobytes.',
'numeric' => 'The :attribute must be less than or equal to :value.',
'string' => 'The :attribute must be less than or equal to :value characters.',
],
'mac_address' => 'The :attribute must be a valid MAC address.',
'max' => [
'array' => 'The :attribute must not have more than :max items.',
'file' => 'The :attribute must not be greater than :max kilobytes.',
'numeric' => 'The :attribute must not be greater than :max.',
'string' => 'The :attribute must not be greater than :max characters.',
],
'max_digits' => 'The :attribute field must not have more than :max digits.',
'mimes' => 'The :attribute must be a file of type: :values.',
'mimetypes' => 'The :attribute must be a file of type: :values.',
'min' => [
'array' => 'The :attribute must have at least :min items.',
'file' => 'The :attribute must be at least :min kilobytes.',
'numeric' => 'The :attribute must be at least :min.',
'string' => 'The :attribute must be at least :min characters.',
],
'min_digits' => 'The :attribute field must have at least :min digits.',
'missing' => 'The :attribute field must be missing.',
'missing_if' => 'The :attribute field must be missing when :other is :value.',
'missing_unless' => 'The :attribute field must be missing unless :other is :value.',
'missing_with' => 'The :attribute field must be missing when :values is present.',
'missing_with_all' => 'The :attribute field must be missing when :values are present.',
'multiple_of' => 'The :attribute must be a multiple of :value.',
'not_in' => 'The :attribute field must not be in the list.',
'not_regex' => 'The :attribute format is invalid.',
'numeric' => 'The :attribute must be a number.',
'password' => [
'letters' => 'The :attribute field must contain at least one letter.',
'mixed' => 'The :attribute field must contain at least one uppercase and one lowercase letter.',
'numbers' => 'The :attribute field must contain at least one number.',
'symbols' => 'The :attribute field must contain at least one symbol.',
'uncompromised' => 'The given :attribute has appeared in a data leak. Please choose a different :attribute.',
],
'present' => 'The :attribute field must be present.',
'present_if' => 'The :attribute field must be present when :other is :value.',
'present_unless' => 'The :attribute field must be present unless :other is :value.',
'present_with' => 'The :attribute field must be present when :values is present.',
'present_with_all' => 'The :attribute field must be present when :values are present.',
'prohibited' => 'The :attribute field is prohibited.',
'prohibited_if' => 'The :attribute field is prohibited when :other is :value.',
'prohibited_unless' => 'The :attribute field is prohibited unless :other is in :values.',
'prohibits' => 'The :attribute field prohibits :other from being present.',
'regex' => 'The :attribute format is invalid.',
'required' => 'The :attribute field is required.',
'required_array_keys' => 'The :attribute field must contain entries for: :values.',
'required_if' => 'The :attribute field is required when :other is :value.',
'required_if_accepted' => 'The :attribute field is required when :other is accepted.',
'required_if_declined' => 'The :attribute field is required when :other is declined.',
'required_unless' => 'The :attribute field is required unless :other is in :values.',
'required_with' => 'The :attribute field is required when :values is present.',
'required_with_all' => 'The :attribute field is required when :values are present.',
'required_without' => 'The :attribute field is required when :values is not present.',
'required_without_all' => 'The :attribute field is required when none of :values are present.',
'same' => 'The :attribute and :other must match.',
'size' => [
'array' => 'The :attribute must contain :size items.',
'file' => 'The :attribute must be :size kilobytes.',
'numeric' => 'The :attribute must be :size.',
'string' => 'The :attribute must be :size characters.',
],
'starts_with' => 'The :attribute must start with one of the following: :values.',
'string' => 'The :attribute must be a string.',
'timezone' => 'The :attribute must be a valid timezone.',
'ulid' => 'The :attribute field must be a valid ULID.',
'unique' => 'The :attribute has already been taken.',
'uploaded' => 'The :attribute failed to upload.',
'uppercase' => 'The :attribute field must be uppercase.',
'url' => 'The :attribute must be a valid URL.',
'uuid' => 'The :attribute must be a valid UUID.',
];

92
lang/ru.json Normal file
View File

@ -0,0 +1,92 @@
{
"(and :count more error)": "(и ещё :count ошибка)",
"(and :count more errors)": "(и ещё :count ошибка)|(и ещё :count ошибки)|(и ещё :count ошибок)",
"A fresh verification link has been sent to your email address.": "Новая ссылка подтверждения отправлена на Ваш адрес электронной почты.",
"A new verification link has been sent to the email address you provided during registration.": "Новая ссылка для подтверждения была отправлена на Ваш адрес электронной почты, указанный при регистрации.",
"A new verification link has been sent to your email address.": "На Ваш адрес электронной почты отправлена новая ссылка для подтверждения.",
"All rights reserved.": "Все права защищены.",
"Already registered?": "Уже зарегистрированы?",
"Are you sure you want to delete your account?": "Вы уверены что хотите удалить свою учётную запись?",
"Before proceeding, please check your email for a verification link.": "Прежде чем продолжить, проверьте свою электронную почту на наличие ссылки для подтверждения.",
"Cancel": "Отмена",
"Click here to re-send the verification email.": "Нажмите здесь, чтобы повторно отправить электронное письмо для подтверждения.",
"click here to request another": "нажмите здесь для запроса другой ссылки",
"Confirm": "Подтвердить",
"Confirm Password": "Подтвердить пароль",
"Current Password": "Текущий пароль",
"Dashboard": "Панель",
"Delete Account": "Удалить аккаунт",
"Email": "Адрес электронной почты",
"email": "Значение поля :attribute должно быть действительным электронным адресом.",
"Email Address": "Адрес электронной почты",
"Email Password Reset Link": "Ссылка для сброса пароля",
"Ensure your account is using a long, random password to stay secure.": "В целях безопасности убедитесь, что Вы используете длинный случайный пароль.",
"errors": "ошибки",
"Forbidden": "Запрещено",
"Forgot Your Password?": "Забыли пароль?",
"Forgot your password?": "Забыли пароль?",
"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.": "Забыли пароль? Нет проблем. Просто сообщите Ваш адрес электронной почты и мы пришлём Вам ссылку для сброса пароля.",
"Go to page :page": "Перейти к :page-й странице",
"Hello!": "Здравствуйте!",
"If you did not create an account, no further action is required.": "Если Вы не создавали учетную запись, никаких дополнительных действий не требуется.",
"If you did not receive the email": "Если Вы не получили письмо",
"If you did not request a password reset, no further action is required.": "Если Вы не запрашивали восстановление пароля, никаких дополнительных действий не требуется.",
"If you're having trouble clicking the \":actionText\" button, copy and paste the URL below\ninto your web browser:": "Если у Вас возникли проблемы с нажатием кнопки \":actionText\", скопируйте и вставьте приведенный ниже URL-адрес в свой браузер:",
"Invalid JSON was returned from the route.": "Маршрут вернул некорректный JSON.",
"length": "длина",
"Location": "Местоположение",
"Log in": "Войти",
"Log Out": "Выйти",
"Login": "Войти",
"Logout": "Выйти",
"Name": "Имя",
"name": "имя",
"New Password": "Новый пароль",
"Not Found": "Не найдено",
"of": "из",
"Once your account is deleted, all of its resources and data will be permanently deleted. Before deleting your account, please download any data or information that you wish to retain.": "После удаления Вашей учётной записи все её ресурсы и данные будут удалены без возможности восстановления. Перед удалением учётной записи загрузите данные и информацию, которую хотите сохранить.",
"Once your account is deleted, all of its resources and data will be permanently deleted. Please enter your password to confirm you would like to permanently delete your account.": "После удаления Вашей учётной записи все её ресурсы и данные будут удалены без возможности восстановления. Пожалуйста, введите свой пароль для подтверждения удаления учётной записи.",
"Page Expired": "Страница устарела",
"Pagination Navigation": "Навигация",
"Password": "Пароль",
"password": "Некорректный пароль.",
"Payment Required": "Требуется оплата",
"Please click the button below to verify your email address.": "Пожалуйста, нажмите кнопку ниже, чтобы подтвердить свой адрес электронной почты.",
"Please confirm your password before continuing.": "Пожалуйста, подтвердите свой пароль, прежде чем продолжить.",
"Profile": "Профиль",
"Profile Information": "Информация профиля",
"Regards": "С уважением",
"Register": "Регистрация",
"Remember Me": "Запомнить меня",
"Remember me": "Запомнить меня",
"Resend Verification Email": "Выслать повторно письмо для подтверждения",
"Reset Password": "Сбросить пароль",
"Reset Password Notification": "Оповещение о сбросе пароля",
"results": "результатов",
"Save": "Сохранить",
"Saved.": "Сохранено.",
"Send Password Reset Link": "Отправить ссылку сброса пароля",
"Server Error": "Ошибка сервера",
"Service Unavailable": "Сервис недоступен",
"Showing": "Показано с",
"Thanks for signing up! Before getting started, 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.": "Спасибо за регистрацию! Прежде чем начать, не могли бы Вы подтвердить адрес своей электронной почты перейдя по ссылке, которую мы Вам отправили? Если Вы не получили письмо, мы с радостью отправим новое.",
"The given data was invalid.": "Указанные данные недействительны.",
"The response is not a streamed response.": "Ответ не является потоковым.",
"The response is not a view.": "Ответ не является представлением.",
"This action is unauthorized.": "Действие не авторизовано.",
"This is a secure area of the application. Please confirm your password before continuing.": "Это защищённая область приложения. Пожалуйста, подтвердите Ваш пароль, прежде чем продолжить.",
"This password reset link will expire in :count minutes.": "Срок действия ссылки для сброса пароля истекает через :count минут.",
"to": "по",
"Toggle navigation": "Переключить навигацию",
"Too Many Requests": "Слишком много запросов",
"Unauthorized": "Не авторизован",
"Update Password": "Обновить пароль",
"Update your account's profile information and email address.": "Обновите информацию и адрес электронной почты в профиле учётной записи.",
"Verify Email Address": "Подтвердить адрес электронной почты",
"Verify Your Email Address": "Подтвердите Ваш адрес электронной почты",
"Whoops!": "Упс!",
"You are logged in!": "Вы вошли в систему.",
"You are receiving this email because we received a password reset request for your account.": "Вы получили это письмо, потому что мы получили запрос на сброс пароля для Вашей учётной записи.",
"You're logged in!": "Вы уже вошли.",
"Your email address is unverified.": "Ваш адрес электронной почты не подтверждён."
}

9
lang/ru/auth.php Normal file
View File

@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
return [
'failed' => 'Неверное имя пользователя или пароль.',
'password' => 'Некорректный пароль.',
'throttle' => 'Слишком много попыток входа. Пожалуйста, попробуйте ещё раз через :seconds секунд.',
];

8
lang/ru/pagination.php Normal file
View File

@ -0,0 +1,8 @@
<?php
declare(strict_types=1);
return [
'next' => 'Вперёд &raquo;',
'previous' => '&laquo; Назад',
];

11
lang/ru/passwords.php Normal file
View File

@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
return [
'reset' => 'Ваш пароль был сброшен.',
'sent' => 'Ссылка на сброс пароля была отправлена.',
'throttled' => 'Пожалуйста, подождите перед повторной попыткой.',
'token' => 'Ошибочный код сброса пароля.',
'user' => 'Не удалось найти пользователя с указанным электронным адресом.',
];

153
lang/ru/validation.php Normal file
View File

@ -0,0 +1,153 @@
<?php
declare(strict_types=1);
return [
'accepted' => 'Вы должны принять :attribute.',
'accepted_if' => 'Вы должны принять :attribute, когда :other содержит :value.',
'active_url' => 'Значение поля :attribute должно быть действительным URL адресом.',
'after' => 'Значение поля :attribute должно быть датой после :date.',
'after_or_equal' => 'Значение поля :attribute должно быть датой после или равной :date.',
'alpha' => 'Значение поля :attribute может содержать только буквы.',
'alpha_dash' => 'Значение поля :attribute может содержать только буквы, цифры, дефис и нижнее подчеркивание.',
'alpha_num' => 'Значение поля :attribute может содержать только буквы и цифры.',
'array' => 'Значение поля :attribute должно быть массивом.',
'ascii' => 'Значение поля :attribute должно содержать только однобайтовые цифро-буквенные символы.',
'before' => 'Значение поля :attribute должно быть датой до :date.',
'before_or_equal' => 'Значение поля :attribute должно быть датой до или равной :date.',
'between' => [
'array' => 'Количество элементов в поле :attribute должно быть от :min до :max.',
'file' => 'Размер файла в поле :attribute должен быть от :min до :max Кб.',
'numeric' => 'Значение поля :attribute должно быть от :min до :max.',
'string' => 'Количество символов в поле :attribute должно быть от :min до :max.',
],
'boolean' => 'Значение поля :attribute должно быть логического типа.',
'can' => 'Значение поля :attribute должно быть авторизованным.',
'confirmed' => 'Значение поля :attribute не совпадает с подтверждаемым.',
'contains' => 'В поле :attribute отсутствует необходимое значение.',
'current_password' => 'Неверный пароль.',
'date' => 'Значение поля :attribute должно быть корректной датой.',
'date_equals' => 'Значение поля :attribute должно быть датой равной :date.',
'date_format' => 'Значение поля :attribute должно соответствовать формату даты: :format.',
'decimal' => 'Значение поля :attribute должно содержать :decimal цифр десятичных разрядов.',
'declined' => 'Значение поля :attribute должно быть отклонено.',
'declined_if' => 'Значение поля :attribute должно быть отклонено, когда :other содержит :value.',
'different' => 'Значения полей :attribute и :other должны различаться.',
'digits' => 'Количество символов в поле :attribute должно быть равным :digits.',
'digits_between' => 'Количество символов в поле :attribute должно быть от :min до :max.',
'dimensions' => 'Изображение, указанное в поле :attribute, имеет недопустимые размеры.',
'distinct' => 'Элементы в значении поля :attribute не должны повторяться.',
'doesnt_end_with' => 'Значение поля :attribute не должно заканчиваться одним из следующих: :values.',
'doesnt_start_with' => 'Значение поля :attribute не должно начинаться с одного из следующих: :values.',
'email' => 'Значение поля :attribute должно быть действительным электронным адресом.',
'ends_with' => 'Значение поля :attribute должно заканчиваться одним из следующих: :values',
'enum' => 'Значение поля :attribute отсутствует в списке разрешённых.',
'exists' => 'Значение поля :attribute не существует.',
'extensions' => 'Файл в поле :attribute должен иметь одно из следующих расширений: :values.',
'file' => 'В поле :attribute должен быть указан файл.',
'filled' => 'Значение поля :attribute обязательно для заполнения.',
'gt' => [
'array' => 'Количество элементов в поле :attribute должно быть больше :value.',
'file' => 'Размер файла, указанный в поле :attribute, должен быть больше :value Кб.',
'numeric' => 'Значение поля :attribute должно быть больше :value.',
'string' => 'Количество символов в поле :attribute должно быть больше :value.',
],
'gte' => [
'array' => 'Количество элементов в поле :attribute должно быть :value или больше.',
'file' => 'Размер файла, указанный в поле :attribute, должен быть :value Кб или больше.',
'numeric' => 'Значение поля :attribute должно быть :value или больше.',
'string' => 'Количество символов в поле :attribute должно быть :value или больше.',
],
'hex_color' => 'Значение поля :attribute должно быть корректным цветом в HEX формате.',
'image' => 'Файл, указанный в поле :attribute, должен быть изображением.',
'in' => 'Значение поля :attribute отсутствует в списке разрешённых.',
'in_array' => 'Значение поля :attribute должно быть указано в поле :other.',
'integer' => 'Значение поля :attribute должно быть целым числом.',
'ip' => 'Значение поля :attribute должно быть действительным IP-адресом.',
'ipv4' => 'Значение поля :attribute должно быть действительным IPv4-адресом.',
'ipv6' => 'Значение поля :attribute должно быть действительным IPv6-адресом.',
'json' => 'Значение поля :attribute должно быть JSON строкой.',
'list' => 'Значение поля :attribute должно быть списком.',
'lowercase' => 'Значение поля :attribute должно быть в нижнем регистре.',
'lt' => [
'array' => 'Количество элементов в поле :attribute должно быть меньше :value.',
'file' => 'Размер файла, указанный в поле :attribute, должен быть меньше :value Кб.',
'numeric' => 'Значение поля :attribute должно быть меньше :value.',
'string' => 'Количество символов в поле :attribute должно быть меньше :value.',
],
'lte' => [
'array' => 'Количество элементов в поле :attribute должно быть :value или меньше.',
'file' => 'Размер файла, указанный в поле :attribute, должен быть :value Кб или меньше.',
'numeric' => 'Значение поля :attribute должно быть равным или меньше :value.',
'string' => 'Количество символов в поле :attribute должно быть :value или меньше.',
],
'mac_address' => 'Значение поля :attribute должно быть корректным MAC-адресом.',
'max' => [
'array' => 'Количество элементов в поле :attribute не может превышать :max.',
'file' => 'Размер файла в поле :attribute не может быть больше :max Кб.',
'numeric' => 'Значение поля :attribute не может быть больше :max.',
'string' => 'Количество символов в значении поля :attribute не может превышать :max.',
],
'max_digits' => 'Значение поля :attribute не должно содержать больше :max цифр.',
'mimes' => 'Файл, указанный в поле :attribute, должен быть одного из следующих типов: :values.',
'mimetypes' => 'Файл, указанный в поле :attribute, должен быть одного из следующих типов: :values.',
'min' => [
'array' => 'Количество элементов в поле :attribute должно быть не меньше :min.',
'file' => 'Размер файла, указанный в поле :attribute, должен быть не меньше :min Кб.',
'numeric' => 'Значение поля :attribute должно быть не меньше :min.',
'string' => 'Количество символов в поле :attribute должно быть не меньше :min.',
],
'min_digits' => 'Значение поля :attribute должно содержать не меньше :min цифр.',
'missing' => 'Значение поля :attribute должно отсутствовать.',
'missing_if' => 'Значение поля :attribute должно отсутствовать, когда :other содержит :value.',
'missing_unless' => 'Значение поля :attribute должно отсутствовать, когда :other не содержит :value.',
'missing_with' => 'Значение поля :attribute должно отсутствовать, если :values указано.',
'missing_with_all' => 'Значение поля :attribute должно отсутствовать, когда указаны все :values.',
'multiple_of' => 'Значение поля :attribute должно быть кратным :value',
'not_in' => 'Значение поля :attribute находится в списке запрета.',
'not_regex' => 'Значение поля :attribute имеет некорректный формат.',
'numeric' => 'Значение поля :attribute должно быть числом.',
'password' => [
'letters' => 'Значение поля :attribute должно содержать хотя бы одну букву.',
'mixed' => 'Значение поля :attribute должно содержать хотя бы одну прописную и одну строчную буквы.',
'numbers' => 'Значение поля :attribute должно содержать хотя бы одну цифру.',
'symbols' => 'Значение поля :attribute должно содержать хотя бы один символ.',
'uncompromised' => 'Значение поля :attribute обнаружено в утёкших данных. Пожалуйста, выберите другое значение для :attribute.',
],
'present' => 'Значение поля :attribute должно быть.',
'present_if' => 'Значение поля :attribute должно быть когда :other содержит :value.',
'present_unless' => 'Значение поля :attribute должно быть, если только :other не содержит :value.',
'present_with' => 'Значение поля :attribute должно быть когда одно из :values присутствуют.',
'present_with_all' => 'Значение поля :attribute должно быть когда все из значений присутствуют: :values.',
'prohibited' => 'Значение поля :attribute запрещено.',
'prohibited_if' => 'Значение поля :attribute запрещено, когда :other содержит :value.',
'prohibited_unless' => 'Значение поля :attribute запрещено, если :other не состоит в :values.',
'prohibits' => 'Значение поля :attribute запрещает присутствие :other.',
'regex' => 'Значение поля :attribute имеет некорректный формат.',
'required' => 'Поле :attribute обязательно.',
'required_array_keys' => 'Массив, указанный в поле :attribute, обязательно должен иметь ключи: :values',
'required_if' => 'Поле :attribute обязательно для заполнения, когда :other содержит :value.',
'required_if_accepted' => 'Поле :attribute обязательно, когда :other принято.',
'required_if_declined' => 'Поле :attribute обязательно, когда :other отклонено.',
'required_unless' => 'Поле :attribute обязательно для заполнения, когда :other не содержит :values.',
'required_with' => 'Поле :attribute обязательно для заполнения, когда :values указано.',
'required_with_all' => 'Поле :attribute обязательно для заполнения, когда :values указано.',
'required_without' => 'Поле :attribute обязательно для заполнения, когда :values не указано.',
'required_without_all' => 'Поле :attribute обязательно для заполнения, когда ни одно из :values не указано.',
'same' => 'Значения полей :attribute и :other должны совпадать.',
'size' => [
'array' => 'Количество элементов в поле :attribute должно быть равным :size.',
'file' => 'Размер файла, указанный в поле :attribute, должен быть равен :size Кб.',
'numeric' => 'Значение поля :attribute должно быть равным :size.',
'string' => 'Количество символов в поле :attribute должно быть равным :size.',
],
'starts_with' => 'Поле :attribute должно начинаться с одного из следующих значений: :values',
'string' => 'Значение поля :attribute должно быть строкой.',
'timezone' => 'Значение поля :attribute должно быть действительным часовым поясом.',
'ulid' => 'Значение поля :attribute должно быть корректным ULID.',
'unique' => 'Такое значение поля :attribute уже существует.',
'uploaded' => 'Загрузка файла из поля :attribute не удалась.',
'uppercase' => 'Значение поля :attribute должно быть в верхнем регистре.',
'url' => 'Значение поля :attribute не является ссылкой или имеет некорректный формат.',
'uuid' => 'Значение поля :attribute должно быть корректным UUID.',
];

1342
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -7,10 +7,15 @@
},
"devDependencies": {
"@popperjs/core": "^2.11.6",
"@tailwindcss/forms": "^0.5.2",
"alpinejs": "^3.4.2",
"autoprefixer": "^10.4.2",
"axios": "^1.6.4",
"bootstrap": "^5.2.3",
"laravel-vite-plugin": "^1.0",
"postcss": "^8.4.31",
"sass": "^1.56.1",
"tailwindcss": "^3.1.0",
"vite": "^5.0"
}
}

6
postcss.config.js Normal file
View File

@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

View File

@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@ -1 +1,7 @@
import './bootstrap';
import Alpine from 'alpinejs';
window.Alpine = Alpine;
Alpine.start();

View File

@ -0,0 +1,27 @@
<x-guest-layout>
<div class="mb-4 text-sm text-gray-600">
{{ __('This is a secure area of the application. Please confirm your password before continuing.') }}
</div>
<form method="POST" action="{{ route('password.confirm') }}">
@csrf
<!-- Password -->
<div>
<x-input-label for="password" :value="__('Password')" />
<x-text-input 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>
</x-guest-layout>

View File

@ -0,0 +1,41 @@
<x-guest-layout>
<!-- Session Status -->
<x-auth-session-status class="mb-4" :status="session('status')" />
<form method="POST" action="{{ route('login') }}">
@csrf
<!-- Email Address -->
<div>
<x-input-label for="email" :value="__('Email')" />
<x-text-input id="email" class="block mt-1 w-full" type="email" name="email" :value="old('email')" required autofocus autocomplete="username" />
<x-input-error :messages="$errors->get('email')" class="mt-2" />
</div>
<!-- Password -->
<div class="mt-4">
<x-input-label for="password" :value="__('Password')" />
<x-text-input 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>
<!-- Remember Me -->
<div class="block mt-4">
<label for="remember_me" class="inline-flex items-center">
<input id="remember_me" type="checkbox" class="rounded border-gray-300 text-indigo-600 shadow-sm focus:ring-indigo-500" name="remember">
<span class="ms-2 text-sm text-gray-600">{{ __('Remember me') }}</span>
</label>
</div>
<div class="flex items-center justify-end mt-4">
<x-primary-button class="ms-3">
{{ __('Log in') }}
</x-primary-button>
</div>
</form>
</x-guest-layout>

View File

@ -0,0 +1,39 @@
<x-guest-layout>
<form method="POST" action="{{ route('password.store') }}">
@csrf
<!-- Password Reset Token -->
<input type="hidden" name="token" value="{{ $request->route('token') }}">
<!-- Email Address -->
<div>
<x-input-label for="email" :value="__('Email')" />
<x-text-input id="email" class="block mt-1 w-full" type="email" name="email" :value="old('email', $request->email)" required autofocus autocomplete="username" />
<x-input-error :messages="$errors->get('email')" class="mt-2" />
</div>
<!-- Password -->
<div class="mt-4">
<x-input-label for="password" :value="__('Password')" />
<x-text-input id="password" class="block mt-1 w-full" type="password" name="password" required autocomplete="new-password" />
<x-input-error :messages="$errors->get('password')" class="mt-2" />
</div>
<!-- Confirm Password -->
<div class="mt-4">
<x-input-label for="password_confirmation" :value="__('Confirm Password')" />
<x-text-input id="password_confirmation" class="block mt-1 w-full"
type="password"
name="password_confirmation" required autocomplete="new-password" />
<x-input-error :messages="$errors->get('password_confirmation')" class="mt-2" />
</div>
<div class="flex items-center justify-end mt-4">
<x-primary-button>
{{ __('Reset Password') }}
</x-primary-button>
</div>
</form>
</x-guest-layout>

View File

@ -0,0 +1,31 @@
<x-guest-layout>
<div class="mb-4 text-sm text-gray-600">
{{ __('Thanks for signing up! Before getting started, 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.') }}
</div>
@if (session('status') == 'verification-link-sent')
<div class="mb-4 font-medium text-sm text-green-600">
{{ __('A new verification link has been sent to the email address you provided during registration.') }}
</div>
@endif
<div class="mt-4 flex items-center justify-between">
<form method="POST" action="{{ route('verification.send') }}">
@csrf
<div>
<x-primary-button>
{{ __('Resend Verification Email') }}
</x-primary-button>
</div>
</form>
<form method="POST" action="{{ route('logout') }}">
@csrf
<button type="submit" class="underline text-sm text-gray-600 hover:text-gray-900 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
{{ __('Log Out') }}
</button>
</form>
</div>
</x-guest-layout>

View File

@ -0,0 +1,3 @@
<svg viewBox="0 0 316 316" xmlns="http://www.w3.org/2000/svg" {{ $attributes }}>
<path d="M305.8 81.125C305.77 80.995 305.69 80.885 305.65 80.755C305.56 80.525 305.49 80.285 305.37 80.075C305.29 79.935 305.17 79.815 305.07 79.685C304.94 79.515 304.83 79.325 304.68 79.175C304.55 79.045 304.39 78.955 304.25 78.845C304.09 78.715 303.95 78.575 303.77 78.475L251.32 48.275C249.97 47.495 248.31 47.495 246.96 48.275L194.51 78.475C194.33 78.575 194.19 78.725 194.03 78.845C193.89 78.955 193.73 79.045 193.6 79.175C193.45 79.325 193.34 79.515 193.21 79.685C193.11 79.815 192.99 79.935 192.91 80.075C192.79 80.285 192.71 80.525 192.63 80.755C192.58 80.875 192.51 80.995 192.48 81.125C192.38 81.495 192.33 81.875 192.33 82.265V139.625L148.62 164.795V52.575C148.62 52.185 148.57 51.805 148.47 51.435C148.44 51.305 148.36 51.195 148.32 51.065C148.23 50.835 148.16 50.595 148.04 50.385C147.96 50.245 147.84 50.125 147.74 49.995C147.61 49.825 147.5 49.635 147.35 49.485C147.22 49.355 147.06 49.265 146.92 49.155C146.76 49.025 146.62 48.885 146.44 48.785L93.99 18.585C92.64 17.805 90.98 17.805 89.63 18.585L37.18 48.785C37 48.885 36.86 49.035 36.7 49.155C36.56 49.265 36.4 49.355 36.27 49.485C36.12 49.635 36.01 49.825 35.88 49.995C35.78 50.125 35.66 50.245 35.58 50.385C35.46 50.595 35.38 50.835 35.3 51.065C35.25 51.185 35.18 51.305 35.15 51.435C35.05 51.805 35 52.185 35 52.575V232.235C35 233.795 35.84 235.245 37.19 236.025L142.1 296.425C142.33 296.555 142.58 296.635 142.82 296.725C142.93 296.765 143.04 296.835 143.16 296.865C143.53 296.965 143.9 297.015 144.28 297.015C144.66 297.015 145.03 296.965 145.4 296.865C145.5 296.835 145.59 296.775 145.69 296.745C145.95 296.655 146.21 296.565 146.45 296.435L251.36 236.035C252.72 235.255 253.55 233.815 253.55 232.245V174.885L303.81 145.945C305.17 145.165 306 143.725 306 142.155V82.265C305.95 81.875 305.89 81.495 305.8 81.125ZM144.2 227.205L100.57 202.515L146.39 176.135L196.66 147.195L240.33 172.335L208.29 190.625L144.2 227.205ZM244.75 114.995V164.795L226.39 154.225L201.03 139.625V89.825L219.39 100.395L244.75 114.995ZM249.12 57.105L292.81 82.265L249.12 107.425L205.43 82.265L249.12 57.105ZM114.49 184.425L96.13 194.995V85.305L121.49 70.705L139.85 60.135V169.815L114.49 184.425ZM91.76 27.425L135.45 52.585L91.76 77.745L48.07 52.585L91.76 27.425ZM43.67 60.135L62.03 70.705L87.39 85.305V202.545V202.555V202.565C87.39 202.735 87.44 202.895 87.46 203.055C87.49 203.265 87.49 203.485 87.55 203.695V203.705C87.6 203.875 87.69 204.035 87.76 204.195C87.84 204.375 87.89 204.575 87.99 204.745C87.99 204.745 87.99 204.755 88 204.755C88.09 204.905 88.22 205.035 88.33 205.175C88.45 205.335 88.55 205.495 88.69 205.635L88.7 205.645C88.82 205.765 88.98 205.855 89.12 205.965C89.28 206.085 89.42 206.225 89.59 206.325C89.6 206.325 89.6 206.325 89.61 206.335C89.62 206.335 89.62 206.345 89.63 206.345L139.87 234.775V285.065L43.67 229.705V60.135ZM244.75 229.705L148.58 285.075V234.775L219.8 194.115L244.75 179.875V229.705ZM297.2 139.625L253.49 164.795V114.995L278.85 100.395L297.21 89.825V139.625H297.2Z"/>
</svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@ -0,0 +1,7 @@
@props(['status'])
@if ($status)
<div {{ $attributes->merge(['class' => 'font-medium text-sm text-green-600']) }}>
{{ $status }}
</div>
@endif

View File

@ -0,0 +1,3 @@
<button {{ $attributes->merge(['type' => 'submit', 'class' => 'inline-flex items-center px-4 py-2 bg-red-600 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-red-500 active:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 transition ease-in-out duration-150']) }}>
{{ $slot }}
</button>

View File

@ -0,0 +1 @@
<a {{ $attributes->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 }}</a>

View File

@ -0,0 +1,43 @@
@props(['align' => 'right', 'width' => '48', 'contentClasses' => 'py-1 bg-white'])
@php
switch ($align) {
case 'left':
$alignmentClasses = 'ltr:origin-top-left rtl:origin-top-right start-0';
break;
case 'top':
$alignmentClasses = 'origin-top';
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
<div class="relative" x-data="{ open: false }" @click.outside="open = false" @close.stop="open = false">
<div @click="open = ! open">
{{ $trigger }}
</div>
<div x-show="open"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0 scale-95"
x-transition:enter-end="opacity-100 scale-100"
x-transition:leave="transition ease-in duration-75"
x-transition:leave-start="opacity-100 scale-100"
x-transition:leave-end="opacity-0 scale-95"
class="absolute z-50 mt-2 {{ $width }} rounded-md shadow-lg {{ $alignmentClasses }}"
style="display: none;"
@click="open = false">
<div class="rounded-md ring-1 ring-black ring-opacity-5 {{ $contentClasses }}">
{{ $content }}
</div>
</div>
</div>

View File

@ -0,0 +1,9 @@
@props(['messages'])
@if ($messages)
<ul {{ $attributes->merge(['class' => 'text-sm text-red-600 space-y-1']) }}>
@foreach ((array) $messages as $message)
<li>{{ $message }}</li>
@endforeach
</ul>
@endif

View File

@ -0,0 +1,5 @@
@props(['value'])
<label {{ $attributes->merge(['class' => 'block font-medium text-sm text-gray-700']) }}>
{{ $value ?? $slot }}
</label>

View File

@ -0,0 +1,78 @@
@props([
'name',
'show' => false,
'maxWidth' => '2xl'
])
@php
$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];
@endphp
<div
x-data="{
show: @js($show),
focusables() {
// All focusable element types...
let selector = 'a, button, input:not([type=\'hidden\']), textarea, select, details, [tabindex]:not([tabindex=\'-1\'])'
return [...$el.querySelectorAll(selector)]
// All non-disabled elements...
.filter(el => ! el.hasAttribute('disabled'))
},
firstFocusable() { return this.focusables()[0] },
lastFocusable() { return this.focusables().slice(-1)[0] },
nextFocusable() { return this.focusables()[this.nextFocusableIndex()] || this.firstFocusable() },
prevFocusable() { return this.focusables()[this.prevFocusableIndex()] || this.lastFocusable() },
nextFocusableIndex() { return (this.focusables().indexOf(document.activeElement) + 1) % (this.focusables().length + 1) },
prevFocusableIndex() { return Math.max(0, this.focusables().indexOf(document.activeElement)) -1 },
}"
x-init="$watch('show', value => {
if (value) {
document.body.classList.add('overflow-y-hidden');
{{ $attributes->has('focusable') ? 'setTimeout(() => firstFocusable().focus(), 100)' : '' }}
} else {
document.body.classList.remove('overflow-y-hidden');
}
})"
x-on:open-modal.window="$event.detail == '{{ $name }}' ? show = true : null"
x-on:close-modal.window="$event.detail == '{{ $name }}' ? show = false : null"
x-on:close.stop="show = false"
x-on:keydown.escape.window="show = false"
x-on:keydown.tab.prevent="$event.shiftKey || nextFocusable().focus()"
x-on:keydown.shift.tab.prevent="prevFocusable().focus()"
x-show="show"
class="fixed inset-0 overflow-y-auto px-4 py-6 sm:px-0 z-50"
style="display: {{ $show ? 'block' : 'none' }};"
>
<div
x-show="show"
class="fixed inset-0 transform transition-all"
x-on:click="show = false"
x-transition:enter="ease-out duration-300"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="ease-in duration-200"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
>
<div class="absolute inset-0 bg-gray-500 opacity-75"></div>
</div>
<div
x-show="show"
class="mb-6 bg-white rounded-lg overflow-hidden shadow-xl transform transition-all sm:w-full {{ $maxWidth }} sm:mx-auto"
x-transition:enter="ease-out duration-300"
x-transition:enter-start="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100"
x-transition:leave="ease-in duration-200"
x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
x-transition:leave-end="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
{{ $slot }}
</div>
</div>

View File

@ -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
<a {{ $attributes->merge(['class' => $classes]) }}>
{{ $slot }}
</a>

View File

@ -0,0 +1,3 @@
<button {{ $attributes->merge(['type' => 'submit', 'class' => 'inline-flex items-center px-4 py-2 bg-gray-800 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-gray-700 focus:bg-gray-700 active:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 transition ease-in-out duration-150']) }}>
{{ $slot }}
</button>

View File

@ -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
<a {{ $attributes->merge(['class' => $classes]) }}>
{{ $slot }}
</a>

View File

@ -0,0 +1,3 @@
<button {{ $attributes->merge(['type' => 'button', 'class' => 'inline-flex items-center px-4 py-2 bg-white border border-gray-300 rounded-md font-semibold text-xs text-gray-700 uppercase tracking-widest shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 disabled:opacity-25 transition ease-in-out duration-150']) }}>
{{ $slot }}
</button>

View File

@ -0,0 +1,3 @@
@props(['disabled' => false])
<input {{ $disabled ? 'disabled' : '' }} {!! $attributes->merge(['class' => 'border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm']) !!}>

View File

@ -1,4 +1,4 @@
@extends('layouts.app')
@extends('layouts.application')
@section('content')
@include('grade-lesson.form', ['route' => route('grades.lessons.store', $grade), 'method' => 'POST'])

View File

@ -1,4 +1,4 @@
@extends('layouts.app')
@extends('layouts.application')
@section('content')
@include('grade-lesson.form', ['route' => route('grades.lessons.update', [$grade, $lesson]), 'method' => 'PUT'])

View File

@ -1,4 +1,4 @@
`@extends('layouts.app')
`@extends('layouts.application')
@section('content')
<div class="container col-md-5">

View File

@ -1,4 +1,4 @@
@extends('layouts.app')
@extends('layouts.application')
@section('content')
<div class="container col-md-6">

View File

@ -1,4 +1,4 @@
@extends('layouts.app')
@extends('layouts.application')
@section('content')
@include('grade-subject.form', ['route' => route('grades.subjects.store', $grade), 'method' => 'POST'])

View File

@ -1,4 +1,4 @@
@extends('layouts.app')
@extends('layouts.application')
@section('content')
@include('grade-subject.form', [

View File

@ -1,4 +1,4 @@
@extends('layouts.app')
@extends('layouts.application')
@section('content')
@include('grade-teacher.form', ['route' => route('teachers.subjects.grades.store', [$teacher, $subject]), 'method' => 'POST'])

View File

@ -1,4 +1,4 @@
@extends('layouts.app')
@extends('layouts.application')
@section('content')
@include('grade-teacher.form', [

View File

@ -1,4 +1,4 @@
@extends('layouts.app')
@extends('layouts.application')
@section('content')
@include('grades.form', ['route' => route('grades.store'), 'method' => 'POST'])

View File

@ -1,4 +1,4 @@
@extends('layouts.app')
@extends('layouts.application')
@section('content')
@include('grades.form', ['route' => route('grades.update', $grade), 'method' => 'PUT'])

View File

@ -1,4 +1,4 @@
@extends('layouts.app')
@extends('layouts.application')
@section('content')
<div class="container col-md-6">

View File

@ -1,4 +1,4 @@
@extends('layouts.app')
@extends('layouts.application')
@section('content')
<div class="container col-md-6">

View File

@ -1,11 +0,0 @@
<div class="container">
<footer class="footer py-3 my-4">
<ul class="nav justify-content-center pb-3 mb-3">
<li class="nav-item"><a href="{{ route('grades.index') }}" class="nav-link px-2 text-muted">Классы</a></li>
<li class="nav-item"><a href="{{ route('subjects.index') }}" class="nav-link px-2 text-muted">Предметы</a></li>
<li class="nav-item"><a href="{{ route('students.index') }}" class="nav-link px-2 text-muted">Студенты</a></li>
<li class="nav-item"><a href="{{ route('teachers.index') }}" class="nav-link px-2 text-muted">Учителя</a></li>
</ul>
<p class="text-center text-muted">&copy; 2024 EduDiary</p>
</footer>
</div>

View File

@ -6,5 +6,17 @@
<li class="nav-item"><a href="{{ route('students.index') }}" class="nav-link @if(request()->is('students*')) active @endif">Ученики</a></li>
<li class="nav-item"><a href="{{ route('teachers.index') }}" class="nav-link @if(request()->is('teachers*')) active @endif">Учителя</a></li>
</ul>
<div class="dropdown">
<button class="btn btn-link dropdown-toggle" type="button" id="dropdownMenuButton" data-bs-toggle="dropdown" aria-expanded="false">
</button>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="dropdownMenuButton">
<li><a class="dropdown-item" href="{{ route('profile.edit') }}">Профиль</a></li>
<li>
<form id="logout-form" action="{{ route('logout') }}" method="POST" style="display: none;">
@csrf
</form>
</li>
</ul>
</div>
</header>
</div>

View File

@ -1,31 +1,36 @@
<!doctype html>
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
<!-- CSRF Token -->
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>{{ config('app.name', 'Laravel') }}</title>
<title>{{ config('app.name', 'Internship') }}</title>
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.bunny.net">
<link href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet" />
<!-- Fonts -->
<link rel="dns-prefetch" href="//fonts.bunny.net">
<link href="https://fonts.bunny.net/css?family=Nunito" rel="stylesheet">
<!-- Scripts -->
@vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<body class="font-sans antialiased">
<div class="min-h-screen bg-gray-100">
@include('layouts.navigation')
<!-- Scripts -->
@vite(['resources/sass/app.scss', 'resources/js/app.js'])
</head>
<body>
<!-- Page Heading -->
@isset($header)
<header class="bg-white shadow">
<div class="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8">
{{ $header }}
</div>
</header>
@endisset
@include('includes.header')
<div>
@yield('content')
</div>
@include('includes.footer')
</body>
<!-- Page Content -->
<main>
{{ $slot }}
</main>
</div>
</body>
</html>

View File

@ -0,0 +1,28 @@
<!doctype html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- CSRF Token -->
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>{{ config('app.name', 'Internship') }}</title>
<!-- Fonts -->
<link rel="dns-prefetch" href="//fonts.bunny.net">
<link href="https://fonts.bunny.net/css?family=Nunito" rel="stylesheet">
<!-- Scripts -->
@vite(['resources/css/app.css', 'resources/sass/app.scss', 'resources/js/app.js'])
</head>
<body>
@include('layouts.navigation')
<div class="mt-4">
@yield('content')
</div>
</body>
</html>

View File

@ -0,0 +1,30 @@
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>{{ config('app.name', 'Laravel') }}</title>
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.bunny.net">
<link href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet" />
<!-- Scripts -->
@vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<body class="font-sans text-gray-900 antialiased">
<div class="min-h-screen flex flex-col sm:justify-center items-center pt-6 sm:pt-0 bg-gray-100">
<div>
<a href="/">
<x-application-logo class="w-20 h-20 fill-current text-gray-500" />
</a>
</div>
<div class="w-full sm:max-w-md mt-6 px-6 py-4 bg-white shadow-md overflow-hidden sm:rounded-lg">
{{ $slot }}
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,118 @@
<nav x-data="{ open: false }" class="bg-white border-b border-gray-100">
<!-- 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('grades.index') }}">
<x-application-logo class="block h-9 w-auto fill-current text-gray-800" />
</a>
</div>
<!-- Navigation Links -->
<div class="hidden space-x-8 sm:-my-px sm:ms-10 sm:flex">
<x-nav-link :href="route('grades.index')" :active="request()->routeIs('dashboard')">
{{ __('Классы') }}
</x-nav-link>
</div>
<div class="hidden space-x-8 sm:-my-px sm:ms-10 sm:flex">
<x-nav-link :href="route('subjects.index')" :active="request()->routeIs('dashboard')">
{{ __('Предметы') }}
</x-nav-link>
</div>
<div class="hidden space-x-8 sm:-my-px sm:ms-10 sm:flex">
<x-nav-link :href="route('students.index')" :active="request()->routeIs('dashboard')">
{{ __('Студенты') }}
</x-nav-link>
</div>
<div class="hidden space-x-8 sm:-my-px sm:ms-10 sm:flex">
<x-nav-link :href="route('teachers.index')" :active="request()->routeIs('dashboard')">
{{ __('Учителя') }}
</x-nav-link>
</div>
</div>
<!-- Settings Dropdown -->
<div class="hidden sm:flex sm:items-center sm:ms-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 bg-white hover:text-gray-700 focus:outline-none transition ease-in-out duration-150">
<div>{{ Auth::user()->name }}</div>
<div class="ms-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.edit')">
{{ __('Profile') }}
</x-dropdown-link>
<!-- Authentication -->
<form method="POST" action="{{ route('logout') }}">
@csrf
<x-dropdown-link :href="route('logout')"
onclick="event.preventDefault();
this.closest('form').submit();">
{{ __('Log Out') }}
</x-dropdown-link>
</form>
</x-slot>
</x-dropdown>
</div>
<!-- Hamburger -->
<div class="-me-2 flex items-center sm:hidden">
<button @click="open = ! open" class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:bg-gray-100 focus:text-gray-500 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')">
{{ __('Dashboard') }}
</x-responsive-nav-link>
</div>
<!-- Responsive Settings Options -->
<div class="pt-4 pb-1 border-t border-gray-200">
<div class="px-4">
<div class="font-medium text-base text-gray-800">{{ Auth::user()->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.edit')">
{{ __('Profile') }}
</x-responsive-nav-link>
<!-- Authentication -->
<form method="POST" action="{{ route('logout') }}">
@csrf
<x-responsive-nav-link :href="route('logout')"
onclick="event.preventDefault();
this.closest('form').submit();">
{{ __('Log Out') }}
</x-responsive-nav-link>
</form>
</div>
</div>
</div>
</nav>

View File

@ -0,0 +1,17 @@
<x-app-layout>
<x-slot name="header">
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
{{ __('Profile') }}
</h2>
</x-slot>
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8 space-y-6">
<div class="p-4 sm:p-8 bg-white shadow sm:rounded-lg">
<div class="max-w-xl">
@include('profile.partials.update-password-form')
</div>
</div>
</div>
</div>
</x-app-layout>

View File

@ -0,0 +1,48 @@
<section>
<header>
<h2 class="text-lg font-medium text-gray-900">
{{ __('Update Password') }}
</h2>
<p class="mt-1 text-sm text-gray-600">
{{ __('Ensure your account is using a long, random password to stay secure.') }}
</p>
</header>
<form method="post" action="{{ route('password.update') }}" class="mt-6 space-y-6">
@csrf
@method('put')
<div>
<x-input-label for="update_password_current_password" :value="__('Current Password')" />
<x-text-input id="update_password_current_password" name="current_password" type="password" class="mt-1 block w-full" autocomplete="current-password" />
<x-input-error :messages="$errors->updatePassword->get('current_password')" class="mt-2" />
</div>
<div>
<x-input-label for="update_password_password" :value="__('New Password')" />
<x-text-input id="update_password_password" name="password" type="password" class="mt-1 block w-full" autocomplete="new-password" />
<x-input-error :messages="$errors->updatePassword->get('password')" class="mt-2" />
</div>
<div>
<x-input-label for="update_password_password_confirmation" :value="__('Confirm Password')" />
<x-text-input id="update_password_password_confirmation" name="password_confirmation" type="password" class="mt-1 block w-full" autocomplete="new-password" />
<x-input-error :messages="$errors->updatePassword->get('password_confirmation')" class="mt-2" />
</div>
<div class="flex items-center gap-4">
<x-primary-button>{{ __('Save') }}</x-primary-button>
@if (session('status') === 'password-updated')
<p
x-data="{ show: true }"
x-show="show"
x-transition
x-init="setTimeout(() => show = false, 2000)"
class="text-sm text-gray-600"
>{{ __('Saved.') }}</p>
@endif
</div>
</form>
</section>

View File

@ -1,4 +1,4 @@
@extends('layouts.app')
@extends('layouts.application')
@section('content')
<div class="container col-md-4">

View File

@ -1,4 +1,4 @@
@extends('layouts.app')
@extends('layouts.application')
@section('content')
@include('students.form', ['route' => route('students.store'), 'method' => 'POST'])

View File

@ -1,4 +1,4 @@
@extends('layouts.app')
@extends('layouts.application')
@section('content')
@include('students.form', ['route' => route('students.update', $student), 'method' => 'PUT'])

View File

@ -1,4 +1,4 @@
@extends('layouts.app')
@extends('layouts.application')
@section('content')
<div class="container col-md-6">

View File

@ -1,4 +1,4 @@
@extends('layouts.app')
@extends('layouts.application')
@section('content')
<div class="container col-md-6">

View File

@ -1,4 +1,4 @@
@extends('layouts.app')
@extends('layouts.application')
@section('content')
@include('subject-teacher.form', ['route' => route('teachers.subjects.store', $teacher), 'method' => 'POST'])

View File

@ -1,4 +1,4 @@
@extends('layouts.app')
@extends('layouts.application')
@section('content')
@include('subject-teacher.form', [

View File

@ -1,4 +1,4 @@
@extends('layouts.app')
@extends('layouts.application')
@section('content')
<div class="container col-md-6">

View File

@ -1,4 +1,4 @@
@extends('layouts.app')
@extends('layouts.application')
@section('content')
@include('subjects.form', ['route' => route('subjects.store'), 'method' => 'POST'])

View File

@ -1,4 +1,4 @@
@extends('layouts.app')
@extends('layouts.application')
@section('content')
@include('subjects.form', ['route' => route('subjects.update', $subject), 'method' => 'PUT'])

View File

@ -1,4 +1,4 @@
@extends('layouts.app')
@extends('layouts.application')
@section('content')
<div class="container col-md-6">

View File

@ -1,4 +1,4 @@
@extends('layouts.app')
@extends('layouts.application')
@section('content')
<div class="container col-md-6">

View File

@ -1,4 +1,4 @@
@extends('layouts.app')
@extends('layouts.application')
@section('content')
@include('teachers.form', ['route' => route('teachers.store'), 'method' => 'POST'])

View File

@ -1,4 +1,4 @@
@extends('layouts.app')
@extends('layouts.application')
@section('content')
@include('teachers.form', ['route' => route('teachers.update', $teacher), 'method' => 'PUT'])

View File

@ -1,4 +1,4 @@
@extends('layouts.app')
@extends('layouts.application')
@section('content')
<div class="container col-md-6">

View File

@ -1,4 +1,4 @@
@extends('layouts.app')
@extends('layouts.application')
@section('content')
<div class="container col-md-6">

48
routes/auth.php Normal file
View File

@ -0,0 +1,48 @@
<?php
use App\Http\Controllers\Auth\AuthenticatedSessionController;
use App\Http\Controllers\Auth\ConfirmablePasswordController;
use App\Http\Controllers\Auth\EmailVerificationNotificationController;
use App\Http\Controllers\Auth\EmailVerificationPromptController;
use App\Http\Controllers\Auth\NewPasswordController;
use App\Http\Controllers\Auth\PasswordController;
use App\Http\Controllers\Auth\PasswordResetLinkController;
use App\Http\Controllers\Auth\RegisteredUserController;
use App\Http\Controllers\Auth\VerifyEmailController;
use Illuminate\Support\Facades\Route;
Route::middleware('guest')->group(function () {
Route::get('login', [AuthenticatedSessionController::class, 'create'])
->name('login');
Route::post('login', [AuthenticatedSessionController::class, 'store']);
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::put('password', [PasswordController::class, 'update'])->name('password.update');
Route::post('logout', [AuthenticatedSessionController::class, 'destroy'])
->name('logout');
});

View File

@ -1,5 +1,7 @@
<?php
use App\Http\Controllers\ProfileController;
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\GradeController;
use App\Http\Controllers\GradeSubjectController;
use App\Http\Controllers\GradeTeacherController;
@ -9,22 +11,34 @@ use App\Http\Controllers\StudentController;
use App\Http\Controllers\SubjectController;
use App\Http\Controllers\SubjectTeacherController;
use App\Http\Controllers\TeacherController;
use Illuminate\Support\Facades\Route;
Route::get('/', function () {
return redirect()->route('grades.index');
return redirect()->route('profile.edit');
});
Route::resources([
'grades' => GradeController::class,
'subjects' => SubjectController::class,
'students' => StudentController::class,
'teachers' => TeacherController::class,
'grades.lessons' => LessonController::class,
]);
Route::get('/dashboard', function () {
return view('dashboard');
})->middleware(['auth', 'verified'])->name('dashboard');
Route::resource('teachers.subjects', SubjectTeacherController::class)->except('index');
Route::resource('teachers.subjects.grades', GradeTeacherController::class)->except('index', 'show');
Route::resource('grades.subjects', GradeSubjectController::class)->except('index', 'show');
Route::get('lessons/{lesson}/scores', [ScoreController::class, 'show'])->name('lessons.scores.show');
Route::put('lessons/{lesson}/scores', [ScoreController::class, 'update'])->name('lessons.scores.update');
Route::middleware('auth')->group(function () {
Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit');
Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update');
Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy');
Route::resources([
'grades' => GradeController::class,
'subjects' => SubjectController::class,
'students' => StudentController::class,
'teachers' => TeacherController::class,
'grades.lessons' => LessonController::class,
]);
Route::resource('teachers.subjects', SubjectTeacherController::class)->except('index');
Route::resource('teachers.subjects.grades', GradeTeacherController::class)->except('index', 'show');
Route::resource('grades.subjects', GradeSubjectController::class)->except('index', 'show');
Route::get('lessons/{lesson}/scores', [ScoreController::class, 'show'])->name('lessons.scores.show');
Route::put('lessons/{lesson}/scores', [ScoreController::class, 'update'])->name('lessons.scores.update');
});
require __DIR__.'/auth.php';

21
tailwind.config.js Normal file
View File

@ -0,0 +1,21 @@
import defaultTheme from 'tailwindcss/defaultTheme';
import forms from '@tailwindcss/forms';
/** @type {import('tailwindcss').Config} */
export default {
content: [
'./vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php',
'./storage/framework/views/*.php',
'./resources/views/**/*.blade.php',
],
theme: {
extend: {
fontFamily: {
sans: ['Figtree', ...defaultTheme.fontFamily.sans],
},
},
},
plugins: [forms],
};

View File

@ -0,0 +1,54 @@
<?php
namespace Tests\Feature\Auth;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class AuthenticationTest extends TestCase
{
use RefreshDatabase;
public function test_login_screen_can_be_rendered(): void
{
$response = $this->get('/login');
$response->assertStatus(200);
}
public function test_users_can_authenticate_using_the_login_screen(): void
{
$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(): void
{
$user = User::factory()->create();
$this->post('/login', [
'email' => $user->email,
'password' => 'wrong-password',
]);
$this->assertGuest();
}
public function test_users_can_logout(): void
{
$user = User::factory()->create();
$response = $this->actingAs($user)->post('/logout');
$this->assertGuest();
$response->assertRedirect('/');
}
}

View File

@ -0,0 +1,58 @@
<?php
namespace Tests\Feature\Auth;
use App\Models\User;
use Illuminate\Auth\Events\Verified;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\URL;
use Tests\TestCase;
class EmailVerificationTest extends TestCase
{
use RefreshDatabase;
public function test_email_verification_screen_can_be_rendered(): void
{
$user = User::factory()->unverified()->create();
$response = $this->actingAs($user)->get('/verify-email');
$response->assertStatus(200);
}
public function test_email_can_be_verified(): void
{
$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(): void
{
$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());
}
}

View File

@ -0,0 +1,44 @@
<?php
namespace Tests\Feature\Auth;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class PasswordConfirmationTest extends TestCase
{
use RefreshDatabase;
public function test_confirm_password_screen_can_be_rendered(): void
{
$user = User::factory()->create();
$response = $this->actingAs($user)->get('/confirm-password');
$response->assertStatus(200);
}
public function test_password_can_be_confirmed(): void
{
$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(): void
{
$user = User::factory()->create();
$response = $this->actingAs($user)->post('/confirm-password', [
'password' => 'wrong-password',
]);
$response->assertSessionHasErrors();
}
}

View File

@ -0,0 +1,73 @@
<?php
namespace Tests\Feature\Auth;
use App\Models\User;
use Illuminate\Auth\Notifications\ResetPassword;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Notification;
use Tests\TestCase;
class PasswordResetTest extends TestCase
{
use RefreshDatabase;
public function test_reset_password_link_screen_can_be_rendered(): void
{
$response = $this->get('/forgot-password');
$response->assertStatus(200);
}
public function test_reset_password_link_can_be_requested(): void
{
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(): void
{
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(): void
{
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;
});
}
}

Some files were not shown because too many files have changed in this diff Show More