Aller au contenu

Aide pour la fondation du projet

Installation et Configuration

Prérequis

Avant de commencer, assurez-vous d'avoir:

Vérifier les versions:

node --version   # Devrait afficher v18.x ou plus
npm --version    # Devrait afficher 9.x ou plus
git --version    # Devrait afficher 2.x ou plus

Créer le Projet

1.1 Initialiser le projet Vite + Vue

# Créer le projet
npm create vite@latest mon-projet -- --template vue

# Entrer dans le dossier
cd mon-projet

# Installer les dépendances de base
npm install

# Tester que ça fonctionne
npm run dev

Ouvrez http://localhost:5173 - Vous devriez voir la page de démo Vue.

1.2 Installer les dépendances du projet

# Dépendances principales
npm install pinia vue-router gsap

# Dépendances de développement
npm install -D eslint prettier eslint-plugin-vue

1.3 Structure de dossiers

Créer la structure suivante:

mon-projet/
├── public/
│   ├── images/
│   └── sounds/
├── src/
│   ├── assets/
│   │   └── styles/
│   │       ├── main.css
│   │       └── reset.css
│   ├── components/
│   │   ├── common/
│   │   ├── specific/
│   │   └── ui/
│   ├── router/
│   │   └── index.js
│   ├── stores/
│   ├── views/
│   ├── utils/
│   ├── App.vue
│   └── main.js
├── .gitignore
├── README.md
└── package.json

1.4 Configurer Vue Router

src/router/index.js:

import { createRouter, createWebHistory } from 'vue-router';
import HomeView from '../views/HomeView.vue';

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    {
      path: '/',
      name: 'home',
      component: HomeView
    }
    // Ajoutez vos routes ici
  ]
});

export default router;

src/main.js (mise à jour):

import { createApp } from 'vue';
import { createPinia } from 'pinia';
import router from './router';
import App from './App.vue';
import './assets/styles/main.css';

const app = createApp(App);

app.use(createPinia());
app.use(router);
app.mount('#app');

1.5 Configurer Pinia

src/stores/example.js:

import { defineStore } from 'pinia';

export const useExampleStore = defineStore('example', {
  state: () => ({
    // Votre state ici
  }),

  getters: {
    // Vos getters ici
  },

  actions: {
    // Vos actions ici
  }
});

1.6 Créer le .gitignore

.gitignore:

# Dependencies
node_modules/

# Build
dist/
dist-ssr/

# Environment
.env
.env.local
.env.*.local

# IDE
.vscode/
.idea/
*.swp
*.swo

# OS
.DS_Store
Thumbs.db

# Logs
*.log
npm-debug.log*

# Testing
coverage/

1.7 Initialiser Git et pousser sur GitHub

Via GitHub Desktop

Templates de fichiers utiles

Création de l'app dans main.js

src/main.js

import { createApp } from 'vue';
import router from './router';
import App from './App.vue';

const app = createApp(App);

app.use(router);

app.mount('#app');

Base de votre App.vue

<template>
  <div id="app">

  </div>
</template>

<script>
export default {
  name: 'App'
};
</script>

<style>
* {
  box-sizing: border-box;
}
</style>

* Composant de base

src/components/ExampleComponent.vue:

<template>
  <div class="example-component">
    <h2>{{ title }}</h2>
    <p>{{ description }}</p>
    <button @click="handleClick">Click me</button>
    <p>Count: {{ count }} | Double: {{ doubleCount }}</p>
  </div>
</template>

<script>
/* import d'autres composants utilisés ici */
import ButtonPrimary from '@/components/ui/ButtonPrimary.vue';


export default {
  name: 'ExampleComponent',

  components: {
    ButtonPrimary
  },

  props: {
    title: {
      type: String,
      required: true
    },
    description: {
      type: String,
      default: ''
    }
  },

  emits: ['click'],

  data() {
    return {
      count: 0
    };
  },

  computed: {
    doubleCount() {
      return this.count * 2;
    }
  },

  methods: {
    handleClick() {
      this.count++;
      this.$emit('click', this.count);
    }
  }
};
</script>

<style scoped>

</style>

* Store Pinia de base

src/stores/exampleStore.js:

import { defineStore } from 'pinia';

export const useExampleStore = defineStore('example', {
  state: () => ({
    items: [],
    currentItem: null,
    isLoading: false,
    error: null
  }),

  getters: {
    itemCount: (state) => state.items.length,

    hasItems: (state) => state.items.length > 0,

    getItemById: (state) => (id) => {
      return state.items.find(item => item.id === id);
    },
    currentChapter: (state) => {
      return state.items[state.currentItem];
    },
  },

  actions: {
    addItem(item) {
      this.items.push({
        ...item,
        id: Date.now().toString(),
        createdAt: new Date().toISOString()
      });
    },

    updateItem(id, updates) {
      const index = this.items.findIndex(item => item.id === id);
      if (index !== -1) {
        this.items[index] = { ...this.items[index], ...updates };
      }
    },

    deleteItem(id) {
      const index = this.items.findIndex(item => item.id === id);
      if (index !== -1) {
        this.items.splice(index, 1);
      }
    },

    setCurrentItem(id) {
      this.currentItem = this.getItemById(id);
    }
  }
});

Exemple complet d'un composant intégrant Pinia Store

<template>
  <div class="items-list">
    <h1>Liste des items ({{ itemCount }})</h1>

    <div v-if="isLoading">Chargement...</div>

    <div v-else-if="hasItems">
      <div 
        v-for="item in items" 
        :key="item.id"
        class="item-card"
      >
        <h3>{{ item.name }}</h3>
        <button @click="selectItem(item.id)">Voir</button>
        <button @click="removeItem(item.id)">Supprimer</button>
      </div>
    </div>

    <div v-else>
      <p>Aucun item</p>
    </div>

    <ButtonPrimary @click="addNewItem">
      Ajouter un item
    </ButtonPrimary>
  </div>
</template>

<script>
import { useExampleStore } from '@/stores/exampleStore';
import { mapStores } from 'pinia';
import ButtonPrimary from '@/components/ui/ButtonPrimary.vue';

export default {
  name: 'ItemsList',

  components: {
    ButtonPrimary
  },

  computed: {
    // Mapper le store complet
    // Cela donne accès à : exampleStore.state, exampleStore.getters, exampleStore.actions
    ...mapStores(useExampleStore)
  },

  methods: {
    addNewItem() {
      // Accès aux actions via exampleStore
      this.exampleStore.addItem({
        name: `Item ${this.exampleStore.itemCount + 1}`,
        description: 'Nouvel item'
      });
    },

    removeItem(id) {
      if (confirm('Supprimer cet item?')) {
        // Accès aux actions via exampleStore
        this.exampleStore.deleteItem(id);
      }
    },

    selectItem(id) {
      // Accès aux actions via exampleStore
      this.exampleStore.setCurrentItem(id);
      this.$router.push(`/item/${id}`);
    }
  }
};
</script>

<style scoped>
.items-list {
  padding: 2rem;
}

.item-card {
  border: 1px solid #ddd;
  padding: 1rem;
  margin: 1rem 0;
  border-radius: 8px;
}
</style>

Composants UI Réutilisables

* ButtonPrimary.vue

src/components/ui/ButtonPrimary.vue:

<template>
  <button
    class="btn-primary"
    :class="{ 'btn-primary--loading': loading, 'btn-primary--disabled': disabled }"
    :disabled="disabled || loading"
    @click="handleClick"
  >
    <span v-if="loading" class="btn-primary__spinner"></span>
    <span v-else><slot>Button</slot></span>
  </button>
</template>

<script>

import ItemText from '@/components/ItemText.vue';

export default {
  name: 'ButtonPrimary',

  components: {
    ItemText
  },

  props: {
    loading: {
      type: Boolean,
      default: false
    },
    disabled: {
      type: Boolean,
      default: false
    }
  },

  emits: ['click'],

  methods: {
    handleClick() {
      if (!this.loading && !this.disabled) {
        this.$emit('click');
      }
    }
  }
};
</script>

<style scoped>


.btn-primary {
  background: blue;
  color: white;
  padding: 10px;
  border: none;
  border-radius: 8px;
  font-weight: 500;
  font-size: 1rem;
  cursor: pointer;
}

.btn-primary:hover{
   background: pink;
}


</style>

* Modal.vue

src/components/ui/Modal.vue:

<template>
<!-- <Teleport> est un composant natif qui nous permet de "téléporter"
   une partie du template d'un composant dans un nœud du DOM qui existe 
   en dehors de la hiérarchie du DOM de ce composant. Ici, la modale se 
   téléporte dans la balise <body>.
   Info à propos de <teleport> : https://fr.vuejs.org/guide/built-ins/teleport-->
  <Teleport to="body">
    <Transition name="modal">
      <div v-if="modelValue" class="modal-overlay" @click.self="close">
        <div class="modal-content" role="dialog" aria-modal="true">
          <button class="modal-close" @click="close" aria-label="Close"></button>

          <div v-if="title" class="modal-header">
            <h2>{{ title }}</h2>
          </div>

          <div class="modal-body">
            <slot></slot>
          </div>

          <div v-if="$slots.footer" class="modal-footer">
            <slot name="footer"></slot>
          </div>
        </div>
      </div>
    </Transition>
  </Teleport>
</template>

<script>
export default {
  name: 'Modal',

  props: {
    modelValue: {
      type: Boolean,
      required: true
    },
    title: {
      type: String,
      default: ''
    }
  },

  emits: ['update:modelValue'],

  methods: {
    close() {
      this.$emit('update:modelValue', false);
    }
  }
};
</script>

<style scoped lang="scss">
// Attention: code démo en format SCSS, veuillez l'adapter en CSS classique 
@import '@/assets/styles/variables';

.modal-overlay {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: rgba(0, 0, 0, 0.5);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 1000;
}

.modal-content {
  background: white;
  border-radius: 12px;
  max-width: 500px;
  width: 90%;
  max-height: 90vh;
  overflow-y: auto;
  position: relative;
  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
}

.modal-close {
  position: absolute;
  top: $spacing-md;
  right: $spacing-md;
  background: none;
  border: none;
  font-size: 1.5rem;
  cursor: pointer;
  color: #6b7280;

  &:hover {
    color: $text-color;
  }
}

.modal-header {
  padding: $spacing-xl $spacing-xl $spacing-md;
  border-bottom: 1px solid #e5e7eb;

  h2 {
    margin: 0;
  }
}

.modal-body {
  padding: $spacing-xl;
}

.modal-footer {
  padding: $spacing-md $spacing-xl $spacing-xl;
  border-top: 1px solid #e5e7eb;
  display: flex;
  justify-content: flex-end;
  gap: $spacing-md;
}

// Transitions
.modal-enter-active,
.modal-leave-active {
  transition: opacity 0.3s ease;

  .modal-content {
    transition: transform 0.3s ease;
  }
}

.modal-enter-from,
.modal-leave-to {
  opacity: 0;

  .modal-content {
    transform: scale(0.9);
  }
}
</style>

Exemple d'utilisation du Modal

<template>
  <div>
    <ButtonPrimary @click="openModal">
      Ouvrir le Modal
    </ButtonPrimary>

    <Modal v-model="isModalOpen" title="Mon Modal">
      <p>Contenu du modal ici</p>

      <template #footer>
        <button @click="isModalOpen = false">Annuler</button>
        <ButtonPrimary @click="handleSave">Sauvegarder</ButtonPrimary>
      </template>
    </Modal>
  </div>
</template>

<script>
import Modal from '@/components/ui/Modal.vue';
import ButtonPrimary from '@/components/ui/ButtonPrimary.vue';

export default {
  name: 'ExamplePage',

  components: {
    Modal,
    ButtonPrimary
  },

  data() {
    return {
      isModalOpen: false
    };
  },

  methods: {
    openModal() {
      this.isModalOpen = true;
    },

    handleSave() {
      console.log('Sauvegarde...');
      this.isModalOpen = false;
    }
  }
};
</script>