auth front and back #2
@ -6,11 +6,12 @@ namespace App\Models;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Laravel\Sanctum\HasApiTokens;
|
||||
|
||||
class User extends Authenticatable
|
||||
{
|
||||
/** @use HasFactory<\Database\Factories\UserFactory> */
|
||||
use HasFactory, Notifiable;
|
||||
use HasApiTokens, HasFactory, Notifiable;
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
|
||||
@ -7,6 +7,7 @@ use Illuminate\Foundation\Configuration\Middleware;
|
||||
return Application::configure(basePath: dirname(__DIR__))
|
||||
->withRouting(
|
||||
web: __DIR__.'/../routes/web.php',
|
||||
api: __DIR__.'/../routes/api.php',
|
||||
commands: __DIR__.'/../routes/console.php',
|
||||
health: '/up',
|
||||
)
|
||||
|
||||
@ -11,6 +11,7 @@
|
||||
"require": {
|
||||
"php": "^8.2",
|
||||
"laravel/framework": "^12.0",
|
||||
"laravel/sanctum": "^4.2",
|
||||
"laravel/tinker": "^2.10.1"
|
||||
},
|
||||
"require-dev": {
|
||||
@ -75,4 +76,4 @@
|
||||
},
|
||||
"minimum-stability": "stable",
|
||||
"prefer-stable": true
|
||||
}
|
||||
}
|
||||
|
||||
66
thanasoft-back/composer.lock
generated
66
thanasoft-back/composer.lock
generated
@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "c514d8f7b9fc5970bdd94287905ef584",
|
||||
"content-hash": "8f387a0734f3bf879214e4aa2fca6e2f",
|
||||
"packages": [
|
||||
{
|
||||
"name": "brick/math",
|
||||
@ -1332,6 +1332,70 @@
|
||||
},
|
||||
"time": "2025-09-19T13:47:56+00:00"
|
||||
},
|
||||
{
|
||||
"name": "laravel/sanctum",
|
||||
"version": "v4.2.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/laravel/sanctum.git",
|
||||
"reference": "fd6df4f79f48a72992e8d29a9c0ee25422a0d677"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/laravel/sanctum/zipball/fd6df4f79f48a72992e8d29a9c0ee25422a0d677",
|
||||
"reference": "fd6df4f79f48a72992e8d29a9c0ee25422a0d677",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-json": "*",
|
||||
"illuminate/console": "^11.0|^12.0",
|
||||
"illuminate/contracts": "^11.0|^12.0",
|
||||
"illuminate/database": "^11.0|^12.0",
|
||||
"illuminate/support": "^11.0|^12.0",
|
||||
"php": "^8.2",
|
||||
"symfony/console": "^7.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"mockery/mockery": "^1.6",
|
||||
"orchestra/testbench": "^9.0|^10.0",
|
||||
"phpstan/phpstan": "^1.10",
|
||||
"phpunit/phpunit": "^11.3"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"providers": [
|
||||
"Laravel\\Sanctum\\SanctumServiceProvider"
|
||||
]
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Laravel\\Sanctum\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Taylor Otwell",
|
||||
"email": "taylor@laravel.com"
|
||||
}
|
||||
],
|
||||
"description": "Laravel Sanctum provides a featherweight authentication system for SPAs and simple APIs.",
|
||||
"keywords": [
|
||||
"auth",
|
||||
"laravel",
|
||||
"sanctum"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/laravel/sanctum/issues",
|
||||
"source": "https://github.com/laravel/sanctum"
|
||||
},
|
||||
"time": "2025-07-09T19:45:24+00:00"
|
||||
},
|
||||
{
|
||||
"name": "laravel/serializable-closure",
|
||||
"version": "v2.0.5",
|
||||
|
||||
28
thanasoft-back/config/cors.php
Normal file
28
thanasoft-back/config/cors.php
Normal file
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
// Apply CORS to API routes and Sanctum's CSRF cookie endpoint (if used)
|
||||
'paths' => ['api/*', 'sanctum/csrf-cookie'],
|
||||
|
||||
// Allow all HTTP methods for simplicity in dev
|
||||
'allowed_methods' => ['*'],
|
||||
|
||||
// IMPORTANT: Do NOT use '*' when sending credentials. List explicit origins.
|
||||
// Set FRONTEND_URL in .env to override the default if needed.
|
||||
'allowed_origins' => [env('FRONTEND_URL', 'http://localhost:8080')],
|
||||
|
||||
// Alternatively, use patterns (kept empty for clarity)
|
||||
'allowed_origins_patterns' => [],
|
||||
|
||||
// Headers the client may send
|
||||
'allowed_headers' => ['*'],
|
||||
|
||||
// Headers exposed to the browser
|
||||
'exposed_headers' => [],
|
||||
|
||||
// Preflight cache duration (in seconds)
|
||||
'max_age' => 0,
|
||||
|
||||
// Must be true if the browser sends cookies or Authorization with withCredentials
|
||||
'supports_credentials' => true,
|
||||
];
|
||||
@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('personal_access_tokens', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->morphs('tokenable');
|
||||
$table->text('name');
|
||||
$table->string('token', 64)->unique();
|
||||
$table->text('abilities')->nullable();
|
||||
$table->timestamp('last_used_at')->nullable();
|
||||
$table->timestamp('expires_at')->nullable()->index();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('personal_access_tokens');
|
||||
}
|
||||
};
|
||||
@ -19,6 +19,8 @@ Route::prefix('auth')->group(function () {
|
||||
|
||||
Route::middleware('auth:sanctum')->group(function () {
|
||||
Route::get('/me', [AuthController::class, 'me']);
|
||||
// Alias to support clients calling /api/auth/user
|
||||
Route::get('/user', [AuthController::class, 'me']);
|
||||
Route::post('/logout', [AuthController::class, 'logout']);
|
||||
Route::post('/logout-all', [AuthController::class, 'logoutAll']);
|
||||
});
|
||||
|
||||
@ -1 +1,3 @@
|
||||
NODE_ENV=
|
||||
# API base URL for axios (used by src/services/http.ts)
|
||||
# For Laravel Sanctum on local dev (default Laravel port):
|
||||
VUE_APP_API_BASE_URL=http://localhost:8000
|
||||
|
||||
@ -1,3 +0,0 @@
|
||||
dist/
|
||||
node_modules/
|
||||
public/
|
||||
@ -1,19 +1,47 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
|
||||
env: {
|
||||
node: true,
|
||||
browser: true,
|
||||
node: true,
|
||||
es6: true,
|
||||
},
|
||||
|
||||
extends: [
|
||||
'plugin:vue/vue3-essential',
|
||||
'@vue/prettier',
|
||||
"eslint:recommended",
|
||||
"plugin:vue/vue3-recommended",
|
||||
"@vue/typescript",
|
||||
"@vue/prettier",
|
||||
],
|
||||
|
||||
parserOptions: {
|
||||
parser: 'babel-eslint',
|
||||
parser: "@typescript-eslint/parser",
|
||||
ecmaVersion: 2020,
|
||||
sourceType: "module",
|
||||
},
|
||||
|
||||
rules: {
|
||||
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
|
||||
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
|
||||
// Relax console/debugger in development
|
||||
"no-console": process.env.NODE_ENV === "production" ? "warn" : "off",
|
||||
"no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off",
|
||||
|
||||
},
|
||||
|
||||
ignorePatterns: [
|
||||
"node_modules/",
|
||||
"dist/",
|
||||
"public/*.min.js",
|
||||
"public/**/*.min.js",
|
||||
],
|
||||
|
||||
// Disable base no-unused-vars in .vue files to avoid false positives with <script setup>
|
||||
overrides: [
|
||||
{
|
||||
files: ["*.vue"],
|
||||
rules: {
|
||||
"no-unused-vars": "off",
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
};
|
||||
|
||||
167
thanasoft-front/AUTH_SETUP.md
Normal file
167
thanasoft-front/AUTH_SETUP.md
Normal file
@ -0,0 +1,167 @@
|
||||
# Authentication Setup - Token Persistence
|
||||
|
||||
## ✅ Fixed: Token persists across page refreshes
|
||||
|
||||
### The Problem
|
||||
After login, refreshing the page redirected to `/login` because the auth state wasn't being restored from the saved token.
|
||||
|
||||
### The Solution
|
||||
**No plugin needed!** The token is already saved in localStorage. I updated the router guard to check for the token on page load.
|
||||
|
||||
---
|
||||
|
||||
## How it works
|
||||
|
||||
### 1. **Login Flow**
|
||||
```
|
||||
User submits login form
|
||||
↓
|
||||
POST /api/auth/login
|
||||
↓
|
||||
Backend returns: { success: true, data: { user, token } }
|
||||
↓
|
||||
Token saved to localStorage.setItem("auth_token", token)
|
||||
↓
|
||||
User saved to Pinia store
|
||||
↓
|
||||
Redirect to dashboard
|
||||
```
|
||||
|
||||
### 2. **Page Refresh Flow**
|
||||
```
|
||||
User refreshes page
|
||||
↓
|
||||
Router guard runs (beforeEach)
|
||||
↓
|
||||
Check: auth.checked? No
|
||||
↓
|
||||
Check: localStorage.getItem("auth_token")? Yes
|
||||
↓
|
||||
Set auth.token = token (from localStorage)
|
||||
↓
|
||||
Fetch user: GET /api/auth/user (with Bearer token)
|
||||
↓
|
||||
Backend returns user data
|
||||
↓
|
||||
auth.user = userData
|
||||
auth.checked = true
|
||||
↓
|
||||
User stays on protected route
|
||||
```
|
||||
|
||||
### 3. **Logout Flow**
|
||||
```
|
||||
User clicks logout
|
||||
↓
|
||||
POST /api/auth/logout
|
||||
↓
|
||||
localStorage.removeItem("auth_token")
|
||||
↓
|
||||
auth.user = null
|
||||
auth.token = null
|
||||
↓
|
||||
Redirect to /login
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Files Modified
|
||||
|
||||
### `src/services/auth.ts`
|
||||
- `login()` saves token to localStorage
|
||||
- `logout()` removes token from localStorage
|
||||
- `me()` fetches user from `/api/auth/user`
|
||||
- `getToken()` retrieves token from localStorage
|
||||
|
||||
### `src/services/http.ts`
|
||||
- Automatically adds `Authorization: Bearer {token}` header to all requests
|
||||
- Reads token from localStorage on every request
|
||||
|
||||
### `src/stores/auth.ts`
|
||||
- Stores both `user` and `token` in state
|
||||
- `isAuthenticated` checks for both user AND token
|
||||
- `login()` extracts user and token from API response
|
||||
|
||||
### `src/router/index.js`
|
||||
- On every navigation, checks if token exists in localStorage
|
||||
- If token exists and auth not checked yet:
|
||||
1. Sets `auth.token = token`
|
||||
2. Fetches user data with `auth.fetchMe()`
|
||||
3. If successful, user stays authenticated
|
||||
4. If fails (invalid token), clears token and redirects to login
|
||||
|
||||
### `src/views/pages/Login.vue`
|
||||
- Form submits to `authStore.login()`
|
||||
- Shows loading state and error messages
|
||||
- Redirects to dashboard on success
|
||||
|
||||
---
|
||||
|
||||
## API Endpoints Used
|
||||
|
||||
| Endpoint | Method | Purpose | Auth Required |
|
||||
|----------|--------|---------|---------------|
|
||||
| `/api/auth/login` | POST | Login with email/password | No |
|
||||
| `/api/auth/user` | GET | Get current user | Yes (Bearer token) |
|
||||
| `/api/auth/logout` | POST | Logout | Yes (Bearer token) |
|
||||
|
||||
---
|
||||
|
||||
## Testing
|
||||
|
||||
### Test 1: Fresh Login
|
||||
1. Go to `/login`
|
||||
2. Enter credentials: `admin@admin.com`
|
||||
3. Click "Se connecter"
|
||||
4. Should redirect to `/dashboards/dashboard-default`
|
||||
5. Check localStorage: `auth_token` should be present
|
||||
|
||||
### Test 2: Page Refresh
|
||||
1. After login, refresh the page
|
||||
2. Should stay on dashboard (not redirect to login)
|
||||
3. Check console: should see "Fetching user from /api/auth/user"
|
||||
|
||||
### Test 3: Invalid Token
|
||||
1. Manually edit token in localStorage to garbage value
|
||||
2. Refresh page
|
||||
3. Should clear token and redirect to login
|
||||
4. Check console: "Invalid token, clearing auth"
|
||||
|
||||
### Test 4: Logout
|
||||
1. After login, click logout
|
||||
2. Should redirect to `/login`
|
||||
3. Check localStorage: `auth_token` should be removed
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Still redirects to /login after refresh"
|
||||
- Open browser DevTools → Network tab
|
||||
- Refresh page
|
||||
- Check if `GET /api/auth/user` is called with `Authorization: Bearer ...` header
|
||||
- Check response:
|
||||
- **200 OK**: Token is valid, check if user data is correct format
|
||||
- **401 Unauthorized**: Token is invalid or backend isn't verifying correctly
|
||||
|
||||
### "Token is saved but still logged out"
|
||||
- Check `auth.isAuthenticated` in Vue DevTools
|
||||
- Verify both `auth.user` and `auth.token` are set
|
||||
- If only token is set, check if `/api/auth/user` endpoint returns user data
|
||||
|
||||
### "Backend doesn't accept token"
|
||||
- Verify backend expects: `Authorization: Bearer {token}`
|
||||
- Check if backend route has auth middleware
|
||||
- Test token manually: `curl -H "Authorization: Bearer YOUR_TOKEN" http://localhost:8000/api/auth/user`
|
||||
|
||||
---
|
||||
|
||||
## No Plugin Required!
|
||||
|
||||
You mentioned Pinia Persisted or Vuex Persisted - you don't need them because:
|
||||
1. Token is already saved in `localStorage` directly
|
||||
2. Router guard reads from `localStorage` on app load
|
||||
3. Token is automatically added to all API requests
|
||||
4. This is simpler and more explicit than a plugin
|
||||
|
||||
The only state that needs to persist is the token (string), and we're handling that manually.
|
||||
890
thanasoft-front/package-lock.json
generated
890
thanasoft-front/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -2,22 +2,12 @@
|
||||
"name": "vue-soft-ui-dashboard-pro",
|
||||
"version": "3.0.0",
|
||||
"private": true,
|
||||
"author": "Creative Tim",
|
||||
"license": "SEE LICENSE IN <https://www.creative-tim.com/license>",
|
||||
"description": "VueJS version of Soft UI Dashboard PRO by Creative Tim",
|
||||
"homepage": "https://demos.creative-tim.com/vue-soft-ui-dashboard-pro/",
|
||||
"bugs": {
|
||||
"url": "https://github.com/creativetimofficial/ct-vue-soft-ui-dashboard-pro/issues"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/creativetimofficial/ct-vue-soft-ui-dashboard-pro.git"
|
||||
},
|
||||
"author": "Creative Tim",
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
"build": "vue-cli-service build",
|
||||
"lint": "vue-cli-service lint",
|
||||
"typecheck": "vue-tsc --noEmit"
|
||||
"lint": "vue-cli-service lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "1.3.0",
|
||||
@ -30,6 +20,7 @@
|
||||
"@fullcalendar/interaction": "5.10.1",
|
||||
"@fullcalendar/timegrid": "5.10.1",
|
||||
"@popperjs/core": "2.10.2",
|
||||
"axios": "^1.12.2",
|
||||
"bootstrap": "5.1.3",
|
||||
"chart.js": "3.6.0",
|
||||
"choices.js": "9.0.1",
|
||||
@ -39,6 +30,7 @@
|
||||
"jkanban": "1.3.1",
|
||||
"leaflet": "1.7.1",
|
||||
"photoswipe": "4.1.3",
|
||||
"pinia": "^2.0.36",
|
||||
"quill": "1.3.6",
|
||||
"round-slider": "1.6.1",
|
||||
"simple-datatables": "3.2.0",
|
||||
@ -54,15 +46,16 @@
|
||||
"vuex": "4.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/minimatch": "^6.0.0",
|
||||
"@types/node": "^24.6.0",
|
||||
"@typescript-eslint/eslint-plugin": "^4.18.0",
|
||||
"@typescript-eslint/parser": "^4.18.0",
|
||||
"@vue/cli-plugin-babel": "4.5.15",
|
||||
"@vue/cli-plugin-eslint": "4.5.15",
|
||||
"@vue/cli-plugin-router": "4.5.15",
|
||||
"@vue/cli-plugin-typescript": "^4.5.15",
|
||||
"@vue/cli-plugin-typescript": "4.5.15",
|
||||
"@vue/cli-service": "4.5.15",
|
||||
"@vue/compiler-sfc": "3.2.0",
|
||||
"@vue/eslint-config-prettier": "6.0.0",
|
||||
"@vue/eslint-config-typescript": "^7.0.0",
|
||||
"babel-eslint": "10.1.0",
|
||||
"eslint": "6.7.2",
|
||||
"eslint-plugin-prettier": "3.3.1",
|
||||
@ -70,7 +63,15 @@
|
||||
"prettier": "2.2.1",
|
||||
"sass": "1.43.3",
|
||||
"sass-loader": "10.1.1",
|
||||
"typescript": "^5.9.2",
|
||||
"vue-tsc": "^3.1.0"
|
||||
"typescript": "~4.1.5"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/creativetimofficial/ct-vue-soft-ui-dashboard-pro/issues"
|
||||
},
|
||||
"homepage": "https://demos.creative-tim.com/vue-soft-ui-dashboard-pro/",
|
||||
"license": "SEE LICENSE IN <https://www.creative-tim.com/license>",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/creativetimofficial/ct-vue-soft-ui-dashboard-pro.git"
|
||||
}
|
||||
}
|
||||
|
||||
@ -19,8 +19,7 @@ Coded by www.creative-tim.com
|
||||
|
||||
<link href="https://fonts.googleapis.com/css?family=Open+Sans:300,400,600,700" rel="stylesheet" />
|
||||
|
||||
<!-- Font Awesome Icons -->
|
||||
<script src="https://kit.fontawesome.com/42d5adcbca.js" crossorigin="anonymous"></script>
|
||||
<!-- Font Awesome Icons: using CSS only to avoid blocked kit.js -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.1/css/all.min.css">
|
||||
|
||||
|
||||
|
||||
@ -1,26 +1,34 @@
|
||||
<template>
|
||||
<sidenav
|
||||
v-if="showSidenav"
|
||||
:custom-class="color"
|
||||
:class="[isTransparent, isRTL ? 'fixed-end' : 'fixed-start']"
|
||||
/>
|
||||
<main
|
||||
class="main-content position-relative max-height-vh-100 h-100 border-radius-lg"
|
||||
>
|
||||
<!-- nav -->
|
||||
<navbar
|
||||
v-if="showNavbar"
|
||||
:class="[isNavFixed ? navbarFixed : '', isAbsolute ? absolute : '']"
|
||||
:text-white="isAbsolute ? 'text-white opacity-8' : ''"
|
||||
:min-nav="navbarMinimize"
|
||||
/>
|
||||
<!-- Guest Layout: for login, signup, auth pages -->
|
||||
<div v-if="isGuestRoute" class="guest-layout">
|
||||
<router-view />
|
||||
<app-footer v-show="showFooter" />
|
||||
<configurator
|
||||
:toggle="toggleConfigurator"
|
||||
:class="[showConfig ? 'show' : '', hideConfigButton ? 'd-none' : '']"
|
||||
</div>
|
||||
|
||||
<!-- Dashboard Layout: for authenticated pages -->
|
||||
<div v-else class="dashboard-layout">
|
||||
<sidenav
|
||||
v-if="showSidenav"
|
||||
:custom-class="color"
|
||||
:class="[isTransparent, isRTL ? 'fixed-end' : 'fixed-start']"
|
||||
/>
|
||||
</main>
|
||||
<main
|
||||
class="main-content position-relative max-height-vh-100 h-100 border-radius-lg"
|
||||
>
|
||||
<!-- nav -->
|
||||
<navbar
|
||||
v-if="showNavbar"
|
||||
:class="[isNavFixed ? navbarFixed : '', isAbsolute ? absolute : '']"
|
||||
:text-white="isAbsolute ? 'text-white opacity-8' : ''"
|
||||
:min-nav="navbarMinimize"
|
||||
/>
|
||||
<router-view />
|
||||
<app-footer v-show="showFooter" />
|
||||
<configurator
|
||||
:toggle="toggleConfigurator"
|
||||
:class="[showConfig ? 'show' : '', hideConfigButton ? 'd-none' : '']"
|
||||
/>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import Sidenav from "./examples/Sidenav";
|
||||
@ -51,6 +59,10 @@ export default {
|
||||
"showConfig",
|
||||
"hideConfigButton",
|
||||
]),
|
||||
isGuestRoute() {
|
||||
// Check if current route has guestLayout meta flag
|
||||
return this.$route.meta?.guestLayout === true;
|
||||
},
|
||||
},
|
||||
beforeMount() {
|
||||
this.$store.state.isTransparent = "bg-transparent";
|
||||
|
||||
@ -0,0 +1,2 @@
|
||||
// Custom custom styles for Soft UI Dashboard
|
||||
// Leave empty or write additional styles.
|
||||
@ -0,0 +1,3 @@
|
||||
// Custom variable overrides for Soft UI Dashboard
|
||||
// Leave empty or override variables, e.g.:
|
||||
// $primary: #5e72e4;
|
||||
108
thanasoft-front/src/components/HelloWorld.vue
Normal file
108
thanasoft-front/src/components/HelloWorld.vue
Normal file
@ -0,0 +1,108 @@
|
||||
<template>
|
||||
<div class="hello">
|
||||
<h1>{{ msg }}</h1>
|
||||
<p>
|
||||
For a guide and recipes on how to configure / customize this project,<br />
|
||||
check out the
|
||||
<a href="https://cli.vuejs.org" target="_blank" rel="noopener"
|
||||
>vue-cli documentation</a
|
||||
>.
|
||||
</p>
|
||||
<h3>Installed CLI Plugins</h3>
|
||||
<ul>
|
||||
<li>
|
||||
<a
|
||||
href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-typescript"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>typescript</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
<h3>Essential Links</h3>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="https://vuejs.org" target="_blank" rel="noopener">Core Docs</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://forum.vuejs.org" target="_blank" rel="noopener"
|
||||
>Forum</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://chat.vuejs.org" target="_blank" rel="noopener"
|
||||
>Community Chat</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://twitter.com/vuejs" target="_blank" rel="noopener"
|
||||
>Twitter</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://news.vuejs.org" target="_blank" rel="noopener">News</a>
|
||||
</li>
|
||||
</ul>
|
||||
<h3>Ecosystem</h3>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="https://router.vuejs.org" target="_blank" rel="noopener"
|
||||
>vue-router</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuex</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://github.com/vuejs/vue-devtools#vue-devtools"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>vue-devtools</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://vue-loader.vuejs.org" target="_blank" rel="noopener"
|
||||
>vue-loader</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://github.com/vuejs/awesome-vue"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>awesome-vue</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "HelloWorld",
|
||||
props: {
|
||||
msg: String,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
||||
<style scoped lang="scss">
|
||||
h3 {
|
||||
margin: 40px 0 0;
|
||||
}
|
||||
ul {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
}
|
||||
li {
|
||||
display: inline-block;
|
||||
margin: 0 10px;
|
||||
}
|
||||
a {
|
||||
color: #42b983;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,15 @@
|
||||
<template>
|
||||
<LoginTemplate>
|
||||
<template #social-media>
|
||||
<SocialMediaButtons />
|
||||
</template>
|
||||
<template #card-login>
|
||||
<LoginForm />
|
||||
</template>
|
||||
</LoginTemplate>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import LoginTemplate from "@/components/templates/LoginTemplate.vue";
|
||||
import LoginForm from "@/components/molecules/auth/LoginForm.vue";
|
||||
import SocialMediaButtons from "@/components/molecules/auth/SocialMedia.buttons.vue";
|
||||
</script>
|
||||
115
thanasoft-front/src/components/molecules/auth/LoginForm.vue
Normal file
115
thanasoft-front/src/components/molecules/auth/LoginForm.vue
Normal file
@ -0,0 +1,115 @@
|
||||
<template>
|
||||
<div class="card-body">
|
||||
<form role="form" class="text-start" @submit.prevent="handleLogin">
|
||||
<div class="mb-3">
|
||||
<SoftInput
|
||||
id="email"
|
||||
:value="email"
|
||||
type="email"
|
||||
placeholder="Email"
|
||||
name="email"
|
||||
:is-required="true"
|
||||
@input="email = $event.target.value"
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<SoftInput
|
||||
id="password"
|
||||
:value="password"
|
||||
name="password"
|
||||
type="password"
|
||||
placeholder="Mot de passe"
|
||||
:is-required="true"
|
||||
@input="password = $event.target.value"
|
||||
/>
|
||||
</div>
|
||||
<SoftSwitch
|
||||
id="rememberMe"
|
||||
:checked="remember"
|
||||
name="rememberMe"
|
||||
@change="remember = $event.target.checked"
|
||||
>
|
||||
Souvenez de moi
|
||||
</SoftSwitch>
|
||||
<div v-if="errorMessage" class="alert alert-danger text-white" role="alert">
|
||||
{{ errorMessage }}
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<SoftButton
|
||||
type="submit"
|
||||
class="my-4 mb-2"
|
||||
variant="gradient"
|
||||
color="info"
|
||||
full-width
|
||||
:disabled="isLoading"
|
||||
>
|
||||
{{ isLoading ? "Connexion..." : "Se connecter" }}
|
||||
</SoftButton>
|
||||
</div>
|
||||
<div class="mb-2 text-center position-relative">
|
||||
<p
|
||||
class="px-3 mb-2 text-sm bg-white font-weight-bold text-secondary text-border d-inline z-index-2"
|
||||
>
|
||||
ou
|
||||
</p>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<SoftButton
|
||||
class="mt-2 mb-4"
|
||||
variant="gradient"
|
||||
color="dark"
|
||||
full-width
|
||||
@click="$router.push('/authentication/signup/basic')"
|
||||
>
|
||||
creer un compte
|
||||
</SoftButton>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import SoftInput from "@/components/SoftInput.vue";
|
||||
import SoftSwitch from "@/components/SoftSwitch.vue";
|
||||
import SoftButton from "@/components/SoftButton.vue";
|
||||
|
||||
const router = useRouter();
|
||||
const authStore = useAuthStore();
|
||||
|
||||
const email = ref("");
|
||||
const password = ref("");
|
||||
const remember = ref(false);
|
||||
const isLoading = ref(false);
|
||||
const errorMessage = ref("");
|
||||
|
||||
const handleLogin = async () => {
|
||||
if (!email.value || !password.value) {
|
||||
errorMessage.value = "Veuillez remplir tous les champs";
|
||||
return;
|
||||
}
|
||||
|
||||
isLoading.value = true;
|
||||
errorMessage.value = "";
|
||||
|
||||
try {
|
||||
await authStore.login({
|
||||
email: email.value,
|
||||
password: password.value,
|
||||
remember: remember.value,
|
||||
});
|
||||
|
||||
// Redirect to dashboard on success
|
||||
router.push("/dashboards/dashboard-default");
|
||||
} catch (error: any) {
|
||||
console.error("Login error:", error);
|
||||
errorMessage.value =
|
||||
error.response?.data?.message ||
|
||||
error.message ||
|
||||
"Email ou mot de passe incorrect";
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@ -0,0 +1,67 @@
|
||||
<template>
|
||||
<div class="px-3 row px-xl-5 px-sm-4">
|
||||
<div class="px-1 col-3 ms-auto">
|
||||
<a class="btn btn-outline-light w-100" href="javascript:;">
|
||||
<svg width="24px" height="32px" viewBox="0 0 64 64" version="1.1">
|
||||
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g transform="translate(3.000000, 3.000000)" fill-rule="nonzero">
|
||||
<circle
|
||||
fill="#3C5A9A"
|
||||
cx="29.5091719"
|
||||
cy="29.4927506"
|
||||
r="29.4882047"
|
||||
></circle>
|
||||
<path
|
||||
d="M39.0974944,9.05587273 L32.5651312,9.05587273 C28.6886088,9.05587273 24.3768224,10.6862851 24.3768224,16.3054653 C24.395747,18.2634019 24.3768224,20.1385313 24.3768224,22.2488655 L19.8922122,22.2488655 L19.8922122,29.3852113 L24.5156022,29.3852113 L24.5156022,49.9295284 L33.0113092,49.9295284 L33.0113092,29.2496356 L38.6187742,29.2496356 L39.1261316,22.2288395 L32.8649196,22.2288395 C32.8649196,22.2288395 32.8789377,19.1056932 32.8649196,18.1987181 C32.8649196,15.9781412 35.1755132,16.1053059 35.3144932,16.1053059 C36.4140178,16.1053059 38.5518876,16.1085101 39.1006986,16.1053059 L39.1006986,9.05587273 L39.0974944,9.05587273 L39.0974944,9.05587273 Z"
|
||||
fill="#FFFFFF"
|
||||
></path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
<div class="px-1 col-3">
|
||||
<a class="btn btn-outline-light w-100" href="javascript:;">
|
||||
<svg width="24px" height="32px" viewBox="0 0 64 64" version="1.1">
|
||||
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g
|
||||
transform="translate(7.000000, 0.564551)"
|
||||
fill="#000000"
|
||||
fill-rule="nonzero"
|
||||
>
|
||||
<path
|
||||
d="M40.9233048,32.8428307 C41.0078713,42.0741676 48.9124247,45.146088 49,45.1851909 C48.9331634,45.4017274 47.7369821,49.5628653 44.835501,53.8610269 C42.3271952,57.5771105 39.7241148,61.2793611 35.6233362,61.356042 C31.5939073,61.431307 30.2982233,58.9340578 25.6914424,58.9340578 C21.0860585,58.9340578 19.6464932,61.27947 15.8321878,61.4314159 C11.8738936,61.5833617 8.85958554,57.4131833 6.33064852,53.7107148 C1.16284874,46.1373849 -2.78641926,32.3103122 2.51645059,22.9768066 C5.15080028,18.3417501 9.85858819,15.4066355 14.9684701,15.3313705 C18.8554146,15.2562145 22.5241194,17.9820905 24.9003639,17.9820905 C27.275104,17.9820905 31.733383,14.7039812 36.4203248,15.1854154 C38.3824403,15.2681959 43.8902255,15.9888223 47.4267616,21.2362369 C47.1417927,21.4153043 40.8549638,25.1251794 40.9233048,32.8428307 M33.3504628,10.1750144 C35.4519466,7.59650964 36.8663676,4.00699306 36.4804992,0.435448578 C33.4513624,0.558856931 29.7884601,2.48154382 27.6157341,5.05863265 C25.6685547,7.34076135 23.9632549,10.9934525 24.4233742,14.4943068 C27.7996959,14.7590956 31.2488715,12.7551531 33.3504628,10.1750144"
|
||||
></path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
<div class="px-1 col-3 me-auto">
|
||||
<a class="btn btn-outline-light w-100" href="javascript:;">
|
||||
<svg width="24px" height="32px" viewBox="0 0 64 64" version="1.1">
|
||||
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g transform="translate(3.000000, 2.000000)" fill-rule="nonzero">
|
||||
<path
|
||||
d="M57.8123233,30.1515267 C57.8123233,27.7263183 57.6155321,25.9565533 57.1896408,24.1212666 L29.4960833,24.1212666 L29.4960833,35.0674653 L45.7515771,35.0674653 C45.4239683,37.7877475 43.6542033,41.8844383 39.7213169,44.6372555 L39.6661883,45.0037254 L48.4223791,51.7870338 L49.0290201,51.8475849 C54.6004021,46.7020943 57.8123233,39.1313952 57.8123233,30.1515267"
|
||||
fill="#4285F4"
|
||||
></path>
|
||||
<path
|
||||
d="M29.4960833,58.9921667 C37.4599129,58.9921667 44.1456164,56.3701671 49.0290201,51.8475849 L39.7213169,44.6372555 C37.2305867,46.3742596 33.887622,47.5868638 29.4960833,47.5868638 C21.6960582,47.5868638 15.0758763,42.4415991 12.7159637,35.3297782 L12.3700541,35.3591501 L3.26524241,42.4054492 L3.14617358,42.736447 C7.9965904,52.3717589 17.959737,58.9921667 29.4960833,58.9921667"
|
||||
fill="#34A853"
|
||||
></path>
|
||||
<path
|
||||
d="M12.7159637,35.3297782 C12.0932812,33.4944915 11.7329116,31.5279353 11.7329116,29.4960833 C11.7329116,27.4640054 12.0932812,25.4976752 12.6832029,23.6623884 L12.6667095,23.2715173 L3.44779955,16.1120237 L3.14617358,16.2554937 C1.14708246,20.2539019 0,24.7439491 0,29.4960833 C0,34.2482175 1.14708246,38.7380388 3.14617358,42.736447 L12.7159637,35.3297782"
|
||||
fill="#FBBC05"
|
||||
></path>
|
||||
<path
|
||||
d="M29.4960833,11.4050769 C35.0347044,11.4050769 38.7707997,13.7975244 40.9011602,15.7968415 L49.2255853,7.66898166 C44.1130815,2.91684746 37.4599129,0 29.4960833,0 C17.959737,0 7.9965904,6.62018183 3.14617358,16.2554937 L12.6832029,23.6623884 C15.0758763,16.5505675 21.6960582,11.4050769 29.4960833,11.4050769"
|
||||
fill="#EB4335"
|
||||
></path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
34
thanasoft-front/src/components/templates/LoginTemplate.vue
Normal file
34
thanasoft-front/src/components/templates/LoginTemplate.vue
Normal file
@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<div
|
||||
class="pt-5 m-3 page-header align-items-start min-vh-50 pb-11 border-radius-lg"
|
||||
:style="{
|
||||
backgroundImage:
|
||||
'url(' + require('@/assets/img/curved-images/curved9.jpg') + ')',
|
||||
}"
|
||||
>
|
||||
<span class="mask bg-gradient-dark opacity-6"></span>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="mx-auto text-center col-lg-5">
|
||||
<h1 class="mt-5 mb-2 text-white">Bonjour !</h1>
|
||||
<p class="text-white text-lead">
|
||||
Veuillez entrer vos identifiants pour vous connecter
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container">
|
||||
<div class="row mt-lg-n10 mt-md-n11 mt-n10 justify-content-center">
|
||||
<div class="mx-auto col-xl-4 col-lg-5 col-md-7">
|
||||
<div class="card z-index-0">
|
||||
<div class="pt-4 text-center card-header">
|
||||
<h5>Connectez-vous</h5>
|
||||
</div>
|
||||
<slot name="social-media" />
|
||||
<slot name="card-login" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -55,16 +55,16 @@
|
||||
</div>
|
||||
<ul class="navbar-nav justify-content-end">
|
||||
<li class="nav-item d-flex align-items-center">
|
||||
<router-link
|
||||
:to="{ name: 'Signin Basic' }"
|
||||
<a
|
||||
href="#"
|
||||
class="px-0 nav-link font-weight-bold"
|
||||
:class="textWhite ? textWhite : 'text-body'"
|
||||
target="_blank"
|
||||
@click.prevent="handleLogout"
|
||||
>
|
||||
<i class="fa fa-user" :class="isRTL ? 'ms-sm-2' : 'me-sm-1'"></i>
|
||||
<span v-if="isRTL" class="d-sm-inline d-none">يسجل دخول</span>
|
||||
<span v-else class="d-sm-inline d-none">Sign In </span>
|
||||
</router-link>
|
||||
<i class="fa fa-sign-out-alt" :class="isRTL ? 'ms-sm-2' : 'me-sm-1'"></i>
|
||||
<span v-if="isRTL" class="d-sm-inline d-none">تسجيل خروج</span>
|
||||
<span v-else class="d-sm-inline d-none">Logout</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item d-xl-none ps-3 d-flex align-items-center">
|
||||
<a
|
||||
@ -223,6 +223,7 @@
|
||||
<script>
|
||||
import Breadcrumbs from "../Breadcrumbs.vue";
|
||||
import { mapMutations, mapActions, mapState } from "vuex";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
|
||||
export default {
|
||||
name: "Navbar",
|
||||
@ -265,6 +266,19 @@ export default {
|
||||
this.toggleSidebarColor("bg-white");
|
||||
this.navbarMinimize();
|
||||
},
|
||||
|
||||
async handleLogout() {
|
||||
try {
|
||||
const authStore = useAuthStore();
|
||||
await authStore.logout();
|
||||
// Redirect to login page
|
||||
this.$router.push("/login");
|
||||
} catch (error) {
|
||||
console.error("Logout error:", error);
|
||||
// Still redirect to login even if logout API fails
|
||||
this.$router.push("/login");
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@ -9,7 +9,8 @@
|
||||
class="navbar-brand font-weight-bolder ms-lg-0 ms-3"
|
||||
:class="darkMode ? 'text-black' : 'text-white'"
|
||||
to="/"
|
||||
>Soft UI Dashboard PRO</router-link>
|
||||
>Soft UI Dashboard PRO</router-link
|
||||
>
|
||||
<button
|
||||
class="shadow-none navbar-toggler ms-2"
|
||||
type="button"
|
||||
|
||||
@ -15,11 +15,9 @@
|
||||
>
|
||||
<slot name="icon"></slot>
|
||||
</div>
|
||||
<span
|
||||
class="nav-link-text"
|
||||
:class="isRTL ? ' me-1' : 'ms-1'"
|
||||
>{{ navText }}</span
|
||||
>
|
||||
<span class="nav-link-text" :class="isRTL ? ' me-1' : 'ms-1'">{{
|
||||
navText
|
||||
}}</span>
|
||||
</a>
|
||||
<div :id="collapseRef" class="collapse">
|
||||
<slot name="list"></slot>
|
||||
@ -32,16 +30,16 @@ export default {
|
||||
props: {
|
||||
collapseRef: {
|
||||
type: String,
|
||||
required: true
|
||||
required: true,
|
||||
},
|
||||
navText: {
|
||||
type: String,
|
||||
required: true
|
||||
required: true,
|
||||
},
|
||||
collapse: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState(["isRTL"]),
|
||||
|
||||
@ -24,16 +24,16 @@ export default {
|
||||
props: {
|
||||
refer: {
|
||||
type: String,
|
||||
required: true
|
||||
required: true,
|
||||
},
|
||||
miniIcon: {
|
||||
type: String,
|
||||
required: true
|
||||
required: true,
|
||||
},
|
||||
text: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
}
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@ -844,9 +844,9 @@ export default {
|
||||
default: "",
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState(["isRTL"]),
|
||||
},
|
||||
computed: {
|
||||
...mapState(["isRTL"]),
|
||||
},
|
||||
methods: {
|
||||
getRoute() {
|
||||
const routeArr = this.$route.path.split("/");
|
||||
|
||||
@ -18,8 +18,10 @@ import "./assets/css/nucleo-svg.css";
|
||||
import VueTilt from "vue-tilt.js";
|
||||
import VueSweetalert2 from "vue-sweetalert2";
|
||||
import SoftUIDashboard from "./soft-ui-dashboard";
|
||||
import pinia from "./plugins/pinia";
|
||||
|
||||
const appInstance = createApp(App);
|
||||
appInstance.use(pinia);
|
||||
appInstance.use(store);
|
||||
appInstance.use(router);
|
||||
appInstance.use(VueTilt);
|
||||
|
||||
5
thanasoft-front/src/plugins/pinia.ts
Normal file
5
thanasoft-front/src/plugins/pinia.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { createPinia } from 'pinia'
|
||||
|
||||
// Single shared Pinia instance so router guards and app use the same store
|
||||
export const pinia = createPinia()
|
||||
export default pinia
|
||||
@ -54,12 +54,30 @@ import lockBasic from "../views/auth/lock/Basic.vue";
|
||||
import lockCover from "../views/auth/lock/Cover.vue";
|
||||
import lockIllustration from "../views/auth/lock/Illustration.vue";
|
||||
|
||||
//ROUTE SHOULD USED
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: "/",
|
||||
name: "/",
|
||||
redirect: "/dashboards/dashboard-default",
|
||||
},
|
||||
{
|
||||
path: "/dashboard",
|
||||
redirect: "/dashboards/dashboard-default",
|
||||
},
|
||||
{
|
||||
path: "/login",
|
||||
name: "Login",
|
||||
component: () => import("@/views/pages/Login.vue"),
|
||||
meta: { public: true, guestOnly: true, guestLayout: true },
|
||||
},
|
||||
{
|
||||
path: "/register",
|
||||
name: "Register",
|
||||
component: () => import("@/views/pages/Register.vue"),
|
||||
meta: { public: true, guestOnly: true, guestLayout: true },
|
||||
},
|
||||
{
|
||||
path: "/dashboards/dashboard-default",
|
||||
name: "Default",
|
||||
@ -249,94 +267,158 @@ const routes = [
|
||||
path: "/authentication/signin/basic",
|
||||
name: "Signin Basic",
|
||||
component: Basic,
|
||||
meta: { guestLayout: true },
|
||||
},
|
||||
{
|
||||
path: "/authentication/signin/cover",
|
||||
name: "Signin Cover",
|
||||
component: Cover,
|
||||
meta: { guestLayout: true },
|
||||
},
|
||||
{
|
||||
path: "/authentication/signin/illustration",
|
||||
name: "Signin Illustration",
|
||||
component: Illustration,
|
||||
meta: { guestLayout: true },
|
||||
},
|
||||
{
|
||||
path: "/authentication/reset/basic",
|
||||
name: "Reset Basic",
|
||||
component: ResetBasic,
|
||||
meta: { guestLayout: true },
|
||||
},
|
||||
{
|
||||
path: "/authentication/reset/cover",
|
||||
name: "Reset Cover",
|
||||
component: ResetCover,
|
||||
meta: { guestLayout: true },
|
||||
},
|
||||
{
|
||||
path: "/authentication/reset/illustration",
|
||||
name: "Reset Illustration",
|
||||
component: ResetIllustration,
|
||||
meta: { guestLayout: true },
|
||||
},
|
||||
{
|
||||
path: "/authentication/lock/basic",
|
||||
name: "Lock Basic",
|
||||
component: lockBasic,
|
||||
meta: { guestLayout: true },
|
||||
},
|
||||
{
|
||||
path: "/authentication/lock/cover",
|
||||
name: "Lock Cover",
|
||||
component: lockCover,
|
||||
meta: { guestLayout: true },
|
||||
},
|
||||
{
|
||||
path: "/authentication/lock/illustration",
|
||||
name: "Lock Illustration",
|
||||
component: lockIllustration,
|
||||
meta: { guestLayout: true },
|
||||
},
|
||||
{
|
||||
path: "/authentication/verification/basic",
|
||||
name: "Verification Basic",
|
||||
component: VerificationBasic,
|
||||
meta: { guestLayout: true },
|
||||
},
|
||||
{
|
||||
path: "/authentication/verification/cover",
|
||||
name: "Verification Cover",
|
||||
component: VerificationCover,
|
||||
meta: { guestLayout: true },
|
||||
},
|
||||
{
|
||||
path: "/authentication/verification/illustration",
|
||||
name: "Verification Illustration",
|
||||
component: VerificationIllustration,
|
||||
meta: { guestLayout: true },
|
||||
},
|
||||
{
|
||||
path: "/authentication/signup/basic",
|
||||
name: "Signup Basic",
|
||||
component: SignupBasic,
|
||||
meta: { guestLayout: true },
|
||||
},
|
||||
{
|
||||
path: "/authentication/signup/cover",
|
||||
name: "Signup Cover",
|
||||
component: SignupCover,
|
||||
meta: { guestLayout: true },
|
||||
},
|
||||
{
|
||||
path: "/authentication/signup/illustration",
|
||||
name: "Signup Illustration",
|
||||
component: SignupIllustration,
|
||||
meta: { guestLayout: true },
|
||||
},
|
||||
{
|
||||
path: "/authentication/error/error404",
|
||||
name: "Error Error404",
|
||||
component: Error404,
|
||||
meta: { guestLayout: true },
|
||||
},
|
||||
{
|
||||
path: "/authentication/error/error500",
|
||||
name: "Error Error500",
|
||||
component: Error500,
|
||||
meta: { guestLayout: true },
|
||||
},
|
||||
];
|
||||
|
||||
const router = createRouter({
|
||||
// Use root base so the app works whether served at / or via /build/index.html
|
||||
history: createWebHistory("/"),
|
||||
history: createWebHistory(process.env.BASE_URL),
|
||||
routes,
|
||||
linkActiveClass: "active",
|
||||
});
|
||||
|
||||
// Auth guard using Pinia store and Laravel Sanctum session
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import pinia from "@/plugins/pinia";
|
||||
|
||||
const DASHBOARD = "/dashboards/dashboard-default";
|
||||
const LOGIN = "/login"; // canonical login path without redirect query
|
||||
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
const auth = useAuthStore(pinia);
|
||||
const isPublic = to.matched.some((r) => r.meta && r.meta.public === true);
|
||||
const isGuestOnly = to.matched.some(
|
||||
(r) => r.meta && r.meta.guestOnly === true
|
||||
);
|
||||
const isLoginRoute = to.name === "Login" || to.name === "Signin";
|
||||
|
||||
// Initialize auth from localStorage token if not already done
|
||||
if (!auth.checked) {
|
||||
const token = localStorage.getItem("auth_token");
|
||||
if (token) {
|
||||
// Token exists, set it and try to fetch user
|
||||
auth.token = token;
|
||||
try {
|
||||
await auth.fetchMe();
|
||||
} catch (error) {
|
||||
// If token is invalid, clear it and continue as unauthenticated
|
||||
console.warn("Invalid token, clearing auth");
|
||||
localStorage.removeItem("auth_token");
|
||||
auth.token = null;
|
||||
auth.user = null;
|
||||
auth.checked = true;
|
||||
}
|
||||
} else {
|
||||
// No token, mark as checked
|
||||
auth.checked = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (auth.isAuthenticated) {
|
||||
// Prevent authenticated users from visiting guest-only routes (like sign-in)
|
||||
if (isGuestOnly || isLoginRoute) return next(DASHBOARD);
|
||||
return next();
|
||||
}
|
||||
|
||||
// Not authenticated: allow public/guest routes (like sign-in), otherwise send to login (no redirect param)
|
||||
if (isPublic || isGuestOnly || isLoginRoute) return next();
|
||||
return next({ path: LOGIN, replace: true });
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
||||
92
thanasoft-front/src/services/auth.ts
Normal file
92
thanasoft-front/src/services/auth.ts
Normal file
@ -0,0 +1,92 @@
|
||||
import { request, http } from "./http";
|
||||
|
||||
export interface LoginPayload {
|
||||
email: string;
|
||||
password: string;
|
||||
remember?: boolean;
|
||||
}
|
||||
|
||||
export interface RegisterPayload {
|
||||
name: string;
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface User {
|
||||
id: number;
|
||||
name: string;
|
||||
email: string;
|
||||
email_verified_at: string | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface LoginResponse {
|
||||
success: boolean;
|
||||
data: {
|
||||
user: User;
|
||||
token: string;
|
||||
};
|
||||
message: string;
|
||||
}
|
||||
|
||||
export const AuthService = {
|
||||
async login(payload: LoginPayload): Promise<LoginResponse> {
|
||||
const response = await request<LoginResponse>({
|
||||
url: "/api/auth/login",
|
||||
method: "post",
|
||||
data: payload,
|
||||
});
|
||||
|
||||
// Save token to localStorage
|
||||
if (response.success && response.data.token) {
|
||||
localStorage.setItem("auth_token", response.data.token);
|
||||
}
|
||||
|
||||
return response;
|
||||
},
|
||||
|
||||
async register(payload: RegisterPayload): Promise<LoginResponse> {
|
||||
const response = await request<LoginResponse>({
|
||||
url: "/api/auth/register",
|
||||
method: "post",
|
||||
data: payload,
|
||||
});
|
||||
|
||||
// Save token to localStorage (user is automatically logged in after registration)
|
||||
if (response.success && response.data.token) {
|
||||
localStorage.setItem("auth_token", response.data.token);
|
||||
}
|
||||
|
||||
return response;
|
||||
},
|
||||
|
||||
async logout() {
|
||||
try {
|
||||
await request<void>({ url: "/api/auth/logout", method: "post" });
|
||||
} finally {
|
||||
// Always remove token from localStorage on logout
|
||||
localStorage.removeItem("auth_token");
|
||||
}
|
||||
},
|
||||
|
||||
async me() {
|
||||
// Fetch current user from API
|
||||
const response = await request<any>({ url: "/api/auth/user", method: "get" });
|
||||
// Handle both direct user response and wrapped response
|
||||
if (response.success && response.data) {
|
||||
return response.data;
|
||||
}
|
||||
return response;
|
||||
},
|
||||
|
||||
getToken(): string | null {
|
||||
return localStorage.getItem("auth_token");
|
||||
},
|
||||
|
||||
hasToken(): boolean {
|
||||
return !!this.getToken();
|
||||
},
|
||||
};
|
||||
|
||||
export default AuthService;
|
||||
66
thanasoft-front/src/services/http.ts
Normal file
66
thanasoft-front/src/services/http.ts
Normal file
@ -0,0 +1,66 @@
|
||||
import axios, { AxiosError, AxiosInstance, AxiosRequestConfig } from "axios";
|
||||
|
||||
// Read base URL from Vue CLI env (must start with VUE_APP_)
|
||||
const baseURL = process.env.VUE_APP_API_BASE_URL || "";
|
||||
|
||||
// Laravel Sanctum defaults
|
||||
// - xsrfCookieName: 'XSRF-TOKEN'
|
||||
// - xsrfHeaderName: 'X-XSRF-TOKEN'
|
||||
// - withCredentials: true (needed for cookie-based session)
|
||||
|
||||
let csrfInitPromise: Promise<void> | null = null;
|
||||
|
||||
async function ensureCSRF(client: AxiosInstance) {
|
||||
if (!csrfInitPromise) {
|
||||
// Hitting /sanctum/csrf-cookie sets the XSRF-TOKEN cookie used by Axios
|
||||
csrfInitPromise = client
|
||||
.get("/sanctum/csrf-cookie", { withCredentials: true })
|
||||
.then(() => void 0);
|
||||
}
|
||||
return csrfInitPromise;
|
||||
}
|
||||
|
||||
export const http: AxiosInstance = axios.create({
|
||||
baseURL,
|
||||
withCredentials: true,
|
||||
// These match Laravel’s defaults and Axios defaults; set explicitly for clarity
|
||||
xsrfCookieName: "XSRF-TOKEN",
|
||||
xsrfHeaderName: "X-XSRF-TOKEN",
|
||||
headers: {
|
||||
"X-Requested-With": "XMLHttpRequest",
|
||||
Accept: "application/json",
|
||||
},
|
||||
timeout: 30000,
|
||||
});
|
||||
|
||||
// Optionally attach a request interceptor to ensure CSRF for state-changing requests
|
||||
http.interceptors.request.use(async (config) => {
|
||||
// Add Bearer token if available
|
||||
const token = localStorage.getItem("auth_token");
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
// Only ensure CSRF for unsafe methods (POST, PUT, PATCH, DELETE)
|
||||
// Skip CSRF if using token-based auth
|
||||
const method = (config.method || "get").toLowerCase();
|
||||
if (!token && method !== "get" && method !== "head" && method !== "options") {
|
||||
await ensureCSRF(http);
|
||||
}
|
||||
return config;
|
||||
});
|
||||
|
||||
// Basic response error normalization (optional)
|
||||
http.interceptors.response.use(
|
||||
(r) => r,
|
||||
(error: AxiosError) => {
|
||||
// You can add global error handling, e.g., redirect on 401, etc.
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
// Helper to build typed requests if needed
|
||||
export async function request<T = any>(config: AxiosRequestConfig) {
|
||||
const response = await http.request<T>(config);
|
||||
return response.data;
|
||||
}
|
||||
2
thanasoft-front/src/services/index.ts
Normal file
2
thanasoft-front/src/services/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './http'
|
||||
export { default as AuthService } from './auth'
|
||||
42
thanasoft-front/src/shims-vue.d.ts
vendored
42
thanasoft-front/src/shims-vue.d.ts
vendored
@ -1,35 +1,13 @@
|
||||
declare module "*.vue" {
|
||||
import type { DefineComponent } from "vue";
|
||||
const component: DefineComponent<{}, {}, any>;
|
||||
export default component;
|
||||
/* eslint-disable */
|
||||
declare module '*.vue' {
|
||||
import type { DefineComponent } from 'vue'
|
||||
const component: DefineComponent<{}, {}, any>
|
||||
export default component
|
||||
}
|
||||
|
||||
declare module "*.png" {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
|
||||
declare module "*.jpg" {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
|
||||
declare module "*.jpeg" {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
|
||||
declare module "*.gif" {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
|
||||
declare module "*.webp" {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
|
||||
declare module "*.svg" {
|
||||
const src: string;
|
||||
export default src;
|
||||
// Type shim for packages without TypeScript declarations
|
||||
declare module 'vue-tilt.js' {
|
||||
import type { Plugin } from 'vue'
|
||||
const VueTilt: Plugin
|
||||
export default VueTilt
|
||||
}
|
||||
|
||||
@ -19,7 +19,7 @@ export default createStore({
|
||||
navbarFixed:
|
||||
"position-sticky blur shadow-blur left-auto top-1 z-index-sticky px-0 mx-4",
|
||||
absolute: "position-absolute px-4 mx-0 w-100 z-index-2",
|
||||
bootstrap
|
||||
bootstrap,
|
||||
},
|
||||
mutations: {
|
||||
toggleConfigurator(state) {
|
||||
@ -57,7 +57,7 @@ export default createStore({
|
||||
},
|
||||
toggleHideConfig(state) {
|
||||
state.hideConfigButton = !state.hideConfigButton;
|
||||
}
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
toggleSidebarColor({ commit }, payload) {
|
||||
@ -67,5 +67,5 @@ export default createStore({
|
||||
commit("cardBackground", payload);
|
||||
},
|
||||
},
|
||||
getters: {}
|
||||
getters: {},
|
||||
});
|
||||
|
||||
86
thanasoft-front/src/stores/auth.ts
Normal file
86
thanasoft-front/src/stores/auth.ts
Normal file
@ -0,0 +1,86 @@
|
||||
import { defineStore } from "pinia";
|
||||
import type { User, LoginPayload, RegisterPayload } from "@/services/auth";
|
||||
import AuthService from "@/services/auth";
|
||||
|
||||
export const useAuthStore = defineStore("auth", {
|
||||
state: () => ({
|
||||
user: null as User | null,
|
||||
token: null as string | null,
|
||||
checked: false as boolean, // whether we've attempted to fetch current user
|
||||
}),
|
||||
getters: {
|
||||
isAuthenticated: (state) => !!state.user && !!state.token,
|
||||
},
|
||||
actions: {
|
||||
async fetchMe() {
|
||||
try {
|
||||
const userData = await AuthService.me();
|
||||
// Validate that we actually received a user object, not HTML or other invalid data
|
||||
if (userData && typeof userData === 'object' && 'id' in userData && 'email' in userData) {
|
||||
this.user = userData;
|
||||
} else {
|
||||
// If backend returned invalid data (like HTML), treat as unauthenticated
|
||||
console.warn('Invalid user data received from /api/user:', userData);
|
||||
this.user = null;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error fetching user:', e);
|
||||
this.user = null;
|
||||
this.token = null;
|
||||
} finally {
|
||||
this.checked = true;
|
||||
}
|
||||
return this.user;
|
||||
},
|
||||
async login(payload: LoginPayload) {
|
||||
const response = await AuthService.login(payload);
|
||||
|
||||
if (response.success && response.data.user && response.data.token) {
|
||||
this.user = response.data.user;
|
||||
this.token = response.data.token;
|
||||
this.checked = true;
|
||||
return this.user;
|
||||
} else {
|
||||
throw new Error(response.message || "Login failed");
|
||||
}
|
||||
},
|
||||
async register(payload: RegisterPayload) {
|
||||
const response = await AuthService.register(payload);
|
||||
|
||||
if (response.success && response.data.user && response.data.token) {
|
||||
this.user = response.data.user;
|
||||
this.token = response.data.token;
|
||||
this.checked = true;
|
||||
return this.user;
|
||||
} else {
|
||||
throw new Error(response.message || "Registration failed");
|
||||
}
|
||||
},
|
||||
async logout() {
|
||||
try {
|
||||
await AuthService.logout();
|
||||
} finally {
|
||||
this.user = null;
|
||||
this.token = null;
|
||||
this.checked = true;
|
||||
}
|
||||
},
|
||||
// Initialize auth state from localStorage token on app load
|
||||
initializeAuth() {
|
||||
const token = AuthService.getToken();
|
||||
if (token) {
|
||||
this.token = token;
|
||||
// Optionally fetch user data if token exists
|
||||
this.fetchMe().catch(() => {
|
||||
// If fetching user fails, clear invalid token
|
||||
this.token = null;
|
||||
AuthService.logout();
|
||||
});
|
||||
} else {
|
||||
this.checked = true;
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default useAuthStore;
|
||||
@ -1,8 +0,0 @@
|
||||
export interface User {
|
||||
id: number;
|
||||
name: string;
|
||||
email: string;
|
||||
email_verified_at?: string | null;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
}
|
||||
@ -1,7 +1,18 @@
|
||||
<template>
|
||||
<default-dashboard />
|
||||
<div class="home">
|
||||
<img alt="Vue logo" src="../assets/logo.png" />
|
||||
<HelloWorld msg="Welcome to Your Vue.js + TypeScript App" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import DefaultDashboard from "@/views/dashboards/Default.vue";
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
import HelloWorld from "@/components/HelloWorld.vue"; // @ is an alias to /src
|
||||
|
||||
export default defineComponent({
|
||||
name: "Home",
|
||||
components: {
|
||||
HelloWorld,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@ -40,11 +40,11 @@
|
||||
value="930"
|
||||
:percentage="{
|
||||
value: '+55%',
|
||||
color: 'text-success'
|
||||
color: 'text-success',
|
||||
}"
|
||||
:icon="{
|
||||
component: 'ni ni-circle-08',
|
||||
background: 'bg-gradient-dark'
|
||||
background: 'bg-gradient-dark',
|
||||
}"
|
||||
class="ms-1"
|
||||
direction-reverse
|
||||
@ -56,11 +56,11 @@
|
||||
value="744"
|
||||
:percentage="{
|
||||
value: '+3%',
|
||||
color: 'text-success'
|
||||
color: 'text-success',
|
||||
}"
|
||||
:icon="{
|
||||
component: 'ni ni-world',
|
||||
background: 'bg-gradient-dark'
|
||||
background: 'bg-gradient-dark',
|
||||
}"
|
||||
class="ms-1"
|
||||
direction-reverse
|
||||
@ -72,11 +72,11 @@
|
||||
value="1,414"
|
||||
:percentage="{
|
||||
value: '-2%',
|
||||
color: 'text-danger'
|
||||
color: 'text-danger',
|
||||
}"
|
||||
:icon="{
|
||||
component: 'ni ni-watch-time',
|
||||
background: 'bg-gradient-dark'
|
||||
background: 'bg-gradient-dark',
|
||||
}"
|
||||
class="ms-1"
|
||||
direction-reverse
|
||||
@ -88,11 +88,11 @@
|
||||
value="1.76"
|
||||
:percentage="{
|
||||
value: '+5%',
|
||||
color: 'text-success'
|
||||
color: 'text-success',
|
||||
}"
|
||||
:icon="{
|
||||
component: 'ni ni-image',
|
||||
background: 'bg-gradient-dark'
|
||||
background: 'bg-gradient-dark',
|
||||
}"
|
||||
class="ms-1"
|
||||
direction-reverse
|
||||
@ -133,22 +133,22 @@
|
||||
'Sep',
|
||||
'Oct',
|
||||
'Nov',
|
||||
'Dec'
|
||||
'Dec',
|
||||
],
|
||||
datasets: [
|
||||
{
|
||||
label: 'Organic Search',
|
||||
data: [50, 40, 300, 220, 500, 250, 400, 230, 500]
|
||||
data: [50, 40, 300, 220, 500, 250, 400, 230, 500],
|
||||
},
|
||||
{
|
||||
label: 'Referral',
|
||||
data: [30, 90, 40, 140, 290, 290, 340, 230, 400]
|
||||
data: [30, 90, 40, 140, 290, 290, 340, 230, 400],
|
||||
},
|
||||
{
|
||||
label: 'Direct',
|
||||
data: [40, 80, 70, 90, 30, 90, 140, 130, 200]
|
||||
}
|
||||
]
|
||||
data: [40, 80, 70, 90, 30, 90, 140, 130, 200],
|
||||
},
|
||||
],
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
@ -160,12 +160,12 @@
|
||||
title="Refferals"
|
||||
:chart="{
|
||||
labels: ['Adobe', 'Atlassian', 'Slack', 'Spotify', 'Jira'],
|
||||
datasets: [{ label: 'Referrals', data: [25, 3, 12, 7, 10] }]
|
||||
datasets: [{ label: 'Referrals', data: [25, 3, 12, 7, 10] }],
|
||||
}"
|
||||
:actions="{
|
||||
route: 'https://creative-tim.com',
|
||||
label: 'See all referrals',
|
||||
color: 'secondary'
|
||||
color: 'secondary',
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
@ -177,28 +177,28 @@
|
||||
{
|
||||
label: 'Facebook',
|
||||
icon: 'facebook',
|
||||
progress: 80
|
||||
progress: 80,
|
||||
},
|
||||
{
|
||||
label: 'Twitter',
|
||||
icon: 'twitter',
|
||||
progress: 40
|
||||
progress: 40,
|
||||
},
|
||||
{
|
||||
label: 'Reddit',
|
||||
icon: 'reddit',
|
||||
progress: 30
|
||||
progress: 30,
|
||||
},
|
||||
{
|
||||
label: 'Youtube',
|
||||
icon: 'youtube',
|
||||
progress: 25
|
||||
progress: 25,
|
||||
},
|
||||
{
|
||||
label: 'Slack',
|
||||
icon: 'slack',
|
||||
progress: 15
|
||||
}
|
||||
progress: 15,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</div>
|
||||
@ -209,44 +209,44 @@
|
||||
url: '/bits',
|
||||
views: 345,
|
||||
time: '00:17:07',
|
||||
rate: '40.91%'
|
||||
rate: '40.91%',
|
||||
},
|
||||
{
|
||||
url: '/pages/argon-dashboard',
|
||||
views: 520,
|
||||
time: '00:23:13',
|
||||
rate: '30.14%'
|
||||
rate: '30.14%',
|
||||
},
|
||||
{
|
||||
url: '/pages/soft-ui-dashboard',
|
||||
views: 122,
|
||||
time: '00:3:10',
|
||||
rate: '54.10%'
|
||||
rate: '54.10%',
|
||||
},
|
||||
{
|
||||
url: '/bootstrap-themes',
|
||||
views: '1,900',
|
||||
time: '00:30:42',
|
||||
rate: '20.93%'
|
||||
rate: '20.93%',
|
||||
},
|
||||
{
|
||||
url: '/react-themes',
|
||||
views: '1,442',
|
||||
time: '00:31:50',
|
||||
rate: '34.98%'
|
||||
rate: '34.98%',
|
||||
},
|
||||
{
|
||||
url: '/product/argon-dashboard-angular',
|
||||
views: 201,
|
||||
time: '00:12:42',
|
||||
rate: '21.4%'
|
||||
rate: '21.4%',
|
||||
},
|
||||
{
|
||||
url: '/product/material-dashboard-pro',
|
||||
views: '2,115',
|
||||
time: '00:50:11',
|
||||
rate: '34.98%'
|
||||
}
|
||||
rate: '34.98%',
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</div>
|
||||
@ -274,7 +274,7 @@ export default {
|
||||
DefaultLineChart,
|
||||
DefaultDoughnutChart,
|
||||
SocialCard,
|
||||
PagesCard
|
||||
PagesCard,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@ -282,11 +282,11 @@ export default {
|
||||
logoAtlassian,
|
||||
logoSlack,
|
||||
logoSpotify,
|
||||
logoJira
|
||||
logoJira,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
setTooltip(this.$store.state.bootstrap);
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
<template>
|
||||
<navbar btn-background="bg-gradient-success" />
|
||||
<div
|
||||
class="pt-5 m-3 page-header align-items-start min-vh-50 pb-11 border-radius-lg"
|
||||
:style="{
|
||||
@ -11,10 +10,9 @@
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="mx-auto text-center col-lg-5">
|
||||
<h1 class="mt-5 mb-2 text-white">Welcome!</h1>
|
||||
<h1 class="mt-5 mb-2 text-white">Bonjour !</h1>
|
||||
<p class="text-white text-lead">
|
||||
Use these awesome forms to login or create new account in your
|
||||
project for free.
|
||||
Veuillez entrer vos identifiants pour vous connecter
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -25,7 +23,7 @@
|
||||
<div class="mx-auto col-xl-4 col-lg-5 col-md-7">
|
||||
<div class="card z-index-0">
|
||||
<div class="pt-4 text-center card-header">
|
||||
<h5>Sign in</h5>
|
||||
<h5>Connectez-vous</h5>
|
||||
</div>
|
||||
<div class="px-3 row px-xl-5 px-sm-4">
|
||||
<div class="px-1 col-3 ms-auto">
|
||||
@ -143,11 +141,11 @@
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
placeholder="Password"
|
||||
placeholder="Mot de passe"
|
||||
/>
|
||||
</div>
|
||||
<soft-switch id="rememberMe" name="rememberMe">
|
||||
Remember me
|
||||
Souvenez de moi
|
||||
</soft-switch>
|
||||
<div class="text-center">
|
||||
<soft-button
|
||||
@ -155,14 +153,14 @@
|
||||
variant="gradient"
|
||||
color="info"
|
||||
full-width
|
||||
>Sign in
|
||||
>Se connecter
|
||||
</soft-button>
|
||||
</div>
|
||||
<div class="mb-2 text-center position-relative">
|
||||
<p
|
||||
class="px-3 mb-2 text-sm bg-white font-weight-bold text-secondary text-border d-inline z-index-2"
|
||||
>
|
||||
or
|
||||
ou
|
||||
</p>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
@ -171,7 +169,7 @@
|
||||
variant="gradient"
|
||||
color="dark"
|
||||
full-width
|
||||
>Sign up
|
||||
>creer un compte
|
||||
</soft-button>
|
||||
</div>
|
||||
</form>
|
||||
@ -180,37 +178,29 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<app-footer />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Navbar from "@/examples/PageLayout/Navbar.vue";
|
||||
import AppFooter from "@/examples/PageLayout/Footer.vue";
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onBeforeUnmount } from "vue";
|
||||
import { useStore } from "vuex";
|
||||
import SoftInput from "@/components/SoftInput.vue";
|
||||
import SoftSwitch from "@/components/SoftSwitch.vue";
|
||||
import SoftButton from "@/components/SoftButton.vue";
|
||||
|
||||
import { mapMutations } from "vuex";
|
||||
export default {
|
||||
name: "SigninBasic",
|
||||
components: {
|
||||
Navbar,
|
||||
AppFooter,
|
||||
SoftInput,
|
||||
SoftSwitch,
|
||||
SoftButton,
|
||||
},
|
||||
const store = useStore();
|
||||
|
||||
created() {
|
||||
this.toggleEveryDisplay();
|
||||
this.toggleHideConfig();
|
||||
},
|
||||
beforeUnmount() {
|
||||
this.toggleEveryDisplay();
|
||||
this.toggleHideConfig();
|
||||
},
|
||||
methods: {
|
||||
...mapMutations(["toggleEveryDisplay", "toggleHideConfig"]),
|
||||
},
|
||||
};
|
||||
// Map mutations
|
||||
const toggleEveryDisplay = () => store.commit("toggleEveryDisplay");
|
||||
const toggleHideConfig = () => store.commit("toggleHideConfig");
|
||||
|
||||
// Lifecycle hooks
|
||||
onMounted(() => {
|
||||
toggleEveryDisplay();
|
||||
toggleHideConfig();
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
toggleEveryDisplay();
|
||||
toggleHideConfig();
|
||||
});
|
||||
</script>
|
||||
|
||||
@ -24,13 +24,19 @@
|
||||
<p class="mb-0">Enter your email and password to sign in</p>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form role="form" class="text-start">
|
||||
<form
|
||||
role="form"
|
||||
class="text-start"
|
||||
@submit.prevent="onSubmit"
|
||||
>
|
||||
<label>Email</label>
|
||||
<soft-input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="Email"
|
||||
name="email"
|
||||
:value="email"
|
||||
@input="onEmailInput"
|
||||
/>
|
||||
<label>Password</label>
|
||||
<soft-input
|
||||
@ -38,8 +44,15 @@
|
||||
type="password"
|
||||
placeholder="Password"
|
||||
name="password"
|
||||
:value="password"
|
||||
@input="onPasswordInput"
|
||||
/>
|
||||
<soft-switch id="rememberMe" name="rememberMe" checked>
|
||||
<soft-switch
|
||||
id="rememberMe"
|
||||
name="rememberMe"
|
||||
:checked="remember"
|
||||
@change="onRememberChange"
|
||||
>
|
||||
Remember me
|
||||
</soft-switch>
|
||||
<div class="text-center">
|
||||
@ -54,14 +67,7 @@
|
||||
</form>
|
||||
</div>
|
||||
<div class="px-1 pt-0 text-center card-footer px-lg-2">
|
||||
<p class="mx-auto mb-4 text-sm">
|
||||
Don't have an account?
|
||||
<router-link
|
||||
:to="{ name: 'Signup Cover' }"
|
||||
class="text-success text-gradient font-weight-bold"
|
||||
>Sign up</router-link
|
||||
>
|
||||
</p>
|
||||
<p class="mx-auto mb-4 text-sm">Don't have an account?</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -94,6 +100,7 @@ import AppFooter from "@/examples/PageLayout/Footer.vue";
|
||||
import SoftInput from "@/components/SoftInput.vue";
|
||||
import SoftSwitch from "@/components/SoftSwitch.vue";
|
||||
import SoftButton from "@/components/SoftButton.vue";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
const body = document.getElementsByTagName("body")[0];
|
||||
import { mapMutations } from "vuex";
|
||||
|
||||
@ -116,8 +123,39 @@ export default {
|
||||
this.toggleHideConfig();
|
||||
body.classList.add("bg-gray-100");
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
email: "",
|
||||
password: "",
|
||||
remember: true,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
...mapMutations(["toggleEveryDisplay", "toggleHideConfig"]),
|
||||
onEmailInput(e) {
|
||||
this.email = e.target.value;
|
||||
},
|
||||
onPasswordInput(e) {
|
||||
this.password = e.target.value;
|
||||
},
|
||||
onRememberChange(e) {
|
||||
this.remember = e.target.checked;
|
||||
},
|
||||
async onSubmit() {
|
||||
try {
|
||||
const auth = useAuthStore();
|
||||
await auth.login({
|
||||
email: this.email,
|
||||
password: this.password,
|
||||
remember: this.remember,
|
||||
});
|
||||
const redirect = this.$route.query.redirect || "/dashboard";
|
||||
this.$router.push(redirect);
|
||||
} catch (e) {
|
||||
// Optionally show a message to the user
|
||||
console.error("Login failed", e);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@ -11,15 +11,15 @@
|
||||
class="bg-gradient-secondary"
|
||||
:title="{
|
||||
text: 'Today\'s Trip',
|
||||
color: 'opacity-7 text-white'
|
||||
color: 'opacity-7 text-white',
|
||||
}"
|
||||
:value="{
|
||||
text: '145 Km',
|
||||
color: 'text-white'
|
||||
color: 'text-white',
|
||||
}"
|
||||
:icon="{
|
||||
component: 'text-dark ni ni-money-coins',
|
||||
background: 'bg-white'
|
||||
background: 'bg-white',
|
||||
}"
|
||||
direction-reverse
|
||||
/>
|
||||
@ -29,15 +29,15 @@
|
||||
class="bg-gradient-secondary"
|
||||
:title="{
|
||||
text: 'Battery Health',
|
||||
color: 'opacity-7 text-white'
|
||||
color: 'opacity-7 text-white',
|
||||
}"
|
||||
:value="{
|
||||
text: '99 %',
|
||||
color: 'text-white'
|
||||
color: 'text-white',
|
||||
}"
|
||||
:icon="{
|
||||
component: 'text-dark ni ni-controller',
|
||||
background: 'bg-white'
|
||||
background: 'bg-white',
|
||||
}"
|
||||
direction-reverse
|
||||
/>
|
||||
@ -47,15 +47,15 @@
|
||||
class="bg-gradient-secondary"
|
||||
:title="{
|
||||
text: 'Average Speed',
|
||||
color: 'opacity-7 text-white'
|
||||
color: 'opacity-7 text-white',
|
||||
}"
|
||||
:value="{
|
||||
text: '56 Km/h',
|
||||
color: 'text-white'
|
||||
color: 'text-white',
|
||||
}"
|
||||
:icon="{
|
||||
component: 'text-dark ni ni-delivery-fast',
|
||||
background: 'bg-white'
|
||||
background: 'bg-white',
|
||||
}"
|
||||
direction-reverse
|
||||
/>
|
||||
@ -65,15 +65,15 @@
|
||||
class="bg-gradient-secondary"
|
||||
:title="{
|
||||
text: 'Music Volume',
|
||||
color: 'opacity-7 text-white'
|
||||
color: 'opacity-7 text-white',
|
||||
}"
|
||||
:value="{
|
||||
text: '15/100',
|
||||
color: 'text-white'
|
||||
color: 'text-white',
|
||||
}"
|
||||
:icon="{
|
||||
component: 'text-dark ni ni-note-03',
|
||||
background: 'bg-white'
|
||||
background: 'bg-white',
|
||||
}"
|
||||
direction-reverse
|
||||
/>
|
||||
@ -98,10 +98,10 @@ export default {
|
||||
components: {
|
||||
MiniStatisticsCard,
|
||||
PlayerCard,
|
||||
CardDetail
|
||||
CardDetail,
|
||||
},
|
||||
mounted() {
|
||||
setTooltip(this.$store.state.bootstrap);
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@ -203,7 +203,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setupe lang="ts">
|
||||
<script>
|
||||
import MiniStatisticsCard from "../../examples/Cards/MiniStatisticsCard.vue";
|
||||
import ReportsBarChart from "../../examples/Charts/ReportsBarChart.vue";
|
||||
import GradientLineChart from "../../examples/Charts/GradientLineChart.vue";
|
||||
|
||||
@ -94,7 +94,7 @@
|
||||
:style="{
|
||||
backgroundImage:
|
||||
'url(' + require('@/assets/img/bg-smart-home-1.jpg') + ')',
|
||||
backgroundSize: 'cover'
|
||||
backgroundSize: 'cover',
|
||||
}"
|
||||
>
|
||||
<div class="top-0 position-absolute d-flex w-100">
|
||||
@ -115,7 +115,7 @@
|
||||
:style="{
|
||||
backgroundImage:
|
||||
'url(' + require('@/assets/img/bg-smart-home-2.jpg') + ')',
|
||||
backgroundSize: 'cover'
|
||||
backgroundSize: 'cover',
|
||||
}"
|
||||
>
|
||||
<div class="top-0 position-absolute d-flex w-100">
|
||||
@ -136,7 +136,7 @@
|
||||
:style="{
|
||||
backgroundImage:
|
||||
'url(' + require('@/assets/img/home-decor-3.jpg') + ')',
|
||||
backgroundSize: 'cover'
|
||||
backgroundSize: 'cover',
|
||||
}"
|
||||
>
|
||||
<div class="top-0 position-absolute d-flex w-100">
|
||||
@ -235,8 +235,8 @@
|
||||
labels: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
|
||||
datasets: {
|
||||
label: 'Watts',
|
||||
data: [150, 230, 380, 220, 420, 200, 70, 500]
|
||||
}
|
||||
data: [150, 230, 380, 220, 420, 200, 70, 500],
|
||||
},
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
@ -270,7 +270,7 @@
|
||||
state: 'Off',
|
||||
label: 'Humidity',
|
||||
description: 'Inactive since: 2 days',
|
||||
classCustom: 'mt-4'
|
||||
classCustom: 'mt-4',
|
||||
}"
|
||||
>
|
||||
<humidity />
|
||||
@ -283,7 +283,7 @@
|
||||
label: 'Temperature',
|
||||
description: 'Active',
|
||||
classCustom: 'mt-2',
|
||||
isChecked: 'true'
|
||||
isChecked: 'true',
|
||||
}"
|
||||
class="text-white bg-gradient-success"
|
||||
>
|
||||
@ -297,7 +297,7 @@
|
||||
label: 'Air Conditioner',
|
||||
description: 'Inactive since: 1 hour',
|
||||
classCustom: 'mt-4',
|
||||
isChecked: true
|
||||
isChecked: true,
|
||||
}"
|
||||
>
|
||||
<air />
|
||||
@ -309,7 +309,7 @@
|
||||
state: 'Off',
|
||||
label: 'Lights',
|
||||
description: 'Inactive since: 27 min',
|
||||
classCustom: 'mt-4'
|
||||
classCustom: 'mt-4',
|
||||
}"
|
||||
>
|
||||
<lights />
|
||||
@ -322,7 +322,7 @@
|
||||
label: 'Wi-fi',
|
||||
description: 'Active',
|
||||
classCustom: 'mt-4',
|
||||
isChecked: true
|
||||
isChecked: true,
|
||||
}"
|
||||
class="text-white bg-gradient-success"
|
||||
>
|
||||
@ -374,11 +374,11 @@ export default {
|
||||
Air,
|
||||
Lights,
|
||||
Wifi,
|
||||
Humidity
|
||||
Humidity,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showMenu: false
|
||||
showMenu: false,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
@ -405,7 +405,7 @@ export default {
|
||||
sliderType: "min-range",
|
||||
lineCap: "round",
|
||||
min: 16,
|
||||
max: 38
|
||||
max: 38,
|
||||
});
|
||||
// Rounded slider
|
||||
const setValue = function (value, active) {
|
||||
@ -436,6 +436,6 @@ export default {
|
||||
else if (ev.detail.high !== undefined) setHigh(ev.detail.high, true);
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
class="mx-3 mt-3 border-radius-xl position-relative"
|
||||
:style="{
|
||||
backgroundImage: 'url(' + require('../../../assets/img/vr-bg.jpg') + ')',
|
||||
backgroundSize: 'cover'
|
||||
backgroundSize: 'cover',
|
||||
}"
|
||||
>
|
||||
<sidenav
|
||||
@ -86,16 +86,16 @@
|
||||
:items="[
|
||||
{
|
||||
time: '08:00',
|
||||
description: `Synk up with Mark<small class='text-secondary font-weight-normal'>Hangouts</small>`
|
||||
description: `Synk up with Mark<small class='text-secondary font-weight-normal'>Hangouts</small>`,
|
||||
},
|
||||
{
|
||||
time: '09:30',
|
||||
description: `Gym<br /><small class='text-secondary font-weight-normal'>World Class</small>`
|
||||
description: `Gym<br /><small class='text-secondary font-weight-normal'>World Class</small>`,
|
||||
},
|
||||
{
|
||||
time: '11:00',
|
||||
description: `Design Review<br /><small class='text-secondary font-weight-normal'>Zoom</small>`
|
||||
}
|
||||
description: `Design Review<br /><small class='text-secondary font-weight-normal'>Zoom</small>`,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</div>
|
||||
@ -110,23 +110,23 @@
|
||||
{
|
||||
route: '/',
|
||||
tooltip: '2 New Messages',
|
||||
image: { url: image1, alt: 'Image Placeholder' }
|
||||
image: { url: image1, alt: 'Image Placeholder' },
|
||||
},
|
||||
{
|
||||
route: '/',
|
||||
tooltip: '1 New Messages',
|
||||
image: { url: image2, alt: 'Image Placeholder' }
|
||||
image: { url: image2, alt: 'Image Placeholder' },
|
||||
},
|
||||
{
|
||||
route: '/',
|
||||
tooltip: '13 New Messages',
|
||||
image: { url: image3, alt: 'Image Placeholder' }
|
||||
image: { url: image3, alt: 'Image Placeholder' },
|
||||
},
|
||||
{
|
||||
route: '/',
|
||||
tooltip: '7 New Messages',
|
||||
image: { url: image4, alt: 'Image Placeholder' }
|
||||
}
|
||||
image: { url: image4, alt: 'Image Placeholder' },
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</div>
|
||||
@ -170,18 +170,18 @@ export default {
|
||||
EmailCard,
|
||||
TodoCard,
|
||||
MiniPlayerCard,
|
||||
MessageCard
|
||||
MessageCard,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
image1,
|
||||
image2,
|
||||
image3,
|
||||
image4
|
||||
image4,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState(["isTransparent", "isNavFixed", "navbarFixed", "mcolor"])
|
||||
...mapState(["isTransparent", "isNavFixed", "navbarFixed", "mcolor"]),
|
||||
},
|
||||
mounted() {
|
||||
setTooltip(this.$store.state.bootstrap);
|
||||
@ -208,7 +208,7 @@ export default {
|
||||
this.$store.state.isTransparent = "bg-transparent";
|
||||
},
|
||||
methods: {
|
||||
...mapMutations(["navbarMinimize", "toggleConfigurator"])
|
||||
}
|
||||
...mapMutations(["navbarMinimize", "toggleConfigurator"]),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@ -306,7 +306,7 @@ export default {
|
||||
item = {
|
||||
src: linkEl.getAttribute("href"),
|
||||
w: parseInt(size[0], 10),
|
||||
h: parseInt(size[1], 10)
|
||||
h: parseInt(size[1], 10),
|
||||
};
|
||||
|
||||
if (figureEl.children.length > 1) {
|
||||
@ -430,9 +430,9 @@ export default {
|
||||
return {
|
||||
x: rect.left,
|
||||
y: rect.top + pageYScroll,
|
||||
w: rect.width
|
||||
w: rect.width,
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// PhotoSwipe opened from URL
|
||||
@ -502,10 +502,10 @@ export default {
|
||||
var element = document.getElementById(id);
|
||||
return new Choices(element, {
|
||||
searchEnabled: false,
|
||||
itemSelectText: ""
|
||||
itemSelectText: "",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@ -916,7 +916,7 @@ export default {
|
||||
const dataTableSearch = new DataTable("#products-list", {
|
||||
searchable: true,
|
||||
fixedHeight: false,
|
||||
perPage: 7
|
||||
perPage: 7,
|
||||
});
|
||||
|
||||
document.querySelectorAll(".export").forEach(function (el) {
|
||||
@ -925,7 +925,7 @@ export default {
|
||||
|
||||
var data = {
|
||||
type: type,
|
||||
filename: "soft-ui-" + type
|
||||
filename: "soft-ui-" + type,
|
||||
};
|
||||
|
||||
if (type === "csv") {
|
||||
@ -937,6 +937,6 @@ export default {
|
||||
});
|
||||
}
|
||||
setTooltip(this.$store.state.bootstrap);
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@ -7,22 +7,22 @@
|
||||
:percentage="{
|
||||
color: 'success',
|
||||
value: '+55%',
|
||||
label: 'since last month'
|
||||
label: 'since last month',
|
||||
}"
|
||||
menu="6 May - 7 May"
|
||||
:dropdown="[
|
||||
{
|
||||
label: 'Last 7 days',
|
||||
route: 'https://creative-tim.com/'
|
||||
route: 'https://creative-tim.com/',
|
||||
},
|
||||
{
|
||||
label: 'Last week',
|
||||
route: '/pages/widgets'
|
||||
route: '/pages/widgets',
|
||||
},
|
||||
{
|
||||
label: 'Last 30 days',
|
||||
route: '/'
|
||||
}
|
||||
route: '/',
|
||||
},
|
||||
]"
|
||||
/>
|
||||
<default-statistics-card
|
||||
@ -31,22 +31,22 @@
|
||||
:percentage="{
|
||||
color: 'success',
|
||||
value: '+12%',
|
||||
label: 'since last month'
|
||||
label: 'since last month',
|
||||
}"
|
||||
menu="9 June - 12 June"
|
||||
:dropdown="[
|
||||
{
|
||||
label: 'Last 7 days',
|
||||
route: 'javascript:;'
|
||||
route: 'javascript:;',
|
||||
},
|
||||
{
|
||||
label: 'Last week',
|
||||
route: 'javascript:;'
|
||||
route: 'javascript:;',
|
||||
},
|
||||
{
|
||||
label: 'Last 30 days',
|
||||
route: 'javascript:;'
|
||||
}
|
||||
route: 'javascript:;',
|
||||
},
|
||||
]"
|
||||
/>
|
||||
<default-statistics-card
|
||||
@ -55,22 +55,22 @@
|
||||
:percentage="{
|
||||
color: 'secondary',
|
||||
value: '+$213',
|
||||
label: 'since last month'
|
||||
label: 'since last month',
|
||||
}"
|
||||
menu="6 August - 9 August"
|
||||
:dropdown="[
|
||||
{
|
||||
label: 'Last 7 days',
|
||||
route: 'javascript:;'
|
||||
route: 'javascript:;',
|
||||
},
|
||||
{
|
||||
label: 'Last week',
|
||||
route: 'javascript:;'
|
||||
route: 'javascript:;',
|
||||
},
|
||||
{
|
||||
label: 'Last 30 days',
|
||||
route: 'javascript:;'
|
||||
}
|
||||
route: 'javascript:;',
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</div>
|
||||
@ -97,9 +97,9 @@
|
||||
datasets: [
|
||||
{
|
||||
label: 'Sales by age',
|
||||
data: [15, 20, 12, 60, 20, 15]
|
||||
}
|
||||
]
|
||||
data: [15, 20, 12, 60, 20, 15],
|
||||
},
|
||||
],
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
@ -180,7 +180,7 @@ export default {
|
||||
RevenueChartCard,
|
||||
HorizontalBarChart,
|
||||
OrdersListCard,
|
||||
DefaultStatisticsCard
|
||||
DefaultStatisticsCard,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@ -192,7 +192,7 @@ export default {
|
||||
info: "Refund rate is lower with 97% than other products",
|
||||
img:
|
||||
"https://raw.githubusercontent.com/creativetimofficial/public-assets/master/soft-ui-design-system/assets/img/ecommerce/blue-shoe.jpg",
|
||||
icon: "bold-down text-success"
|
||||
icon: "bold-down text-success",
|
||||
},
|
||||
{
|
||||
title: "Business Kit (Mug + Notebook)",
|
||||
@ -200,7 +200,7 @@ export default {
|
||||
values: ["$80.250", "$4.200", "40"],
|
||||
img:
|
||||
"https://raw.githubusercontent.com/creativetimofficial/public-assets/master/soft-ui-design-system/assets/img/ecommerce/black-mug.jpg",
|
||||
icon: "bold-down text-success"
|
||||
icon: "bold-down text-success",
|
||||
},
|
||||
{
|
||||
title: "Black Chair",
|
||||
@ -208,7 +208,7 @@ export default {
|
||||
values: ["$40.600", "$9.430", "54"],
|
||||
img:
|
||||
"https://raw.githubusercontent.com/creativetimofficial/public-assets/master/soft-ui-design-system/assets/img/ecommerce/black-chair.jpg",
|
||||
icon: "bold-up text-danger"
|
||||
icon: "bold-up text-danger",
|
||||
},
|
||||
{
|
||||
title: "Wireless Charger",
|
||||
@ -216,7 +216,7 @@ export default {
|
||||
values: ["$91.300", "$7.364", "5"],
|
||||
img:
|
||||
"https://raw.githubusercontent.com/creativetimofficial/public-assets/master/soft-ui-design-system/assets/img/ecommerce/bang-sound.jpg",
|
||||
icon: "bold-down text-success"
|
||||
icon: "bold-down text-success",
|
||||
},
|
||||
{
|
||||
title: "Mountain Trip Kit (Camera + Backpack)",
|
||||
@ -225,45 +225,45 @@ export default {
|
||||
info: "Refund rate is higher with 70% than other products",
|
||||
img:
|
||||
"https://raw.githubusercontent.com/creativetimofficial/public-assets/master/soft-ui-design-system/assets/img/ecommerce/photo-tools.jpg",
|
||||
icon: "bold-up text-danger"
|
||||
}
|
||||
icon: "bold-up text-danger",
|
||||
},
|
||||
],
|
||||
sales: [
|
||||
{
|
||||
country: "United States",
|
||||
sales: 2500,
|
||||
bounce: "29.9%",
|
||||
flag: US
|
||||
flag: US,
|
||||
},
|
||||
{
|
||||
country: "Germany",
|
||||
sales: "3.900",
|
||||
bounce: "40.22%",
|
||||
flag: DE
|
||||
flag: DE,
|
||||
},
|
||||
{
|
||||
country: "Great Britain",
|
||||
sales: "1.400",
|
||||
bounce: "23.44%",
|
||||
flag: GB
|
||||
flag: GB,
|
||||
},
|
||||
{
|
||||
country: "Brasil",
|
||||
sales: "562",
|
||||
bounce: "32.14%",
|
||||
flag: BR
|
||||
flag: BR,
|
||||
},
|
||||
{
|
||||
country: "Australia",
|
||||
sales: "400",
|
||||
bounce: "56.83%",
|
||||
flag: AU
|
||||
}
|
||||
]
|
||||
flag: AU,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
setTooltip(this.$store.state.bootstrap);
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
149
thanasoft-front/src/views/pages/Login.vue
Normal file
149
thanasoft-front/src/views/pages/Login.vue
Normal file
@ -0,0 +1,149 @@
|
||||
<template>
|
||||
<div
|
||||
class="pt-5 m-3 page-header align-items-start min-vh-50 pb-11 border-radius-lg"
|
||||
:style="{
|
||||
backgroundImage:
|
||||
'url(' + require('@/assets/img/curved-images/curved9.jpg') + ')',
|
||||
}"
|
||||
>
|
||||
<span class="mask bg-gradient-dark opacity-6"></span>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="mx-auto text-center col-lg-5">
|
||||
<h1 class="mt-5 mb-2 text-white">Bonjour !</h1>
|
||||
<p class="text-white text-lead">
|
||||
Veuillez entrer vos identifiants pour vous connecter
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container">
|
||||
<div class="row mt-lg-n10 mt-md-n11 mt-n10 justify-content-center">
|
||||
<div class="mx-auto col-xl-4 col-lg-5 col-md-7">
|
||||
<div class="card z-index-0">
|
||||
<div class="pt-4 text-center card-header">
|
||||
<h5>Connectez-vous</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form role="form" class="text-start" @submit.prevent="handleLogin">
|
||||
<div class="mb-3">
|
||||
<SoftInput
|
||||
id="email"
|
||||
:value="email"
|
||||
type="email"
|
||||
placeholder="Email"
|
||||
name="email"
|
||||
:is-required="true"
|
||||
@input="email = $event.target.value"
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<SoftInput
|
||||
id="password"
|
||||
:value="password"
|
||||
name="password"
|
||||
type="password"
|
||||
placeholder="Mot de passe"
|
||||
:is-required="true"
|
||||
@input="password = $event.target.value"
|
||||
/>
|
||||
</div>
|
||||
<SoftSwitch
|
||||
id="rememberMe"
|
||||
:checked="remember"
|
||||
name="rememberMe"
|
||||
@change="remember = $event.target.checked"
|
||||
>
|
||||
Souvenez de moi
|
||||
</SoftSwitch>
|
||||
<div
|
||||
v-if="errorMessage"
|
||||
class="alert alert-danger text-white"
|
||||
role="alert"
|
||||
>
|
||||
{{ errorMessage }}
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<SoftButton
|
||||
type="submit"
|
||||
class="my-4 mb-2"
|
||||
variant="gradient"
|
||||
color="info"
|
||||
full-width
|
||||
:disabled="isLoading"
|
||||
>
|
||||
{{ isLoading ? "Connexion..." : "Se connecter" }}
|
||||
</SoftButton>
|
||||
</div>
|
||||
<div class="mb-2 text-center position-relative">
|
||||
<p
|
||||
class="px-3 mb-2 text-sm bg-white font-weight-bold text-secondary text-border d-inline z-index-2"
|
||||
>
|
||||
ou
|
||||
</p>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<SoftButton
|
||||
class="mt-2 mb-4"
|
||||
variant="gradient"
|
||||
color="dark"
|
||||
full-width
|
||||
@click="$router.push('/register')"
|
||||
>
|
||||
Creer un compte
|
||||
</SoftButton>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import SoftInput from "@/components/SoftInput.vue";
|
||||
import SoftSwitch from "@/components/SoftSwitch.vue";
|
||||
import SoftButton from "@/components/SoftButton.vue";
|
||||
|
||||
const router = useRouter();
|
||||
const authStore = useAuthStore();
|
||||
|
||||
const email = ref("");
|
||||
const password = ref("");
|
||||
const remember = ref(false);
|
||||
const isLoading = ref(false);
|
||||
const errorMessage = ref("");
|
||||
|
||||
const handleLogin = async () => {
|
||||
if (!email.value || !password.value) {
|
||||
errorMessage.value = "Veuillez remplir tous les champs";
|
||||
return;
|
||||
}
|
||||
|
||||
isLoading.value = true;
|
||||
errorMessage.value = "";
|
||||
|
||||
try {
|
||||
await authStore.login({
|
||||
email: email.value,
|
||||
password: password.value,
|
||||
remember: remember.value,
|
||||
});
|
||||
|
||||
// Redirect to dashboard on success
|
||||
router.push("/dashboards/dashboard-default");
|
||||
} catch (error: any) {
|
||||
console.error("Login error:", error);
|
||||
errorMessage.value =
|
||||
error.response?.data?.message ||
|
||||
error.message ||
|
||||
"Email ou mot de passe incorrect";
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
153
thanasoft-front/src/views/pages/Register.vue
Normal file
153
thanasoft-front/src/views/pages/Register.vue
Normal file
@ -0,0 +1,153 @@
|
||||
<template>
|
||||
<div
|
||||
class="pt-5 m-3 page-header align-items-start min-vh-50 pb-11 border-radius-lg"
|
||||
:style="{
|
||||
backgroundImage:
|
||||
'url(' + require('@/assets/img/curved-images/curved9.jpg') + ')',
|
||||
}"
|
||||
>
|
||||
<span class="mask bg-gradient-dark opacity-6"></span>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="mx-auto text-center col-lg-5">
|
||||
<h1 class="mt-5 mb-2 text-white">Bienvenue !</h1>
|
||||
<p class="text-white text-lead">Créez votre compte pour commencer</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container">
|
||||
<div class="row mt-lg-n10 mt-md-n11 mt-n10 justify-content-center">
|
||||
<div class="mx-auto col-xl-4 col-lg-5 col-md-7">
|
||||
<div class="card z-index-0">
|
||||
<div class="pt-4 text-center card-header">
|
||||
<h5>Créer un compte</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form
|
||||
role="form"
|
||||
class="text-start"
|
||||
@submit.prevent="handleRegister"
|
||||
>
|
||||
<div class="mb-3">
|
||||
<SoftInput
|
||||
id="name"
|
||||
:value="name"
|
||||
type="text"
|
||||
placeholder="Nom"
|
||||
name="name"
|
||||
:is-required="true"
|
||||
@input="name = $event.target.value"
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<SoftInput
|
||||
id="email"
|
||||
:value="email"
|
||||
type="email"
|
||||
placeholder="Email"
|
||||
name="email"
|
||||
:is-required="true"
|
||||
@input="email = $event.target.value"
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<SoftInput
|
||||
id="password"
|
||||
:value="password"
|
||||
name="password"
|
||||
type="password"
|
||||
placeholder="Mot de passe"
|
||||
:is-required="true"
|
||||
@input="password = $event.target.value"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="errorMessage"
|
||||
class="alert alert-danger text-white"
|
||||
role="alert"
|
||||
>
|
||||
{{ errorMessage }}
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<SoftButton
|
||||
type="submit"
|
||||
class="my-4 mb-2"
|
||||
variant="gradient"
|
||||
color="success"
|
||||
full-width
|
||||
:disabled="isLoading"
|
||||
>
|
||||
{{ isLoading ? "Inscription..." : "Creer un compte" }}
|
||||
</SoftButton>
|
||||
</div>
|
||||
<div class="mb-2 text-center position-relative">
|
||||
<p
|
||||
class="px-3 mb-2 text-sm bg-white font-weight-bold text-secondary text-border d-inline z-index-2"
|
||||
>
|
||||
ou
|
||||
</p>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<SoftButton
|
||||
class="mt-2 mb-4"
|
||||
variant="gradient"
|
||||
color="dark"
|
||||
full-width
|
||||
@click="$router.push('/login')"
|
||||
>
|
||||
Se connecter
|
||||
</SoftButton>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import SoftInput from "@/components/SoftInput.vue";
|
||||
import SoftButton from "@/components/SoftButton.vue";
|
||||
|
||||
const router = useRouter();
|
||||
const authStore = useAuthStore();
|
||||
|
||||
const name = ref("");
|
||||
const email = ref("");
|
||||
const password = ref("");
|
||||
const isLoading = ref(false);
|
||||
const errorMessage = ref("");
|
||||
|
||||
const handleRegister = async () => {
|
||||
if (!name.value || !email.value || !password.value) {
|
||||
errorMessage.value = "Veuillez remplir tous les champs";
|
||||
return;
|
||||
}
|
||||
|
||||
isLoading.value = true;
|
||||
errorMessage.value = "";
|
||||
|
||||
try {
|
||||
await authStore.register({
|
||||
name: name.value,
|
||||
email: email.value,
|
||||
password: password.value,
|
||||
});
|
||||
|
||||
// Redirect to dashboard on success (user is automatically logged in)
|
||||
router.push("/dashboards/dashboard-default");
|
||||
} catch (error: any) {
|
||||
console.error("Registration error:", error);
|
||||
errorMessage.value =
|
||||
error.response?.data?.message ||
|
||||
error.message ||
|
||||
"Erreur lors de l'inscription";
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@ -5,7 +5,7 @@
|
||||
:style="{
|
||||
backgroundImage:
|
||||
'url(' + require('@/assets/img/curved-images/curved14.jpg') + ')',
|
||||
backgroundPositionY: '50%'
|
||||
backgroundPositionY: '50%',
|
||||
}"
|
||||
>
|
||||
<span class="mask bg-gradient-success opacity-6"></span>
|
||||
@ -283,25 +283,25 @@
|
||||
fullName: 'Alec M. Thompson',
|
||||
mobile: '(44) 123 1234 123',
|
||||
email: 'alecthompson@mail.com',
|
||||
location: 'USA'
|
||||
location: 'USA',
|
||||
}"
|
||||
:social="[
|
||||
{
|
||||
link: 'https://www.facebook.com/CreativeTim/',
|
||||
icon: faFacebook
|
||||
icon: faFacebook,
|
||||
},
|
||||
{
|
||||
link: 'https://twitter.com/creativetim',
|
||||
icon: faTwitter
|
||||
icon: faTwitter,
|
||||
},
|
||||
{
|
||||
link: 'https://www.instagram.com/creativetimofficial/',
|
||||
icon: faInstagram
|
||||
}
|
||||
icon: faInstagram,
|
||||
},
|
||||
]"
|
||||
:action="{
|
||||
route: 'javascript:;',
|
||||
tooltip: 'Edit Profile'
|
||||
tooltip: 'Edit Profile',
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
@ -445,24 +445,24 @@
|
||||
:authors="[
|
||||
{
|
||||
image: team1,
|
||||
name: 'Elena Morison'
|
||||
name: 'Elena Morison',
|
||||
},
|
||||
{
|
||||
image: team2,
|
||||
name: 'Ryan Milly'
|
||||
name: 'Ryan Milly',
|
||||
},
|
||||
{
|
||||
image: team3,
|
||||
name: 'Nick Daniel'
|
||||
name: 'Nick Daniel',
|
||||
},
|
||||
{
|
||||
image: team4,
|
||||
name: 'Peterson'
|
||||
}
|
||||
name: 'Peterson',
|
||||
},
|
||||
]"
|
||||
:action="{
|
||||
color: 'success',
|
||||
label: 'View Project'
|
||||
label: 'View Project',
|
||||
}"
|
||||
/>
|
||||
|
||||
@ -475,24 +475,24 @@
|
||||
:authors="[
|
||||
{
|
||||
image: team3,
|
||||
name: 'Nick Daniel'
|
||||
name: 'Nick Daniel',
|
||||
},
|
||||
{
|
||||
image: team4,
|
||||
name: 'Peterson'
|
||||
name: 'Peterson',
|
||||
},
|
||||
{
|
||||
image: team1,
|
||||
name: 'Elena Morison'
|
||||
name: 'Elena Morison',
|
||||
},
|
||||
{
|
||||
image: team2,
|
||||
name: 'Ryan Milly'
|
||||
}
|
||||
name: 'Ryan Milly',
|
||||
},
|
||||
]"
|
||||
:action="{
|
||||
color: 'success',
|
||||
label: 'View Project'
|
||||
label: 'View Project',
|
||||
}"
|
||||
/>
|
||||
|
||||
@ -505,24 +505,24 @@
|
||||
:authors="[
|
||||
{
|
||||
image: team4,
|
||||
name: 'Peterson'
|
||||
name: 'Peterson',
|
||||
},
|
||||
{
|
||||
image: team3,
|
||||
name: 'Nick Daniel'
|
||||
name: 'Nick Daniel',
|
||||
},
|
||||
{
|
||||
image: team1,
|
||||
name: 'Elena Morison'
|
||||
name: 'Elena Morison',
|
||||
},
|
||||
{
|
||||
image: team2,
|
||||
name: 'Ryan Milly'
|
||||
}
|
||||
name: 'Ryan Milly',
|
||||
},
|
||||
]"
|
||||
:action="{
|
||||
color: 'success',
|
||||
label: 'View Project'
|
||||
label: 'View Project',
|
||||
}"
|
||||
/>
|
||||
|
||||
@ -558,7 +558,7 @@ import team4 from "@/assets/img/team-4.jpg";
|
||||
import {
|
||||
faFacebook,
|
||||
faTwitter,
|
||||
faInstagram
|
||||
faInstagram,
|
||||
} from "@fortawesome/free-brands-svg-icons";
|
||||
import DefaultProjectCard from "./components/DefaultProjectCard.vue";
|
||||
import PlaceHolderCard from "@/examples/Cards/PlaceHolderCard.vue";
|
||||
@ -572,7 +572,7 @@ export default {
|
||||
ProfileInfoCard,
|
||||
SoftAvatar,
|
||||
DefaultProjectCard,
|
||||
PlaceHolderCard
|
||||
PlaceHolderCard,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@ -591,7 +591,7 @@ export default {
|
||||
img3,
|
||||
faFacebook,
|
||||
faTwitter,
|
||||
faInstagram
|
||||
faInstagram,
|
||||
};
|
||||
},
|
||||
|
||||
@ -602,6 +602,6 @@ export default {
|
||||
},
|
||||
beforeUnmount() {
|
||||
this.$store.state.isAbsolute = false;
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
:style="{
|
||||
backgroundImage:
|
||||
'url(' + require('@/assets/img/curved-images/curved14.jpg') + ')',
|
||||
backgroundPositionY: '50%'
|
||||
backgroundPositionY: '50%',
|
||||
}"
|
||||
>
|
||||
<span class="mask bg-gradient-success opacity-6"></span>
|
||||
@ -232,34 +232,34 @@
|
||||
:members="[
|
||||
{
|
||||
name: 'Alexa Tompson',
|
||||
image: team1
|
||||
image: team1,
|
||||
},
|
||||
{
|
||||
name: 'Romina Hadid',
|
||||
image: team2
|
||||
image: team2,
|
||||
},
|
||||
{
|
||||
name: 'Alexander Smith',
|
||||
image: team3
|
||||
image: team3,
|
||||
},
|
||||
{
|
||||
name: 'Martin Doe',
|
||||
image: team4
|
||||
}
|
||||
image: team4,
|
||||
},
|
||||
]"
|
||||
:dropdown="[
|
||||
{
|
||||
label: 'Action',
|
||||
route: 'javascript:;'
|
||||
route: 'javascript:;',
|
||||
},
|
||||
{
|
||||
label: 'Another action',
|
||||
route: 'javascript:;'
|
||||
route: 'javascript:;',
|
||||
},
|
||||
{
|
||||
label: 'Something else here',
|
||||
route: 'javascript:;'
|
||||
}
|
||||
route: 'javascript:;',
|
||||
},
|
||||
]"
|
||||
/>
|
||||
<team-profile-card
|
||||
@ -272,34 +272,34 @@
|
||||
:members="[
|
||||
{
|
||||
name: 'Martin Doe',
|
||||
image: team4
|
||||
image: team4,
|
||||
},
|
||||
{
|
||||
name: 'Alexander Smith',
|
||||
image: team3
|
||||
image: team3,
|
||||
},
|
||||
{
|
||||
name: 'Romina Hadid',
|
||||
image: team2
|
||||
image: team2,
|
||||
},
|
||||
{
|
||||
name: 'Alexa Tompson',
|
||||
image: team1
|
||||
}
|
||||
image: team1,
|
||||
},
|
||||
]"
|
||||
:dropdown="[
|
||||
{
|
||||
label: 'Action',
|
||||
route: 'javascript:;'
|
||||
route: 'javascript:;',
|
||||
},
|
||||
{
|
||||
label: 'Another action',
|
||||
route: 'javascript:;'
|
||||
route: 'javascript:;',
|
||||
},
|
||||
{
|
||||
label: 'Something else here',
|
||||
route: 'javascript:;'
|
||||
}
|
||||
route: 'javascript:;',
|
||||
},
|
||||
]"
|
||||
/>
|
||||
<event-card
|
||||
@ -312,12 +312,12 @@
|
||||
{ name: 'Alexa tompson', image: team1 },
|
||||
{ name: 'Romina Hadid', image: team2 },
|
||||
{ name: 'Alexander Smith', image: team3 },
|
||||
{ name: 'Martin Doe', image: ivana }
|
||||
{ name: 'Martin Doe', image: ivana },
|
||||
]"
|
||||
:action="{
|
||||
route: '',
|
||||
label: 'Join',
|
||||
color: 'success'
|
||||
color: 'success',
|
||||
}"
|
||||
/>
|
||||
<event-card
|
||||
@ -331,12 +331,12 @@
|
||||
{ name: 'Alexa tompson', image: team1 },
|
||||
{ name: 'Romina Hadid', image: team2 },
|
||||
{ name: 'Alexander Smith', image: team3 },
|
||||
{ name: 'Martin Doe', image: ivana }
|
||||
{ name: 'Martin Doe', image: ivana },
|
||||
]"
|
||||
:action="{
|
||||
route: '',
|
||||
label: 'Join',
|
||||
color: 'primary'
|
||||
color: 'primary',
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
@ -372,7 +372,7 @@ export default {
|
||||
TeamProfileCard,
|
||||
PostCard,
|
||||
StoryAvatar,
|
||||
EventCard
|
||||
EventCard,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@ -380,49 +380,49 @@ export default {
|
||||
stories: [
|
||||
{
|
||||
name: "Abbie W",
|
||||
image: team1
|
||||
image: team1,
|
||||
},
|
||||
{
|
||||
name: "Boris U",
|
||||
image: team2
|
||||
image: team2,
|
||||
},
|
||||
{
|
||||
name: "Kay R",
|
||||
image: team3
|
||||
image: team3,
|
||||
},
|
||||
{
|
||||
name: "Tom M",
|
||||
image: team4
|
||||
image: team4,
|
||||
},
|
||||
{
|
||||
name: "Nicole N",
|
||||
image: team5
|
||||
image: team5,
|
||||
},
|
||||
{
|
||||
name: "Marie P",
|
||||
image: marie
|
||||
image: marie,
|
||||
},
|
||||
{
|
||||
name: "Bruce M",
|
||||
image: bruce
|
||||
image: bruce,
|
||||
},
|
||||
{
|
||||
name: "Sandra A",
|
||||
image: ivana
|
||||
image: ivana,
|
||||
},
|
||||
{
|
||||
name: "Katty L",
|
||||
image: kal
|
||||
image: kal,
|
||||
},
|
||||
{
|
||||
name: "Emma O",
|
||||
image: emma
|
||||
image: emma,
|
||||
},
|
||||
{
|
||||
name: "Tao G",
|
||||
image:
|
||||
"https://raw.githubusercontent.com/creativetimofficial/public-assets/master/soft-ui-design-system/assets/img/team-9.jpg"
|
||||
}
|
||||
"https://raw.githubusercontent.com/creativetimofficial/public-assets/master/soft-ui-design-system/assets/img/team-9.jpg",
|
||||
},
|
||||
],
|
||||
nick,
|
||||
slackLogo,
|
||||
@ -436,7 +436,7 @@ export default {
|
||||
team2,
|
||||
team3,
|
||||
team4,
|
||||
team5
|
||||
team5,
|
||||
};
|
||||
},
|
||||
|
||||
@ -447,6 +447,6 @@ export default {
|
||||
},
|
||||
beforeUnmount() {
|
||||
this.$store.state.isAbsolute = false;
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@ -1,30 +1,40 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2018",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"target": "esnext",
|
||||
"module": "esnext",
|
||||
"strict": true,
|
||||
"jsx": "preserve",
|
||||
"importHelpers": true,
|
||||
"moduleResolution": "node",
|
||||
"allowJs": true,
|
||||
"checkJs": false,
|
||||
"noEmit": true,
|
||||
"strict": false,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"sourceMap": true,
|
||||
"baseUrl": ".",
|
||||
"types": [
|
||||
"webpack-env"
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
"@/*": [
|
||||
"src/*"
|
||||
]
|
||||
},
|
||||
"types": ["node"]
|
||||
"lib": [
|
||||
"esnext",
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"scripthost"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"src/**/*.tsx",
|
||||
"src/**/*.vue",
|
||||
"src/shims-vue.d.ts"
|
||||
"tests/**/*.ts",
|
||||
"tests/**/*.tsx"
|
||||
],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
|
||||
@ -1,24 +0,0 @@
|
||||
const path = require("path");
|
||||
|
||||
module.exports = {
|
||||
// Dossier de sortie du build
|
||||
outputDir: path.resolve(__dirname, "../thanasoft-back/public/build"),
|
||||
|
||||
// Le index.html généré
|
||||
indexPath: "index.html",
|
||||
|
||||
// En prod on sert les assets depuis /build/, en dev on reste sur la racine
|
||||
publicPath: process.env.NODE_ENV === "production" ? "/build/" : "/",
|
||||
|
||||
// Dev server config to work locally alongside Laravel backend
|
||||
devServer: {
|
||||
historyApiFallback: true,
|
||||
// proxy API calls to Laravel (adjust if your API prefix differs)
|
||||
proxy: {
|
||||
"^/api": {
|
||||
target: "http://127.0.0.1:8000",
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
Loading…
x
Reference in New Issue
Block a user