Доделал первую версию

This commit is contained in:
p.belezov 2024-10-28 16:36:45 +08:00
parent aa485fa2b1
commit 0fdd2cf7e0
20 changed files with 440 additions and 48 deletions

View File

@ -92,6 +92,16 @@ class AuthController extends Controller
return response()->json($request->user()); return response()->json($request->user());
} }
/**
* Get the authenticated User
*
* @return [json] user object
*/
public function username(Request $request)
{
return response()->json($request->user()['name']);
}
/** /**
* Logout user (Revoke the token) * Logout user (Revoke the token)
* *

View File

@ -4,6 +4,7 @@ namespace App\Http\Controllers;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use App\Models\Wish; use App\Models\Wish;
use App\Models\User;
class WishesController extends Controller class WishesController extends Controller
{ {
@ -12,6 +13,19 @@ class WishesController extends Controller
return Wish::select('id', 'name', 'price', 'url')->where('user_id', '=', $user_id)->get(); return Wish::select('id', 'name', 'price', 'url')->where('user_id', '=', $user_id)->get();
} }
public function getUsername(Request $request){
$request->validate([
'user_id' => 'required|exists:users,id'
]);
$username = ['username' => User::find($request->get('user_id'))['name']];
return response()->json($username, 201);
}
public function getWishById(Request $request, string $id)
{
return Wish::select('id', 'name', 'price', 'url')->where('id', '=', $id)->get();
}
public function create(Request $request) public function create(Request $request)
{ {
$request->validate([ $request->validate([
@ -25,15 +39,20 @@ class WishesController extends Controller
return response()->json($wish, 201); return response()->json($wish, 201);
} }
public function update(Request $request, Wish $wish) public function update(Request $request)
{ {
$request->validate([ $request->validate([
'id' => 'required|exists:wishes,id',
'user_id' => 'required|exists:users,id',
'name' => 'required|string|max:256', 'name' => 'required|string|max:256',
'price' => 'nullable|numeric', 'price' => 'nullable|numeric',
'url' => 'nullable|url', 'url' => 'nullable|url',
]); ]);
$wish = Wish::find($request->get('id'));
$wish->update($request->all()); $wish->name = $request->get('name');
$wish->price = $request->get('price');
$wish->url = $request->get('url');
$wish->save();
return response()->json($wish); return response()->json($wish);
} }

16
resources/PublicApp.vue Normal file
View File

@ -0,0 +1,16 @@
<template>
<v-app>
<Public></Public>
</v-app>
</template>
<script>
import Public from "./views/Public.vue";
export default {
name: "PublicApp",
components: {Public}
}
</script>

View File

@ -16,3 +16,15 @@
background-color: #212022!important; background-color: #212022!important;
color: white!important; color: white!important;
} }
a:link {
color: aqua;
}
a:visited {
color: darkorange;
}
a:active {
color: crimson;
}

View File

@ -5,5 +5,11 @@ export const rules = {
}, },
notNull: value => { notNull: value => {
return (value !== null && value !== undefined && value !== '') || 'Поле не может быть пустым'; return (value !== null && value !== undefined && value !== '') || 'Поле не может быть пустым';
},
id: value => {
return (value !== null && value !== undefined && typeof value == 'number') || 'Неверный id';
},
price: value => {
return (typeof value == 'number' || typeof value == 'undefined' || value === null) || 'Стоимость должна быть числом';
} }
} }

View File

@ -0,0 +1,19 @@
import './js/bootstrap';
import {createApp} from 'vue'
import PublicApp from './PublicApp.vue';
import { createVuetify } from 'vuetify'
import 'vuetify/styles'
import * as components from 'vuetify/components'
import * as directives from 'vuetify/directives'
import '@mdi/font/css/materialdesignicons.css'
import { createMemoryHistory, createRouter } from 'vue-router'
import {createPinia} from "pinia";
const vuetify = createVuetify({
components,
directives
})
const pinia = createPinia()
createApp(PublicApp).use(vuetify).use(pinia).mount("#app")

View File

@ -82,6 +82,20 @@ export const useUserStore = defineStore('user', {
nullifyUser() { nullifyUser() {
this.setUser(null); this.setUser(null);
this.setToken(null); this.setToken(null);
},
async getUsername(id){
let result = null;
await axios.get('/api/wish/username',
{
params:
{
user_id: id
}
}
).then((res) => {
result = res;
});
return result;
} }
}, },
}) })

View File

@ -57,6 +57,39 @@ export const useWishStore = defineStore('wish', {
).then((response)=>{ ).then((response)=>{
return response; return response;
}); });
} },
async update(id, user_id, name, price, url, token){
await axios.post(`/api/wish/update`,
{
id: id,
user_id: user_id,
name: name,
price: price,
url: url
},
{
headers: {
Authorization: `Bearer ${token}`,
token: token
},
}
).then((response)=>{
return response;
});
},
async getWishById(id, token){
let result = null;
await axios.get(`/api/wish/by_id/${id.toString()}`,
{
headers: {
Authorization: `Bearer ${token}`,
token: token
}
}
).then((response)=>{
result = response.data;
});
return result;
},
}, },
}) })

View File

@ -17,7 +17,8 @@ export default {
rememberMe: false, rememberMe: false,
errorMessage: '', errorMessage: '',
errorMessageContainerStyle: '', errorMessageContainerStyle: '',
showPassword: false showPassword: false,
loading: false
}), }),
methods: { methods: {
validate(){ validate(){
@ -32,12 +33,14 @@ export default {
} }
}, },
loginAction(){ loginAction(){
this.loading = true;
let validation = this.validate(); let validation = this.validate();
if (validation !== true){ if (validation !== true){
alert(validation); alert(validation);
return; return;
} }
this.userStore.login(this.email, this.password, this.rememberMe).then((isLogged) => { this.userStore.login(this.email, this.password, this.rememberMe).then((isLogged) => {
this.loading = false;
if (typeof isLogged == "boolean") { if (typeof isLogged == "boolean") {
if (isLogged){ if (isLogged){
this.errorMessage = ''; this.errorMessage = '';
@ -83,7 +86,7 @@ export default {
<v-checkbox v-model="rememberMe" label="Запомнить меня"></v-checkbox> <v-checkbox v-model="rememberMe" label="Запомнить меня"></v-checkbox>
</div> </div>
<v-label :style="errorMessageContainerStyle">{{ errorMessage }}</v-label> <v-label :style="errorMessageContainerStyle">{{ errorMessage }}</v-label>
<v-btn @click="this.loginAction">Войти</v-btn> <v-btn @click="this.loginAction" :loading="loading">Войти</v-btn>
</div> </div>
</template> </template>

View File

@ -14,7 +14,8 @@ export default {
errorMessage: '', errorMessage: '',
errorMessageContainerStyle: 'display: none;', errorMessageContainerStyle: 'display: none;',
showPassword: false, showPassword: false,
showRepeatPassword: false showRepeatPassword: false,
loading: false
}), }),
computed: { computed: {
rules() { rules() {
@ -37,12 +38,14 @@ export default {
return check === null ? true : check; return check === null ? true : check;
}, },
registrationAction(){ registrationAction(){
this.loading = true;
let validation = this.validate(); let validation = this.validate();
if (validation !== true){ if (validation !== true){
alert(validation); alert(validation);
return; return;
} }
this.userStore.registration(this.login, this.email, this.password, this.repeatPassword).then((isRegistred)=>{ this.userStore.registration(this.login, this.email, this.password, this.repeatPassword).then((isRegistred)=>{
this.loading = false;
if (typeof isRegistred == "boolean") { if (typeof isRegistred == "boolean") {
if (isRegistred){ if (isRegistred){
this.errorMessage = ''; this.errorMessage = '';
@ -100,7 +103,7 @@ export default {
required required
></v-text-field> ></v-text-field>
<v-label :style="errorMessageContainerStyle">{{ errorMessage }}</v-label> <v-label :style="errorMessageContainerStyle">{{ errorMessage }}</v-label>
<v-btn @click="this.registrationAction">Регистрация</v-btn> <v-btn @click="this.registrationAction" :loading="loading">Регистрация</v-btn>
</div> </div>
<!-- <v-form v-model="valid" @submit.prevent>--> <!-- <v-form v-model="valid" @submit.prevent>-->
<!-- --> <!-- -->

View File

@ -0,0 +1,36 @@
<template>
<v-card class="bg-gradient" style="height: 100%">
<v-card-text class="d-flex justify-center align-center">
<v-card class="align-center justify-center h-auto w-66 card-bg">
<v-card-title class="d-flex justify-space-between">
<div>
<span>Добро пожаловать в </span>
<span><a href="/" class="link-no-decor">Wishlist</a></span>
</div>
</v-card-title>
<v-card-text class="d-flex justify-center align-center h-auto">
<ShowWhishlist/>
</v-card-text>
</v-card>
</v-card-text>
</v-card>
</template>
<script>
import ShowWhishlist from "./PublicWishlist/ShowWhishlist.vue";
export default {
name: "Public",
components: {ShowWhishlist}
}
</script>
<style scoped>
.bg-gradient {
background: linear-gradient(-45deg, #000610, #000f25, #00152f);
background-size: 100% 100%;
height: 100vh;
}
</style>

View File

@ -0,0 +1,54 @@
<script>
import { useWishStore } from "../../store/wish.js";
import { useUserStore } from "../../store/user.js";
export default {
name: "ShowWhishlist",
data: () => ({
wishes: [],
wishStore: useWishStore(),
userStore: useUserStore(),
fetching: false,
username: ''
}),
mounted() {
let urlArray = window.location.href.split('/');
let user_id = urlArray[urlArray.length - 1];
this.fetching = true;
this.userStore.getUsername(user_id).then((res)=>{
console.log(res);
this.username = res.data.username;
this.wishStore.getUserWishes(user_id).then((responce)=>{
this.wishes = responce;
this.fetching = false;
});
});
}
}
</script>
<template>
<v-skeleton-loader color="grey-darken-4" type="table" v-if="fetching"></v-skeleton-loader>
<v-table v-else class="card-bg w-100 h-auto mt-5 pa-3">
<thead>
<tr>
<th colspan="3" class="text-center text-h5">Список пользователя {{ this.username }}</th>
</tr>
<tr>
<th class="text-subtitle-1">Наименование</th>
<th class="text-subtitle-1">Цена</th>
<th class="text-subtitle-1">Ссылка</th>
</tr>
</thead>
<tbody>
<tr v-for="wish in wishes">
<td>{{ wish['name'] }}</td>
<td>{{ wish['price'] }}</td>
<td><a target="_blank" :href="wish['url']">{{ wish['url'] }}</a></td>
</tr>
</tbody>
</v-table>
</template>
<style scoped>
</style>

View File

@ -12,7 +12,8 @@
<span v-if="isAuthenticated" class="link-no-decor align-end" @click="logout">Выйти</span> <span v-if="isAuthenticated" class="link-no-decor align-end" @click="logout">Выйти</span>
</v-card-title> </v-card-title>
<v-card-text class="d-flex justify-center align-center"> <v-card-text class="d-flex justify-center align-center">
<router-view/> <v-skeleton-loader class="w-100" color="grey-darken-4" type="card" v-if="fetchingUser"></v-skeleton-loader>
<router-view v-else/>
</v-card-text> </v-card-text>
</v-card> </v-card>
</v-card-text> </v-card-text>
@ -26,7 +27,8 @@ export default {
name: "Welcome", name: "Welcome",
data: () => ({ data: () => ({
isAuthenticated: false, isAuthenticated: false,
userStore: useUserStore() userStore: useUserStore(),
fetchingUser: false
}), }),
computed: { computed: {
user() { user() {
@ -40,9 +42,11 @@ export default {
} }
}, },
mounted() { mounted() {
this.fetchingUser = true;
this.$router.push('/auth_options'); this.$router.push('/auth_options');
watch(this.userStore, (newStore, oldStore)=>{ watch(this.userStore, (newStore, oldStore)=>{
this.isAuthenticated = newStore.user !== null && newStore.user !== undefined; this.isAuthenticated = newStore.user !== null && newStore.user !== undefined;
this.fetchingUser = false;
if (this.isAuthenticated) { if (this.isAuthenticated) {
this.$router.push('/wishlist'); this.$router.push('/wishlist');
} else { } else {

View File

@ -9,7 +9,8 @@ export default {
wishStore: useWishStore(), wishStore: useWishStore(),
name: null, name: null,
price: null, price: null,
url: null url: null,
creating: false
}), }),
computed: { computed: {
rules() { rules() {
@ -22,15 +23,33 @@ export default {
}, },
methods: { methods: {
validate(){ validate(){
return rules.notNull(this.name); if (this.price == ''){
this.price = null;
}
if (typeof this.price == 'string' && /^\d+[\.,\,]?\d+$/.test(this.price)){
this.price = parseFloat(this.price)
}
let validateName = rules.notNull(this.name);
let validatePrice = rules.price(this.price);
let check = null;
let valid = [validateName, validatePrice];
valid.forEach((element)=>{
if (typeof element !== "boolean"){
check = element;
}
});
return check === null ? true : check;
}, },
createWish(){ createWish(){
this.creating = true;
let validation = this.validate(); let validation = this.validate();
if (typeof validation !== "boolean"){ if (typeof validation !== "boolean"){
this.creating = false;
alert(validation); alert(validation);
return; return;
} }
this.wishStore.create(this.userStore.user['id'], this.name, this.price, this.url, this.userStore.token).then(()=>{ this.wishStore.create(this.userStore.user['id'], this.name, this.price, this.url, this.userStore.token).then(()=>{
this.creating = false;
this.updateFrontWishes(); this.updateFrontWishes();
this.dialogCreate(); this.dialogCreate();
}); });
@ -51,7 +70,7 @@ export default {
<v-text-field class="w-100" label="Наименование" v-model="name" :rules="[rules.notNull]"></v-text-field> <v-text-field class="w-100" label="Наименование" v-model="name" :rules="[rules.notNull]"></v-text-field>
<v-text-field class="w-100" label="Стоимость" v-model="price"></v-text-field> <v-text-field class="w-100" label="Стоимость" v-model="price"></v-text-field>
<v-text-field class="w-100" label="Ссылка" v-model="url"></v-text-field> <v-text-field class="w-100" label="Ссылка" v-model="url"></v-text-field>
<v-btn class="w-33" @click="createWish">Создать</v-btn> <v-btn class="w-33" @click="createWish" :loading="creating">Создать</v-btn>
</v-card-text> </v-card-text>
</v-card> </v-card>
</template> </template>

View File

@ -0,0 +1,99 @@
<script>
import {useUserStore} from "../../store/user.js";
import {useWishStore} from "../../store/wish.js";
import {rules} from "../../js/rules.js";
import {watch} from "vue";
import {toRef, ref} from "vue";
export default {
name: "EditWish",
props: {
dialogEdit: Function,
updateFrontWishes: Function,
wish_id: Number
},
data: () => ({
userStore: useUserStore(),
wishStore: useWishStore(),
name: null,
price: null,
url: null,
fetching: true,
updating: false
}),
computed: {
rules() {
return rules
}
},
setup(props) {
const id = toRef(props, 'wish_id')
},
methods: {
validate(){
if (this.price == ""){
this.price = null;
}
if (typeof this.price == 'string' && /^\d+[\.,\,]?\d+$/.test(this.price)){
this.price = parseFloat(this.price)
}
let validateId = rules.id(this.wish_id);
let validateName = rules.notNull(this.name);
let validatePrice = rules.price(this.price);
let check = null;
let valid = [validateId, validateName, validatePrice];
valid.forEach((element)=>{
if (typeof element !== "boolean"){
check = element;
}
});
return check === null ? true : check;
},
editWish(){
this.updating = true;
let validation = this.validate();
if (typeof validation !== "boolean"){
console.log(this.price);
this.updating = false;
alert(validation);
return;
}
this.wishStore.update(this.wish_id, this.userStore.user['id'], this.name, this.price, this.url, this.userStore.token).then(()=>{
this.updating = false;
this.updateFrontWishes();
this.dialogEdit();
});
}
},
mounted() {
this.wishStore.getWishById(this.wish_id, this.userStore.token).then((response)=>{
this.name = response[0]['name'];
this.price = response[0]['price'];
this.url = response[0]['url'];
this.fetching = false;
})
}
}
</script>
<template>
<v-skeleton-loader color="grey-darken-4" type="card" v-if="fetching"></v-skeleton-loader>
<v-card class="card-bg" v-else>
<v-card-title class="d-flex justify-space-between">
<span>Редактирование элемента</span>
<span>
<v-icon @click="dialogEdit" class="cursor-pointer" color="white" icon="mdi-close-thick"></v-icon>
</span>
</v-card-title>
<v-card-text class="d-flex align-center flex-column w-100">
<v-text-field class="w-100" label="Наименование" v-model="name" :rules="[rules.notNull]"></v-text-field>
<v-text-field class="w-100" label="Стоимость" v-model="price"></v-text-field>
<v-text-field class="w-100" label="Ссылка" v-model="url"></v-text-field>
<v-btn :loading="updating" class="w-33" @click="editWish">Сохранить</v-btn>
</v-card-text>
</v-card>
</template>
<style scoped>
</style>

View File

@ -3,20 +3,25 @@ import {useUserStore} from "../../store/user.js";
import {useWishStore} from "../../store/wish.js"; import {useWishStore} from "../../store/wish.js";
import CreateWish from "./CreateWish.vue"; import CreateWish from "./CreateWish.vue";
import {ref} from "vue"; import {ref} from "vue";
import EditWish from "./EditWish.vue";
export default { export default {
name: "Wishlist", name: "Wishlist",
components: {CreateWish}, components: {EditWish, CreateWish},
data: () => ({ data: () => ({
userStore: useUserStore(), userStore: useUserStore(),
wishStore: useWishStore(), wishStore: useWishStore(),
wishesList: [], wishesList: [],
fetching: true, fetching: true,
dialogCreate: ref(false), dialogCreate: ref(false),
dialogEdit: ref(false) dialogEdit: ref(false),
wishToEditId: ref(0),
wishlistLink: '',
snackbar: false
}), }),
mounted() { mounted() {
this.wishStore.getUserWishes(this.userStore.user['id']).then((wishes)=>{ this.wishStore.getUserWishes(this.userStore.user['id']).then((wishes)=>{
this.wishesList = wishes; this.wishesList = wishes;
this.wishlistLink = window.location.origin + '/wishlist/' + this.userStore.user['id'];
this.fetching = false this.fetching = false
}); });
}, },
@ -44,44 +49,64 @@ export default {
}) })
}, },
editWish(id){ editWish(id){
this.wishToEditId = id;
this.dialogEdit = true;
}, },
createWish(){ getWishToEditId(){
return this.wishToEditId;
},
copyLink(){
navigator.clipboard.writeText(this.wishlistLink);
this.snackbar = true;
} }
} }
} }
</script> </script>
<template> <template>
<!-- <v-label class="link-no-decor" @click="this.tryWishes">Hello world!</v-label>--> <div class="d-flex flex-column">
<v-skeleton-loader color="grey-darken-4" type="table" v-if="fetching"></v-skeleton-loader> <v-skeleton-loader color="grey-darken-4" type="table" v-if="fetching"></v-skeleton-loader>
<v-table v-else class="card-bg w-100 h-auto mt-5 pa-3"> <div v-if="!fetching" class="d-flex justify-center align-center w-100 pt-5">
<thead> <v-text-field
<tr> class="w-33"
<th>Наименование</th> append-inner-icon="mdi-content-copy"
<th>Цена</th> @click:append-inner="copyLink"
<th>Ссылка</th> readonly
<th></th> >
<th></th> {{ wishlistLink }}
</tr> </v-text-field>
</thead> <v-snackbar v-model="snackbar">Текст скопирован!</v-snackbar>
<tbody> </div>
<tr v-for="wish in wishesList"> <v-table v-if="!fetching" class="card-bg w-100 h-auto mt-5 pa-3">
<td>{{ wish['name'] }}</td> <thead>
<td>{{ wish['price'] }}</td> <tr>
<td><a target="_blank" :href="wish['url']">{{ wish['url'] }}</a></td> <th class="text-subtitle-1">Наименование</th>
<td><v-icon @click="" class="cursor-pointer" color="white" icon="mdi-pencil"></v-icon></td> <th class="text-subtitle-1">Цена</th>
<td><v-icon @click="removeWish(wish['id'])" class="cursor-pointer" color="white" icon="mdi-trash-can"></v-icon></td> <th class="text-subtitle-1">Ссылка</th>
</tr> <th class="text-subtitle-1"></th>
<tr class="text-center"> <th class="text-subtitle-1"></th>
<td colspan="5"><v-btn @click="dialogCreate = true" color="#212022" elevation="0" block><v-icon class="cursor-pointer" icon="mdi-plus-thick"></v-icon></v-btn></td> </tr>
</tr> </thead>
</tbody> <tbody>
<v-dialog v-model="dialogCreate" class="w-66"> <tr v-for="wish in wishesList">
<CreateWish :dialogCreate="dialogCreateClose" :updateFrontWishes="updateFrontWishes"/> <td>{{ wish['name'] }}</td>
</v-dialog> <td>{{ wish['price'] }}</td>
</v-table> <td><a target="_blank" :href="wish['url']">{{ wish['url'] }}</a></td>
<td><v-icon @click="editWish(wish['id'])" class="cursor-pointer" color="white" icon="mdi-pencil"></v-icon></td>
<td><v-icon @click="removeWish(wish['id'])" class="cursor-pointer" color="white" icon="mdi-trash-can"></v-icon></td>
</tr>
<tr class="text-center">
<td colspan="5"><v-btn @click="dialogCreate = true" color="#212022" elevation="0" block><v-icon class="cursor-pointer" icon="mdi-plus-thick"></v-icon></v-btn></td>
</tr>
</tbody>
<v-dialog v-model="dialogCreate" class="w-66">
<CreateWish :dialogCreate="dialogCreateClose" :updateFrontWishes="updateFrontWishes"/>
</v-dialog>
<v-dialog v-model="dialogEdit" class="w-66">
<EditWish :dialogEdit="dialogEditClose" :updateFrontWishes="updateFrontWishes" :wish_id="wishToEditId"/>
</v-dialog>
</v-table>
</div>
</template> </template>
<style scoped> <style scoped>

View File

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Wishlist</title>
<link rel="icon" href="/images/favicon.svg" sizes="any" type="image/svg+xml">
@vite('resources/publicWishlist.js')
@vite('resources/css/app.css')
</head>
<body class="antialiased">
<div id="app"></div>
</body>
</html>

View File

@ -31,10 +31,12 @@ Route::group(['prefix' => 'auth'], function () {
}); });
Route::group(['prefix' => 'wish'], function () { Route::group(['prefix' => 'wish'], function () {
Route::get('username', [WishesController::class, 'getUsername']);
Route::get('user_wishes/{user_id}', [WishesController::class, 'getUserWishes']); Route::get('user_wishes/{user_id}', [WishesController::class, 'getUserWishes']);
Route::group(['middleware' => 'auth:sanctum'], function() { Route::group(['middleware' => 'auth:sanctum'], function() {
Route::post('create', [WishesController::class, 'create']); Route::post('create', [WishesController::class, 'create']);
Route::post('update', [WishesController::class, 'update']); Route::post('update', [WishesController::class, 'update']);
Route::post('destroy', [WishesController::class, 'destroy']); Route::post('destroy', [WishesController::class, 'destroy']);
Route::get('by_id/{id}', [WishesController::class, 'getWishById']);
}); });
}); });

View File

@ -16,3 +16,7 @@ use Illuminate\Support\Facades\Route;
Route::get('/', function () { Route::get('/', function () {
return view('welcome'); return view('welcome');
}); });
Route::get('/wishlist/{user_id}', function () {
return view('public');
});

View File

@ -6,7 +6,7 @@ export default defineConfig({
plugins: [ plugins: [
vue(), vue(),
laravel({ laravel({
input: ['resources/css/app.css', 'resources/app.js'], input: ['resources/css/app.css', 'resources/app.js', 'resources/publicWishlist.js'],
refresh: true, refresh: true,
}), }),
], ],