Compare commits

..

No commits in common. "8f06d79f42f9bd109677e9b963d6862cc9ddf692" and "e8fe78577bebc3b2ddee5b52b594d5b4e52d860a" have entirely different histories.

54 changed files with 598 additions and 2409 deletions

View File

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

View File

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

View File

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

View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "8f387a0734f3bf879214e4aa2fca6e2f",
"content-hash": "c514d8f7b9fc5970bdd94287905ef584",
"packages": [
{
"name": "brick/math",
@ -1332,70 +1332,6 @@
},
"time": "2025-09-19T13:47:56+00:00"
},
{
"name": "laravel/sanctum",
"version": "v4.2.0",
"source": {
"type": "git",
"url": "https://github.com/laravel/sanctum.git",
"reference": "fd6df4f79f48a72992e8d29a9c0ee25422a0d677"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/sanctum/zipball/fd6df4f79f48a72992e8d29a9c0ee25422a0d677",
"reference": "fd6df4f79f48a72992e8d29a9c0ee25422a0d677",
"shasum": ""
},
"require": {
"ext-json": "*",
"illuminate/console": "^11.0|^12.0",
"illuminate/contracts": "^11.0|^12.0",
"illuminate/database": "^11.0|^12.0",
"illuminate/support": "^11.0|^12.0",
"php": "^8.2",
"symfony/console": "^7.0"
},
"require-dev": {
"mockery/mockery": "^1.6",
"orchestra/testbench": "^9.0|^10.0",
"phpstan/phpstan": "^1.10",
"phpunit/phpunit": "^11.3"
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"Laravel\\Sanctum\\SanctumServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"Laravel\\Sanctum\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Taylor Otwell",
"email": "taylor@laravel.com"
}
],
"description": "Laravel Sanctum provides a featherweight authentication system for SPAs and simple APIs.",
"keywords": [
"auth",
"laravel",
"sanctum"
],
"support": {
"issues": "https://github.com/laravel/sanctum/issues",
"source": "https://github.com/laravel/sanctum"
},
"time": "2025-07-09T19:45:24+00:00"
},
{
"name": "laravel/serializable-closure",
"version": "v2.0.5",

View File

@ -1,28 +0,0 @@
<?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

@ -1,33 +0,0 @@
<?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,8 +19,6 @@ Route::prefix('auth')->group(function () {
Route::middleware('auth:sanctum')->group(function () {
Route::get('/me', [AuthController::class, 'me']);
// Alias to support clients calling /api/auth/user
Route::get('/user', [AuthController::class, 'me']);
Route::post('/logout', [AuthController::class, 'logout']);
Route::post('/logout-all', [AuthController::class, 'logoutAll']);
});

View File

@ -1,3 +1 @@
# 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
NODE_ENV=

View File

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

View File

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

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

View File

@ -19,7 +19,8 @@ Coded by www.creative-tim.com
<link href="https://fonts.googleapis.com/css?family=Open+Sans:300,400,600,700" rel="stylesheet" />
<!-- Font Awesome Icons: using CSS only to avoid blocked kit.js -->
<!-- Font Awesome Icons -->
<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">

View File

@ -1,34 +1,26 @@
<template>
<!-- Guest Layout: for login, signup, auth pages -->
<div v-if="isGuestRoute" class="guest-layout">
<router-view />
</div>
<!-- Dashboard Layout: for authenticated pages -->
<div v-else class="dashboard-layout">
<sidenav
v-if="showSidenav"
:custom-class="color"
:class="[isTransparent, isRTL ? 'fixed-end' : 'fixed-start']"
<sidenav
v-if="showSidenav"
:custom-class="color"
:class="[isTransparent, isRTL ? 'fixed-end' : 'fixed-start']"
/>
<main
class="main-content position-relative max-height-vh-100 h-100 border-radius-lg"
>
<!-- nav -->
<navbar
v-if="showNavbar"
:class="[isNavFixed ? navbarFixed : '', isAbsolute ? absolute : '']"
:text-white="isAbsolute ? 'text-white opacity-8' : ''"
:min-nav="navbarMinimize"
/>
<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>
<router-view />
<app-footer v-show="showFooter" />
<configurator
:toggle="toggleConfigurator"
:class="[showConfig ? 'show' : '', hideConfigButton ? 'd-none' : '']"
/>
</main>
</template>
<script>
import Sidenav from "./examples/Sidenav";
@ -59,10 +51,6 @@ export default {
"showConfig",
"hideConfigButton",
]),
isGuestRoute() {
// Check if current route has guestLayout meta flag
return this.$route.meta?.guestLayout === true;
},
},
beforeMount() {
this.$store.state.isTransparent = "bg-transparent";

View File

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

View File

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

View File

@ -1,108 +0,0 @@
<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

@ -1,15 +0,0 @@
<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

@ -1,115 +0,0 @@
<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

@ -1,67 +0,0 @@
<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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +0,0 @@
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,30 +54,12 @@ import lockBasic from "../views/auth/lock/Basic.vue";
import lockCover from "../views/auth/lock/Cover.vue";
import lockIllustration from "../views/auth/lock/Illustration.vue";
//ROUTE SHOULD USED
const routes = [
{
path: "/",
name: "/",
redirect: "/dashboards/dashboard-default",
},
{
path: "/dashboard",
redirect: "/dashboards/dashboard-default",
},
{
path: "/login",
name: "Login",
component: () => import("@/views/pages/Login.vue"),
meta: { public: true, guestOnly: true, guestLayout: true },
},
{
path: "/register",
name: "Register",
component: () => import("@/views/pages/Register.vue"),
meta: { public: true, guestOnly: true, guestLayout: true },
},
{
path: "/dashboards/dashboard-default",
name: "Default",
@ -267,158 +249,94 @@ const routes = [
path: "/authentication/signin/basic",
name: "Signin Basic",
component: Basic,
meta: { guestLayout: true },
},
{
path: "/authentication/signin/cover",
name: "Signin Cover",
component: Cover,
meta: { guestLayout: true },
},
{
path: "/authentication/signin/illustration",
name: "Signin Illustration",
component: Illustration,
meta: { guestLayout: true },
},
{
path: "/authentication/reset/basic",
name: "Reset Basic",
component: ResetBasic,
meta: { guestLayout: true },
},
{
path: "/authentication/reset/cover",
name: "Reset Cover",
component: ResetCover,
meta: { guestLayout: true },
},
{
path: "/authentication/reset/illustration",
name: "Reset Illustration",
component: ResetIllustration,
meta: { guestLayout: true },
},
{
path: "/authentication/lock/basic",
name: "Lock Basic",
component: lockBasic,
meta: { guestLayout: true },
},
{
path: "/authentication/lock/cover",
name: "Lock Cover",
component: lockCover,
meta: { guestLayout: true },
},
{
path: "/authentication/lock/illustration",
name: "Lock Illustration",
component: lockIllustration,
meta: { guestLayout: true },
},
{
path: "/authentication/verification/basic",
name: "Verification Basic",
component: VerificationBasic,
meta: { guestLayout: true },
},
{
path: "/authentication/verification/cover",
name: "Verification Cover",
component: VerificationCover,
meta: { guestLayout: true },
},
{
path: "/authentication/verification/illustration",
name: "Verification Illustration",
component: VerificationIllustration,
meta: { guestLayout: true },
},
{
path: "/authentication/signup/basic",
name: "Signup Basic",
component: SignupBasic,
meta: { guestLayout: true },
},
{
path: "/authentication/signup/cover",
name: "Signup Cover",
component: SignupCover,
meta: { guestLayout: true },
},
{
path: "/authentication/signup/illustration",
name: "Signup Illustration",
component: SignupIllustration,
meta: { guestLayout: true },
},
{
path: "/authentication/error/error404",
name: "Error Error404",
component: Error404,
meta: { guestLayout: true },
},
{
path: "/authentication/error/error500",
name: "Error Error500",
component: Error500,
meta: { guestLayout: true },
},
];
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
// Use root base so the app works whether served at / or via /build/index.html
history: createWebHistory("/"),
routes,
linkActiveClass: "active",
});
// Auth guard using Pinia store and Laravel Sanctum session
import { useAuthStore } from "@/stores/auth";
import pinia from "@/plugins/pinia";
const DASHBOARD = "/dashboards/dashboard-default";
const LOGIN = "/login"; // canonical login path without redirect query
router.beforeEach(async (to, from, next) => {
const auth = useAuthStore(pinia);
const isPublic = to.matched.some((r) => r.meta && r.meta.public === true);
const isGuestOnly = to.matched.some(
(r) => r.meta && r.meta.guestOnly === true
);
const isLoginRoute = to.name === "Login" || to.name === "Signin";
// Initialize auth from localStorage token if not already done
if (!auth.checked) {
const token = localStorage.getItem("auth_token");
if (token) {
// Token exists, set it and try to fetch user
auth.token = token;
try {
await auth.fetchMe();
} catch (error) {
// If token is invalid, clear it and continue as unauthenticated
console.warn("Invalid token, clearing auth");
localStorage.removeItem("auth_token");
auth.token = null;
auth.user = null;
auth.checked = true;
}
} else {
// No token, mark as checked
auth.checked = true;
}
}
if (auth.isAuthenticated) {
// Prevent authenticated users from visiting guest-only routes (like sign-in)
if (isGuestOnly || isLoginRoute) return next(DASHBOARD);
return next();
}
// Not authenticated: allow public/guest routes (like sign-in), otherwise send to login (no redirect param)
if (isPublic || isGuestOnly || isLoginRoute) return next();
return next({ path: LOGIN, replace: true });
});
export default router;

View File

@ -1,92 +0,0 @@
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

@ -1,66 +0,0 @@
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

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

View File

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

View File

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

View File

@ -1,86 +0,0 @@
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

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

View File

@ -1,18 +1,7 @@
<template>
<div class="home">
<img alt="Vue logo" src="../assets/logo.png" />
<HelloWorld msg="Welcome to Your Vue.js + TypeScript App" />
</div>
<default-dashboard />
</template>
<script lang="ts">
import { defineComponent } from "vue";
import HelloWorld from "@/components/HelloWorld.vue"; // @ is an alias to /src
export default defineComponent({
name: "Home",
components: {
HelloWorld,
},
});
<script setup lang="ts">
import DefaultDashboard from "@/views/dashboards/Default.vue";
</script>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -10,7 +10,7 @@
class="mx-3 mt-3 border-radius-xl position-relative"
:style="{
backgroundImage: 'url(' + require('../../../assets/img/vr-bg.jpg') + ')',
backgroundSize: 'cover',
backgroundSize: 'cover'
}"
>
<sidenav
@ -86,16 +86,16 @@
:items="[
{
time: '08:00',
description: `Synk up with Mark<small class='text-secondary font-weight-normal'>Hangouts</small>`,
description: `Synk up with Mark<small class='text-secondary font-weight-normal'>Hangouts</small>`
},
{
time: '09:30',
description: `Gym<br /><small class='text-secondary font-weight-normal'>World Class</small>`,
description: `Gym<br /><small class='text-secondary font-weight-normal'>World Class</small>`
},
{
time: '11:00',
description: `Design Review<br /><small class='text-secondary font-weight-normal'>Zoom</small>`,
},
description: `Design Review<br /><small class='text-secondary font-weight-normal'>Zoom</small>`
}
]"
/>
</div>
@ -110,23 +110,23 @@
{
route: '/',
tooltip: '2 New Messages',
image: { url: image1, alt: 'Image Placeholder' },
image: { url: image1, alt: 'Image Placeholder' }
},
{
route: '/',
tooltip: '1 New Messages',
image: { url: image2, alt: 'Image Placeholder' },
image: { url: image2, alt: 'Image Placeholder' }
},
{
route: '/',
tooltip: '13 New Messages',
image: { url: image3, alt: 'Image Placeholder' },
image: { url: image3, alt: 'Image Placeholder' }
},
{
route: '/',
tooltip: '7 New Messages',
image: { url: image4, alt: 'Image Placeholder' },
},
image: { url: image4, alt: 'Image Placeholder' }
}
]"
/>
</div>
@ -170,18 +170,18 @@ export default {
EmailCard,
TodoCard,
MiniPlayerCard,
MessageCard,
MessageCard
},
data() {
return {
image1,
image2,
image3,
image4,
image4
};
},
computed: {
...mapState(["isTransparent", "isNavFixed", "navbarFixed", "mcolor"]),
...mapState(["isTransparent", "isNavFixed", "navbarFixed", "mcolor"])
},
mounted() {
setTooltip(this.$store.state.bootstrap);
@ -208,7 +208,7 @@ export default {
this.$store.state.isTransparent = "bg-transparent";
},
methods: {
...mapMutations(["navbarMinimize", "toggleConfigurator"]),
},
...mapMutations(["navbarMinimize", "toggleConfigurator"])
}
};
</script>

View File

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

View File

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

View File

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

View File

@ -1,149 +0,0 @@
<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

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

View File

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

View File

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

View File

@ -0,0 +1,24 @@
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,
},
},
},
};