auth front and back

This commit is contained in:
Nyavokevin 2025-10-06 18:38:16 +03:00
parent e8fe78577b
commit 2f7ced5961
54 changed files with 2408 additions and 597 deletions

View File

@ -6,11 +6,12 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable; use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;
class User extends Authenticatable class User extends Authenticatable
{ {
/** @use HasFactory<\Database\Factories\UserFactory> */ /** @use HasFactory<\Database\Factories\UserFactory> */
use HasFactory, Notifiable; use HasApiTokens, HasFactory, Notifiable;
/** /**
* The attributes that are mass assignable. * The attributes that are mass assignable.

View File

@ -7,6 +7,7 @@ use Illuminate\Foundation\Configuration\Middleware;
return Application::configure(basePath: dirname(__DIR__)) return Application::configure(basePath: dirname(__DIR__))
->withRouting( ->withRouting(
web: __DIR__.'/../routes/web.php', web: __DIR__.'/../routes/web.php',
api: __DIR__.'/../routes/api.php',
commands: __DIR__.'/../routes/console.php', commands: __DIR__.'/../routes/console.php',
health: '/up', health: '/up',
) )

View File

@ -11,6 +11,7 @@
"require": { "require": {
"php": "^8.2", "php": "^8.2",
"laravel/framework": "^12.0", "laravel/framework": "^12.0",
"laravel/sanctum": "^4.2",
"laravel/tinker": "^2.10.1" "laravel/tinker": "^2.10.1"
}, },
"require-dev": { "require-dev": {
@ -75,4 +76,4 @@
}, },
"minimum-stability": "stable", "minimum-stability": "stable",
"prefer-stable": true "prefer-stable": true
} }

View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "c514d8f7b9fc5970bdd94287905ef584", "content-hash": "8f387a0734f3bf879214e4aa2fca6e2f",
"packages": [ "packages": [
{ {
"name": "brick/math", "name": "brick/math",
@ -1332,6 +1332,70 @@
}, },
"time": "2025-09-19T13:47:56+00:00" "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", "name": "laravel/serializable-closure",
"version": "v2.0.5", "version": "v2.0.5",

View 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,
];

View File

@ -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');
}
};

View File

@ -19,6 +19,8 @@ Route::prefix('auth')->group(function () {
Route::middleware('auth:sanctum')->group(function () { Route::middleware('auth:sanctum')->group(function () {
Route::get('/me', [AuthController::class, 'me']); 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', [AuthController::class, 'logout']);
Route::post('/logout-all', [AuthController::class, 'logoutAll']); Route::post('/logout-all', [AuthController::class, 'logoutAll']);
}); });

View File

@ -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

View File

@ -1,3 +0,0 @@
dist/
node_modules/
public/

View File

@ -1,19 +1,47 @@
module.exports = { module.exports = {
root: true, root: true,
env: { env: {
node: true,
browser: true, browser: true,
node: true,
es6: true, es6: true,
}, },
extends: [ extends: [
'plugin:vue/vue3-essential', "eslint:recommended",
'@vue/prettier', "plugin:vue/vue3-recommended",
"@vue/typescript",
"@vue/prettier",
], ],
parserOptions: { parserOptions: {
parser: 'babel-eslint', parser: "@typescript-eslint/parser",
ecmaVersion: 2020,
sourceType: "module",
}, },
rules: { rules: {
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', // Relax console/debugger in development
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off', "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",
},
},
],
}; };

View 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.

File diff suppressed because it is too large Load Diff

View File

@ -2,22 +2,12 @@
"name": "vue-soft-ui-dashboard-pro", "name": "vue-soft-ui-dashboard-pro",
"version": "3.0.0", "version": "3.0.0",
"private": true, "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", "description": "VueJS version of Soft UI Dashboard PRO by Creative Tim",
"homepage": "https://demos.creative-tim.com/vue-soft-ui-dashboard-pro/", "author": "Creative Tim",
"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"
},
"scripts": { "scripts": {
"serve": "vue-cli-service serve", "serve": "vue-cli-service serve",
"build": "vue-cli-service build", "build": "vue-cli-service build",
"lint": "vue-cli-service lint", "lint": "vue-cli-service lint"
"typecheck": "vue-tsc --noEmit"
}, },
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-svg-core": "1.3.0", "@fortawesome/fontawesome-svg-core": "1.3.0",
@ -30,6 +20,7 @@
"@fullcalendar/interaction": "5.10.1", "@fullcalendar/interaction": "5.10.1",
"@fullcalendar/timegrid": "5.10.1", "@fullcalendar/timegrid": "5.10.1",
"@popperjs/core": "2.10.2", "@popperjs/core": "2.10.2",
"axios": "^1.12.2",
"bootstrap": "5.1.3", "bootstrap": "5.1.3",
"chart.js": "3.6.0", "chart.js": "3.6.0",
"choices.js": "9.0.1", "choices.js": "9.0.1",
@ -39,6 +30,7 @@
"jkanban": "1.3.1", "jkanban": "1.3.1",
"leaflet": "1.7.1", "leaflet": "1.7.1",
"photoswipe": "4.1.3", "photoswipe": "4.1.3",
"pinia": "^2.0.36",
"quill": "1.3.6", "quill": "1.3.6",
"round-slider": "1.6.1", "round-slider": "1.6.1",
"simple-datatables": "3.2.0", "simple-datatables": "3.2.0",
@ -54,15 +46,16 @@
"vuex": "4.0.2" "vuex": "4.0.2"
}, },
"devDependencies": { "devDependencies": {
"@types/minimatch": "^6.0.0", "@typescript-eslint/eslint-plugin": "^4.18.0",
"@types/node": "^24.6.0", "@typescript-eslint/parser": "^4.18.0",
"@vue/cli-plugin-babel": "4.5.15", "@vue/cli-plugin-babel": "4.5.15",
"@vue/cli-plugin-eslint": "4.5.15", "@vue/cli-plugin-eslint": "4.5.15",
"@vue/cli-plugin-router": "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/cli-service": "4.5.15",
"@vue/compiler-sfc": "3.2.0", "@vue/compiler-sfc": "3.2.0",
"@vue/eslint-config-prettier": "6.0.0", "@vue/eslint-config-prettier": "6.0.0",
"@vue/eslint-config-typescript": "^7.0.0",
"babel-eslint": "10.1.0", "babel-eslint": "10.1.0",
"eslint": "6.7.2", "eslint": "6.7.2",
"eslint-plugin-prettier": "3.3.1", "eslint-plugin-prettier": "3.3.1",
@ -70,7 +63,15 @@
"prettier": "2.2.1", "prettier": "2.2.1",
"sass": "1.43.3", "sass": "1.43.3",
"sass-loader": "10.1.1", "sass-loader": "10.1.1",
"typescript": "^5.9.2", "typescript": "~4.1.5"
"vue-tsc": "^3.1.0" },
"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"
} }
} }

View File

@ -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" /> <link href="https://fonts.googleapis.com/css?family=Open+Sans:300,400,600,700" rel="stylesheet" />
<!-- Font Awesome Icons --> <!-- Font Awesome Icons: using CSS only to avoid blocked kit.js -->
<script src="https://kit.fontawesome.com/42d5adcbca.js" crossorigin="anonymous"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.1/css/all.min.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.1/css/all.min.css">

View File

@ -1,26 +1,34 @@
<template> <template>
<sidenav <!-- Guest Layout: for login, signup, auth pages -->
v-if="showSidenav" <div v-if="isGuestRoute" class="guest-layout">
: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"
/>
<router-view /> <router-view />
<app-footer v-show="showFooter" /> </div>
<configurator
:toggle="toggleConfigurator" <!-- Dashboard Layout: for authenticated pages -->
:class="[showConfig ? 'show' : '', hideConfigButton ? 'd-none' : '']" <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> </template>
<script> <script>
import Sidenav from "./examples/Sidenav"; import Sidenav from "./examples/Sidenav";
@ -51,6 +59,10 @@ export default {
"showConfig", "showConfig",
"hideConfigButton", "hideConfigButton",
]), ]),
isGuestRoute() {
// Check if current route has guestLayout meta flag
return this.$route.meta?.guestLayout === true;
},
}, },
beforeMount() { beforeMount() {
this.$store.state.isTransparent = "bg-transparent"; this.$store.state.isTransparent = "bg-transparent";

View File

@ -0,0 +1,2 @@
// Custom custom styles for Soft UI Dashboard
// Leave empty or write additional styles.

View File

@ -0,0 +1,3 @@
// Custom variable overrides for Soft UI Dashboard
// Leave empty or override variables, e.g.:
// $primary: #5e72e4;

View 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>

View File

@ -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>

View 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>

View File

@ -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>

View 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>

View File

@ -55,16 +55,16 @@
</div> </div>
<ul class="navbar-nav justify-content-end"> <ul class="navbar-nav justify-content-end">
<li class="nav-item d-flex align-items-center"> <li class="nav-item d-flex align-items-center">
<router-link <a
:to="{ name: 'Signin Basic' }" href="#"
class="px-0 nav-link font-weight-bold" class="px-0 nav-link font-weight-bold"
:class="textWhite ? textWhite : 'text-body'" :class="textWhite ? textWhite : 'text-body'"
target="_blank" @click.prevent="handleLogout"
> >
<i class="fa fa-user" :class="isRTL ? 'ms-sm-2' : 'me-sm-1'"></i> <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-if="isRTL" class="d-sm-inline d-none">تسجيل خروج</span>
<span v-else class="d-sm-inline d-none">Sign In </span> <span v-else class="d-sm-inline d-none">Logout</span>
</router-link> </a>
</li> </li>
<li class="nav-item d-xl-none ps-3 d-flex align-items-center"> <li class="nav-item d-xl-none ps-3 d-flex align-items-center">
<a <a
@ -223,6 +223,7 @@
<script> <script>
import Breadcrumbs from "../Breadcrumbs.vue"; import Breadcrumbs from "../Breadcrumbs.vue";
import { mapMutations, mapActions, mapState } from "vuex"; import { mapMutations, mapActions, mapState } from "vuex";
import { useAuthStore } from "@/stores/auth";
export default { export default {
name: "Navbar", name: "Navbar",
@ -265,6 +266,19 @@ export default {
this.toggleSidebarColor("bg-white"); this.toggleSidebarColor("bg-white");
this.navbarMinimize(); 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> </script>

View File

@ -9,7 +9,8 @@
class="navbar-brand font-weight-bolder ms-lg-0 ms-3" class="navbar-brand font-weight-bolder ms-lg-0 ms-3"
:class="darkMode ? 'text-black' : 'text-white'" :class="darkMode ? 'text-black' : 'text-white'"
to="/" to="/"
>Soft UI Dashboard PRO</router-link> >Soft UI Dashboard PRO</router-link
>
<button <button
class="shadow-none navbar-toggler ms-2" class="shadow-none navbar-toggler ms-2"
type="button" type="button"

View File

@ -15,11 +15,9 @@
> >
<slot name="icon"></slot> <slot name="icon"></slot>
</div> </div>
<span <span class="nav-link-text" :class="isRTL ? ' me-1' : 'ms-1'">{{
class="nav-link-text" navText
:class="isRTL ? ' me-1' : 'ms-1'" }}</span>
>{{ navText }}</span
>
</a> </a>
<div :id="collapseRef" class="collapse"> <div :id="collapseRef" class="collapse">
<slot name="list"></slot> <slot name="list"></slot>
@ -32,16 +30,16 @@ export default {
props: { props: {
collapseRef: { collapseRef: {
type: String, type: String,
required: true required: true,
}, },
navText: { navText: {
type: String, type: String,
required: true required: true,
}, },
collapse: { collapse: {
type: Boolean, type: Boolean,
default: true default: true,
} },
}, },
computed: { computed: {
...mapState(["isRTL"]), ...mapState(["isRTL"]),

View File

@ -24,16 +24,16 @@ export default {
props: { props: {
refer: { refer: {
type: String, type: String,
required: true required: true,
}, },
miniIcon: { miniIcon: {
type: String, type: String,
required: true required: true,
}, },
text: { text: {
type: String, type: String,
required: true required: true,
} },
} },
}; };
</script> </script>

View File

@ -844,9 +844,9 @@ export default {
default: "", default: "",
}, },
}, },
computed: { computed: {
...mapState(["isRTL"]), ...mapState(["isRTL"]),
}, },
methods: { methods: {
getRoute() { getRoute() {
const routeArr = this.$route.path.split("/"); const routeArr = this.$route.path.split("/");

View File

@ -18,8 +18,10 @@ import "./assets/css/nucleo-svg.css";
import VueTilt from "vue-tilt.js"; import VueTilt from "vue-tilt.js";
import VueSweetalert2 from "vue-sweetalert2"; import VueSweetalert2 from "vue-sweetalert2";
import SoftUIDashboard from "./soft-ui-dashboard"; import SoftUIDashboard from "./soft-ui-dashboard";
import pinia from "./plugins/pinia";
const appInstance = createApp(App); const appInstance = createApp(App);
appInstance.use(pinia);
appInstance.use(store); appInstance.use(store);
appInstance.use(router); appInstance.use(router);
appInstance.use(VueTilt); appInstance.use(VueTilt);

View 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

View File

@ -54,12 +54,30 @@ import lockBasic from "../views/auth/lock/Basic.vue";
import lockCover from "../views/auth/lock/Cover.vue"; import lockCover from "../views/auth/lock/Cover.vue";
import lockIllustration from "../views/auth/lock/Illustration.vue"; import lockIllustration from "../views/auth/lock/Illustration.vue";
//ROUTE SHOULD USED
const routes = [ const routes = [
{ {
path: "/", path: "/",
name: "/", name: "/",
redirect: "/dashboards/dashboard-default", 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", path: "/dashboards/dashboard-default",
name: "Default", name: "Default",
@ -249,94 +267,158 @@ const routes = [
path: "/authentication/signin/basic", path: "/authentication/signin/basic",
name: "Signin Basic", name: "Signin Basic",
component: Basic, component: Basic,
meta: { guestLayout: true },
}, },
{ {
path: "/authentication/signin/cover", path: "/authentication/signin/cover",
name: "Signin Cover", name: "Signin Cover",
component: Cover, component: Cover,
meta: { guestLayout: true },
}, },
{ {
path: "/authentication/signin/illustration", path: "/authentication/signin/illustration",
name: "Signin Illustration", name: "Signin Illustration",
component: Illustration, component: Illustration,
meta: { guestLayout: true },
}, },
{ {
path: "/authentication/reset/basic", path: "/authentication/reset/basic",
name: "Reset Basic", name: "Reset Basic",
component: ResetBasic, component: ResetBasic,
meta: { guestLayout: true },
}, },
{ {
path: "/authentication/reset/cover", path: "/authentication/reset/cover",
name: "Reset Cover", name: "Reset Cover",
component: ResetCover, component: ResetCover,
meta: { guestLayout: true },
}, },
{ {
path: "/authentication/reset/illustration", path: "/authentication/reset/illustration",
name: "Reset Illustration", name: "Reset Illustration",
component: ResetIllustration, component: ResetIllustration,
meta: { guestLayout: true },
}, },
{ {
path: "/authentication/lock/basic", path: "/authentication/lock/basic",
name: "Lock Basic", name: "Lock Basic",
component: lockBasic, component: lockBasic,
meta: { guestLayout: true },
}, },
{ {
path: "/authentication/lock/cover", path: "/authentication/lock/cover",
name: "Lock Cover", name: "Lock Cover",
component: lockCover, component: lockCover,
meta: { guestLayout: true },
}, },
{ {
path: "/authentication/lock/illustration", path: "/authentication/lock/illustration",
name: "Lock Illustration", name: "Lock Illustration",
component: lockIllustration, component: lockIllustration,
meta: { guestLayout: true },
}, },
{ {
path: "/authentication/verification/basic", path: "/authentication/verification/basic",
name: "Verification Basic", name: "Verification Basic",
component: VerificationBasic, component: VerificationBasic,
meta: { guestLayout: true },
}, },
{ {
path: "/authentication/verification/cover", path: "/authentication/verification/cover",
name: "Verification Cover", name: "Verification Cover",
component: VerificationCover, component: VerificationCover,
meta: { guestLayout: true },
}, },
{ {
path: "/authentication/verification/illustration", path: "/authentication/verification/illustration",
name: "Verification Illustration", name: "Verification Illustration",
component: VerificationIllustration, component: VerificationIllustration,
meta: { guestLayout: true },
}, },
{ {
path: "/authentication/signup/basic", path: "/authentication/signup/basic",
name: "Signup Basic", name: "Signup Basic",
component: SignupBasic, component: SignupBasic,
meta: { guestLayout: true },
}, },
{ {
path: "/authentication/signup/cover", path: "/authentication/signup/cover",
name: "Signup Cover", name: "Signup Cover",
component: SignupCover, component: SignupCover,
meta: { guestLayout: true },
}, },
{ {
path: "/authentication/signup/illustration", path: "/authentication/signup/illustration",
name: "Signup Illustration", name: "Signup Illustration",
component: SignupIllustration, component: SignupIllustration,
meta: { guestLayout: true },
}, },
{ {
path: "/authentication/error/error404", path: "/authentication/error/error404",
name: "Error Error404", name: "Error Error404",
component: Error404, component: Error404,
meta: { guestLayout: true },
}, },
{ {
path: "/authentication/error/error500", path: "/authentication/error/error500",
name: "Error Error500", name: "Error Error500",
component: Error500, component: Error500,
meta: { guestLayout: true },
}, },
]; ];
const router = createRouter({ const router = createRouter({
// Use root base so the app works whether served at / or via /build/index.html history: createWebHistory(process.env.BASE_URL),
history: createWebHistory("/"),
routes, routes,
linkActiveClass: "active", 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; export default router;

View 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;

View 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 Laravels 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;
}

View File

@ -0,0 +1,2 @@
export * from './http'
export { default as AuthService } from './auth'

View File

@ -1,35 +1,13 @@
declare module "*.vue" { /* eslint-disable */
import type { DefineComponent } from "vue"; declare module '*.vue' {
const component: DefineComponent<{}, {}, any>; import type { DefineComponent } from 'vue'
export default component; const component: DefineComponent<{}, {}, any>
export default component
} }
declare module "*.png" { // Type shim for packages without TypeScript declarations
const src: string; declare module 'vue-tilt.js' {
export default src; import type { Plugin } from 'vue'
} const VueTilt: Plugin
export default VueTilt
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;
} }

View File

@ -19,7 +19,7 @@ export default createStore({
navbarFixed: navbarFixed:
"position-sticky blur shadow-blur left-auto top-1 z-index-sticky px-0 mx-4", "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", absolute: "position-absolute px-4 mx-0 w-100 z-index-2",
bootstrap bootstrap,
}, },
mutations: { mutations: {
toggleConfigurator(state) { toggleConfigurator(state) {
@ -57,7 +57,7 @@ export default createStore({
}, },
toggleHideConfig(state) { toggleHideConfig(state) {
state.hideConfigButton = !state.hideConfigButton; state.hideConfigButton = !state.hideConfigButton;
} },
}, },
actions: { actions: {
toggleSidebarColor({ commit }, payload) { toggleSidebarColor({ commit }, payload) {
@ -67,5 +67,5 @@ export default createStore({
commit("cardBackground", payload); commit("cardBackground", payload);
}, },
}, },
getters: {} getters: {},
}); });

View 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;

View File

@ -1,8 +0,0 @@
export interface User {
id: number;
name: string;
email: string;
email_verified_at?: string | null;
created_at?: string;
updated_at?: string;
}

View File

@ -1,7 +1,18 @@
<template> <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> </template>
<script setup lang="ts"> <script lang="ts">
import DefaultDashboard from "@/views/dashboards/Default.vue"; import { defineComponent } from "vue";
import HelloWorld from "@/components/HelloWorld.vue"; // @ is an alias to /src
export default defineComponent({
name: "Home",
components: {
HelloWorld,
},
});
</script> </script>

View File

@ -40,11 +40,11 @@
value="930" value="930"
:percentage="{ :percentage="{
value: '+55%', value: '+55%',
color: 'text-success' color: 'text-success',
}" }"
:icon="{ :icon="{
component: 'ni ni-circle-08', component: 'ni ni-circle-08',
background: 'bg-gradient-dark' background: 'bg-gradient-dark',
}" }"
class="ms-1" class="ms-1"
direction-reverse direction-reverse
@ -56,11 +56,11 @@
value="744" value="744"
:percentage="{ :percentage="{
value: '+3%', value: '+3%',
color: 'text-success' color: 'text-success',
}" }"
:icon="{ :icon="{
component: 'ni ni-world', component: 'ni ni-world',
background: 'bg-gradient-dark' background: 'bg-gradient-dark',
}" }"
class="ms-1" class="ms-1"
direction-reverse direction-reverse
@ -72,11 +72,11 @@
value="1,414" value="1,414"
:percentage="{ :percentage="{
value: '-2%', value: '-2%',
color: 'text-danger' color: 'text-danger',
}" }"
:icon="{ :icon="{
component: 'ni ni-watch-time', component: 'ni ni-watch-time',
background: 'bg-gradient-dark' background: 'bg-gradient-dark',
}" }"
class="ms-1" class="ms-1"
direction-reverse direction-reverse
@ -88,11 +88,11 @@
value="1.76" value="1.76"
:percentage="{ :percentage="{
value: '+5%', value: '+5%',
color: 'text-success' color: 'text-success',
}" }"
:icon="{ :icon="{
component: 'ni ni-image', component: 'ni ni-image',
background: 'bg-gradient-dark' background: 'bg-gradient-dark',
}" }"
class="ms-1" class="ms-1"
direction-reverse direction-reverse
@ -133,22 +133,22 @@
'Sep', 'Sep',
'Oct', 'Oct',
'Nov', 'Nov',
'Dec' 'Dec',
], ],
datasets: [ datasets: [
{ {
label: 'Organic Search', 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', label: 'Referral',
data: [30, 90, 40, 140, 290, 290, 340, 230, 400] data: [30, 90, 40, 140, 290, 290, 340, 230, 400],
}, },
{ {
label: 'Direct', label: 'Direct',
data: [40, 80, 70, 90, 30, 90, 140, 130, 200] data: [40, 80, 70, 90, 30, 90, 140, 130, 200],
} },
] ],
}" }"
/> />
</div> </div>
@ -160,12 +160,12 @@
title="Refferals" title="Refferals"
:chart="{ :chart="{
labels: ['Adobe', 'Atlassian', 'Slack', 'Spotify', 'Jira'], 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="{ :actions="{
route: 'https://creative-tim.com', route: 'https://creative-tim.com',
label: 'See all referrals', label: 'See all referrals',
color: 'secondary' color: 'secondary',
}" }"
/> />
</div> </div>
@ -177,28 +177,28 @@
{ {
label: 'Facebook', label: 'Facebook',
icon: 'facebook', icon: 'facebook',
progress: 80 progress: 80,
}, },
{ {
label: 'Twitter', label: 'Twitter',
icon: 'twitter', icon: 'twitter',
progress: 40 progress: 40,
}, },
{ {
label: 'Reddit', label: 'Reddit',
icon: 'reddit', icon: 'reddit',
progress: 30 progress: 30,
}, },
{ {
label: 'Youtube', label: 'Youtube',
icon: 'youtube', icon: 'youtube',
progress: 25 progress: 25,
}, },
{ {
label: 'Slack', label: 'Slack',
icon: 'slack', icon: 'slack',
progress: 15 progress: 15,
} },
]" ]"
/> />
</div> </div>
@ -209,44 +209,44 @@
url: '/bits', url: '/bits',
views: 345, views: 345,
time: '00:17:07', time: '00:17:07',
rate: '40.91%' rate: '40.91%',
}, },
{ {
url: '/pages/argon-dashboard', url: '/pages/argon-dashboard',
views: 520, views: 520,
time: '00:23:13', time: '00:23:13',
rate: '30.14%' rate: '30.14%',
}, },
{ {
url: '/pages/soft-ui-dashboard', url: '/pages/soft-ui-dashboard',
views: 122, views: 122,
time: '00:3:10', time: '00:3:10',
rate: '54.10%' rate: '54.10%',
}, },
{ {
url: '/bootstrap-themes', url: '/bootstrap-themes',
views: '1,900', views: '1,900',
time: '00:30:42', time: '00:30:42',
rate: '20.93%' rate: '20.93%',
}, },
{ {
url: '/react-themes', url: '/react-themes',
views: '1,442', views: '1,442',
time: '00:31:50', time: '00:31:50',
rate: '34.98%' rate: '34.98%',
}, },
{ {
url: '/product/argon-dashboard-angular', url: '/product/argon-dashboard-angular',
views: 201, views: 201,
time: '00:12:42', time: '00:12:42',
rate: '21.4%' rate: '21.4%',
}, },
{ {
url: '/product/material-dashboard-pro', url: '/product/material-dashboard-pro',
views: '2,115', views: '2,115',
time: '00:50:11', time: '00:50:11',
rate: '34.98%' rate: '34.98%',
} },
]" ]"
/> />
</div> </div>
@ -274,7 +274,7 @@ export default {
DefaultLineChart, DefaultLineChart,
DefaultDoughnutChart, DefaultDoughnutChart,
SocialCard, SocialCard,
PagesCard PagesCard,
}, },
data() { data() {
return { return {
@ -282,11 +282,11 @@ export default {
logoAtlassian, logoAtlassian,
logoSlack, logoSlack,
logoSpotify, logoSpotify,
logoJira logoJira,
}; };
}, },
mounted() { mounted() {
setTooltip(this.$store.state.bootstrap); setTooltip(this.$store.state.bootstrap);
} },
}; };
</script> </script>

View File

@ -1,5 +1,4 @@
<template> <template>
<navbar btn-background="bg-gradient-success" />
<div <div
class="pt-5 m-3 page-header align-items-start min-vh-50 pb-11 border-radius-lg" class="pt-5 m-3 page-header align-items-start min-vh-50 pb-11 border-radius-lg"
:style="{ :style="{
@ -11,10 +10,9 @@
<div class="container"> <div class="container">
<div class="row justify-content-center"> <div class="row justify-content-center">
<div class="mx-auto text-center col-lg-5"> <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"> <p class="text-white text-lead">
Use these awesome forms to login or create new account in your Veuillez entrer vos identifiants pour vous connecter
project for free.
</p> </p>
</div> </div>
</div> </div>
@ -25,7 +23,7 @@
<div class="mx-auto col-xl-4 col-lg-5 col-md-7"> <div class="mx-auto col-xl-4 col-lg-5 col-md-7">
<div class="card z-index-0"> <div class="card z-index-0">
<div class="pt-4 text-center card-header"> <div class="pt-4 text-center card-header">
<h5>Sign in</h5> <h5>Connectez-vous</h5>
</div> </div>
<div class="px-3 row px-xl-5 px-sm-4"> <div class="px-3 row px-xl-5 px-sm-4">
<div class="px-1 col-3 ms-auto"> <div class="px-1 col-3 ms-auto">
@ -143,11 +141,11 @@
id="password" id="password"
name="password" name="password"
type="password" type="password"
placeholder="Password" placeholder="Mot de passe"
/> />
</div> </div>
<soft-switch id="rememberMe" name="rememberMe"> <soft-switch id="rememberMe" name="rememberMe">
Remember me Souvenez de moi
</soft-switch> </soft-switch>
<div class="text-center"> <div class="text-center">
<soft-button <soft-button
@ -155,14 +153,14 @@
variant="gradient" variant="gradient"
color="info" color="info"
full-width full-width
>Sign in >Se connecter
</soft-button> </soft-button>
</div> </div>
<div class="mb-2 text-center position-relative"> <div class="mb-2 text-center position-relative">
<p <p
class="px-3 mb-2 text-sm bg-white font-weight-bold text-secondary text-border d-inline z-index-2" class="px-3 mb-2 text-sm bg-white font-weight-bold text-secondary text-border d-inline z-index-2"
> >
or ou
</p> </p>
</div> </div>
<div class="text-center"> <div class="text-center">
@ -171,7 +169,7 @@
variant="gradient" variant="gradient"
color="dark" color="dark"
full-width full-width
>Sign up >creer un compte
</soft-button> </soft-button>
</div> </div>
</form> </form>
@ -180,37 +178,29 @@
</div> </div>
</div> </div>
</div> </div>
<app-footer />
</template> </template>
<script> <script setup lang="ts">
import Navbar from "@/examples/PageLayout/Navbar.vue"; import { onMounted, onBeforeUnmount } from "vue";
import AppFooter from "@/examples/PageLayout/Footer.vue"; import { useStore } from "vuex";
import SoftInput from "@/components/SoftInput.vue"; import SoftInput from "@/components/SoftInput.vue";
import SoftSwitch from "@/components/SoftSwitch.vue"; import SoftSwitch from "@/components/SoftSwitch.vue";
import SoftButton from "@/components/SoftButton.vue"; import SoftButton from "@/components/SoftButton.vue";
import { mapMutations } from "vuex"; const store = useStore();
export default {
name: "SigninBasic",
components: {
Navbar,
AppFooter,
SoftInput,
SoftSwitch,
SoftButton,
},
created() { // Map mutations
this.toggleEveryDisplay(); const toggleEveryDisplay = () => store.commit("toggleEveryDisplay");
this.toggleHideConfig(); const toggleHideConfig = () => store.commit("toggleHideConfig");
},
beforeUnmount() { // Lifecycle hooks
this.toggleEveryDisplay(); onMounted(() => {
this.toggleHideConfig(); toggleEveryDisplay();
}, toggleHideConfig();
methods: { });
...mapMutations(["toggleEveryDisplay", "toggleHideConfig"]),
}, onBeforeUnmount(() => {
}; toggleEveryDisplay();
toggleHideConfig();
});
</script> </script>

View File

@ -24,13 +24,19 @@
<p class="mb-0">Enter your email and password to sign in</p> <p class="mb-0">Enter your email and password to sign in</p>
</div> </div>
<div class="card-body"> <div class="card-body">
<form role="form" class="text-start"> <form
role="form"
class="text-start"
@submit.prevent="onSubmit"
>
<label>Email</label> <label>Email</label>
<soft-input <soft-input
id="email" id="email"
type="email" type="email"
placeholder="Email" placeholder="Email"
name="email" name="email"
:value="email"
@input="onEmailInput"
/> />
<label>Password</label> <label>Password</label>
<soft-input <soft-input
@ -38,8 +44,15 @@
type="password" type="password"
placeholder="Password" placeholder="Password"
name="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 Remember me
</soft-switch> </soft-switch>
<div class="text-center"> <div class="text-center">
@ -54,14 +67,7 @@
</form> </form>
</div> </div>
<div class="px-1 pt-0 text-center card-footer px-lg-2"> <div class="px-1 pt-0 text-center card-footer px-lg-2">
<p class="mx-auto mb-4 text-sm"> <p class="mx-auto mb-4 text-sm">Don't have an account?</p>
Don't have an account?
<router-link
:to="{ name: 'Signup Cover' }"
class="text-success text-gradient font-weight-bold"
>Sign up</router-link
>
</p>
</div> </div>
</div> </div>
</div> </div>
@ -94,6 +100,7 @@ import AppFooter from "@/examples/PageLayout/Footer.vue";
import SoftInput from "@/components/SoftInput.vue"; import SoftInput from "@/components/SoftInput.vue";
import SoftSwitch from "@/components/SoftSwitch.vue"; import SoftSwitch from "@/components/SoftSwitch.vue";
import SoftButton from "@/components/SoftButton.vue"; import SoftButton from "@/components/SoftButton.vue";
import { useAuthStore } from "@/stores/auth";
const body = document.getElementsByTagName("body")[0]; const body = document.getElementsByTagName("body")[0];
import { mapMutations } from "vuex"; import { mapMutations } from "vuex";
@ -116,8 +123,39 @@ export default {
this.toggleHideConfig(); this.toggleHideConfig();
body.classList.add("bg-gray-100"); body.classList.add("bg-gray-100");
}, },
data() {
return {
email: "",
password: "",
remember: true,
};
},
methods: { methods: {
...mapMutations(["toggleEveryDisplay", "toggleHideConfig"]), ...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> </script>

View File

@ -11,15 +11,15 @@
class="bg-gradient-secondary" class="bg-gradient-secondary"
:title="{ :title="{
text: 'Today\'s Trip', text: 'Today\'s Trip',
color: 'opacity-7 text-white' color: 'opacity-7 text-white',
}" }"
:value="{ :value="{
text: '145 Km', text: '145 Km',
color: 'text-white' color: 'text-white',
}" }"
:icon="{ :icon="{
component: 'text-dark ni ni-money-coins', component: 'text-dark ni ni-money-coins',
background: 'bg-white' background: 'bg-white',
}" }"
direction-reverse direction-reverse
/> />
@ -29,15 +29,15 @@
class="bg-gradient-secondary" class="bg-gradient-secondary"
:title="{ :title="{
text: 'Battery Health', text: 'Battery Health',
color: 'opacity-7 text-white' color: 'opacity-7 text-white',
}" }"
:value="{ :value="{
text: '99 %', text: '99 %',
color: 'text-white' color: 'text-white',
}" }"
:icon="{ :icon="{
component: 'text-dark ni ni-controller', component: 'text-dark ni ni-controller',
background: 'bg-white' background: 'bg-white',
}" }"
direction-reverse direction-reverse
/> />
@ -47,15 +47,15 @@
class="bg-gradient-secondary" class="bg-gradient-secondary"
:title="{ :title="{
text: 'Average Speed', text: 'Average Speed',
color: 'opacity-7 text-white' color: 'opacity-7 text-white',
}" }"
:value="{ :value="{
text: '56 Km/h', text: '56 Km/h',
color: 'text-white' color: 'text-white',
}" }"
:icon="{ :icon="{
component: 'text-dark ni ni-delivery-fast', component: 'text-dark ni ni-delivery-fast',
background: 'bg-white' background: 'bg-white',
}" }"
direction-reverse direction-reverse
/> />
@ -65,15 +65,15 @@
class="bg-gradient-secondary" class="bg-gradient-secondary"
:title="{ :title="{
text: 'Music Volume', text: 'Music Volume',
color: 'opacity-7 text-white' color: 'opacity-7 text-white',
}" }"
:value="{ :value="{
text: '15/100', text: '15/100',
color: 'text-white' color: 'text-white',
}" }"
:icon="{ :icon="{
component: 'text-dark ni ni-note-03', component: 'text-dark ni ni-note-03',
background: 'bg-white' background: 'bg-white',
}" }"
direction-reverse direction-reverse
/> />
@ -98,10 +98,10 @@ export default {
components: { components: {
MiniStatisticsCard, MiniStatisticsCard,
PlayerCard, PlayerCard,
CardDetail CardDetail,
}, },
mounted() { mounted() {
setTooltip(this.$store.state.bootstrap); setTooltip(this.$store.state.bootstrap);
} },
}; };
</script> </script>

View File

@ -203,7 +203,7 @@
</div> </div>
</div> </div>
</template> </template>
<script setupe lang="ts"> <script>
import MiniStatisticsCard from "../../examples/Cards/MiniStatisticsCard.vue"; import MiniStatisticsCard from "../../examples/Cards/MiniStatisticsCard.vue";
import ReportsBarChart from "../../examples/Charts/ReportsBarChart.vue"; import ReportsBarChart from "../../examples/Charts/ReportsBarChart.vue";
import GradientLineChart from "../../examples/Charts/GradientLineChart.vue"; import GradientLineChart from "../../examples/Charts/GradientLineChart.vue";

View File

@ -94,7 +94,7 @@
:style="{ :style="{
backgroundImage: backgroundImage:
'url(' + require('@/assets/img/bg-smart-home-1.jpg') + ')', 'url(' + require('@/assets/img/bg-smart-home-1.jpg') + ')',
backgroundSize: 'cover' backgroundSize: 'cover',
}" }"
> >
<div class="top-0 position-absolute d-flex w-100"> <div class="top-0 position-absolute d-flex w-100">
@ -115,7 +115,7 @@
:style="{ :style="{
backgroundImage: backgroundImage:
'url(' + require('@/assets/img/bg-smart-home-2.jpg') + ')', 'url(' + require('@/assets/img/bg-smart-home-2.jpg') + ')',
backgroundSize: 'cover' backgroundSize: 'cover',
}" }"
> >
<div class="top-0 position-absolute d-flex w-100"> <div class="top-0 position-absolute d-flex w-100">
@ -136,7 +136,7 @@
:style="{ :style="{
backgroundImage: backgroundImage:
'url(' + require('@/assets/img/home-decor-3.jpg') + ')', 'url(' + require('@/assets/img/home-decor-3.jpg') + ')',
backgroundSize: 'cover' backgroundSize: 'cover',
}" }"
> >
<div class="top-0 position-absolute d-flex w-100"> <div class="top-0 position-absolute d-flex w-100">
@ -235,8 +235,8 @@
labels: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'], labels: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
datasets: { datasets: {
label: 'Watts', label: 'Watts',
data: [150, 230, 380, 220, 420, 200, 70, 500] data: [150, 230, 380, 220, 420, 200, 70, 500],
} },
}" }"
/> />
</div> </div>
@ -270,7 +270,7 @@
state: 'Off', state: 'Off',
label: 'Humidity', label: 'Humidity',
description: 'Inactive since: 2 days', description: 'Inactive since: 2 days',
classCustom: 'mt-4' classCustom: 'mt-4',
}" }"
> >
<humidity /> <humidity />
@ -283,7 +283,7 @@
label: 'Temperature', label: 'Temperature',
description: 'Active', description: 'Active',
classCustom: 'mt-2', classCustom: 'mt-2',
isChecked: 'true' isChecked: 'true',
}" }"
class="text-white bg-gradient-success" class="text-white bg-gradient-success"
> >
@ -297,7 +297,7 @@
label: 'Air Conditioner', label: 'Air Conditioner',
description: 'Inactive since: 1 hour', description: 'Inactive since: 1 hour',
classCustom: 'mt-4', classCustom: 'mt-4',
isChecked: true isChecked: true,
}" }"
> >
<air /> <air />
@ -309,7 +309,7 @@
state: 'Off', state: 'Off',
label: 'Lights', label: 'Lights',
description: 'Inactive since: 27 min', description: 'Inactive since: 27 min',
classCustom: 'mt-4' classCustom: 'mt-4',
}" }"
> >
<lights /> <lights />
@ -322,7 +322,7 @@
label: 'Wi-fi', label: 'Wi-fi',
description: 'Active', description: 'Active',
classCustom: 'mt-4', classCustom: 'mt-4',
isChecked: true isChecked: true,
}" }"
class="text-white bg-gradient-success" class="text-white bg-gradient-success"
> >
@ -374,11 +374,11 @@ export default {
Air, Air,
Lights, Lights,
Wifi, Wifi,
Humidity Humidity,
}, },
data() { data() {
return { return {
showMenu: false showMenu: false,
}; };
}, },
mounted() { mounted() {
@ -405,7 +405,7 @@ export default {
sliderType: "min-range", sliderType: "min-range",
lineCap: "round", lineCap: "round",
min: 16, min: 16,
max: 38 max: 38,
}); });
// Rounded slider // Rounded slider
const setValue = function (value, active) { const setValue = function (value, active) {
@ -436,6 +436,6 @@ export default {
else if (ev.detail.high !== undefined) setHigh(ev.detail.high, true); else if (ev.detail.high !== undefined) setHigh(ev.detail.high, true);
}); });
}); });
} },
}; };
</script> </script>

View File

@ -10,7 +10,7 @@
class="mx-3 mt-3 border-radius-xl position-relative" class="mx-3 mt-3 border-radius-xl position-relative"
:style="{ :style="{
backgroundImage: 'url(' + require('../../../assets/img/vr-bg.jpg') + ')', backgroundImage: 'url(' + require('../../../assets/img/vr-bg.jpg') + ')',
backgroundSize: 'cover' backgroundSize: 'cover',
}" }"
> >
<sidenav <sidenav
@ -86,16 +86,16 @@
:items="[ :items="[
{ {
time: '08:00', 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', 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', 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> </div>
@ -110,23 +110,23 @@
{ {
route: '/', route: '/',
tooltip: '2 New Messages', tooltip: '2 New Messages',
image: { url: image1, alt: 'Image Placeholder' } image: { url: image1, alt: 'Image Placeholder' },
}, },
{ {
route: '/', route: '/',
tooltip: '1 New Messages', tooltip: '1 New Messages',
image: { url: image2, alt: 'Image Placeholder' } image: { url: image2, alt: 'Image Placeholder' },
}, },
{ {
route: '/', route: '/',
tooltip: '13 New Messages', tooltip: '13 New Messages',
image: { url: image3, alt: 'Image Placeholder' } image: { url: image3, alt: 'Image Placeholder' },
}, },
{ {
route: '/', route: '/',
tooltip: '7 New Messages', tooltip: '7 New Messages',
image: { url: image4, alt: 'Image Placeholder' } image: { url: image4, alt: 'Image Placeholder' },
} },
]" ]"
/> />
</div> </div>
@ -170,18 +170,18 @@ export default {
EmailCard, EmailCard,
TodoCard, TodoCard,
MiniPlayerCard, MiniPlayerCard,
MessageCard MessageCard,
}, },
data() { data() {
return { return {
image1, image1,
image2, image2,
image3, image3,
image4 image4,
}; };
}, },
computed: { computed: {
...mapState(["isTransparent", "isNavFixed", "navbarFixed", "mcolor"]) ...mapState(["isTransparent", "isNavFixed", "navbarFixed", "mcolor"]),
}, },
mounted() { mounted() {
setTooltip(this.$store.state.bootstrap); setTooltip(this.$store.state.bootstrap);
@ -208,7 +208,7 @@ export default {
this.$store.state.isTransparent = "bg-transparent"; this.$store.state.isTransparent = "bg-transparent";
}, },
methods: { methods: {
...mapMutations(["navbarMinimize", "toggleConfigurator"]) ...mapMutations(["navbarMinimize", "toggleConfigurator"]),
} },
}; };
</script> </script>

View File

@ -306,7 +306,7 @@ export default {
item = { item = {
src: linkEl.getAttribute("href"), src: linkEl.getAttribute("href"),
w: parseInt(size[0], 10), w: parseInt(size[0], 10),
h: parseInt(size[1], 10) h: parseInt(size[1], 10),
}; };
if (figureEl.children.length > 1) { if (figureEl.children.length > 1) {
@ -430,9 +430,9 @@ export default {
return { return {
x: rect.left, x: rect.left,
y: rect.top + pageYScroll, y: rect.top + pageYScroll,
w: rect.width w: rect.width,
}; };
} },
}; };
// PhotoSwipe opened from URL // PhotoSwipe opened from URL
@ -502,10 +502,10 @@ export default {
var element = document.getElementById(id); var element = document.getElementById(id);
return new Choices(element, { return new Choices(element, {
searchEnabled: false, searchEnabled: false,
itemSelectText: "" itemSelectText: "",
}); });
} }
} },
} },
}; };
</script> </script>

View File

@ -916,7 +916,7 @@ export default {
const dataTableSearch = new DataTable("#products-list", { const dataTableSearch = new DataTable("#products-list", {
searchable: true, searchable: true,
fixedHeight: false, fixedHeight: false,
perPage: 7 perPage: 7,
}); });
document.querySelectorAll(".export").forEach(function (el) { document.querySelectorAll(".export").forEach(function (el) {
@ -925,7 +925,7 @@ export default {
var data = { var data = {
type: type, type: type,
filename: "soft-ui-" + type filename: "soft-ui-" + type,
}; };
if (type === "csv") { if (type === "csv") {
@ -937,6 +937,6 @@ export default {
}); });
} }
setTooltip(this.$store.state.bootstrap); setTooltip(this.$store.state.bootstrap);
} },
}; };
</script> </script>

View File

@ -7,22 +7,22 @@
:percentage="{ :percentage="{
color: 'success', color: 'success',
value: '+55%', value: '+55%',
label: 'since last month' label: 'since last month',
}" }"
menu="6 May - 7 May" menu="6 May - 7 May"
:dropdown="[ :dropdown="[
{ {
label: 'Last 7 days', label: 'Last 7 days',
route: 'https://creative-tim.com/' route: 'https://creative-tim.com/',
}, },
{ {
label: 'Last week', label: 'Last week',
route: '/pages/widgets' route: '/pages/widgets',
}, },
{ {
label: 'Last 30 days', label: 'Last 30 days',
route: '/' route: '/',
} },
]" ]"
/> />
<default-statistics-card <default-statistics-card
@ -31,22 +31,22 @@
:percentage="{ :percentage="{
color: 'success', color: 'success',
value: '+12%', value: '+12%',
label: 'since last month' label: 'since last month',
}" }"
menu="9 June - 12 June" menu="9 June - 12 June"
:dropdown="[ :dropdown="[
{ {
label: 'Last 7 days', label: 'Last 7 days',
route: 'javascript:;' route: 'javascript:;',
}, },
{ {
label: 'Last week', label: 'Last week',
route: 'javascript:;' route: 'javascript:;',
}, },
{ {
label: 'Last 30 days', label: 'Last 30 days',
route: 'javascript:;' route: 'javascript:;',
} },
]" ]"
/> />
<default-statistics-card <default-statistics-card
@ -55,22 +55,22 @@
:percentage="{ :percentage="{
color: 'secondary', color: 'secondary',
value: '+$213', value: '+$213',
label: 'since last month' label: 'since last month',
}" }"
menu="6 August - 9 August" menu="6 August - 9 August"
:dropdown="[ :dropdown="[
{ {
label: 'Last 7 days', label: 'Last 7 days',
route: 'javascript:;' route: 'javascript:;',
}, },
{ {
label: 'Last week', label: 'Last week',
route: 'javascript:;' route: 'javascript:;',
}, },
{ {
label: 'Last 30 days', label: 'Last 30 days',
route: 'javascript:;' route: 'javascript:;',
} },
]" ]"
/> />
</div> </div>
@ -97,9 +97,9 @@
datasets: [ datasets: [
{ {
label: 'Sales by age', label: 'Sales by age',
data: [15, 20, 12, 60, 20, 15] data: [15, 20, 12, 60, 20, 15],
} },
] ],
}" }"
/> />
</div> </div>
@ -180,7 +180,7 @@ export default {
RevenueChartCard, RevenueChartCard,
HorizontalBarChart, HorizontalBarChart,
OrdersListCard, OrdersListCard,
DefaultStatisticsCard DefaultStatisticsCard,
}, },
data() { data() {
return { return {
@ -192,7 +192,7 @@ export default {
info: "Refund rate is lower with 97% than other products", info: "Refund rate is lower with 97% than other products",
img: img:
"https://raw.githubusercontent.com/creativetimofficial/public-assets/master/soft-ui-design-system/assets/img/ecommerce/blue-shoe.jpg", "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)", title: "Business Kit (Mug + Notebook)",
@ -200,7 +200,7 @@ export default {
values: ["$80.250", "$4.200", "40"], values: ["$80.250", "$4.200", "40"],
img: img:
"https://raw.githubusercontent.com/creativetimofficial/public-assets/master/soft-ui-design-system/assets/img/ecommerce/black-mug.jpg", "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", title: "Black Chair",
@ -208,7 +208,7 @@ export default {
values: ["$40.600", "$9.430", "54"], values: ["$40.600", "$9.430", "54"],
img: img:
"https://raw.githubusercontent.com/creativetimofficial/public-assets/master/soft-ui-design-system/assets/img/ecommerce/black-chair.jpg", "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", title: "Wireless Charger",
@ -216,7 +216,7 @@ export default {
values: ["$91.300", "$7.364", "5"], values: ["$91.300", "$7.364", "5"],
img: img:
"https://raw.githubusercontent.com/creativetimofficial/public-assets/master/soft-ui-design-system/assets/img/ecommerce/bang-sound.jpg", "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)", title: "Mountain Trip Kit (Camera + Backpack)",
@ -225,45 +225,45 @@ export default {
info: "Refund rate is higher with 70% than other products", info: "Refund rate is higher with 70% than other products",
img: img:
"https://raw.githubusercontent.com/creativetimofficial/public-assets/master/soft-ui-design-system/assets/img/ecommerce/photo-tools.jpg", "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: [ sales: [
{ {
country: "United States", country: "United States",
sales: 2500, sales: 2500,
bounce: "29.9%", bounce: "29.9%",
flag: US flag: US,
}, },
{ {
country: "Germany", country: "Germany",
sales: "3.900", sales: "3.900",
bounce: "40.22%", bounce: "40.22%",
flag: DE flag: DE,
}, },
{ {
country: "Great Britain", country: "Great Britain",
sales: "1.400", sales: "1.400",
bounce: "23.44%", bounce: "23.44%",
flag: GB flag: GB,
}, },
{ {
country: "Brasil", country: "Brasil",
sales: "562", sales: "562",
bounce: "32.14%", bounce: "32.14%",
flag: BR flag: BR,
}, },
{ {
country: "Australia", country: "Australia",
sales: "400", sales: "400",
bounce: "56.83%", bounce: "56.83%",
flag: AU flag: AU,
} },
] ],
}; };
}, },
mounted() { mounted() {
setTooltip(this.$store.state.bootstrap); setTooltip(this.$store.state.bootstrap);
} },
}; };
</script> </script>

View 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>

View 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>

View File

@ -5,7 +5,7 @@
:style="{ :style="{
backgroundImage: backgroundImage:
'url(' + require('@/assets/img/curved-images/curved14.jpg') + ')', 'url(' + require('@/assets/img/curved-images/curved14.jpg') + ')',
backgroundPositionY: '50%' backgroundPositionY: '50%',
}" }"
> >
<span class="mask bg-gradient-success opacity-6"></span> <span class="mask bg-gradient-success opacity-6"></span>
@ -283,25 +283,25 @@
fullName: 'Alec M. Thompson', fullName: 'Alec M. Thompson',
mobile: '(44) 123 1234 123', mobile: '(44) 123 1234 123',
email: 'alecthompson@mail.com', email: 'alecthompson@mail.com',
location: 'USA' location: 'USA',
}" }"
:social="[ :social="[
{ {
link: 'https://www.facebook.com/CreativeTim/', link: 'https://www.facebook.com/CreativeTim/',
icon: faFacebook icon: faFacebook,
}, },
{ {
link: 'https://twitter.com/creativetim', link: 'https://twitter.com/creativetim',
icon: faTwitter icon: faTwitter,
}, },
{ {
link: 'https://www.instagram.com/creativetimofficial/', link: 'https://www.instagram.com/creativetimofficial/',
icon: faInstagram icon: faInstagram,
} },
]" ]"
:action="{ :action="{
route: 'javascript:;', route: 'javascript:;',
tooltip: 'Edit Profile' tooltip: 'Edit Profile',
}" }"
/> />
</div> </div>
@ -445,24 +445,24 @@
:authors="[ :authors="[
{ {
image: team1, image: team1,
name: 'Elena Morison' name: 'Elena Morison',
}, },
{ {
image: team2, image: team2,
name: 'Ryan Milly' name: 'Ryan Milly',
}, },
{ {
image: team3, image: team3,
name: 'Nick Daniel' name: 'Nick Daniel',
}, },
{ {
image: team4, image: team4,
name: 'Peterson' name: 'Peterson',
} },
]" ]"
:action="{ :action="{
color: 'success', color: 'success',
label: 'View Project' label: 'View Project',
}" }"
/> />
@ -475,24 +475,24 @@
:authors="[ :authors="[
{ {
image: team3, image: team3,
name: 'Nick Daniel' name: 'Nick Daniel',
}, },
{ {
image: team4, image: team4,
name: 'Peterson' name: 'Peterson',
}, },
{ {
image: team1, image: team1,
name: 'Elena Morison' name: 'Elena Morison',
}, },
{ {
image: team2, image: team2,
name: 'Ryan Milly' name: 'Ryan Milly',
} },
]" ]"
:action="{ :action="{
color: 'success', color: 'success',
label: 'View Project' label: 'View Project',
}" }"
/> />
@ -505,24 +505,24 @@
:authors="[ :authors="[
{ {
image: team4, image: team4,
name: 'Peterson' name: 'Peterson',
}, },
{ {
image: team3, image: team3,
name: 'Nick Daniel' name: 'Nick Daniel',
}, },
{ {
image: team1, image: team1,
name: 'Elena Morison' name: 'Elena Morison',
}, },
{ {
image: team2, image: team2,
name: 'Ryan Milly' name: 'Ryan Milly',
} },
]" ]"
:action="{ :action="{
color: 'success', color: 'success',
label: 'View Project' label: 'View Project',
}" }"
/> />
@ -558,7 +558,7 @@ import team4 from "@/assets/img/team-4.jpg";
import { import {
faFacebook, faFacebook,
faTwitter, faTwitter,
faInstagram faInstagram,
} from "@fortawesome/free-brands-svg-icons"; } from "@fortawesome/free-brands-svg-icons";
import DefaultProjectCard from "./components/DefaultProjectCard.vue"; import DefaultProjectCard from "./components/DefaultProjectCard.vue";
import PlaceHolderCard from "@/examples/Cards/PlaceHolderCard.vue"; import PlaceHolderCard from "@/examples/Cards/PlaceHolderCard.vue";
@ -572,7 +572,7 @@ export default {
ProfileInfoCard, ProfileInfoCard,
SoftAvatar, SoftAvatar,
DefaultProjectCard, DefaultProjectCard,
PlaceHolderCard PlaceHolderCard,
}, },
data() { data() {
return { return {
@ -591,7 +591,7 @@ export default {
img3, img3,
faFacebook, faFacebook,
faTwitter, faTwitter,
faInstagram faInstagram,
}; };
}, },
@ -602,6 +602,6 @@ export default {
}, },
beforeUnmount() { beforeUnmount() {
this.$store.state.isAbsolute = false; this.$store.state.isAbsolute = false;
} },
}; };
</script> </script>

View File

@ -5,7 +5,7 @@
:style="{ :style="{
backgroundImage: backgroundImage:
'url(' + require('@/assets/img/curved-images/curved14.jpg') + ')', 'url(' + require('@/assets/img/curved-images/curved14.jpg') + ')',
backgroundPositionY: '50%' backgroundPositionY: '50%',
}" }"
> >
<span class="mask bg-gradient-success opacity-6"></span> <span class="mask bg-gradient-success opacity-6"></span>
@ -232,34 +232,34 @@
:members="[ :members="[
{ {
name: 'Alexa Tompson', name: 'Alexa Tompson',
image: team1 image: team1,
}, },
{ {
name: 'Romina Hadid', name: 'Romina Hadid',
image: team2 image: team2,
}, },
{ {
name: 'Alexander Smith', name: 'Alexander Smith',
image: team3 image: team3,
}, },
{ {
name: 'Martin Doe', name: 'Martin Doe',
image: team4 image: team4,
} },
]" ]"
:dropdown="[ :dropdown="[
{ {
label: 'Action', label: 'Action',
route: 'javascript:;' route: 'javascript:;',
}, },
{ {
label: 'Another action', label: 'Another action',
route: 'javascript:;' route: 'javascript:;',
}, },
{ {
label: 'Something else here', label: 'Something else here',
route: 'javascript:;' route: 'javascript:;',
} },
]" ]"
/> />
<team-profile-card <team-profile-card
@ -272,34 +272,34 @@
:members="[ :members="[
{ {
name: 'Martin Doe', name: 'Martin Doe',
image: team4 image: team4,
}, },
{ {
name: 'Alexander Smith', name: 'Alexander Smith',
image: team3 image: team3,
}, },
{ {
name: 'Romina Hadid', name: 'Romina Hadid',
image: team2 image: team2,
}, },
{ {
name: 'Alexa Tompson', name: 'Alexa Tompson',
image: team1 image: team1,
} },
]" ]"
:dropdown="[ :dropdown="[
{ {
label: 'Action', label: 'Action',
route: 'javascript:;' route: 'javascript:;',
}, },
{ {
label: 'Another action', label: 'Another action',
route: 'javascript:;' route: 'javascript:;',
}, },
{ {
label: 'Something else here', label: 'Something else here',
route: 'javascript:;' route: 'javascript:;',
} },
]" ]"
/> />
<event-card <event-card
@ -312,12 +312,12 @@
{ name: 'Alexa tompson', image: team1 }, { name: 'Alexa tompson', image: team1 },
{ name: 'Romina Hadid', image: team2 }, { name: 'Romina Hadid', image: team2 },
{ name: 'Alexander Smith', image: team3 }, { name: 'Alexander Smith', image: team3 },
{ name: 'Martin Doe', image: ivana } { name: 'Martin Doe', image: ivana },
]" ]"
:action="{ :action="{
route: '', route: '',
label: 'Join', label: 'Join',
color: 'success' color: 'success',
}" }"
/> />
<event-card <event-card
@ -331,12 +331,12 @@
{ name: 'Alexa tompson', image: team1 }, { name: 'Alexa tompson', image: team1 },
{ name: 'Romina Hadid', image: team2 }, { name: 'Romina Hadid', image: team2 },
{ name: 'Alexander Smith', image: team3 }, { name: 'Alexander Smith', image: team3 },
{ name: 'Martin Doe', image: ivana } { name: 'Martin Doe', image: ivana },
]" ]"
:action="{ :action="{
route: '', route: '',
label: 'Join', label: 'Join',
color: 'primary' color: 'primary',
}" }"
/> />
</div> </div>
@ -372,7 +372,7 @@ export default {
TeamProfileCard, TeamProfileCard,
PostCard, PostCard,
StoryAvatar, StoryAvatar,
EventCard EventCard,
}, },
data() { data() {
return { return {
@ -380,49 +380,49 @@ export default {
stories: [ stories: [
{ {
name: "Abbie W", name: "Abbie W",
image: team1 image: team1,
}, },
{ {
name: "Boris U", name: "Boris U",
image: team2 image: team2,
}, },
{ {
name: "Kay R", name: "Kay R",
image: team3 image: team3,
}, },
{ {
name: "Tom M", name: "Tom M",
image: team4 image: team4,
}, },
{ {
name: "Nicole N", name: "Nicole N",
image: team5 image: team5,
}, },
{ {
name: "Marie P", name: "Marie P",
image: marie image: marie,
}, },
{ {
name: "Bruce M", name: "Bruce M",
image: bruce image: bruce,
}, },
{ {
name: "Sandra A", name: "Sandra A",
image: ivana image: ivana,
}, },
{ {
name: "Katty L", name: "Katty L",
image: kal image: kal,
}, },
{ {
name: "Emma O", name: "Emma O",
image: emma image: emma,
}, },
{ {
name: "Tao G", name: "Tao G",
image: 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, nick,
slackLogo, slackLogo,
@ -436,7 +436,7 @@ export default {
team2, team2,
team3, team3,
team4, team4,
team5 team5,
}; };
}, },
@ -447,6 +447,6 @@ export default {
}, },
beforeUnmount() { beforeUnmount() {
this.$store.state.isAbsolute = false; this.$store.state.isAbsolute = false;
} },
}; };
</script> </script>

View File

@ -1,30 +1,40 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "ES2018", "target": "esnext",
"module": "ESNext", "module": "esnext",
"moduleResolution": "Node", "strict": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"jsx": "preserve", "jsx": "preserve",
"importHelpers": true,
"moduleResolution": "node",
"allowJs": true, "allowJs": true,
"checkJs": false,
"noEmit": true,
"strict": false,
"skipLibCheck": true, "skipLibCheck": true,
"esModuleInterop": true, "esModuleInterop": true,
"forceConsistentCasingInFileNames": true, "allowSyntheticDefaultImports": true,
"resolveJsonModule": true, "sourceMap": true,
"isolatedModules": true,
"baseUrl": ".", "baseUrl": ".",
"types": [
"webpack-env"
],
"paths": { "paths": {
"@/*": ["src/*"] "@/*": [
"src/*"
]
}, },
"types": ["node"] "lib": [
"esnext",
"dom",
"dom.iterable",
"scripthost"
]
}, },
"include": [ "include": [
"src/**/*.ts", "src/**/*.ts",
"src/**/*.tsx", "src/**/*.tsx",
"src/**/*.vue", "src/**/*.vue",
"src/shims-vue.d.ts" "tests/**/*.ts",
"tests/**/*.tsx"
], ],
"exclude": ["node_modules", "dist"] "exclude": [
"node_modules"
]
} }

View File

@ -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,
},
},
},
};