Compare commits

57 Commits

Author SHA1 Message Date
45fbdd2759 - fix edit link on notes list 2023-08-07 01:45:53 +02:00
7c2f888702 - update response message object 2023-08-07 01:41:04 +02:00
2f112db339 - improving the content of comments 2023-08-07 01:36:31 +02:00
a08c6c9529 - edit destroy methods 2023-08-07 01:36:19 +02:00
2d1d51f777 - fix empty states 2023-08-07 01:30:56 +02:00
fcf422678c - add notes 2023-08-07 01:30:38 +02:00
ffee3b257d - add error message for request 2023-08-05 01:28:17 +02:00
957e90aff6 - add empty states 2023-08-05 00:08:47 +02:00
252f5ee6b7 - add delete for messages 2023-08-04 16:45:15 +02:00
2a2869e2c6 - add send CV status 2023-08-04 16:25:09 +02:00
6a033d108c - wip 2023-08-03 22:37:57 +02:00
620217e1d5 - prepare messages 2023-08-03 18:32:27 +02:00
a7e93681f3 - wip 2023-08-03 12:26:42 +02:00
9d7da548e3 - fix notes 2023-08-01 23:57:09 +02:00
2349008131 - add notes 2023-08-01 23:54:03 +02:00
908b1e4bec - wip 2023-08-01 18:13:45 +02:00
54db2e8e2b -wip 2023-07-30 12:10:23 +02:00
127ebe79ae - fix phone number format 2023-07-30 11:42:07 +02:00
010f8cf278 - fix urls for images 2023-07-29 01:13:37 +02:00
a7ccb3100f - fix navigation style 2023-07-29 00:59:06 +02:00
753421a5a0 - restore nullable for position 2023-07-29 00:58:05 +02:00
7fd3be06ea - fix null returned category 2023-07-29 00:44:31 +02:00
1ccc934561 - add cv management 2023-07-29 00:34:32 +02:00
f32f13604f - wip 2023-07-28 16:50:46 +02:00
8974721c9c - add category management 2023-07-28 15:43:53 +02:00
d47c719d13 - add messages 2023-07-28 14:16:58 +02:00
af3aa905bd - add delete project page 2023-07-28 13:34:34 +02:00
d0b3f9094c - add fontawesome 2023-07-28 12:23:55 +02:00
d943e81da4 - wip 2023-07-28 12:18:13 +02:00
f5977c1b5d - wip dashboard 2023-07-28 00:47:36 +02:00
992326ebf0 - change url 2023-07-27 23:03:49 +02:00
021dfc85f9 - update login page 2023-07-27 20:49:26 +02:00
93bbf2296d - add package for inertia 2023-07-27 20:49:06 +02:00
0c7d2c9f24 - lang EN 2023-07-27 15:44:04 +02:00
a09a75b69e - add remove options for ip address 2023-07-27 14:03:21 +02:00
58003e5d18 - show elements as table 2023-07-27 14:02:54 +02:00
f500c8691f - add support for console 2023-07-27 14:01:55 +02:00
5b71fa9781 - update cv info model 2023-07-27 14:01:18 +02:00
7c58a0bc63 - update npm depends 2023-07-27 10:52:12 +02:00
e294dc07e0 - fix problem with informations about views 2023-07-27 10:47:24 +02:00
996b1b4faf - add position 2023-07-04 01:25:02 +02:00
99f0bafe93 - append more informations from api 2023-07-04 01:12:05 +02:00
ac90d45519 - add phone resource 2023-07-03 21:37:58 +02:00
95ae8c562d - fix problem with default css file 2023-07-03 19:32:16 +02:00
65f0a49cdb - fix work addresses 2023-07-03 19:31:46 +02:00
5839ef2a54 - fix problem with phone number attribute 2023-07-03 19:31:03 +02:00
e049990606 - block dynamic files 2023-07-03 19:30:08 +02:00
d35421a97e - add cv info 2023-06-16 14:23:34 +02:00
390c5b8087 - add CV management 2023-06-16 13:41:39 +02:00
9518d6a811 - fix docker files 2023-06-16 11:22:01 +02:00
08133d0b05 - wip 2023-06-16 00:47:37 +02:00
2cbcaee7f5 - upgrade depends 2023-06-16 00:19:02 +02:00
26a56e6e5b wip 2023-03-08 14:47:59 +01:00
e8c8932630 laravel framework upgrade from version 8 to 9 (#7)
* laravel framework upgrade from version 8 to 9
2023-03-08 13:57:22 +01:00
7e242d5aa2 Docker reorganisation (#6)
* - new file location for docker

* update of docker containers

* update readme
2023-03-07 23:45:45 +01:00
dependabot[bot]
36a355f604 Bump loader-utils from 1.4.0 to 1.4.2 (#5) 2022-11-24 21:27:21 +00:00
dependabot[bot]
1b9ade4e06 Bump terser from 4.8.0 to 4.8.1 (#4) 2022-08-25 21:02:28 +00:00
118 changed files with 7280 additions and 16535 deletions

View File

@@ -1,10 +1,19 @@
USER_UID=1000
USER_NAME=laravel
APP_NAME=KamilCraftAPI
APP_ENV=local
APP_KEY=
APP_DEBUG=true
APP_URL=http://localhost
DB_CONNECTION=sqlite
DB_CONNECTION=mysql
DB_HOST=kamilcraft-api_db
DB_PORT=3306
DB_DATABASE=kamilcraft-api
DB_USERNAME=kamilcraft-api
DB_PASSWORD=password
EXTERNAL_WEBSERVER_PORT=80
CURRENT_UID=1000
XDG_CONFIG_HOME=/tmp
VITE_CV_APP_URL=https://cv.kamilcraft.com
VITE_PORT=3001

6
.gitignore vendored
View File

@@ -1,8 +1,10 @@
/.idea
/.vscode
/node_modules
/public/hot
/public/storage
/storage/*.key
/vendor
/.composer
.env
.env.backup
.phpunit.result.cache
@@ -11,5 +13,3 @@ Homestead.json
Homestead.yaml
npm-debug.log
yarn-error.log
/.idea
/.vscode

View File

@@ -10,9 +10,9 @@ API for kamilcraft.com projects
### Optional
* PHP 8.0 or later
* Composer 2.3.x or later
* Nodejs 16.14.x or later
* PHP 8.1.x or later
* Composer 2.4.x or later
* Nodejs 18.14.x or later
## Preparation and installation
@@ -21,9 +21,9 @@ API for kamilcraft.com projects
cp .env.example .env
```
2) Build the image needed for Laravel
2) Build the image needed for Laravel and Node.js
```shell
docker-compose build
docker-compose build --no-cache --pull
```
3) Run the images prepared in ``docker-compose.yml``
@@ -31,11 +31,23 @@ API for kamilcraft.com projects
docker-compose up -d
```
4) Install the dependencies needed for Laravel and Nodejs. \
**The installer for Laravel generates the key and migrates the database.** \
**In the case of Nodejs, it generates page styles.**
4) Install the dependencies needed for Laravel and Nodejs
```shell
docker-compose exec laravel install
docker-compose exec -u "$(id -u):$(id -g)" php composer install
```
5) Go to ``http://localhost/dashboard`` in your browser.
```shell
docker-compose run --rm -u "$(id -u):$(id -g)" npm install
```
5) Key and data generation
```shell
docker-compose exec -u "$(id -u):$(id -g)" php php artisan key:generate
```
```shell
docker-compose exec -u "$(id -u):$(id -g)" php php artisan migrate:fresh --seed
```
```shell
docker-compose run --rm -u "$(id -u):$(id -g)" npm run dev
```
6) Go to ``http://localhost/dashboard`` in your browser.

View File

@@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
namespace App\Console\Commands;
use App\Models\CV;
use Illuminate\Console\Command;
class CVInfo extends Command
{
protected $signature = 'cv:info {id}';
protected $description = 'Show info about CV';
public function handle(): int
{
if (! ($cv = CV::find($id = $this->argument('id')))) {
$this->error('CV not found!');
return Command::FAILURE;
}
$this->line('ID: '. $cv->id);
$this->line('Token: '. $cv->token);
$this->line('Company: '. $cv->recipient);
$this->line('Phone: '. $cv->formattedPhoneNumber .', '. $cv->PhoneNumber);
$this->line('Locations: '. implode(' / ', $cv->locations));
$this->line('Actual views: '. $cv->info()->select('id')->get()->count());
$this->line('Registered views: '. $cv->views);
$this->line('Mission: '. (is_null($mission = $cv->mission) ? 'default' : $mission));
$this->line('RODO: '. (is_null($rodo = $cv->rodo) ? 'default' : $rodo));
$this->line('');
$this->line('Showed list:');
$listCVInfo = [];
foreach (($cvInfoList = $cv->info()->orderByDesc('id')->get(['id', 'ip', 'created_at'])) as $cvInfo) {
$listCVInfo[] = [
'id' => $cvInfo->id,
'ip' => $cvInfo->ip,
'created' => $cvInfo->created_at->format('d-m-Y H:i:s'),
];
}
if ($cvInfoList->count() === 0)
$this->warn('Empty!');
else
$this->table(
['Id', 'IP', 'Showed'],
$listCVInfo,
);
return Command::SUCCESS;
}
}

View File

@@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace App\Console\Commands;
use App\Models\CV;
use Illuminate\Console\Command;
use Illuminate\Support\Str;
use Symfony\Component\Console\Command\Command as CommandAlias;
class CreateCV extends Command
{
protected $signature = 'cv:create
{recipient : Company}
{email : E-mail address}
{phone : Phone number - with spaces}
{location?* : List of locations}
{--mission= : Description of mission}
{--rodo= : Description of rodo}
{--position= : Set position value}';
protected $description = 'Create CV';
public function handle(): int
{
$recipient = $this->argument('recipient');
$email = $this->argument('email');
$phone = $this->argument('phone');
$locations = $this->argument('location');
$mission = $this->option('mission');
$rodo = $this->option('rodo');
$position = $this->option('position');
CV::query()
->create([
'token' => Str::random(50),
'recipient' => $recipient,
'email' => $email,
'phone_number' => $phone,
'locations' => $locations,
'mission' => $mission,
'rodo' => $rodo,
'position' => $position,
]);
$this->info('Created!');
return CommandAlias::SUCCESS;
}
}

View File

@@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
namespace App\Console\Commands;
use App\Http\Resources\CVResource;
use App\Models\CV;
use Illuminate\Console\Command;
class ListCV extends Command
{
protected $signature = 'cv:list';
protected $description = 'List of CV';
public function handle(): int
{
$cvList = CV::all();
$cvListCollection = [];
/** @var CV $cv */
foreach ($cvList as $cv) {
$cvResource = (new CVResource($cv))->setAsConsole()->toArray();
$cvListCollection[] = [
'id' => $cvResource['id'],
'token' => $cvResource['token'],
'email' => $cvResource['email'],
'recipient' => $cvResource['recipient'],
'locations' => implode(' / ', $cvResource['locations']),
'phone' => $cvResource['phone']['formattedPhoneNumber'],
'views' => $cvResource['views'] . '/' . $cvResource['registeredViews'],
];
}
if (count($cvListCollection) > 0)
$this->table(
['Id', 'Token', 'E-mail', 'Company', 'Locations', 'Phone', 'Views'],
$cvListCollection
);
return Command::SUCCESS;
}
}

View File

@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Models\CVInfo;
class RemoveIPFromList extends Command
{
protected $signature = 'cv:rm-ip {ip : IP address on list.}';
protected $description = 'Remove IP address from show list.';
public function handle(): int
{
$ip = $this->argument('ip');
$cvInfo = CVInfo::query()->where('ip', $ip);
$cvInfo->delete();
$this->info('IP '. $ip .' deleted from database.');
return Command::SUCCESS;
}
}

View File

@@ -0,0 +1,80 @@
<?php
declare(strict_types=1);
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Models\CV;
class UpdateCV extends Command
{
protected $signature = 'cv:update
{id : ID CV element}
{--company= : Company name}
{--phone= : Phone number}
{--begin-location : Add begin}
{--add-location=* : Add locations}
{--remove-location=* : Remove lcoations}
{--mission= : Set new text value}
{--rodo= : Set new text value}
{--position= : Set position value}';
protected $description = 'Update CV element';
public function handle(): int
{
if (! ($id = $this->argument('id')) || $id <= 0) {
$this->error('Incorrect id');
return Command::FAILURE;
}
$cv = CV::find($id);
if ($company = $this->option('company')) {
$cv->recipient = $company;
}
if ($phone = $this->option('phone')) {
$cv->phone_number = $phone;
}
if (count($addLocations = $this->option('remove-location')) > 0) {
$locations = $cv->locations;
$locations = array_diff($locations, $addLocations);
$cv->locations = $locations;
}
if (count($addLocations = $this->option('add-location')) > 0) {
$locations = $cv->locations;
$clearLocations = [];
foreach ($addLocations as $location) {
if (in_array($location, $locations)) {
$this->warn('"'. $location .'" exists! This value was not added.');
$clearLocations[] = $location;
}
}
$addLocations = array_diff($addLocations, $clearLocations);
if ($this->option('begin-location'))
$locations = array_merge($addLocations, $locations);
else
$locations = array_merge($locations, $addLocations);
$cv->locations = $locations;
}
if ($mission = $this->option('mission')) {
$cv->mission = $mission === 'null' ? null : $mission;
}
if ($rodo = $this->option('rodo')) {
$cv->rodo = $rodo === 'null' ? null : $rodo;
}
if ($position = $this->option('position')) {
$cv->position = $position === 'null' ? null : $position;
}
$cv->save();
$this->info('Updated!');
return Command::SUCCESS;
}
}

View File

@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Resources\CVResource;
use App\Models\CV;
use Illuminate\Http\Resources\Json\JsonResource;
class CVController extends Controller
{
public function show(CV $cv): JsonResource
{
$cv->info()
->create([
'ip' => $_SERVER['REMOTE_ADDR'],
]);
$cv->update(['views' => $cv->views+=1]);
return new CVResource($cv);
}
}

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Requests\MessageRequest;
use App\Models\Message;
use Illuminate\Http\JsonResponse;
class MessageController extends Controller
{
public function store(MessageRequest $request): JsonResponse
{
$data = $request->toArray();
Message::query()->create([
'message' => $data['message'],
'email' => $data['email'],
'sender' => $data['sender'],
]);
return response()->json([
'message' => 'Dziękuję za wiadomość! Odpowiem możliwie najszybciej.'
]);
}
}

View File

@@ -8,8 +8,7 @@ use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\View\View;
use Inertia\Response as InertiaResponse;
class LoginController extends Controller
{
@@ -43,12 +42,12 @@ class LoginController extends Controller
return redirect()->route('admin.auth.login');
}
public function login(): View|RedirectResponse
public function login(): InertiaResponse|RedirectResponse
{
if (Auth::check())
return redirect()->route('admin.home');
return view('auth.login');
return inertia('Login');
}
}

View File

@@ -6,7 +6,7 @@ use App\Http\Controllers\Controller;
use App\Repository\Interfaces\CategoryRepository;
use App\Repository\Interfaces\ProjectRepository;
use Illuminate\Http\Request;
use Illuminate\View\View;
use Inertia\Response as InertiaResponse;
class AdminPanelController extends Controller
{
@@ -16,14 +16,15 @@ class AdminPanelController extends Controller
private ProjectRepository $projectRepository
) {
$this->categoryRepository->auth = true;
$this->projectRepository->auth = true;
}
public function __invoke(Request $request): View
public function __invoke(Request $request): InertiaResponse
{
$categories = $this->categoryRepository->all();
$projects = $this->projectRepository->all();
return view('dashboard.home', compact('categories', 'projects'));
return inertia('Dashboard/Index', compact('categories', 'projects'));
}
}

View File

@@ -0,0 +1,122 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Dashboard;
use App\Http\Controllers\Controller;
use App\Http\Requests\CVRequest;
use App\Http\Resources\CVInfoCollection;
use App\Http\Resources\FullCVCollection;
use App\Http\Resources\FullCVResource;
use App\Models\CV;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use Inertia\Response as InertiaResponse;
class CVController extends Controller
{
public function index(Request $request): InertiaResponse
{
return inertia('CV/Index', [
'cvs' => new FullCVCollection(CV::all()),
]);
}
public function show(CV $cv): InertiaResponse
{
return inertia('CV/Show', [
'cv' => new FullCVResource($cv),
'cvInfo' => new CVInfoCollection($cv->info()->orderByDesc('id')->get()),
]);
}
public function create(): InertiaResponse
{
return inertia('CV/Create');
}
public function store(CVRequest $request): RedirectResponse
{
CV::query()
->create([
'token' => Str::random(50),
'recipient' => $request->get('recipient'),
'email' => $request->get('email'),
'phone_number' => $request->get('phone_number'),
'locations' => ($locations = $request->get('locations')) === [''] ? [] : $locations,
'mission' => ($mission = $request->get('mission')) === [''] ? [] : $mission,
'rodo' => ($rodo =$request->get('rodo')) === '' ? null : $rodo,
'position' => $request->get('position'),
'notes' => $request->get('notes'),
]);
return redirect()
->route('admin.cv.store')
->with('success', 'Utworzono nowe CV dla firmy ' . $request->get('recipient'));
}
public function updateSendStatus(CV $cv): RedirectResponse
{
$cv->update([
'sended' => true,
'sended_timestamp' => now()
]);
return redirect()
->route('admin.cv.show', ['cv' => $cv])
->with('success', 'Status wysłania ustawiono jako "wysłano".');
}
public function edit(CV $cv): InertiaResponse
{
return inertia('CV/Edit', [
'cv' => new FullCVResource($cv),
]);
}
public function update(CVRequest $request, CV $cv): RedirectResponse
{
$toUpdate = [
'recipient' => $request->get('recipient'),
'email' => $request->get('email'),
'phone_number' => $request->get('phone_number'),
'locations' => ($locations = $request->get('locations')) === [''] ? [] : $locations,
'mission' => ($mission = $request->get('mission')) === [''] ? [] : $mission,
'rodo' => ($rodo =$request->get('rodo')) === '' ? null : $rodo,
'position' => $request->get('position'),
'notes' => $request->get('notes'),
];
if ($cv->sended && ! $request->boolean('sended')) {
$toUpdate = array_merge($toUpdate, [
'sended' => false,
'sended_timestamp' => null,
]);
} else if (! $cv->sended && $request->boolean('sended')) {
$toUpdate = array_merge($toUpdate, [
'sended' => true,
'sended_timestamp' => now(),
]);
}
$cv->update($toUpdate);
return redirect()
->back()
->with('success', 'Zaktualizowano CV dla firmy ' . $request->get('recipient'));
}
public function delete(CV $cv): InertiaResponse
{
return inertia('CV/ConfirmDelete', compact('cv'));
}
public function destroy(CV $cv): RedirectResponse
{
$name = $cv->recipient;
$cv->delete();
return redirect()
->route('admin.cv.index')
->with('info', 'Usunięto CV dla firmy '. $name .'.');
}
}

View File

@@ -9,6 +9,7 @@ use App\Models\Category;
use App\Repository\Interfaces\CategoryRepository;
use Illuminate\Http\RedirectResponse;
use Illuminate\View\View;
use Inertia\Response as InertiaResponse;
class CategoryController
{
@@ -21,35 +22,35 @@ class CategoryController
{
$validate = $request->validated();
if ($this->categoryRepository->update($category, $validate)) {
return back()->with('message', 'Zaktualizowano kategorię!');
return back()
->with('success', 'Zaktualizowano kategorię!');
}
return back()->withError(['message_error', 'Wystąpił błąd podczas aktualizacji!']);
return back()
->with(['error', 'Wystąpił błąd podczas aktualizacji!']);
}
public function store(CategoryRequest $request)
{
$validate = $request->validated();
if ($category = $this->categoryRepository->create($validate)) {
return redirect()->route('admin.category.update', ['category' => $category])->with('message', 'Utworzono kategorię!');
}
return back()->withError(['message_error', 'Wystąpił błąd podczas tworzenia!']);
$category = $this->categoryRepository->create($request->validated());
return redirect()
->route('admin.category.update', compact('category'))
->with('message', 'Utworzono kategorię!');
}
public function create(): View
public function create(): InertiaResponse
{
return view('dashboard.categories.create');
return inertia('Categories/Create');
}
public function edit(Category $category): View
public function edit(Category $category): InertiaResponse
{
return view('dashboard.categories.edit', compact('category'));
return inertia('Categories/Edit', compact('category'));
}
public function delete(Category $category): View
public function delete(Category $category): InertiaResponse
{
return view('dashboard.categories.delete', compact('category'));
return inertia('Categories/ConfirmDelete', compact('category'));
}
public function destroy(Category $category): RedirectResponse
@@ -57,7 +58,9 @@ class CategoryController
$name = $category->name;
$category->delete();
return redirect()->route('admin.home')->with('message', 'Usunięto kategorię "'. $name .'"');
return redirect()
->route('admin.home')
->with('info', 'Usunięto kategorię "'. $name .'"');
}
}

View File

@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Dashboard;
use App\Http\Controllers\Controller;
use App\Http\Resources\MessageCollection;
use App\Http\Resources\MessageResource;
use App\Models\Message;
use Illuminate\Http\RedirectResponse;
use Inertia\Response as InertiaResponse;
class MessageController extends Controller
{
public function index() : InertiaResponse {
return inertia('Messages/Index', [
'messages' => new MessageCollection(Message::query()->orderByDesc('id')->get()),
]);
}
public function show(Message $message) : InertiaResponse
{
return inertia('Messages/Show', [
'message' => new MessageResource($message),
]);
}
public function delete(Message $message) : InertiaResponse
{
return inertia('Messages/ConfirmDelete', [
'message' => new MessageResource($message),
]);
}
public function destroy(Message $message) : RedirectResponse
{
$sender = $message->sender;
$message->delete();
return redirect()
->route('admin.message.index')
->with('info', 'Wiadomość od '. $sender .' została usunięta.');
}
}

View File

@@ -0,0 +1,84 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Dashboard;
use App\Http\Controllers\Controller;
use App\Http\Requests\NoteRequest;
use App\Http\Resources\NoteCollection;
use App\Http\Resources\NoteResource;
use App\Models\Note;
use Illuminate\Http\RedirectResponse;
use Inertia\Response as InertiaResponse;
class NoteController extends Controller
{
public function index(): InertiaResponse
{
return inertia(
'Notes/Index',
[
'notes' => new NoteCollection(
Note::query()
->orderByDesc('id')
->get()
),
]
);
}
public function create(): InertiaResponse
{
return inertia('Notes/Create');
}
public function store(NoteRequest $request): RedirectResponse
{
$note = Note::query()->create([
'title' => $request->get('title'),
'note' => $request->get('note'),
]);
return redirect()
->route('admin.note.show', compact('note'))
->with('success', 'Utworzono nową notatkę.');
}
public function show(Note $note): InertiaResponse
{
return inertia('Notes/Show', ['note' => new NoteResource($note)]);
}
public function edit(Note $note): InertiaResponse
{
return inertia('Notes/Edit', ['note' => new NoteResource($note)]);
}
public function update(NoteRequest $request, Note $note): RedirectResponse
{
$note->update([
'title' => $request->get('title'),
'note' => $request->get('note'),
]);
return redirect()
->route('admin.note.show', compact('note'))
->with('success', 'Notatka ' . $request->get('title') . ' została zaktualizowana.');
}
public function delete(Note $note): InertiaResponse
{
return inertia('Notes/ConfirmDelete', ['note' => new NoteResource($note)]);
}
public function destroy(Note $note): RedirectResponse
{
$title = $note->title;
$note->delete();
return redirect()
->route('admin.note.index')
->with('info', 'Notatka ' . $title . ' została usunięta.');
}
}

View File

@@ -1,13 +1,14 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Dashboard;
use App\Http\Requests\ProjectRequest;
use App\Models\Project;
use App\Repository\Interfaces\ProjectRepository;
use Carbon\Carbon;
use Illuminate\Http\RedirectResponse;
use Illuminate\View\View;
use Inertia\Response as InertiaResponse;
class ProjectController
{
@@ -18,24 +19,25 @@ class ProjectController
$this->projectRepository->auth = true;
}
public function edit(Project $project): View
public function edit(Project $project): InertiaResponse
{
return view('dashboard.projects.edit', compact('project'));
return inertia('Projects/Edit', compact('project'));
}
public function update(ProjectRequest $request, Project $project): RedirectResponse
{
$validated = $request->validated();
if ($this->projectRepository->update($project, $validated)) {
return back()->with('message', 'Zaktualizowano projekt!');
if ($this->projectRepository->update($project, $request->validated())) {
return back()
->with('success', 'Zaktualizowano projekt!');
}
return back()->withError(['message_error', 'Wystąpił błąd podczas aktualizacji!']);
return back()
->with(['error', 'Wystąpił błąd podczas aktualizacji!']);
}
public function create(): View
public function create(): InertiaResponse
{
return view('dashboard.projects.create');
return inertia('Projects/Create');
}
public function store(ProjectRequest $request): RedirectResponse
@@ -44,22 +46,24 @@ class ProjectController
if ($project = $this->projectRepository->create($validated)) {
return redirect()
->route('admin.project.update', compact('project'))
->with('message', 'Utworzono projekt!');
->with('success', 'Utworzono projekt!');
}
return back()->withError(['message_error', 'Wystąpił błąd podczas tworzenia!']);
return back()->withError(['error', 'Wystąpił błąd podczas tworzenia!']);
}
public function delete(Project $project): View
public function delete(Project $project): InertiaResponse
{
return view('dashboard.projects.delete', compact('project'));
return inertia('Projects/ConfirmDelete', compact('project'));
}
public function destroy(Project $project): RedirectResponse
{
$title = $project->title;
$project->delete();
return redirect()->route('admin.home')->with('message', 'Usunięto projekt "'. $title .'"');
return redirect()
->route('admin.home')
->with('info', 'Usunięto projekt "'. $title .'"');
}
}

View File

@@ -12,12 +12,12 @@ class Kernel extends HttpKernel
protected $middleware = [
\App\Http\Middleware\TrustProxies::class,
\Fruitcake\Cors\HandleCors::class,
\Illuminate\Http\Middleware\HandleCors::class,
\Illuminate\Foundation\Http\Middleware\PreventRequestsDuringMaintenance::class,
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
\App\Http\Middleware\TrimStrings::class,
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
Core::class
Core::class,
];
protected $middlewareGroups = [
@@ -28,6 +28,7 @@ class Kernel extends HttpKernel
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\Illuminate\Foundation\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
\App\Http\Middleware\HandleInertiaRequests::class,
],
'api' => [
@@ -39,6 +40,7 @@ class Kernel extends HttpKernel
protected $routeMiddleware = [
'auth' => \App\Http\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class,
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
'can' => \Illuminate\Auth\Middleware\Authorize::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,

View File

@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Inertia\Middleware;
class HandleInertiaRequests extends Middleware
{
public function share(Request $request): array
{
return array_merge(parent::share($request), [
'responseMessages' => $this->getFlashData($request),
]);
}
protected function getFlashData(Request $request): Closure
{
return fn(): array => [
'success' => $request->session()->get('success'),
'error' => $request->session()->get('error'),
'info' => $request->session()->get('info'),
];
}
}

View File

@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Http\Middleware;
use Illuminate\Http\Middleware\TrustProxies as Middleware;

View File

@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class CVRequest extends FormRequest
{
public function rules(): array
{
return [
'recipient' => 'required|string',
'email' => 'required|email',
'phone_number' => 'required|regex:/^([0-9\s\+]*)$/i|min:12|max:15',
'locations' => 'required|array',
'mission' => 'nullable|string',
'rodo' => 'nullable|string',
'position' => 'nullable|string',
'notes' => 'nullable|string',
'sended' => 'nullable|boolean',
];
}
protected function prepareForValidation(): void
{
$this->merge([
'sended' => $this->toBoolean($this->sended),
]);
}
private function toBoolean($booleable): bool
{
return filter_var($booleable, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
}
}

View File

@@ -1,34 +1,34 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class CategoryRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
public function rules(): array
{
return [
'name' => 'required|string|min:3|max:25',
'slug' => 'required|string|min:3|max:25',
'priority' => 'required|numeric|min:0|max:10',
'default' => 'nullable|in:yes,1,true,on',
'visible' => 'nullable|in:yes,1,true,on'
'default' => 'nullable|boolean',
'visible' => 'required|boolean',
];
}
protected function prepareForValidation(): void
{
$this->merge([
'default' => $this->toBoolean($this->default),
'visible' => $this->toBoolean($this->visible),
]);
}
private function toBoolean($booleable): bool
{
return filter_var($booleable, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
}
}

View File

@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class MessageRequest extends FormRequest
{
public function rules(): array
{
return [
'message' => 'required|string|min:3|max:500',
'sender' => 'required|string|min:3|max:50',
'email' => 'required|email|max:250',
];
}
public function messages(): array
{
return [
'message.required' => 'Pole wiadomości jest wymagane.',
'sender.required' => 'Pole nadawcy jest wymagane.',
'email.required' => 'Pole e-mail jest wymagane.',
'message.min' => 'Pole wiadomości wymaga 3 znaki.',
'sender.min' => 'Pole nadawcy wymaga 3 znaki.',
'message.max' => 'Pole wiadomości może mieć maksymalnie 50 znaków.',
'sender.max' => 'Pole nadawcy może mieć maksymalnie 500 znaków.',
'email.email' => 'Pole musi być e-mailem.',
'email.max' => 'Pole e-mail może mieć maksymalnie 250 znaków.',
];
}
}

View File

@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class NoteRequest extends FormRequest
{
public function rules(): array
{
return [
'title' => 'required|string|min:3|max:250',
'note' => 'required|string|min:3|max:1000',
];
}
}

View File

@@ -1,27 +1,14 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class ProjectRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
public function rules(): array
{
return [
'title' => 'required|string|min:3|max:255',
@@ -36,8 +23,20 @@ class ProjectRequest extends FormRequest
'project_url' => 'nullable|string',
'project_version' => 'nullable|string',
'description' => 'nullable|string',
'visible' => 'nullable|in:yes,1,true,on'
'description' => 'required|string|min:3',
'visible' => 'required|boolean'
];
}
protected function prepareForValidation(): void
{
$this->merge([
'visible' => $this->toBoolean($this->visible),
]);
}
private function toBoolean($booleable): bool
{
return filter_var($booleable, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
}
}

View File

@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\ResourceCollection;
class CVCollection extends ResourceCollection
{
public function toArray($request): array
{
return [
'data' => $this->collection,
];
}
}

View File

@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\ResourceCollection;
class CVInfoCollection extends ResourceCollection
{
public function toArray($request): array
{
return [
'data' => $this->collection,
];
}
}

View File

@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class CVInfoResource extends JsonResource
{
public static $wrap = null;
public function toArray($request): array
{
return [
'id' => $this->id,
'ip' => $this->ip,
'created_at' => $this->created_at->format('d-m-Y H:i:s'),
];
}
}

View File

@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class CVResource extends JsonResource
{
public static $wrap = null;
private bool $isConsole = false;
public function toArray($request = null): array
{
return [
'id' => $this->when($this->isConsole, fn (): int => $this->id),
'token' => $this->token,
'email' => $this->email,
'recipient' => $this->when($this->isConsole, fn (): string => $this->recipient),
'phone' => new PhoneResource($this->resource),
'locations' => $this->locations,
'views' => $this->when(
$this->isConsole,
fn (): int => $this->resource->info()->select('id')->get()->count()
),
'registeredViews' => $this->when($this->isConsole, fn (): int => $this->views),
'mission' => $this->when(
!is_null($this->mission),
fn (): array => explode(PHP_EOL, $this->mission, 5)
),
'rodo' => $this->when(
!is_null($this->rodo),
$this->rodo
),
'position' => $this->when(
!is_null($this->position),
$this->position
),
];
}
public function setAsConsole(): self
{
$this->isConsole = true;
return $this;
}
}

View File

@@ -1,17 +1,13 @@
<?php
declare(strict_types=1);
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\ResourceCollection;
class CategoryCollection extends ResourceCollection
{
/**
* Transform the resource collection into an array.
*
* @param \Illuminate\Http\Request $request
* @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable
*/
public function toArray($request)
{
return CategoryResource::collection($this->collection);

View File

@@ -1,21 +1,16 @@
<?php
declare(strict_types=1);
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class CategoryResource extends JsonResource
{
public static $wrap = null;
/**
* Transform the resource into an array.
*
* @param \Illuminate\Http\Request $request
* @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable
*/
public function toArray($request)
public function toArray($request): array
{
return [
'id' => $this->id,

View File

@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Http\Resources\Json\ResourceCollection;
class FullCVCollection extends ResourceCollection
{
public function toArray($request): array|JsonResource
{
return FullCVResource::collection($this->collection);
}
}

View File

@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class FullCVResource extends JsonResource
{
public static $wrap = null;
public function toArray($request = null): array
{
return [
'id' => $this->id,
'token' => $this->token,
'email' => $this->email,
'recipient' => $this->recipient,
'phone' => new PhoneResource($this->resource),
'locations' => $this->locations,
'views' => $this->resource->info()->select('id')->get()->count(),
'registeredViews' => $this->views,
'sended' => [
'status' => $this->sended,
'datetime' => $this->sended_timestamp?->format('d-m-Y H:i:s'),
],
'position' => $this->position,
'mission' => explode(PHP_EOL, $this->mission ?? '', 5),
'rodo' => $this->rodo,
'notes' => $this->notes,
'created' => $this->created_at->format('d-m-Y H:i:s'),
'updated' => $this->updated_at->format('d-m-Y H:i:s'),
];
}
}

View File

@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\ResourceCollection;
class MessageCollection extends ResourceCollection
{
public function toArray($request): array
{
return [
'data' => $this->collection,
];
}
}

View File

@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class MessageResource extends JsonResource
{
public static $wrap = null;
public function toArray($request): array
{
return [
'id' => $this->id,
'sender' => $this->sender,
'email' => $this->email,
'message' => $this->message,
];
}
}

View File

@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\ResourceCollection;
class NoteCollection extends ResourceCollection
{
public function toArray($request): array
{
return [
'data' => $this->collection,
];
}
}

View File

@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace App\Http\Resources;
use App\Models\Note;
use Illuminate\Http\Resources\Json\JsonResource;
/**
* @property Note $resource
*/
class NoteResource extends JsonResource
{
public static $wrap = null;
public function toArray($request): array
{
return [
'id' => $this->resource->id,
'title' => $this->resource->title,
'note' => $this->resource->note,
];
}
}

View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class PhoneResource extends JsonResource
{
public static $wrap = null;
public function toArray($request): array
{
return [
'number' => $this->phone_number,
'formattedNumber' => $this->formattedPhoneNumber,
];
}
}

View File

@@ -1,18 +1,15 @@
<?php
declare(strict_types=1);
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Http\Resources\Json\ResourceCollection;
class ProjectCollection extends ResourceCollection
{
/**
* Transform the resource collection into an array.
*
* @param \Illuminate\Http\Request $request
* @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable
*/
public function toArray($request)
public function toArray($request): JsonResource
{
return ProjectResource::collection($this->collection);
}

View File

@@ -1,21 +1,16 @@
<?php
declare(strict_types=1);
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class ProjectResource extends JsonResource
{
public static $wrap = null;
/**
* Transform the resource into an array.
*
* @param \Illuminate\Http\Request $request
* @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable
*/
public function toArray($request)
public function toArray($request): array
{
return [
'id' => $this->id,
@@ -30,5 +25,4 @@ class ProjectResource extends JsonResource
'description' => $this->description,
];
}
}

69
app/Models/CV.php Normal file
View File

@@ -0,0 +1,69 @@
<?php
declare(strict_types=1);
namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
/**
* @property int $id
* @property string $token
* @property string $recipient
* @property string $email
* @property string|null $phoneNumber
* @property array $locations
* @property string|null $mission
* @property string|null $rodo
* @property string|null $position
* @property string|null $notes
* @property int $views
* @property bool $sended
*/
class CV extends Model
{
use HasFactory;
protected $table = 'cvs';
protected $guarded = [];
protected $casts = [
'locations' => 'array',
'views' => 'integer',
'sended' => 'boolean',
'sended_timestamp' => 'datetime',
];
protected function phoneNumber(): Attribute
{
return Attribute::make(
get: fn (mixed $value): string => str_replace(' ', '', $value ?? ''),
set: fn (mixed $value): string => str_replace(' ', '', $value ?? ''),
);
}
protected function formattedPhoneNumber(): Attribute
{
return Attribute::make(
get: function (mixed $value, array $attributes): ?string {
$number = str_replace(' ', '', $attributes['phone_number'] ?? '');
for ($i = 3; $i < 12; $i+=4) {
$number = substr_replace($number, ' ', $i, 0);
}
return $number;
},
);
}
public function info(): HasMany
{
return $this->hasMany(CVInfo::class, 'cv_id');
}
public function getRouteKeyName(): string
{
return 'token';
}
}

28
app/Models/CVInfo.php Normal file
View File

@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
/**
* @property int $id
* @property int $cv_id
* @property int $ip
*/
class CVInfo extends Model
{
use HasFactory;
protected $table = 'cv_infos';
protected $guarded = [];
public function cv(): BelongsTo
{
return $this->belongsTo(CV::class, ownerKey: 'cv_id');
}
}

23
app/Models/Message.php Normal file
View File

@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
/**
* @property int $id
* @property string $message
* @property string $email
* @property string $sender
*/
class Message extends Model
{
use HasFactory,
SoftDeletes;
protected $guarded = [];
}

20
app/Models/Note.php Normal file
View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
/**
* @property int $id
* @property string $title
* @property string $note
*/
class Note extends Model
{
use HasFactory;
protected $guarded = [];
}

View File

@@ -34,7 +34,7 @@ class RouteServiceProvider extends ServiceProvider
protected function configureRateLimiting()
{
RateLimiter::for('api', function (Request $request) {
return Limit::perMinute(60)->by(optional($request->user())->id ?: $request->ip());
return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
});
}

View File

@@ -48,7 +48,7 @@ class CategoryRepository implements CategoryRepositoryInterface
public function update(Category $category, array $data = []): bool
{
$data = $this->parseToArray($data);
if (!$category->default && isset($data['default']) && $data['default'] === true)
if (!$category->default && $data['default'] === true)
$this->unsetDefault();
return $category
@@ -58,7 +58,7 @@ class CategoryRepository implements CategoryRepositoryInterface
public function create(array $data = []): Category
{
$data = $this->parseToArray($data);
if (isset($data['default']) && $data['default'] === true)
if ($data['default'] === true)
$this->unsetDefault();
return $this->category
@@ -85,18 +85,12 @@ class CategoryRepository implements CategoryRepositoryInterface
if (isset($data['priority']) && !is_integer($data['priority']))
$toSave['priority'] = (int)$data['priority'];
if (
isset($data['default']) &&
in_array($data['default'], ['yes', 'on', 1, true])
) $toSave['default'] = true;
else $toSave['default'] = false;
$toSave['default'] = $data['default'];
if (
(isset($toSave['default']) && $toSave['default'] === true) ||
(isset($data['visible']) &&
in_array($data['visible'], ['yes', 'on', 1, true]))
) $toSave['visible'] = true;
else $toSave['visible'] = false;
if ($toSave['default'] === true)
$toSave['visible'] = true;
else
$toSave['visible'] = $data['visible'];
return $toSave;
}

View File

@@ -8,7 +8,6 @@ use App\Http\Resources\ProjectCollection;
use App\Http\Resources\ProjectResource;
use App\Models\Project;
use App\Repository\Interfaces\ProjectRepository as ProjectRepositoryInterface;
use Illuminate\Database\Query\Builder;
use Illuminate\Support\Collection;
class ProjectRepository implements ProjectRepositoryInterface
@@ -104,11 +103,7 @@ class ProjectRepository implements ProjectRepositoryInterface
else
$toSave['update_date'] = null;
if (
isset($data['visible']) &&
in_array($data['visible'], ['yes', 'on', 1, true])
) $toSave['visible'] = true;
else $toSave['visible'] = false;
$toSave['visible'] = $data['visible'];
return $toSave;
}

View File

@@ -5,19 +5,21 @@
"keywords": ["kamilcraft", "api"],
"license": "MIT",
"require": {
"php": "^8.0",
"fruitcake/laravel-cors": "^2.0",
"guzzlehttp/guzzle": "^7.0.1",
"laravel/framework": "^8.75",
"laravel/tinker": "^2.5"
"php": "^8.1",
"guzzlehttp/guzzle": "^7.2",
"inertiajs/inertia-laravel": "^0.6.9",
"laravel/framework": "^9.19",
"laravel/tinker": "^2.7",
"spatie/server-side-rendering": "^0.3.2"
},
"require-dev": {
"barryvdh/laravel-debugbar": "^3.6",
"facade/ignition": "^2.5",
"fakerphp/faker": "^1.9.1",
"laravel/pint": "^1.0",
"mockery/mockery": "^1.4.4",
"nunomaduro/collision": "^5.10",
"phpunit/phpunit": "^9.5.10"
"nunomaduro/collision": "^6.1",
"phpunit/phpunit": "^9.5.10",
"spatie/laravel-ignition": "^1.0"
},
"autoload": {
"psr-4": {
@@ -54,7 +56,10 @@
"config": {
"optimize-autoloader": true,
"preferred-install": "dist",
"sort-packages": true
"sort-packages": true,
"allow-plugins": {
"pestphp/pest-plugin": true
}
},
"minimum-stability": "dev",
"prefer-stable": true

3545
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,6 +2,8 @@
declare(strict_types=1);
use Illuminate\Support\Facades\Facade;
return [
'name' => env('APP_NAME', 'KamilCraft API'),
'env' => env('APP_ENV', 'production'),
@@ -14,7 +16,9 @@ return [
'faker_locale' => 'en_US',
'key' => env('APP_KEY'),
'cipher' => 'AES-256-CBC',
/* Laravel Framework Service Providers */
'maintenance' => [
'driver' => 'file',
],
'providers' => [
Illuminate\Auth\AuthServiceProvider::class,
Illuminate\Bus\BusServiceProvider::class,
@@ -35,55 +39,13 @@ return [
Illuminate\Translation\TranslationServiceProvider::class,
Illuminate\Validation\ValidationServiceProvider::class,
Illuminate\View\ViewServiceProvider::class,
/* Package Service Providers */
/* Application Service Providers */
App\Providers\AppServiceProvider::class,
App\Providers\AuthServiceProvider::class,
App\Providers\RouteServiceProvider::class,
/* Custom */
App\Providers\CategoryServiceProvider::class,
App\Providers\ProjectServiceProvider::class,
],
'aliases' => [
'App' => Illuminate\Support\Facades\App::class,
'Arr' => Illuminate\Support\Arr::class,
'Artisan' => Illuminate\Support\Facades\Artisan::class,
'Auth' => Illuminate\Support\Facades\Auth::class,
'Blade' => Illuminate\Support\Facades\Blade::class,
'Bus' => Illuminate\Support\Facades\Bus::class,
'Cache' => Illuminate\Support\Facades\Cache::class,
'Config' => Illuminate\Support\Facades\Config::class,
'Cookie' => Illuminate\Support\Facades\Cookie::class,
'Crypt' => Illuminate\Support\Facades\Crypt::class,
'Date' => Illuminate\Support\Facades\Date::class,
'DB' => Illuminate\Support\Facades\DB::class,
'Eloquent' => Illuminate\Database\Eloquent\Model::class,
'Event' => Illuminate\Support\Facades\Event::class,
'File' => Illuminate\Support\Facades\File::class,
'Gate' => Illuminate\Support\Facades\Gate::class,
'Hash' => Illuminate\Support\Facades\Hash::class,
'Http' => Illuminate\Support\Facades\Http::class,
'Js' => Illuminate\Support\Js::class,
'Lang' => Illuminate\Support\Facades\Lang::class,
'Log' => Illuminate\Support\Facades\Log::class,
'Notification' => Illuminate\Support\Facades\Notification::class,
'Password' => Illuminate\Support\Facades\Password::class,
'Queue' => Illuminate\Support\Facades\Queue::class,
'RateLimiter' => Illuminate\Support\Facades\RateLimiter::class,
'Redirect' => Illuminate\Support\Facades\Redirect::class,
'Request' => Illuminate\Support\Facades\Request::class,
'Response' => Illuminate\Support\Facades\Response::class,
'Route' => Illuminate\Support\Facades\Route::class,
'Schema' => Illuminate\Support\Facades\Schema::class,
'Session' => Illuminate\Support\Facades\Session::class,
'Storage' => Illuminate\Support\Facades\Storage::class,
'Str' => Illuminate\Support\Str::class,
'URL' => Illuminate\Support\Facades\URL::class,
'Validator' => Illuminate\Support\Facades\Validator::class,
'View' => Illuminate\Support\Facades\View::class,
],
'aliases' => Facade::defaultAliases()->merge([])->toArray()
];

View File

@@ -43,6 +43,5 @@ return [
],
],
],
'prefix' => env('CACHE_PREFIX', Str::slug(env('APP_NAME', 'KamilCraftAPI'), '_').'_cache'),
'prefix' => env('CACHE_PREFIX', Str::slug(env('APP_NAME', 'KamilCraftAPI'), '_').'_cache_'),
];

View File

@@ -25,6 +25,7 @@ return [
'collation' => 'utf8mb4_unicode_ci',
'prefix' => '',
'prefix_indexes' => true,
'search_path' => 'public',
'strict' => true,
'engine' => null,
'options' => extension_loaded('pdo_mysql') ? array_filter([

View File

@@ -1,27 +0,0 @@
FROM php:8.0-fpm
ARG USER_UID
ARG USER_NAME
ENV COMPOSER_HOME=/home/$USER_NAME/.composer
RUN useradd -G www-data,root -u $USER_UID -d /home/$USER_NAME $USER_NAME
RUN mkdir -p /home/$USER_NAME/.composer && \
chown $USER_NAME:$USER_NAME -R /home/$USER_NAME
RUN set -eux \
&& apt-get update \
&& apt-get upgrade -y \
&& apt-get install git zip unzip dos2unix -y
RUN curl -sS https://getcomposer.org/installer \
| php -- --version=2.3.5 --install-dir=/usr/local/bin --filename=composer
RUN curl -sL https://deb.nodesource.com/setup_16.x | bash -
RUN set -eux \
&& apt-get install nodejs -y
RUN npm install --global npm@latest
COPY ./install.sh /usr/local/bin/install
RUN dos2unix /usr/local/bin/install \
&& chmod +x /usr/local/bin/install
USER $USER_UID
EXPOSE 9000

View File

@@ -1,28 +0,0 @@
#!/bin/sh
if [ ! -d "vendor" ] && [ -f "composer.json" ]; then
echo ""
echo "########################################"
echo "# vendor directory not found... #"
echo "########################################"
composer install \
&& php artisan key:generate
if [ ! -f "database/database.sqlite" ]; then
touch database/database.sqlite
fi
php artisan migrate:fresh --seed
fi
if [ ! -d "node_modules" ] && [ -f "package.json" ]; then
echo ""
echo "########################################"
echo "# node_modules directory not found... #"
echo "########################################"
npm install
npm run dev
fi
echo "$@"
exec "$@"

View File

@@ -8,12 +8,14 @@ return [
'local' => [
'driver' => 'local',
'root' => storage_path('app'),
'throw' => false,
],
'public' => [
'driver' => 'local',
'root' => storage_path('app/public'),
'url' => env('APP_URL').'/storage',
'visibility' => 'public',
'throw' => false,
],
],

View File

@@ -8,7 +8,10 @@ use Monolog\Handler\SyslogUdpHandler;
return [
'default' => env('LOG_CHANNEL', 'stack'),
'deprecations' => env('LOG_DEPRECATIONS_CHANNEL', 'null'),
'deprecations' => [
'channel' => env('LOG_DEPRECATIONS_CHANNEL', 'null'),
'trace' => false,
],
'channels' => [
'stack' => [
'driver' => 'stack',
@@ -36,10 +39,11 @@ return [
'papertrail' => [
'driver' => 'monolog',
'level' => env('LOG_LEVEL', 'debug'),
'handler' => SyslogUdpHandler::class,
'handler' => env('LOG_PAPERTRAIL_HANDLER', SyslogUdpHandler::class),
'handler_with' => [
'host' => env('PAPERTRAIL_URL'),
'port' => env('PAPERTRAIL_PORT'),
'connectionString' => 'tls://'.env('PAPERTRAIL_URL').':'.env('PAPERTRAIL_PORT'),
],
],
'stderr' => [

View File

@@ -1,10 +1,12 @@
<?php
declare(strict_types=1);
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateUsersTable extends Migration
return new class extends Migration
{
public function up(): void
{
@@ -22,4 +24,4 @@ class CreateUsersTable extends Migration
{
Schema::dropIfExists('users');
}
}
};

View File

@@ -1,10 +1,12 @@
<?php
declare(strict_types=1);
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreatePasswordResetsTable extends Migration
return new class extends Migration
{
public function up(): void
{
@@ -19,4 +21,4 @@ class CreatePasswordResetsTable extends Migration
{
Schema::dropIfExists('password_resets');
}
}
};

View File

@@ -1,10 +1,12 @@
<?php
declare(strict_types=1);
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateFailedJobsTable extends Migration
return new class extends Migration
{
public function up(): void
{
@@ -23,4 +25,4 @@ class CreateFailedJobsTable extends Migration
{
Schema::dropIfExists('failed_jobs');
}
}
};

View File

@@ -1,10 +1,12 @@
<?php
declare(strict_types=1);
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreatePersonalAccessTokensTable extends Migration
return new class extends Migration
{
public function up(): void
{
@@ -23,4 +25,4 @@ class CreatePersonalAccessTokensTable extends Migration
{
Schema::dropIfExists('personal_access_tokens');
}
}
};

View File

@@ -1,10 +1,12 @@
<?php
declare(strict_types=1);
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateCategoriesTable extends Migration
return new class extends Migration
{
public function up(): void
{
@@ -23,4 +25,4 @@ class CreateCategoriesTable extends Migration
{
Schema::dropIfExists('categories');
}
}
};

View File

@@ -6,7 +6,7 @@ use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateProjectsTable extends Migration
return new class extends Migration
{
public function up(): void
{
@@ -30,5 +30,4 @@ class CreateProjectsTable extends Migration
{
Schema::dropIfExists('projects');
}
}
};

View File

@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('cvs', function (Blueprint $table) {
$table->id();
$table->string('token', 50);
$table->string('recipient', 255);
$table->string('email', 255);
$table->string('phone_number', 15);
$table->json('locations');
$table->integer('views')->nullable()->default(0);
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('cvs');
}
};

View File

@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('cv_infos', function (Blueprint $table) {
$table->id();
$table->integer('cv_id')->index();
$table->string('ip', 255);
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('cv_infos');
}
};

View File

@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('cvs', function (Blueprint $table) {
$table->text('mission')->nullable();
});
}
public function down(): void
{
Schema::table('cvs', function (Blueprint $table) {
$table->dropColumn('mission');
});
}
};

View File

@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('cvs', function (Blueprint $table) {
$table->text('rodo')->nullable();
});
}
public function down(): void
{
Schema::table('cvs', function (Blueprint $table) {
$table->dropColumn('rodo');
});
}
};

View File

@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('cvs', function (Blueprint $table) {
$table->string('position', 255)->nullable();
});
}
public function down(): void
{
Schema::table('cvs', function (Blueprint $table) {
$table->dropColumn('position');
});
}
};

View File

@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('cvs', function (Blueprint $table) {
$table->text('notes')->nullable();
});
}
public function down(): void
{
Schema::table('cvs', function (Blueprint $table) {
$table->dropColumn('notes');
});
}
};

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('messages', function (Blueprint $table) {
$table->id();
$table->string('message', 500);
$table->string('email', 250);
$table->string('sender', 50);
$table->softDeletes();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('messages');
}
};

View File

@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('cvs', function (Blueprint $table) {
$table->boolean('sended')->nullable()->default(false)->after('views');
$table->timestamp('sended_timestamp')->nullable()->after('sended');
});
}
public function down(): void
{
Schema::table('cvs', function (Blueprint $table) {
$table->dropColumn('sended');
$table->dropColumn('sended_timestamp');
});
}
};

View File

@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('notes', function (Blueprint $table) {
$table->id();
$table->string('title', 250);
$table->text('note');
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('notes');
}
};

View File

@@ -1,33 +1,72 @@
version: "3.9"
version: "3.8"
services:
nginx:
image: nginx:latest
image: nginx:1.21-alpine
container_name: kamilcraft-api_www
working_dir: /application
ports:
- "80:80"
- ${EXTERNAL_WEBSERVER_PORT:-80}:80
volumes:
- ./environment/dev/nginx/default.conf:/etc/nginx/conf.d/default.conf
- .:/application
- ./config/docker/dev/nginx/default.conf:/etc/nginx/conf.d/default.conf
links:
- laravel
networks:
- localnet
- kamilcraft
depends_on:
- php
- db
laravel:
build:
args:
USER_UID: ${USER_UID}
USER_NAME: ${USER_NAME}
context: ./config/docker/dev/laravel
container_name: kamilcraft-api_laravel
working_dir: /application
volumes:
- .:/application
networks:
- localnet
php:
build:
context: environment/dev/php
container_name: kamilcraft-api_php
working_dir: /application
user: ${CURRENT_UID:-1000}
volumes:
- .:/application
- ./environment/dev/php/php.ini:/usr/local/etc/php/conf.d/php.ini
networks:
- kamilcraft
extra_hosts:
- host.docker.internal:host-gateway
restart: unless-stopped
npm:
build:
context: environment/dev/npm
container_name: kamilcraft-api_node
working_dir: /application
entrypoint: [ 'npm' ]
ports:
- '3000:3000'
- '${VITE_PORT:-3001}:${VITE_PORT:-3001}'
volumes:
- .:/application
networks:
- kamilcraft
db:
image: mysql:8.0
container_name: kamilcraft-api_db
ports:
- '${DB_PORT}:3306'
environment:
MYSQL_ROOT_PASSWORD: '${DB_PASSWORD}'
MYSQL_DATABASE: '${DB_DATABASE}'
MYSQL_USER: '${DB_USERNAME}'
MYSQL_PASSWORD: '${DB_PASSWORD}'
MYSQL_ALLOW_EMPTY_PASSWORD: 'yes'
volumes:
- 'mysql-db-data:/var/lib/mysql'
networks:
- kamilcraft
restart: unless-stopped
networks:
localnet:
kamilcraft:
driver: bridge
volumes:
mysql-db-data:
name: kamilcraft-api-mysql-data
driver: local

View File

@@ -9,7 +9,7 @@ server {
}
location ~ \.php$ {
fastcgi_pass laravel:9000;
fastcgi_pass php:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;

View File

@@ -0,0 +1,3 @@
FROM node:18-alpine
RUN npm install -g npm@latest

View File

@@ -0,0 +1,34 @@
FROM php:8.1-fpm-alpine
ARG XDEBUG_VERSION=3.2.1
ARG INSTALL_XDEBUG=false
ARG COMPOSER_VERSION=2.5.8
ENV COMPOSER_HOME=/application/.composer
ENV COMPOSER_MEMORY_LIMIT=-1
RUN if [ ${INSTALL_XDEBUG} = true ]; then \
apk --no-cache add $PHPIZE_DEPS \
&& pecl install xdebug-${XDEBUG_VERSION} \
&& docker-php-ext-enable xdebug \
;fi
RUN apk update && apk upgrade \
&& apk add --no-cache pcre-dev $PHPIZE_DEPS \
icu-dev \
zip \
libzip-dev \
libpng-dev \
&& curl -sS https://getcomposer.org/installer | php -- --version="${COMPOSER_VERSION}" --install-dir=/usr/local/bin --filename=composer \
&& pecl install redis \
&& docker-php-ext-install \
mysqli \
pdo \
pdo_mysql \
zip \
gd \
bcmath \
&& docker-php-ext-configure \
zip \
&& docker-php-ext-enable \
redis

View File

@@ -0,0 +1,9 @@
[PHP]
memory_limit = 1G
[xdebug]
xdebug.client_host=host.docker.internal
xdebug.client_port=9003
xdebug.mode=debug
xdebug.start_with_request=yes
xdebug.log_level=0

16004
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,21 +1,36 @@
{
"private": true,
"scripts": {
"dev": "npm run development",
"development": "mix",
"watch": "mix watch",
"watch-poll": "mix watch -- --watch-options-poll=1000",
"hot": "mix watch --hot",
"prod": "npm run production",
"production": "mix --production"
"dev": "vite",
"build": "vite build",
"build:ssr": "npm run build && vite build --ssr",
"ssr": "npm run build:ssr && node bootstrap/ssr/ssr.mjs"
},
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^6.4.0",
"@fortawesome/free-brands-svg-icons": "^6.4.0",
"@fortawesome/free-regular-svg-icons": "^6.4.0",
"@fortawesome/free-solid-svg-icons": "^6.4.0",
"@fortawesome/vue-fontawesome": "^3.0.3",
"@inertiajs/inertia": "^0.11.1",
"@inertiajs/inertia-vue3": "^0.6.0",
"@inertiajs/server": "^0.1.0",
"@inertiajs/vue3": "^1.0.9",
"@vue/compiler-sfc": "^3.2.45",
"@vue/server-renderer": "^3.2.45",
"vue": "^3.2.45"
},
"devDependencies": {
"axios": "^0.21",
"laravel-mix": "^6.0.6",
"@vitejs/plugin-vue": "^3.2.0",
"autoprefixer": "^10.4.13",
"axios": "^1.3.4",
"eslint": "^8.35.0",
"eslint-plugin-vue": "^9.7.0",
"laravel-vite-plugin": "^0.7.4",
"lodash": "^4.17.19",
"postcss": "^8.1.14",
"resolve-url-loader": "^5.0.0",
"sass": "^1.49.7",
"sass-loader": "^12.4.0"
"postcss": "^8.4.19",
"tailwindcss": "^3.2.7",
"vite": "^3.0.0",
"vite-plugin-iso-import": "^1.0.0"
}
}

6
postcss.config.js vendored Normal file
View File

@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

2
public/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
hot
build/

View File

@@ -0,0 +1,37 @@
<script setup>
import { router } from '@inertiajs/vue3';
const props = defineProps({
cv: {
type: Object,
required: true,
},
});
function confirmDelete() {
router.delete(`/dashboard/cv/${props.cv.token}/delete`);
}
</script>
<template>
<InertiaHead title="Usuwanie CV" />
<div class="p-4">
<header class="pb-4">
<h1 class="text-3xl font-roboto font-light">Usuwanie CV</h1>
</header>
<div class="max-w-[600px]">
<p class="mb-4">Na pewno usunąć CV dla firmy {{ cv.recipient }}?</p>
<div class="grid grid-cols-3 gap-2">
<InertiaLink
as="button"
href="/dashboard/cv"
class="col-span-1 flex justify-center items-center gap-3 w-full px-2 py-1 border-t-4 border-b-4 border-transparent hover:border-b-black"
><FontAwesomeIcon :icon="['fas', 'backward']" />Anuluj</InertiaLink>
<button
@click.prevent="confirmDelete"
class="col-span-2 flex justify-center items-center gap-3 w-full px-2 py-1 rounded-md bg-red-600 border-4 border-red-600 text-white text-lg hover:bg-transparent hover:text-red-600"
><FontAwesomeIcon :icon="['fas', 'trash']" /><span class="whitespace-nowrap overflow-hidden overflow-ellipsis">Usuń CV dla firmy {{ cv.recipient }}</span></button>
</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,122 @@
<script setup>
import { computed, ref } from 'vue';
import { useForm } from '@inertiajs/inertia-vue3';
import Input from '../../Share/Components/Input.vue';
const locations = ref([]);
const locationsToString = computed({
get: () => locations.value.join(', '),
set: (val) => {
val = val.replace(', ', ',').replace(' , ', ',').replace(' ,', ',');
val = val.split(',');
val.forEach((element, key) => {
val[key] = element.trim();
});
locations.value = val;
}
});
const mission = ref([]);
const missionToString = computed({
get: () => mission.value.join("\n"),
set: (value) => {
mission.value = value.split("\n");
}
});
const form = useForm({
recipient: null,
email: null,
phone_number: null,
locations: locations,
mission: missionToString,
rodo: null,
position: null,
notes: null,
});
function createCV() {
form.post('/dashboard/cv');
}
</script>
<template>
<InertiaHead title="Nowe dane do CV" />
<div class="p-4">
<header class="pb-4">
<div class="flex items-center gap-2">
<InertiaLink
as="button"
href="/dashboard/cv"
class="px-2 text-xl text-gray-700 hover:text-black"
title="Wróć do listy CV"><FontAwesomeIcon :icon="['fas', 'caret-left']" /></InertiaLink>
<h1 class="text-3xl font-roboto font-light">Nowe dane do CV</h1>
</div>
</header>
<div>
<form class="flex flex-col gap-4" @submit.prevent="createCV">
<Input
id="recipient"
label="Firma gdzie jest składane CV"
placeholder="Oki doki sp. z.o.o"
v-model="form.recipient"
:error="form.errors.recipient"
/>
<Input
id="email"
type="email"
label="E-mail"
placeholder="Adres e-mail wyświetlany na CV"
v-model="form.email"
:error="form.errors.email"
/>
<Input
id="phone"
label="Numer telefonu"
placeholder="+48 123 456 789"
v-model="form.phone_number"
:error="form.errors.phone_number"
/>
<Input
id="locations"
label="Lokalizacje"
placeholder="Miejsca pracy."
v-model="locationsToString"
:error="form.errors.locations"
/>
<Input
id="position"
label="Stanowisko (opcjonalne)"
placeholder="Stanowisko na jakie jest rekrutacja."
v-model="form.position"
:error="form.errors.position"
/>
<Input
id="notes"
type="textarea"
label="Notatki (opcjonalne)"
placeholder="Notatka dla administratora"
v-model="form.notes"
:error="form.errors.notes"
/>
<Input
id="mission"
type="textarea"
label="Misja - wstęp (opcjonalne)"
placeholder="Krótki opis, list motywacyjny."
v-model="form.mission"
:error="form.errors.mission"
/>
<Input
id="rodo"
type="textarea"
label="RODO (opcjonalne)"
placeholder="Klauzula informacyjna RODO"
v-model="form.rodo"
:error="form.errors.rodo"
/>
<button class="px-0.5 py-1 rounded-lg bg-[#436da7] border-4 border-[#436da7] text-white text-lg hover:bg-transparent hover:text-[#436da7]">Utwórz nowe CV</button>
</form>
</div>
</div>
</template>

View File

@@ -0,0 +1,149 @@
<script setup>
import { computed, ref } from 'vue';
import { useForm } from '@inertiajs/inertia-vue3';
import Input from '../../Share/Components/Input.vue';
const props = defineProps({
cv: {
type: Object,
required: true,
}
});
const locations = ref(props.cv.locations);
const locationsToString = computed({
get: () => locations.value.join(', '),
set: (val) => {
val = val.replace(', ', ',').replace(' , ', ',').replace(' ,', ',');
val = val.split(',');
val.forEach((element, key) => {
val[key] = element.trim();
});
locations.value = val;
}
});
const mission = ref(props.cv.mission);
const missionToString = computed({
get: () => mission.value.join("\n"),
set: (value) => {
mission.value = value.split("\n");
}
});
const form = useForm({
recipient: props.cv.recipient,
email: props.cv.email,
phone_number: props.cv.phone.formattedNumber,
locations: locations,
mission: missionToString,
rodo: props.cv.rodo,
position: props.cv.position,
notes: props.cv.notes,
sended: props.cv.sended.status,
});
function updateCV() {
form.put(`/dashboard/cv/${props.cv.token}`);
}
</script>
<template>
<InertiaHead title="Edycja danych do CV" />
<div class="p-4">
<header class="flex items-center justify-between pb-4">
<div class="flex items-center gap-2">
<InertiaLink
as="button"
href="/dashboard/cv"
class="px-2 text-xl text-gray-700 hover:text-black"
title="Wróć do listy CV"><FontAwesomeIcon :icon="['fas', 'caret-left']" /></InertiaLink>
<h1 class="text-3xl font-roboto font-light">Edycja danych do CV</h1>
</div>
<InertiaLink
as="button"
:href="`/dashboard/cv/${cv.token}/delete`"
class="flex items-center gap-2 px-2 py-1 text-red-600 hover:text-white hover:bg-red-600 rounded-md"
title="Usuń CV"
><FontAwesomeIcon :icon="['fas', 'trash']" />Usuń</InertiaLink>
</header>
<div>
<form class="flex flex-col gap-4" @submit.prevent="updateCV">
<Input
id="recipient"
label="Firma gdzie jest składane CV"
placeholder="Oki doki sp. z.o.o"
v-model="form.recipient"
:error="form.errors.recipient"
/>
<Input
id="email"
type="email"
label="E-mail"
placeholder="Adres e-mail wyświetlany na CV"
v-model="form.email"
:error="form.errors.email"
/>
<Input
id="phone"
label="Numer telefonu"
placeholder="+48 123 456 789"
v-model="form.phone_number"
:error="form.errors.phone_number"
/>
<Input
id="locations"
label="Lokalizacje"
placeholder="Miejsca pracy."
v-model="locationsToString"
:error="form.errors.locations"
/>
<Input
id="position"
label="Stanowisko"
placeholder="Stanowisko na jakie jest rekrutacja."
v-model="form.position"
:error="form.errors.position"
/>
<Input
id="notes"
type="textarea"
label="Notatki (opcjonalne)"
placeholder="Notatka dla administratora"
v-model="form.notes"
:error="form.errors.notes"
/>
<Input
id="mission"
type="textarea"
label="Misja - wstęp"
placeholder="Krótki opis, list motywacyjny."
v-model="form.mission"
:error="form.errors.mission"
/>
<Input
id="rodo"
type="textarea"
label="RODO"
placeholder="Klauzula informacyjna RODO"
v-model="form.rodo"
:error="form.errors.rodo"
/>
<Input
id="sended"
label="Wysłany"
type="checkbox"
v-model="form.sended"
/>
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-3 sm:gap-2 items-center">
<InertiaLink
as="button"
:href="`/dashboard/cv/${cv.token}`"
class="col-span-1 flex justify-center items-center gap-3 w-full px-2 py-1 border-t-4 border-b-4 border-transparent hover:border-b-black"
><FontAwesomeIcon :icon="['fas', 'backward']" />Anuluj</InertiaLink>
<button class="col-span-1 md:col-span-2 px-0.5 py-1 rounded-lg bg-[#436da7] border-4 border-[#436da7] text-white text-lg hover:bg-transparent hover:text-[#436da7]">Edytuj CV</button>
</div>
</form>
</div>
</div>
</template>

View File

@@ -0,0 +1,93 @@
<script setup>
import EmptyState from '@/Share/Components/EmptyState.vue';
defineProps({
cvs: {
type: Object,
required: true,
}
});
function copySlug(slug) {
const input = document.createElement('input');
input.value = slug;
input.select();
input.setSelectionRange(0, 99999);
navigator
.clipboard
.writeText(input.value)
.then((value, reason) => { });
}
</script>
<template>
<InertiaHead title="Lista CV" />
<div class="p-4">
<header class="flex justify-between items-center pb-4">
<div class="flex items-center gap-2">
<InertiaLink
as="button"
href="/dashboard"
class="px-2 text-xl text-gray-700 hover:text-black"
title="Wróc do dashboard"><FontAwesomeIcon :icon="['fas', 'caret-left']" /></InertiaLink>
<h1 class="text-3xl font-roboto font-light">Lista CV</h1>
</div>
<InertiaLink
as="button"
href="/dashboard/cv/create"
class="bg-blue-400 hover:bg-blue-500 text-white px-2.5 py-1 rounded-full"
title="Dodaj nowe dane dla CV"
><FontAwesomeIcon :icon="['fas', 'plus']" /></InertiaLink>
</header>
<div class="overflow-x-auto">
<table v-if="cvs.data.length" class="w-full min-w-[600px] border-separate border-spacing-y-2 cursor-pointer">
<colgroup>
<col class="w-min" />
</colgroup>
<thead class="text-left bg-gray-100">
<th class="p-2 text-center">ID</th>
<th class="p-2">Token</th>
<th class="w-[100px] p-2 whitespace-nowrap">Firma</th>
<th class="hidden md:table-cell p-2">Lokalizacje</th>
<th class="p-2">Telefon</th>
<th class="p-2"></th>
</thead>
<tbody>
<InertiaLink
as="tr"
v-for="(cv, key) in cvs.data"
:key="key"
class="px-3 py-2 bg-white hover:bg-neutral-200 rounded-md z-10"
:href="`/dashboard/cv/${cv.token}`">
<td class="p-2 text-center">#{{ cv.id }}</td>
<td class="p-2"
><button
class="bg-gray-50 p-1 rounded-md hover:bg-gray-100"
@click.prevent="copySlug(cv.token)"
:title="cv.token">{{ cv.token.slice(0, 7) }}</button></td>
<td class="max-w-[150px] md:max-w-[250px] p-2 whitespace-nowrap overflow-hidden overflow-ellipsis" :title="cv.recipient">{{ cv.recipient }}</td>
<td class="hidden md:table-cell p-2">{{ cv.locations.join(' / ') }}</td>
<td class="p-2">{{ cv.phone.formattedNumber }}</td>
<td class="flex items-center justify-center gap-2 p-3 z-50">
<InertiaLink
as="button"
class="px-3 py-3 text-lime-600 hover:text-lime-800 border-t-2 border-b-2 border-transparent hover:border-b-lime-600"
:href="`/dashboard/cv/${cv.token}/edit`"
title="Edytuj CV"><FontAwesomeIcon :icon="['fas', 'pen-to-square']" /></InertiaLink>
<InertiaLink
as="button"
class="px-3 py-3 text-red-600 hover:text-red-800"
:href="`/dashboard/cv/${cv.token}/delete`"
title="Usuń CV z listy"><FontAwesomeIcon :icon="['fas', 'trash']" /></InertiaLink>
</td>
</InertiaLink>
</tbody>
</table>
<EmptyState v-else :icon="['fas', 'file']">
<template #title>Nie znaleziono</template>
<template #text>Nie dodano jeszcze CV do listy.</template>
</EmptyState>
</div>
</div>
</template>

View File

@@ -0,0 +1,169 @@
<script setup>
import { computed } from 'vue';
import { router } from '@inertiajs/inertia';
const props = defineProps({
cv: {
type: Object,
required: true,
},
cvInfo: {
type: Object,
required: true,
},
});
const CV_URL = import.meta.env.VITE_CV_APP_URL;
const cvNotes = computed(() => {
const notes = props.cv.notes;
return notes ? props.cv.notes.split("\n") : null;
});
</script>
<template>
<InertiaHead title="Szczegóły CV" />
<div class="px-3 py-2">
<InertiaLink
v-if="!cv.sended.status"
as="button"
method="post"
:href="`/dashboard/cv/${cv.token}/sended`"
class="w-full px-0.5 py-1 rounded-lg bg-[#436da7] border-4 border-[#436da7] text-white text-lg hover:bg-transparent hover:text-[#436da7]"
title="Ustaw jako wysłane">Ustaw jako wysłane do odbiorcy.</InertiaLink>
</div>
<div class="p-4">
<header class="flex justify-between items-center pb-4">
<div class="flex items-center gap-2">
<InertiaLink
as="button"
href="/dashboard/cv"
class="px-2 text-xl text-gray-700 hover:text-black"
title="Wróć do listy CV"><FontAwesomeIcon :icon="['fas', 'caret-left']" /></InertiaLink>
<h1 class="text-3xl font-roboto font-light">Szczegóły CV</h1>
</div>
<div class="flex gap-3 sm:gap-2">
<a
class="flex items-center gap-2 px-2 py-1 text-blue-600 hover:text-blue-800"
:href="`${CV_URL}/show/${cv.token}`"
target="_blank"
rel="noopener noreferrer"
title="Przekieruj do CV"><FontAwesomeIcon :icon="['fas', 'arrow-up-right-from-square']" /><span class="hidden sm:inline-block">Przejdź do CV</span></a>
<InertiaLink
as="button"
:href="`/dashboard/cv/${cv.token}/edit`"
class="flex items-center gap-2 px-2 py-1 text-lime-600 hover:text-white hover:bg-lime-600 rounded-md"
title="Usuń CV"
><FontAwesomeIcon :icon="['fas', 'pen-to-square']" /><span class="hidden sm:inline-block">Edytuj</span></InertiaLink>
<InertiaLink
as="button"
:href="`/dashboard/cv/${cv.token}/delete`"
class="flex items-center gap-2 px-2 py-1 text-red-600 hover:text-white hover:bg-red-600 rounded-md"
title="Usuń CV"
><FontAwesomeIcon :icon="['fas', 'trash']" /><span class="hidden sm:inline-block">Usuń</span></InertiaLink>
</div>
</header>
<div v-if="cv.sended.status" class="max-w-screen-lg my-2 lg:mx-auto px-2 py-3 rounded-md bg-yellow-100 text-yellow-600 text-center">
CV jest oznaczone jako wysłane - {{ cv.sended.datetime }}
</div>
<div class="mb-4">
<header>
<h2 class="text-2xl font-roboto font-light pb-3">Podstawowe informacje</h2>
</header>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<div class="text-gray-500 pb-0.5">Token</div>
<p class="w-full min-w-full max-w-full px-2.5 py-2 border-b-2 rounded-md bg-white whitespace-nowrap overflow-hidden overflow-ellipsis">{{ cv.token }}</p>
</div>
<div>
<div class="text-gray-500 pb-0.5">Firma</div>
<p class="w-full min-w-full max-w-full px-2.5 py-2 border-b-2 rounded-md bg-white">{{ cv.recipient }}</p>
</div>
<div>
<div class="text-gray-500 pb-0.5">Stanowisko</div>
<p class="w-full min-w-full max-w-full px-2.5 py-2 border-b-2 rounded-md bg-white">{{ cv.position ?? '[Wartość domyślna]' }}</p>
</div>
<div>
<div class="text-gray-500 pb-0.5">E-mail</div>
<p class="w-full min-w-full max-w-full px-2.5 py-2 border-b-2 rounded-md bg-white">{{ cv.email }}</p>
</div>
<div>
<div class="text-gray-500 pb-0.5">Numer telefonu</div>
<p class="w-full min-w-full max-w-full px-2.5 py-2 border-b-2 rounded-md bg-white">{{ cv.phone.formattedNumber }}</p>
</div>
<div>
<div class="text-gray-500 pb-0.5">Lokalizacje</div>
<p class="w-full min-w-full max-w-full px-2.5 py-2 border-b-2 rounded-md bg-white">{{ cv.locations.join(' / ') }}</p>
</div>
<div v-if="cvNotes" class="md:col-span-2">
<div class="text-gray-500 pb-0.5">Notatki</div>
<div class="w-full min-w-full max-w-full px-2.5 py-2 border-b-2 rounded-md bg-white">
<p
v-for="(noteLine, key) in cvNotes"
:key="key"
>{{ noteLine }}</p>
</div>
</div>
</div>
</div>
<div class="mb-4">
<header>
<h2 class="text-2xl font-roboto font-light pb-3">Statystyka</h2>
</header>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<div class="text-gray-500 pb-0.5">Utworzono</div>
<p class="w-full min-w-full max-w-full px-2.5 py-2 border-b-2 rounded-md bg-white">{{ cv.created }}</p>
</div>
<div>
<div class="text-gray-500 pb-0.5">Zmodyfikowano</div>
<p class="w-full min-w-full max-w-full px-2.5 py-2 border-b-2 rounded-md bg-white">{{ cv.updated }}</p>
</div>
<div>
<div class="text-gray-500 pb-0.5">Zachowane wyświetlenia</div>
<p class="w-full min-w-full max-w-full px-2.5 py-2 border-b-2 rounded-md bg-white">{{ cv.views }}</p>
</div>
<div>
<div class="text-gray-500 pb-0.5">Łączne wyświetlenia</div>
<p class="w-full min-w-full max-w-full px-2.5 py-2 border-b-2 rounded-md bg-white">{{ cv.registeredViews }}</p>
</div>
</div>
</div>
<div class="mb-4">
<header>
<h2 class="text-2xl font-roboto font-light pb-3">Opisy</h2>
</header>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<div class="text-gray-500 pb-0.5">Misja</div>
<div v-if="cv.mission.length > 0 && cv.mission[0] !== ''" class="w-full min-w-full max-w-full px-2.5 py-2 border-b-2 rounded-md bg-white">
<p
v-for="(mission, key) in cv.mission"
:key="key">{{ mission }}</p>
</div>
<p v-else class="w-full min-w-full max-w-full px-2.5 py-2 border-b-2 rounded-md bg-white">[Wartość domyślna]</p>
</div>
<div>
<div class="text-gray-500 pb-0.5">RODO</div>
<p class="w-full min-w-full max-w-full px-2.5 py-2 border-b-2 rounded-md bg-white">{{ cv.rodo ?? '[Wartość domyślna]' }}</p>
</div>
</div>
</div>
<div>
<header>
<h2 class="text-2xl font-roboto font-light pb-3">Lista wejść</h2>
</header>
<ul v-if="cvInfo.data.length" class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-2">
<li
v-for="(info, key) in cvInfo.data"
:key="key"
class="flex flex-col px-3 py-2 bg-white rounded-md">
<span>#{{ info.id }} {{ info.ip }}</span>
<span class="text-xs">{{ info.created_at }}</span>
</li>
</ul>
<div v-else>
Brak wyświetleń
</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,37 @@
<script setup>
import { router } from '@inertiajs/vue3';
const props = defineProps({
category: {
type: Object,
required: true,
},
});
function confirmDelete() {
router.delete(`/dashboard/category/${props.category.id}/delete`);
}
</script>
<template>
<InertiaHead title="Usuwanie kategorii" />
<div class="p-4">
<header class="pb-4">
<h1 class="text-3xl font-roboto font-light">Usuwanie kategorii</h1>
</header>
<div class="max-w-[600px]">
<p class="mb-4">Na pewno usunąć kategorię o nazwie {{ category.name }}?</p>
<div class="grid grid-cols-3 gap-2">
<InertiaLink
as="button"
href="/dashboard"
class="col-span-1 flex justify-center items-center gap-3 w-full px-2 py-1 border-t-4 border-b-4 border-transparent hover:border-b-black"
><FontAwesomeIcon :icon="['fas', 'backward']" />Anuluj</InertiaLink>
<button
@click.prevent="confirmDelete"
class="col-span-2 flex justify-center items-center gap-3 w-full px-2 py-1 rounded-md bg-red-600 border-4 border-red-600 text-white text-lg hover:bg-transparent hover:text-red-600"
><FontAwesomeIcon :icon="['fas', 'trash']" />Usuń kategorię {{ category.name }}</button>
</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,71 @@
<script setup>
import { useForm } from '@inertiajs/inertia-vue3';
import Input from '../../Share/Components/Input.vue';
const form = useForm({
name: null,
slug: null,
priority: Number(0),
default: Boolean(false),
visible: Boolean(false),
});
function createCategory() {
form.post('/dashboard/category');
}
</script>
<template>
<InertiaHead title="Nowa kategoria" />
<div class="p-4">
<header class="pb-4">
<div class="flex items-center gap-2">
<InertiaLink
as="button"
href="/dashboard"
class="px-2 text-xl text-gray-700 hover:text-black"
title="Wróc do dashboard"><FontAwesomeIcon :icon="['fas', 'caret-left']" /></InertiaLink>
<h1 class="text-3xl font-roboto font-light">Nowa kategoria</h1>
</div>
</header>
<div>
<form class="flex flex-col gap-4" @submit.prevent="createCategory">
<Input
id="name"
label="Nazwa"
placeholder="Nazwa kategorii"
v-model="form.name"
:error="form.errors.name"
/>
<Input
id="slug"
label="Slug"
placeholder="Slug dla kategorii"
v-model="form.slug"
:error="form.errors.slug"
/>
<Input
id="priority"
label="Priorytet"
type="number"
placeholder="Priorytet dla danej kategorii"
v-model="form.priority"
:error="form.errors.priority"
/>
<Input
id="visible"
label="Widoczny"
type="checkbox"
v-model="form.visible"
/>
<Input
id="default"
label="Domyślny"
type="checkbox"
v-model="form.default"
/>
<button class="px-0.5 py-1 rounded-lg bg-[#436da7] border-4 border-[#436da7] text-white text-lg hover:bg-transparent hover:text-[#436da7]">Dodaj kategorię</button>
</form>
</div>
</div>
</template>

View File

@@ -0,0 +1,91 @@
<script setup>
import { ref } from 'vue';
import { useForm } from '@inertiajs/inertia-vue3';
import Input from '../../Share/Components/Input.vue';
const props = defineProps({
category: {
type: Object,
required: true,
},
});
const visibleCheckbox = ref(props.category.visible);
const defaultCheckbox = ref(props.category.default);
const form = useForm({
name: props.category.name,
slug: props.category.slug,
priority: props.category.priority,
visible: visibleCheckbox,
default: defaultCheckbox,
});
function updateProject() {
form.clearErrors();
form.put(`/dashboard/category/${props.category.id}`);
if (defaultCheckbox.value)
visibleCheckbox.value = true;
}
</script>
<template>
<InertiaHead title="Edytuj projekt" />
<div class="p-4">
<header class="flex items-center justify-between pb-4">
<div class="flex items-center gap-2">
<InertiaLink
as="button"
href="/dashboard"
class="px-2 text-xl text-gray-700 hover:text-black"
title="Wróc do dashboard"><FontAwesomeIcon :icon="['fas', 'caret-left']" /></InertiaLink>
<h1 class="text-3xl font-roboto font-light">Edytuj {{ category.name }}</h1>
</div>
<InertiaLink
as="button"
:href="`/dashboard/category/${category.id}/delete`"
class="flex items-center gap-2 px-2 py-1 text-red-600 hover:text-white hover:bg-red-600 rounded-md"
title="Usuń kategorię"
><FontAwesomeIcon :icon="['fas', 'trash']" />Usuń</InertiaLink>
</header>
<div>
<form class="flex flex-col gap-4" @submit.prevent="updateProject">
<Input
id="name"
label="Nazwa"
placeholder="Nazwa kategorii"
v-model="form.name"
:error="form.errors.name"
/>
<Input
id="slug"
label="Slug"
placeholder="Slug dla kategorii"
v-model="form.slug"
:error="form.errors.slug"
/>
<Input
id="priority"
label="Priorytet"
type="number"
placeholder="Priorytet dla danej kategorii"
v-model="form.priority"
:error="form.errors.priority"
/>
<Input
id="visible"
label="Widoczny"
type="checkbox"
v-model="form.visible"
/>
<Input
id="default"
label="Domyślny"
type="checkbox"
v-model="form.default"
/>
<button class="px-0.5 py-1 rounded-lg bg-[#436da7] border-4 border-[#436da7] text-white text-lg hover:bg-transparent hover:text-[#436da7]">Aktualizuj kategorię</button>
</form>
</div>
</div>
</template>

View File

@@ -0,0 +1,28 @@
<script setup>
import ProjectsList from '../../Share/ProjectsList.vue';
import CategoriesList from '../../Share/CategoriesList.vue';
defineProps({
categories: {
type: Array,
required: true,
},
projects: {
type: Array,
required: true,
},
});
</script>
<template>
<InertiaHead title="Dashboard" />
<div class="p-4">
<header class="pb-4">
<h1 class="text-3xl font-roboto font-light">Dashboard</h1>
</header>
<div class="grid md:grid-cols-3 gap-3">
<ProjectsList class="md:col-span-2" :projects="projects" />
<CategoriesList class="col-span-1" :categories="categories" />
</div>
</div>
</template>

View File

@@ -0,0 +1,45 @@
<script setup>
import { useForm } from '@inertiajs/inertia-vue3';
import GuestLayout from '../Share/Layout/Guest.vue';
defineOptions({ layout: GuestLayout });
const form = useForm({
'email': null,
'password': null,
});
function login() {
form.post('/dashboard/login');
}
</script>
<template>
<InertiaHead title="Logowanie" />
<div class="max-w-screen-sm mx-auto p-4 bg-neutral-100 rounded-md border border-gray-200 shadow">
<h1 class="pb-4 text-3xl font-robot font-light">Logowanie</h1>
<form class="flex flex-col gap-4" @submit.prevent="form.post('/dashboard/login')">
<div class="flex flex-col gap-1 w-full">
<label for="email"
class="text-gray-500">E-mail</label>
<input id="email"
:class="['w-full px-2.5 py-2 border-b-2 rounded-md', form.errors.email ? 'border-red-300 focus:border-red-400 hover:border-red-500 outline-none text-red-900 placeholder-red-400' : 'border-neutral-300 focus:border-neutral-400 hover:border-neutral-500 outline-none text-gray-900 placeholder-gray-400']"
type="email"
v-model="form.email"
placeholder="Podaj swój e-mail" />
<span class="text-red-400" v-if="form.errors.email">{{ form.errors.email }}</span>
</div>
<div class="flex flex-col">
<label for="password"
class="text-gray-500">Hasło</label>
<input
id="password"
:class="['w-full px-2.5 py-2 border-b-2 rounded-md', form.errors.email ? 'border-red-300 focus:border-red-400 hover:border-red-500 outline-none text-red-900 placeholder-red-400' : 'border-neutral-300 focus:border-neutral-400 hover:border-neutral-500 outline-none text-gray-900 placeholder-gray-400']"
type="password"
v-model="form.password"
placeholder="Podaj swoje hasło" />
</div>
<button class="px-0.5 py-1 rounded-lg bg-[#436da7] border-4 border-[#436da7] text-white text-lg hover:bg-transparent hover:text-[#436da7]">Zaloguj</button>
</form>
</div>
</template>

View File

@@ -0,0 +1,37 @@
<script setup>
import { router } from '@inertiajs/vue3';
const props = defineProps({
message: {
type: Object,
required: true,
},
});
function confirmDelete() {
router.delete(`/dashboard/message/${props.message.id}/delete`);
}
</script>
<template>
<InertiaHead title="Usuwanie wiadomości" />
<div class="p-4">
<header class="pb-4">
<h1 class="text-3xl font-roboto font-light">Usuwanie wiadomości</h1>
</header>
<div class="max-w-[600px]">
<p class="mb-4">Na pewno usunąć wiadomość od {{ message.sender }}?</p>
<div class="grid grid-cols-3 gap-2">
<InertiaLink
as="button"
:href="`/dashboard/message/${message.id}`"
class="col-span-1 flex justify-center items-center gap-3 w-full px-2 py-1 border-t-4 border-b-4 border-transparent hover:border-b-black"
><FontAwesomeIcon :icon="['fas', 'backward']" />Anuluj</InertiaLink>
<button
@click.prevent="confirmDelete"
class="col-span-2 flex justify-center items-center gap-3 w-full px-2 py-1 rounded-md bg-red-600 border-4 border-red-600 text-white text-lg hover:bg-transparent hover:text-red-600"
><FontAwesomeIcon :icon="['fas', 'trash']" /><span class="whitespace-nowrap overflow-hidden overflow-ellipsis">Usuń wiadomość od {{ message.sender }}</span></button>
</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,65 @@
<script setup>
import EmptyState from '@/Share/Components/EmptyState.vue';
defineProps({
messages: {
type: Object,
default: {},
},
});
</script>
<template>
<InertiaHead title="Wiadomości" />
<div class="p-4">
<header class="flex justify-between items-center pb-4">
<div class="flex items-center gap-2">
<InertiaLink
as="button"
href="/dashboard"
class="px-2 text-xl text-gray-700 hover:text-black"
title="Wróc do dashboard"><FontAwesomeIcon :icon="['fas', 'caret-left']" /></InertiaLink>
<h1 class="text-3xl font-roboto font-light">Wiadomości</h1>
</div>
</header>
<div class="overflow-x-auto">
<table v-if="messages.data.length" class="table-fixed w-full min-w-[600px] border-separate border-spacing-y-2 cursor-pointer">
<colgroup>
<col class="w-[40px] max-w-[60px]" />
<col class="w-[250px]" />
<col class="w-auto" />
<col class="w-[50px]" />
</colgroup>
<thead class="text-left bg-gray-100">
<th class="w-[40px] max-w-[60px] p-2 text-center">ID</th>
<th class="w-[250px] p-2">Wysyłający</th>
<th class="p-2">E-mail</th>
<th class="w-[50px] p-2"></th>
</thead>
<tbody>
<InertiaLink
as="tr"
v-for="(message, key) in messages.data"
:key="key"
class="px-3 py-2 bg-white hover:bg-neutral-200 rounded-md z-10"
:href="`/dashboard/message/${message.id}`">
<td class="p-2 w-[60px] text-center">#{{ message.id }}</td>
<td class="p-2 whitespace-nowrap overflow-hidden overflow-ellipsis">{{ message.sender }}</td>
<td class="p-2">{{ message.email }}</td>
<td class="flex items-center justify-end gap-2 p-3 z-50">
<InertiaLink
as="button"
class="px-3 py-3 text-red-600 hover:text-red-800"
:href="`/dashboard/message/${message.id}/delete`"
title="Usuń wiadomość z listy"><FontAwesomeIcon :icon="['fas', 'trash']" /></InertiaLink>
</td>
</InertiaLink>
</tbody>
</table>
<EmptyState v-else :icon="['fas', 'message']">
<template #title>Brak wiadomości</template>
<template #text>Nie przesłano jeszcze żadnej wiadomości.</template>
</EmptyState>
</div>
</div>
</template>

View File

@@ -0,0 +1,71 @@
<script setup>
import { computed } from 'vue';
const props = defineProps({
message: {
type: Object,
required: true,
},
});
const splitMessage = computed(() => props.message.message.split("\n"));
</script>
<template>
<InertiaHead title="Szczegóły wiadomości" />
<div class="p-4">
<header class="flex justify-between items-center pb-4">
<div class="flex items-center gap-2">
<InertiaLink
as="button"
href="/dashboard/message"
class="px-2 text-xl text-gray-700 hover:text-black"
title="Wróć do listy wiadomości"><FontAwesomeIcon :icon="['fas', 'caret-left']" /></InertiaLink>
<h1 class="text-3xl font-roboto font-light">Szczegóły wiadomości</h1>
</div>
<div class="flex gap-3 sm:gap-2">
<InertiaLink
as="button"
:href="`/dashboard/message/${message.id}/delete`"
class="flex items-center gap-2 px-2 py-1 text-red-600 hover:text-white hover:bg-red-600 rounded-md"
title="Usuń wiadomość"
><FontAwesomeIcon :icon="['fas', 'trash']" /><span class="hidden sm:inline-block">Usuń</span></InertiaLink>
</div>
</header>
<div class="mb-4">
<header>
<h2 class="text-2xl font-roboto font-light pb-3">Podstawowe informacje</h2>
</header>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<div class="text-gray-500 pb-0.5">ID</div>
<p class="w-full min-w-full max-w-full px-2.5 py-2 border-b-2 rounded-md bg-white whitespace-nowrap overflow-hidden overflow-ellipsis">{{ message.id }}</p>
</div>
<div>
<div class="text-gray-500 pb-0.5">Nadawca</div>
<p class="w-full min-w-full max-w-full px-2.5 py-2 border-b-2 rounded-md bg-white">{{ message.sender }}</p>
</div>
<div>
<div class="text-gray-500 pb-0.5">E-mail</div>
<p class="w-full min-w-full max-w-full px-2.5 py-2 border-b-2 rounded-md bg-white">{{ message.email }}</p>
</div>
</div>
</div>
<div class="mb-4">
<header>
<h2 class="text-2xl font-roboto font-light pb-3">Treść wiadomości</h2>
</header>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div class="col-span-1 sm:col-span-2">
<div class="text-gray-500 pb-0.5">Wiadomość</div>
<div class="w-full min-w-full max-w-full px-2.5 py-2 border-b-2 rounded-md bg-white">
<p
v-for="(messageLine, key) in splitMessage"
:key="key"
>{{ messageLine }}</p>
</div>
</div>
</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,37 @@
<script setup>
import { router } from '@inertiajs/vue3';
const props = defineProps({
note: {
type: Object,
required: true,
},
});
function confirmDelete() {
router.delete(`/dashboard/note/${props.note.id}/delete`);
}
</script>
<template>
<InertiaHead title="Usuwanie notatki" />
<div class="p-4">
<header class="pb-4">
<h1 class="text-3xl font-roboto font-light">Usuwanie notatki</h1>
</header>
<div class="max-w-[600px]">
<p class="mb-4">Na pewno usunąć notatkę {{ note.title }}?</p>
<div class="grid grid-cols-3 gap-2">
<InertiaLink
as="button"
:href="`/dashboard/note/${note.id}`"
class="col-span-1 flex justify-center items-center gap-3 w-full px-2 py-1 border-t-4 border-b-4 border-transparent hover:border-b-black"
><FontAwesomeIcon :icon="['fas', 'backward']" />Anuluj</InertiaLink>
<button
@click.prevent="confirmDelete"
class="col-span-2 flex justify-center items-center gap-3 w-full px-2 py-1 rounded-md bg-red-600 border-4 border-red-600 text-white text-lg hover:bg-transparent hover:text-red-600"
><FontAwesomeIcon :icon="['fas', 'trash']" /><span class="whitespace-nowrap overflow-hidden overflow-ellipsis">Usuń notatkę {{ note.title }}</span></button>
</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,49 @@
<script setup>
import { useForm } from '@inertiajs/inertia-vue3';
import Input from '../../Share/Components/Input.vue';
const form = useForm({
title: null,
note: null,
});
function createNote() {
form.post('/dashboard/note');
}
</script>
<template>
<InertiaHead title="Nowe dane do CV" />
<div class="p-4">
<header class="pb-4">
<div class="flex items-center gap-2">
<InertiaLink
as="button"
href="/dashboard/note"
class="px-2 text-xl text-gray-700 hover:text-black"
title="Wróć do listy notatek"><FontAwesomeIcon :icon="['fas', 'caret-left']" /></InertiaLink>
<h1 class="text-3xl font-roboto font-light">Nowe dane do CV</h1>
</div>
</header>
<div>
<form class="flex flex-col gap-4" @submit.prevent="createNote">
<Input
id="title"
label="Tytuł notatki"
placeholder="np. Witaj świecie!"
v-model="form.title"
:error="form.errors.title"
/>
<Input
id="note"
type="textarea"
label="Treść notatki"
placeholder="Treść"
v-model="form.note"
:error="form.errors.note"
/>
<button class="px-0.5 py-1 rounded-lg bg-[#436da7] border-4 border-[#436da7] text-white text-lg hover:bg-transparent hover:text-[#436da7]">Utwórz notatkę</button>
</form>
</div>
</div>
</template>

View File

@@ -0,0 +1,69 @@
<script setup>
import { useForm } from '@inertiajs/inertia-vue3';
import Input from '../../Share/Components/Input.vue';
const props = defineProps({
note: {
type: Object,
required: true,
}
});
const form = useForm({
title: props.note.title,
note: props.note.note,
});
function updateNote() {
form.put(`/dashboard/note/${props.note.id}`);
}
</script>
<template>
<InertiaHead title="Edycja notatki" />
<div class="p-4">
<header class="flex items-center justify-between pb-4">
<div class="flex items-center gap-2">
<InertiaLink
as="button"
:href="`/dashboard/note/${note.id}`"
class="px-2 text-xl text-gray-700 hover:text-black"
title="Wróć do listy notatek"><FontAwesomeIcon :icon="['fas', 'caret-left']" /></InertiaLink>
<h1 class="text-3xl font-roboto font-light">Edycja notatki</h1>
</div>
<InertiaLink
as="button"
:href="`/dashboard/note/${note.id}/delete`"
class="flex items-center gap-2 px-2 py-1 text-red-600 hover:text-white hover:bg-red-600 rounded-md"
title="Usuń notatkę"
><FontAwesomeIcon :icon="['fas', 'trash']" />Usuń</InertiaLink>
</header>
<div>
<form class="flex flex-col gap-4" @submit.prevent="updateNote">
<Input
id="title"
label="Tytuł notatki"
placeholder="np. Witaj świecie!"
v-model="form.title"
:error="form.errors.title"
/>
<Input
id="note"
type="textarea"
label="Treść notatki"
placeholder="Treść"
v-model="form.note"
:error="form.errors.note"
/>
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-3 sm:gap-2 items-center">
<InertiaLink
as="button"
:href="`/dashboard/note/${note.id}`"
class="col-span-1 flex justify-center items-center gap-3 w-full px-2 py-1 border-t-4 border-b-4 border-transparent hover:border-b-black"
><FontAwesomeIcon :icon="['fas', 'backward']" />Anuluj</InertiaLink>
<button class="col-span-1 md:col-span-2 px-0.5 py-1 rounded-lg bg-[#436da7] border-4 border-[#436da7] text-white text-lg hover:bg-transparent hover:text-[#436da7]">Edytuj notatkę</button>
</div>
</form>
</div>
</div>
</template>

View File

@@ -0,0 +1,57 @@
<script setup>
import EmptyState from '@/Share/Components/EmptyState.vue';
defineProps({
notes: {
type: Object,
required: true,
}
});
</script>
<template>
<InertiaHead title="Lista notatek" />
<div class="p-4">
<header class="flex justify-between items-center pb-4">
<div class="flex items-center gap-2">
<InertiaLink
as="button"
href="/dashboard"
class="px-2 text-xl text-gray-700 hover:text-black"
title="Wróc do dashboard"><FontAwesomeIcon :icon="['fas', 'caret-left']" /></InertiaLink>
<h1 class="text-3xl font-roboto font-light">Lista notatek</h1>
</div>
<InertiaLink
as="button"
href="/dashboard/note/create"
class="bg-blue-400 hover:bg-blue-500 text-white px-2.5 py-1 rounded-full"
title="Dodaj nowe dane dla CV"
><FontAwesomeIcon :icon="['fas', 'plus']" /></InertiaLink>
</header>
<ul v-if="notes.data.length" class="flex flex-col gap-2">
<li
v-for="(note, key) in notes.data"
:key="key"
class="flex items-center justify-between px-3 py-2 bg-white hover:bg-neutral-200"
>
<InertiaLink :href="`/dashboard/note/${note.id}`">{{ note.title }}</InertiaLink>
<div class="flex items-center gap-2">
<InertiaLink
as="button"
class="px-2 py-1 text-lime-600 hover:text-lime-800 border-t-2 border-b-2 border-transparent hover:border-b-lime-600"
:href="`/dashboard/note/${note.id}/edit`"
title="Edytuj notatkę"><FontAwesomeIcon :icon="['fas', 'pen-to-square']" /></InertiaLink>
<InertiaLink
as="button"
class="px-2 py-1 text-red-600 hover:text-red-800"
:href="`/dashboard/note/${note.id}/delete`"
title="Usuń notatkę z listy"><FontAwesomeIcon :icon="['fas', 'trash']" /></InertiaLink>
</div>
</li>
</ul>
<EmptyState v-else>
<template #title>Nie znaleziono notatek</template>
<template #text>Nie dodano jeszcze notatek do listy.</template>
</EmptyState>
</div>
</template>

View File

@@ -0,0 +1,64 @@
<script setup>
import { computed } from 'vue';
const props = defineProps({
note: {
type: Object,
required: true,
},
});
const noteLines = computed(() => props.note.note.split("\n"));
</script>
<template>
<InertiaHead title="Szczegóły notatki" />
<div class="p-4">
<header class="flex justify-between items-center pb-4">
<div class="flex items-center gap-2">
<InertiaLink
as="button"
href="/dashboard/note"
class="px-2 text-xl text-gray-700 hover:text-black"
title="Wróć do listy notatek"><FontAwesomeIcon :icon="['fas', 'caret-left']" /></InertiaLink>
<h1 class="text-3xl font-roboto font-light">Szczegóły notatki</h1>
</div>
<div class="flex gap-3 sm:gap-2">
<InertiaLink
as="button"
:href="`/dashboard/note/${note.id}/edit`"
class="flex items-center gap-2 px-2 py-1 text-lime-600 hover:text-white hover:bg-lime-600 rounded-md"
title="Edytuj notatkę"
><FontAwesomeIcon :icon="['fas', 'pen-to-square']" /><span class="hidden sm:inline-block">Edytuj</span></InertiaLink>
<InertiaLink
as="button"
:href="`/dashboard/note/${note.id}/delete`"
class="flex items-center gap-2 px-2 py-1 text-red-600 hover:text-white hover:bg-red-600 rounded-md"
title="Usuń notatkę"
><FontAwesomeIcon :icon="['fas', 'trash']" /><span class="hidden sm:inline-block">Usuń</span></InertiaLink>
</div>
</header>
<div class="mb-4">
<header>
<h2 class="text-2xl font-roboto font-light pb-3">Podstawowe informacje</h2>
</header>
<div class="flex flex-col gap-4">
<div>
<div class="text-gray-500 pb-0.5">Tytuł</div>
<p
class="w-full min-w-full max-w-full px-2.5 py-2 border-b-2 rounded-md bg-white whitespace-nowrap overflow-hidden overflow-ellipsis"
>{{ note.title }}</p>
</div>
<div class="md:col-span-2">
<div class="text-gray-500 pb-0.5">Notatka</div>
<div class="w-full min-w-full max-w-full px-2.5 py-2 border-b-2 rounded-md bg-white">
<p
v-for="(noteLine, key) in noteLines"
:key="key"
>{{ noteLine }}</p>
</div>
</div>
</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,37 @@
<script setup>
import { router } from '@inertiajs/vue3';
const props = defineProps({
project: {
type: Object,
required: true,
},
});
function confirmDelete() {
router.delete(`/dashboard/project/${props.project.id}/delete`);
}
</script>
<template>
<InertiaHead title="Nowy projekt" />
<div class="p-4">
<header class="pb-4">
<h1 class="text-3xl font-roboto font-light">Usuwanie projektu</h1>
</header>
<div class="max-w-[600px]">
<p class="mb-4">Na pewno usunąć projekt o nazwie {{ project.title }}?</p>
<div class="grid grid-cols-3 gap-2">
<InertiaLink
as="button"
href="/dashboard"
class="col-span-1 flex justify-center items-center gap-3 w-full px-2 py-1 border-t-4 border-b-4 border-transparent hover:border-b-black"
><FontAwesomeIcon :icon="['fas', 'backward']" />Anuluj</InertiaLink>
<button
@click.prevent="confirmDelete"
class="col-span-2 flex justify-center items-center gap-3 w-full px-2 py-1 rounded-md bg-red-600 border-4 border-red-600 text-white text-lg hover:bg-transparent hover:text-red-600"
><FontAwesomeIcon :icon="['fas', 'trash']" />Usuń projekt {{ project.title }}</button>
</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,160 @@
<script setup>
import { computed, ref } from 'vue';
import { useForm } from '@inertiajs/inertia-vue3';
import Input from '../../Share/Components/Input.vue';
const categories = ref([]);
const categoryToString = computed({
get: () => categories.value.join(', '),
set: (val) => {
val = val.replace(', ', ',').replace(' , ', ',').replace(' ,', ',');
val = val.split(',');
val.forEach((element, key) => {
element = element.trim();
val[key] = slug(element);
});
categories.value = val;
}
});
const form = useForm({
title: null,
author: null,
categories: categoryToString,
release_date: null,
update_date: null,
image_small: null,
image_medium: null,
image_large: null,
project_url: null,
project_version: null,
description: null,
visible: Boolean(false),
});
function createProject() {
form.post('/dashboard/project');
}
function slug(str) {
str = str.replace(/^\s+|\s+$/g, ''); // trim
str = str.toLowerCase();
// remove accents, swap ñ for n, etc
var from = "ãàáäâẽèéëêìíïîõòóöôùúüûñç·/_,:;";
var to = "aaaaaeeeeeiiiiooooouuuunc------";
for (var i = 0, l = from.length; i < l; i++) {
str = str.replace(new RegExp(from.charAt(i), 'g'), to.charAt(i));
}
str = str.replace(/[^a-z0-9 -]/g, '') // remove invalid chars
.replace(/\s+/g, '-') // collapse whitespace and replace by -
.replace(/-+/g, '-'); // collapse dashes
return str;
};
</script>
<template>
<InertiaHead title="Nowy projekt" />
<div class="p-4">
<header class="pb-4">
<div class="flex items-center gap-2">
<InertiaLink
as="button"
href="/dashboard"
class="px-2 text-xl text-gray-700 hover:text-black"
title="Wróc do dashboard"><FontAwesomeIcon :icon="['fas', 'caret-left']" /></InertiaLink>
<h1 class="text-3xl font-roboto font-light">Nowy projekt</h1>
</div>
</header>
<div>
<form class="flex flex-col gap-4" @submit.prevent="createProject">
<Input
id="title"
label="Tytuł"
placeholder="Nazwa projektu"
v-model="form.title"
:error="form.errors.title"
/>
<Input
id="author"
label="Autor"
placeholder="Imię i nazwisko"
v-model="form.author"
:error="form.errors.author"
/>
<Input
id="categories"
label="Kategorie"
placeholder="Kategorie projektu"
v-model="form.categories"
:error="form.errors.categories"
/>
<Input
id="release_date"
label="Data pierwszego wydania"
type="date"
v-model="form.release_date"
:error="form.errors.release_date"
/>
<Input
id="update_date"
label="Data aktualizacji"
type="date"
v-model="form.update_date"
:error="form.errors.update_date"
/>
<Input
id="image_small"
label="Zdjęcie projekty - małe"
v-model="form.image_small"
:error="form.errors.image_small"
/>
<Input
id="image_medium"
label="Zdjęcie projekty - średnie"
v-model="form.image_medium"
:error="form.errors.image_medium"
/>
<Input
id="image_large"
label="Zdjęcie projekty - duże"
v-model="form.image_large"
:error="form.errors.image_large"
/>
<Input
id="project_url"
label="Adres projektu"
placeholder="Adres www strony projektu"
v-model="form.project_url"
:error="form.errors.project_url"
/>
<Input
id="project_version"
label="Wersja projektu"
placeholder="v1.0.0"
v-model="form.project_version"
:error="form.errors.project_version"
/>
<Input
id="description"
label="Opis"
type="textarea"
placeholder="Ładny opis"
v-model="form.description"
:error="form.errors.description"
textareaHeight="200px"
/>
<Input
id="visible"
label="Widoczny"
type="checkbox"
v-model="form.visible"
/>
<button class="px-0.5 py-1 rounded-lg bg-[#436da7] border-4 border-[#436da7] text-white text-lg hover:bg-transparent hover:text-[#436da7]">Dodaj projekt</button>
</form>
</div>
</div>
</template>

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