Compare commits
10 Commits
54db2e8e2b
...
ffee3b257d
Author | SHA1 | Date | |
---|---|---|---|
ffee3b257d | |||
957e90aff6 | |||
252f5ee6b7 | |||
2a2869e2c6 | |||
6a033d108c | |||
620217e1d5 | |||
a7e93681f3 | |||
9d7da548e3 | |||
2349008131 | |||
908b1e4bec |
@ -14,3 +14,6 @@ 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
|
||||
|
27
app/Http/Controllers/Api/MessageController.php
Normal file
27
app/Http/Controllers/Api/MessageController.php
Normal 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.'
|
||||
]);
|
||||
}
|
||||
}
|
@ -49,12 +49,24 @@ class CVController extends Controller
|
||||
'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', [
|
||||
@ -64,15 +76,30 @@ class CVController extends Controller
|
||||
|
||||
public function update(CVRequest $request, CV $cv): RedirectResponse
|
||||
{
|
||||
$cv->update([
|
||||
'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'),
|
||||
$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'));
|
||||
|
45
app/Http/Controllers/Dashboard/MessageController.php
Normal file
45
app/Http/Controllers/Dashboard/MessageController.php
Normal 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(['success' => 'Wiadomość od '. $sender .' została usunięta']);
|
||||
}
|
||||
}
|
@ -18,7 +18,6 @@ class Kernel extends HttpKernel
|
||||
\App\Http\Middleware\TrimStrings::class,
|
||||
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
|
||||
Core::class,
|
||||
\App\Http\Middleware\HandleInertiaRequests::class,
|
||||
];
|
||||
|
||||
protected $middlewareGroups = [
|
||||
@ -29,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' => [
|
||||
|
@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Illuminate\Http\Middleware\TrustProxies as Middleware;
|
||||
|
@ -18,6 +18,20 @@ class CVRequest extends FormRequest
|
||||
'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);
|
||||
}
|
||||
}
|
||||
|
34
app/Http/Requests/MessageRequest.php
Normal file
34
app/Http/Requests/MessageRequest.php
Normal 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.',
|
||||
];
|
||||
}
|
||||
}
|
@ -23,7 +23,7 @@ class ProjectRequest extends FormRequest
|
||||
|
||||
'project_url' => 'nullable|string',
|
||||
'project_version' => 'nullable|string',
|
||||
'description' => 'nullable|string',
|
||||
'description' => 'required|string|min:3',
|
||||
'visible' => 'required|boolean'
|
||||
];
|
||||
}
|
||||
|
@ -21,9 +21,16 @@ class FullCVResource extends JsonResource
|
||||
'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,
|
||||
'position' => $this->position,
|
||||
'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'),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
17
app/Http/Resources/MessageCollection.php
Normal file
17
app/Http/Resources/MessageCollection.php
Normal 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,
|
||||
];
|
||||
}
|
||||
}
|
22
app/Http/Resources/MessageResource.php
Normal file
22
app/Http/Resources/MessageResource.php
Normal 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,
|
||||
];
|
||||
}
|
||||
}
|
@ -19,7 +19,9 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
* @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
|
||||
{
|
||||
@ -30,6 +32,8 @@ class CV extends Model
|
||||
protected $casts = [
|
||||
'locations' => 'array',
|
||||
'views' => 'integer',
|
||||
'sended' => 'boolean',
|
||||
'sended_timestamp' => 'datetime',
|
||||
];
|
||||
|
||||
protected function phoneNumber(): Attribute
|
||||
|
23
app/Models/Message.php
Normal file
23
app/Models/Message.php
Normal 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;
|
||||
|
||||
/**
|
||||
* @param int $id
|
||||
* @param string $message
|
||||
* @param string $email
|
||||
* @param string $sender
|
||||
*/
|
||||
class Message extends Model
|
||||
{
|
||||
use HasFactory,
|
||||
SoftDeletes;
|
||||
|
||||
protected $guarded = [];
|
||||
}
|
@ -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');
|
||||
});
|
||||
}
|
||||
};
|
@ -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');
|
||||
}
|
||||
};
|
@ -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');
|
||||
});
|
||||
}
|
||||
};
|
@ -39,7 +39,7 @@ services:
|
||||
entrypoint: [ 'npm' ]
|
||||
ports:
|
||||
- '3000:3000'
|
||||
- '3001:3001'
|
||||
- '${VITE_PORT:-3001}:${VITE_PORT:-3001}'
|
||||
volumes:
|
||||
- .:/application
|
||||
networks:
|
||||
|
@ -32,6 +32,7 @@ const form = useForm({
|
||||
mission: missionToString,
|
||||
rodo: null,
|
||||
position: null,
|
||||
notes: null,
|
||||
});
|
||||
|
||||
function createCV() {
|
||||
@ -85,15 +86,23 @@ function createCV() {
|
||||
/>
|
||||
<Input
|
||||
id="position"
|
||||
label="Stanowisko"
|
||||
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"
|
||||
label="Misja - wstęp (opcjonalne)"
|
||||
placeholder="Krótki opis, list motywacyjny."
|
||||
v-model="form.mission"
|
||||
:error="form.errors.mission"
|
||||
@ -101,7 +110,7 @@ function createCV() {
|
||||
<Input
|
||||
id="rodo"
|
||||
type="textarea"
|
||||
label="RODO"
|
||||
label="RODO (opcjonalne)"
|
||||
placeholder="Klauzula informacyjna RODO"
|
||||
v-model="form.rodo"
|
||||
:error="form.errors.rodo"
|
||||
|
@ -39,6 +39,8 @@ const form = useForm({
|
||||
mission: missionToString,
|
||||
rodo: props.cv.rodo,
|
||||
position: props.cv.position,
|
||||
notes: props.cv.notes,
|
||||
sended: props.cv.sended.status,
|
||||
});
|
||||
|
||||
function updateCV() {
|
||||
@ -103,6 +105,14 @@ function updateCV() {
|
||||
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"
|
||||
@ -119,7 +129,20 @@ function updateCV() {
|
||||
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]">Edytuj CV</button>
|
||||
<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>
|
||||
|
@ -1,4 +1,6 @@
|
||||
<script setup>
|
||||
import EmptyState from '@/Share/Components/EmptyState.vue';
|
||||
|
||||
defineProps({
|
||||
cvs: {
|
||||
type: Object,
|
||||
@ -39,7 +41,7 @@ function copySlug(slug) {
|
||||
><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">
|
||||
<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>
|
||||
@ -72,19 +74,20 @@ function copySlug(slug) {
|
||||
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 projekt"><FontAwesomeIcon :icon="['fas', 'pen-to-square']" /></InertiaLink>
|
||||
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ń projekt z listy"><FontAwesomeIcon :icon="['fas', 'trash']" /></InertiaLink>
|
||||
title="Usuń CV z listy"><FontAwesomeIcon :icon="['fas', 'trash']" /></InertiaLink>
|
||||
</td>
|
||||
</InertiaLink>
|
||||
</tbody>
|
||||
</table>
|
||||
<div v-else>
|
||||
Pusta lista
|
||||
</div>
|
||||
<EmptyState v-else :icon="['fas', 'file']">
|
||||
<template #title>Nie znaleziono</template>
|
||||
<template #text>Nie dodano jeszcze CV do listy.</template>
|
||||
</EmptyState>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -1,5 +1,8 @@
|
||||
<script setup>
|
||||
defineProps({
|
||||
import { computed } from 'vue';
|
||||
import { router } from '@inertiajs/inertia';
|
||||
|
||||
const props = defineProps({
|
||||
cv: {
|
||||
type: Object,
|
||||
required: true,
|
||||
@ -9,10 +12,25 @@ defineProps({
|
||||
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">
|
||||
@ -23,27 +41,30 @@ defineProps({
|
||||
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-2">
|
||||
<div class="flex gap-3 sm:gap-2">
|
||||
<a
|
||||
class="px-2 py-1 text-blue-600 hover:text-blue-800"
|
||||
:href="`https://cv.kamilcraft.com/show/${cv.token}`"
|
||||
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']" /></a>
|
||||
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']" />Edytuj</InertiaLink>
|
||||
><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']" />Usuń</InertiaLink>
|
||||
><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>
|
||||
@ -73,6 +94,30 @@ defineProps({
|
||||
<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>
|
||||
|
37
resources/js/Pages/Messages/ConfirmDelete.vue
Normal file
37
resources/js/Pages/Messages/ConfirmDelete.vue
Normal 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>
|
65
resources/js/Pages/Messages/Index.vue
Normal file
65
resources/js/Pages/Messages/Index.vue
Normal 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>
|
71
resources/js/Pages/Messages/Show.vue
Normal file
71
resources/js/Pages/Messages/Show.vue
Normal 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>
|
@ -1,4 +1,6 @@
|
||||
<script setup>
|
||||
import EmptyState from '@/Share/Components/EmptyState.vue';
|
||||
|
||||
defineProps({
|
||||
categories: {
|
||||
type: Array,
|
||||
@ -16,7 +18,7 @@ defineProps({
|
||||
href="/dashboard/category/create"
|
||||
class="bg-blue-400 hover:bg-blue-500 text-white px-2.5 py-1 rounded-full"><FontAwesomeIcon :icon="['fas', 'plus']" /></InertiaLink>
|
||||
</header>
|
||||
<ul class="flex flex-col gap-2">
|
||||
<ul v-if="categories.length" class="flex flex-col gap-2">
|
||||
<li
|
||||
v-for="(category, key) in categories"
|
||||
:key="key"
|
||||
@ -38,5 +40,9 @@ defineProps({
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<EmptyState v-else :icon="['fas', 'list']">
|
||||
<template #title>Brak kategorii</template>
|
||||
<template #text>Nie dodano jeszcze żadnej kategorii.</template>
|
||||
</EmptyState>
|
||||
</section>
|
||||
</template>
|
||||
|
31
resources/js/Share/Components/EmptyState.vue
Normal file
31
resources/js/Share/Components/EmptyState.vue
Normal file
@ -0,0 +1,31 @@
|
||||
<script setup>
|
||||
defineProps({
|
||||
icon: {
|
||||
type: String,
|
||||
defaut: ['far', 'folder-open'],
|
||||
},
|
||||
showDescription: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="text-center my-5 text-gray-500">
|
||||
<slot name="head">
|
||||
<FontAwesomeIcon :icon="icon" class="mx-auto w-12 h-12" />
|
||||
</slot>
|
||||
<h3 class="mt-2 text-sm font-medium">
|
||||
<slot name="title">Brak danych</slot>
|
||||
</h3>
|
||||
<p
|
||||
v-if="showDescription"
|
||||
class="text-sm"
|
||||
>
|
||||
<slot name="text">
|
||||
Nie znaleziono danych.
|
||||
</slot>
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
@ -27,13 +27,13 @@ defineProps({
|
||||
</InertiaLink>
|
||||
<nav>
|
||||
<ul class="flex gap-3 items-center font-bold">
|
||||
<li><InertiaLink class="text-white active:text-kamilcraft-green hover:text-black hover:underline" href="/dashboard">Dashboard</InertiaLink></li>
|
||||
<li><InertiaLink class="text-white active:text-kamilcraft-green hover:text-black hover:underline" href="/dashboard/cv">CV</InertiaLink></li>
|
||||
<li><InertiaLink class="text-white active:text-kamilcraft-green hover:text-black hover:underline" href="/dashboard/message">Msg</InertiaLink></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
<div v-if="messages?.info" class="max-w-screen-lg mx-2 lg:mx-auto mt-2 px-2 py-3 rounded-md bg-yellow-100 text-yellow-600 text-center">
|
||||
<div v-if="messages?.info" class="max-w-screen-lg mx-2 lg:mx-auto mt-2 px-2 py-3 rounded-md bg-yellow-100 text-yellow-600 text-center">
|
||||
{{ messages.info }}
|
||||
</div>
|
||||
<div v-if="messages?.error" class="max-w-screen-lg mx-2 lg:mx-auto mt-2 px-2 py-3 rounded-md bg-red-100 text-red-600 text-center">
|
||||
|
@ -1,4 +1,6 @@
|
||||
<script setup>
|
||||
import EmptyState from '@/Share/Components/EmptyState.vue';
|
||||
|
||||
defineProps({
|
||||
projects: {
|
||||
type: Array,
|
||||
@ -38,8 +40,9 @@ defineProps({
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<div v-else>
|
||||
Empty
|
||||
</div>
|
||||
<EmptyState v-else :icon="['fas', 'bars-progress']">
|
||||
<template #title>Brak projektów</template>
|
||||
<template #text>Nie dodano jeszcze żadnego projektu.</template>
|
||||
</EmptyState>
|
||||
</section>
|
||||
</template>
|
||||
|
@ -19,3 +19,5 @@ Route::prefix('project')->group(function() {
|
||||
});
|
||||
|
||||
Route::get('cv/{cv}', 'CVController@show');
|
||||
|
||||
Route::post('message', 'MessageController@store');
|
||||
|
@ -5,9 +5,19 @@ declare(strict_types=1);
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::name('admin.')->group(function () {
|
||||
Route::namespace('Dashboard')->middleware('auth')->group(function () {
|
||||
Route::namespace('Dashboard')->middleware('auth')->group(function (): void {
|
||||
Route::get('', 'AdminPanelController')->name('home');
|
||||
Route::name('cv.')->prefix('cv')->group(function () {
|
||||
Route::name('message.')->prefix('message')->group(function (): void {
|
||||
Route::get('', 'MessageController@index')
|
||||
->name('index');
|
||||
Route::get('{message}', 'MessageController@show')
|
||||
->name('show');
|
||||
Route::get('{message}/delete', 'MessageController@delete')
|
||||
->name('delete');
|
||||
Route::delete('{message}/delete', 'MessageController@destroy')
|
||||
->name('destroy');
|
||||
});
|
||||
Route::name('cv.')->prefix('cv')->group(function (): void {
|
||||
Route::get('', 'CVController@index')
|
||||
->name('index');
|
||||
Route::get('create', 'CVController@create')
|
||||
@ -16,6 +26,8 @@ Route::name('admin.')->group(function () {
|
||||
->name('store');
|
||||
Route::get('{cv}', 'CVController@show')
|
||||
->name('show');
|
||||
Route::post('{cv}/sended', 'CVController@updateSendStatus')
|
||||
->name('sended');
|
||||
Route::post('', 'CVController@store')
|
||||
->name('store');
|
||||
Route::get('{cv}/edit', 'CVController@edit')
|
||||
@ -27,7 +39,7 @@ Route::name('admin.')->group(function () {
|
||||
Route::delete('{cv}/delete', 'CVController@destroy')
|
||||
->name('destroy');
|
||||
});
|
||||
Route::name('category.')->prefix('category')->group(function () {
|
||||
Route::name('category.')->prefix('category')->group(function (): void {
|
||||
Route::get('create', 'CategoryController@create')
|
||||
->name('create');
|
||||
Route::post('', 'CategoryController@store')
|
||||
@ -44,7 +56,7 @@ Route::name('admin.')->group(function () {
|
||||
->name('destroy');
|
||||
});
|
||||
|
||||
Route::name('project.')->prefix('project')->group(function () {
|
||||
Route::name('project.')->prefix('project')->group(function (): void {
|
||||
Route::get('create', 'ProjectController@create')
|
||||
->name('create');
|
||||
Route::post('', 'ProjectController@store')
|
||||
@ -62,7 +74,7 @@ Route::name('admin.')->group(function () {
|
||||
});
|
||||
});
|
||||
|
||||
Route::name('auth.')->namespace('Auth')->group(function () {
|
||||
Route::name('auth.')->namespace('Auth')->group(function (): void {
|
||||
Route::get('login', 'LoginController@login')
|
||||
->name('login');
|
||||
Route::post('login', 'LoginController@authenticate')
|
||||
|
71
vite.config.js
vendored
71
vite.config.js
vendored
@ -1,40 +1,43 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import { defineConfig, loadEnv } from 'vite';
|
||||
import vue from '@vitejs/plugin-vue';
|
||||
import laravel from 'laravel-vite-plugin';
|
||||
import { networkInterfaces } from 'os'
|
||||
import { networkInterfaces } from 'os';
|
||||
|
||||
export default defineConfig({
|
||||
server: {
|
||||
host: Object.values(networkInterfaces()).flat().find(i => i.family === 'IPv4' && !i.internal).address,
|
||||
port: 3001,
|
||||
hmr: {
|
||||
host: 'localhost',
|
||||
},
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': '/resources/js',
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
vue({
|
||||
template: {
|
||||
transformAssetUrls: {
|
||||
base: null,
|
||||
includeAbsolute: false,
|
||||
},
|
||||
export default defineConfig((mode) => {
|
||||
const env = loadEnv(mode, process.cwd(), "");
|
||||
return {
|
||||
server: {
|
||||
host: Object.values(networkInterfaces()).flat().find(i => i.family === 'IPv4' && !i.internal).address,
|
||||
port: parseInt(env.VITE_PORT ?? 3001),
|
||||
hmr: {
|
||||
host: 'localhost',
|
||||
},
|
||||
}),
|
||||
laravel({
|
||||
input: 'resources/js/app.js',
|
||||
ssr: 'resources/js/ssr.js',
|
||||
refresh: true,
|
||||
}),
|
||||
],
|
||||
ssr: {
|
||||
noExternal: [
|
||||
'@inertiajs/server',
|
||||
'@vue/runtime-dom'
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': '/resources/js',
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
vue({
|
||||
template: {
|
||||
transformAssetUrls: {
|
||||
base: null,
|
||||
includeAbsolute: false,
|
||||
},
|
||||
},
|
||||
}),
|
||||
laravel({
|
||||
input: 'resources/js/app.js',
|
||||
ssr: 'resources/js/ssr.js',
|
||||
refresh: true,
|
||||
}),
|
||||
],
|
||||
},
|
||||
ssr: {
|
||||
noExternal: [
|
||||
'@inertiajs/server',
|
||||
'@vue/runtime-dom'
|
||||
],
|
||||
},
|
||||
}
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user