1. Navigation et Indication de Route Active
Pour améliorer l'orientation de l'utilisateur, une mise en surbrillance dynamique a été ajoutée aux menus de navigation. Plutôt que de dupliquer le code HTML pour chaque élément, une approche basée sur un tableau de configuration et une boucle v-for est utilisée pour rendre le composant plus maintenable et réactif.
<template>
<nav class="sidebar-container">
<div class="nav-section top">
<div
v-for="item in topMenu"
:key="item.route"
class="nav-item"
:class="{ active: currentPath === item.route }"
@click="navigateTo(item.route)"
>
<component :is="item.icon" class="nav-icon" />
<span class="nav-label">{{ item.label }}</span>
</div>
</div>
<div class="nav-section bottom">
<div
v-for="item in bottomMenu"
:key="item.route"
class="nav-item"
:class="{ active: currentPath === item.route }"
@click="navigateTo(item.route)"
>
<component :is="item.icon" class="nav-icon" />
<span class="nav-label">{{ item.label }}</span>
</div>
</div>
</nav>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import { useRouter, useRoute } from 'vue-router';
import { House, Search, User, Setting } from '@element-plus/icons-vue';
const router = useRouter();
const route = useRoute();
const currentPath = computed(() => route.path);
const topMenu = [
{ route: '/', label: 'Accueil', icon: House },
{ route: '/search', label: 'Recherche', icon: Search },
{ route: '/profile', label: 'Profil', icon: User }
];
const bottomMenu = [
{ route: '/settings', label: 'Paramètres', icon: Setting }
];
const navigateTo = (path: string) => {
router.push(path);
};
</script>
<style scoped lang="scss">
.sidebar-container {
height: 100vh;
display: flex;
flex-direction: column;
justify-content: space-between;
background-color: #2c3e50;
padding: 20px 0;
.nav-item {
display: flex;
flex-direction: column;
align-items: center;
padding: 15px;
color: #bdc3c7;
cursor: pointer;
transition: all 0.3s ease;
&.active {
color: #ffffff;
background-color: rgba(255, 255, 255, 0.1);
}
.nav-icon {
width: 24px;
height: 24px;
margin-bottom: 8px;
}
}
}
</style>
2. Tableau de Bord du Profil Utilisateur
La page de profil a été entièrement repensée pour afficher les métadonnées de l'utilisateur ainsi que la liste de ses dépôts. Les appels API ont été structurés de manière plus robuste pour gérer l'asynchronicité et le typage des données.
<template>
<div class="profile-dashboard">
<header class="dashboard-header">
<h1>Mon Profil</h1>
</header>
<section class="user-info-card">
<el-avatar :src="profileData.avatar_url" :size="120" />
<h2>{{ profileData.name }} <span class="login-name">({{ profileData.login }})</span></h2>
<p class="bio-text">{{ profileData.bio || 'Aucune biographie disponible.' }}</p>
<div class="stats-row">
<span>Abonnements: {{ profileData.following }}</span>
<span>Abonnés: {{ profileData.followers }}</span>
</div>
</section>
<section class="repositories-section">
<h3>Mes Dépôts</h3>
<div class="repo-list">
<div v-for="repo in repositories" :key="repo.id" class="repo-card">
<div class="repo-header">
<span class="repo-name">{{ repo.full_name }}</span>
<el-tag :type="repo.private ? 'danger' : 'success'" size="small">
{{ repo.private ? 'Privé' : 'Public' }}
</el-tag>
</div>
<p class="repo-desc">{{ repo.description || 'Pas de description.' }}</p>
<el-tag v-if="repo.language" type="info" size="small">{{ repo.language }}</el-tag>
</div>
</div>
</section>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { fetchUserProfile, fetchUserRepositories } from '~/services/api';
interface UserProfile {
avatar_url: string;
name: string;
login: string;
bio: string;
following: number;
followers: number;
}
interface Repository {
id: number;
full_name: string;
private: boolean;
description: string;
language: string;
}
const profileData = ref<UserProfile>({} as UserProfile);
const repositories = ref<Repository[]>([]);
const loadDashboardData = async () => {
const token = localStorage.getItem('auth_token');
if (!token) return;
try {
profileData.value = await fetchUserProfile(token);
repositories.value = await fetchUserRepositories(token);
} catch (error) {
console.error('Erreur lors du chargement des données:', error);
}
};
onMounted(() => {
loadDashboardData();
});
</script>
<style scoped lang="scss">
.profile-dashboard {
padding: 20px;
max-width: 800px;
margin: 0 auto;
.user-info-card {
text-align: center;
margin-bottom: 30px;
padding: 20px;
background: #f8f9fa;
border-radius: 8px;
.login-name { color: #6c757d; font-weight: normal; }
.bio-text { color: #495057; margin: 10px 0; }
.stats-row { display: flex; justify-content: center; gap: 20px; color: #6c757d; }
}
.repositories-section {
.repo-list {
display: flex;
flex-direction: column;
gap: 15px;
.repo-card {
padding: 15px;
background: #ffffff;
border: 1px solid #e9ecef;
border-radius: 6px;
.repo-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
.repo-name { font-weight: bold; color: #0366d6; }
}
.repo-desc { color: #586069; font-size: 14px; margin-bottom: 10px; }
}
}
}
}
</style>
3. Refonte de l'Interface de Recherche
L'ancienne interface de recherche a été remplacée par un champ de saisie composite utilisant les composants d'Element Plus. Cela permet de basculer entre la recherche d'utilisateurs et de dépôts sans encomrber l'interface, tout en intégrent des validations de fomrulaire natives.
<template>
<div class="search-interface">
<div class="search-bar-wrapper">
<el-input
v-model="searchTerm"
placeholder="Rechercher..."
class="composite-input"
clearable
@keyup.enter="executeSearch"
>
<template #prepend>
<el-select v-model="searchCategory" style="width: 110px">
<el-option label="Utilisateurs" value="users" />
<el-option label="Dépôts" value="repos" />
</el-select>
</template>
<template #append>
<el-button :icon="SearchIcon" @click="executeSearch" />
</template>
</el-input>
</div>
<div class="search-results">
<div v-for="result in searchResults" :key="result.id" class="result-card">
<el-avatar v-if="result.avatar_url" :src="result.avatar_url" :size="60" class="result-avatar" />
<div class="result-details">
<h4>
{{ result.name || result.login }}
<span v-if="result.login" class="sub-text">({{ result.login }})</span>
</h4>
<p class="result-bio">{{ result.bio || 'Aucune description' }}</p>
<div class="result-meta">
<span>{{ formatDate(result.created_at) }}</span>
<a v-if="result.html_url" :href="result.html_url" target="_blank">Voir le profil</a>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { Search as SearchIcon } from '@element-plus/icons-vue';
import { ElMessage } from 'element-plus';
import { queryUsers, queryRepositories } from '~/services/searchApi';
import dayjs from 'dayjs';
const searchTerm = ref('');
const searchCategory = ref<'users' | 'repos'>('users');
const searchResults = ref<any[]>([]);
const formatDate = (dateString: string) => {
return dateString ? dayjs(dateString).format('DD/MM/YYYY') : '';
};
const executeSearch = async () => {
const token = localStorage.getItem('auth_token');
if (!token) {
ElMessage.warning('Veuillez configurer votre token d\'accès dans les paramètres.');
return;
}
if (!searchTerm.value.trim()) {
ElMessage.warning('Veuillez entrer un terme de recherche.');
return;
}
try {
const params = { access_token: token, q: searchTerm.value };
if (searchCategory.value === 'users') {
searchResults.value = await queryUsers(params);
} else {
searchResults.value = await queryRepositories(params);
}
} catch (error) {
ElMessage.error('Erreur lors de la recherche.');
}
};
</script>
<style scoped lang="scss">
.search-interface {
padding: 30px;
.search-bar-wrapper {
display: flex;
justify-content: center;
margin-bottom: 30px;
.composite-input {
max-width: 600px;
width: 100%;
}
}
.search-results {
.result-card {
display: flex;
align-items: flex-start;
gap: 20px;
padding: 20px;
margin-bottom: 15px;
background: #ffffff;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
.result-details {
flex: 1;
h4 { margin: 0 0 5px 0; color: #24292e; }
.sub-text { color: #586069; font-weight: normal; font-size: 14px; }
.result-bio { color: #586069; margin: 8px 0; }
.result-meta {
display: flex;
gap: 15px;
font-size: 12px;
color: #0366d6;
a { text-decoration: none; }
}
}
}
}
}
</style>
4. Configuration et Gestion du Token d'Accès
Étant donné que l'API nécessite un jeton d'authentification, une instruction claire a été intégrée dans la page des paramètres pour guider les utilisateurs. Le champ de saisie du token est masqué par défaut pour des raisons de sécurité, et un lien direct permet de générer un nouveau jeton.
<template>
<div class="settings-panel">
<header class="panel-header">
<h1>Paramètres</h1>
</header>
<div class="config-card">
<el-form label-position="top">
<el-form-item label="Token d'Accès Personnel">
<el-input
v-model="authToken"
type="password"
show-password
placeholder="Entrez votre token"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="saveConfiguration">Enregistrer</el-button>
</el-form-item>
</el-form>
<div class="help-section">
<p>Vous n'avez pas encore de token ?</p>
<el-link type="danger" :underline="false" href="https://atomgit.com/setting/token-classic" target="_blank">
Générer un nouveau token sur AtomGit
</el-link>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { ElMessage } from 'element-plus';
const authToken = ref('');
const saveConfiguration = () => {
if (!authToken.value.trim()) {
ElMessage.warning('Le token ne peut pas être vide.');
return;
}
localStorage.setItem('auth_token', authToken.value);
ElMessage.success('Configuration enregistrée avec succès.');
};
onMounted(() => {
const savedToken = localStorage.getItem('auth_token');
if (savedToken) {
authToken.value = savedToken;
}
});
</script>
<style scoped lang="scss">
.settings-panel {
max-width: 600px;
margin: 0 auto;
padding: 30px;
.panel-header {
margin-bottom: 20px;
}
.config-card {
background: #f8f9fa;
padding: 30px;
border-radius: 10px;
.help-section {
margin-top: 20px;
padding-top: 20px;
border-top: 1px solid #dee2e6;
p { margin: 0 0 10px 0; color: #495057; }
}
}
}
</style>