This commit is contained in:
sirir 2025-04-06 20:33:44 +02:00
commit 39a3f96d16
23 changed files with 6971 additions and 0 deletions

75
README.md Normal file
View File

@ -0,0 +1,75 @@
# Nuxt Minimal Starter
Look at the [Nuxt documentation](https://nuxt.com/docs/getting-started/introduction) to learn more.
## Setup
Make sure to install dependencies:
```bash
# npm
npm install
# pnpm
pnpm install
# yarn
yarn install
# bun
bun install
```
## Development Server
Start the development server on `http://localhost:3000`:
```bash
# npm
npm run dev
# pnpm
pnpm dev
# yarn
yarn dev
# bun
bun run dev
```
## Production
Build the application for production:
```bash
# npm
npm run build
# pnpm
pnpm build
# yarn
yarn build
# bun
bun run build
```
Locally preview production build:
```bash
# npm
npm run preview
# pnpm
pnpm preview
# yarn
yarn preview
# bun
bun run preview
```
Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information.

195
app.vue Normal file
View File

@ -0,0 +1,195 @@
<template>
<div class="app">
<Header />
<main>
<NuxtPage />
</main>
<Footer />
<!-- Top Flying Duck (Left to Right) -->
<div class="bg-duck bg-duck-1 flying-duck">
<Duck1 />
</div>
<!-- Bottom Flying Duck (Right to Left) -->
<div class="second-duck">
<Duck1 />
</div>
</div>
</template>
<script>
import Duck1 from './components/ducks/Duck1.vue';
export default {
components: {
Duck1
}
}
</script>
<style>
/* Global Styles */
:root {
--duck-yellow: #ffdb58;
--duck-orange: #f97316;
--duck-blue: #4682b4;
--duck-dark-blue: #1e3a8a;
--duck-green: #228b22;
--bg-color: #fffdf7;
--text-color: #333;
--gray-600: #4b5563;
--gray-700: #374151;
--gray-500: #6b7280;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family:
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu,
Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
background-color: var(--bg-color);
color: var(--text-color);
overflow-x: hidden;
position: relative;
}
.app {
min-height: 100vh;
display: flex;
flex-direction: column;
background-color: var(--bg-color);
position: relative;
overflow: hidden;
}
main {
flex: 1;
max-width: 1200px;
margin: 0 auto;
width: 100%;
}
/* Animation Classes */
@keyframes float {
0%,
100% {
transform: translateY(0);
}
50% {
transform: translateY(-10px);
}
}
@keyframes wave {
0%,
100% {
transform: rotate(-5deg);
}
50% {
transform: rotate(5deg);
}
}
@keyframes splash {
0% {
opacity: 0;
transform: translateY(20px);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
.animate-float {
animation: float 6s ease-in-out infinite;
}
.animate-wave {
animation: wave 3s ease-in-out infinite;
transform-origin: bottom center;
}
.animate-splash {
animation: splash 0.5s ease-out forwards;
}
/* Top Duck Styles */
.bg-duck {
position: absolute;
opacity: 0.16;
z-index: 1;
}
.bg-duck-1 {
top: 8rem;
}
.bg-duck-1 svg {
width: 16rem;
height: 16rem;
}
/* Second Duck Styles - Completely Separate */
.second-duck {
position: absolute; /* Changed from fixed to absolute */
top: 66vh; /* Position at 20% from the bottom of the initial viewport */
right: -20vw;
z-index: 1;
opacity: 0.17;
animation: fly-right-to-left 20s linear infinite, float-duck 6s ease-in-out infinite;
}
.second-duck svg {
width: 14rem;
height: 14rem;
transform: scaleX(-1); /* Flip the duck to face left */
}
/* Flying duck animations */
.flying-duck {
animation: fly-across 20s linear infinite, float-up-down 8s ease-in-out infinite;
}
@keyframes fly-across {
0% {
transform: translateX(-20vw) rotate(12deg);
}
100% {
transform: translateX(100vw) rotate(12deg);
}
}
@keyframes fly-right-to-left {
0% {
transform: translateX(0);
}
100% {
transform: translateX(-120vw);
}
}
@keyframes float-up-down {
0%, 100% {
margin-top: 0;
}
50% {
margin-top: -40px;
}
}
@keyframes float-duck {
0%, 100% {
margin-top: 0;
}
50% {
margin-top: -20px;
}
}
</style>

BIN
assets/duck.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

1310
bun.lock Normal file

File diff suppressed because it is too large Load Diff

323
components/DuckTerminal.vue Normal file
View File

@ -0,0 +1,323 @@
<template>
<div class="duck-terminal">
<div class="terminal-header">
<div class="terminal-buttons">
<span class="terminal-button close"></span>
<span class="terminal-button minimize"></span>
<span class="terminal-button maximize"></span>
</div>
<div class="terminal-title">portfolio@duck:~</div>
</div>
<div class="terminal-body">
<div class="terminal-content" ref="terminalContent">
<div
class="terminal-line"
v-for="(line, index) in displayedLines"
:key="index"
>
<template v-if="line.type === 'command'">
<span class="terminal-prompt">portfolio@duck:~$</span>
<span class="terminal-text">{{ line.text }}</span>
</template>
<template v-else-if="line.type === 'skills'">
<div class="terminal-output skills-grid">
<div class="skill-item" v-for="(skill, skillIndex) in line.skills" :key="skillIndex">
{{ skill }}
</div>
</div>
</template>
<template v-else>
<span class="terminal-output">{{ line.text }}</span>
</template>
</div>
<div
class="terminal-line typing-line"
v-if="showCursor"
>
<span class="terminal-prompt">portfolio@duck:~$</span>
<span class="terminal-text typing">{{ currentText }}</span>
<span class="terminal-cursor"></span>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "DuckTerminal",
data() {
return {
terminalLines: [
{ type: "command", text: "cat profil.txt" },
{
type: "output",
text: "Développeur PHP et Admin Sys/Réseau avec passion pour le web",
},
{ type: "command", text: "cat experience.txt" },
{
type: "output",
text: "Plus de 4 ans d'expérience dans différents contextes professionnels",
},
{ type: "command", text: "ls -la skills/" },
{
type: "skills",
skills: [
"PHP", "Laravel", "MySQL", "Linux",
"Symfony", "Docker", "Git", "Networking",
"JavaScript", "REST API", "DevOps", "Cloud"
]
},
{ type: "command", text: "whoami" },
{
type: "output",
text: "Expert en développement PHP et en administration système/réseau",
},
],
displayedLines: [],
currentLineIndex: 0,
currentText: "",
currentCharIndex: 0,
typingSpeed: 40,
commandDelay: 300,
outputDelay: 300,
showCursor: false
};
},
mounted() {
setTimeout(() => {
this.startTyping();
}, 500);
},
methods: {
scrollToBottom() {
if (this.$refs.terminalContent) {
this.$refs.terminalContent.scrollTop = this.$refs.terminalContent.scrollHeight;
}
},
startTyping() {
this.processNextLine();
},
processNextLine() {
if (this.currentLineIndex >= this.terminalLines.length) {
return;
}
const currentLine = this.terminalLines[this.currentLineIndex];
if (currentLine.type === "command") {
// For command lines, animate typing
this.showCursor = true;
this.currentText = "";
this.currentCharIndex = 0;
this.typeCurrentLine();
} else {
// For output and skills lines, just add them after a delay
this.showCursor = false;
setTimeout(() => {
this.displayedLines.push(currentLine);
this.currentLineIndex++;
this.scrollToBottom();
setTimeout(() => {
this.processNextLine();
}, 200);
}, this.outputDelay);
}
},
typeCurrentLine() {
if (!this.showCursor || this.currentLineIndex >= this.terminalLines.length) {
return;
}
const currentLine = this.terminalLines[this.currentLineIndex];
if (this.currentCharIndex < currentLine.text.length) {
// Still typing the current character
this.currentText = currentLine.text.substring(
0,
this.currentCharIndex + 1
);
this.currentCharIndex++;
setTimeout(this.typeCurrentLine, this.typingSpeed);
} else {
// Finished typing this command
setTimeout(() => {
// Add the completed command to displayed lines
this.displayedLines.push({ ...currentLine });
this.showCursor = false;
this.currentLineIndex++;
this.scrollToBottom();
// Wait a bit before processing the next line
setTimeout(this.processNextLine, this.commandDelay);
}, 300);
}
}
},
updated() {
this.scrollToBottom();
}
};
</script>
<style>
.duck-terminal {
width: 100%;
max-width: 620px;
background-color: #1a1b26;
border-radius: 8px;
overflow: hidden;
font-family: "Courier New", monospace;
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2);
border: 1px solid #30374b;
margin-bottom: 2rem;
height: 320px;
}
.terminal-header {
height: 36px;
background-color: #24283b;
display: flex;
align-items: center;
padding: 0 12px;
border-bottom: 1px solid #30374b;
}
.terminal-buttons {
display: flex;
gap: 8px;
}
.terminal-button {
width: 12px;
height: 12px;
border-radius: 50%;
}
.terminal-button.close {
background-color: #ff5f57;
}
.terminal-button.minimize {
background-color: #febc2e;
}
.terminal-button.maximize {
background-color: #28c840;
}
.terminal-title {
flex-grow: 1;
text-align: center;
color: #a9b1d6;
font-size: 0.875rem;
}
.terminal-body {
height: calc(100% - 36px);
position: relative;
overflow: hidden;
}
.terminal-content {
padding: 1.25rem;
height: 100%;
overflow-y: auto;
color: #a9b1d6;
scrollbar-width: thin;
scrollbar-color: #30374b #1a1b26;
text-align: left;
}
.terminal-content::-webkit-scrollbar {
width: 8px;
}
.terminal-content::-webkit-scrollbar-track {
background: #1a1b26;
}
.terminal-content::-webkit-scrollbar-thumb {
background-color: #30374b;
border-radius: 4px;
}
.terminal-line {
line-height: 1.6;
white-space: pre-wrap;
font-size: 1rem;
margin-bottom: 0.5rem;
text-align: left;
display: flex;
flex-wrap: wrap;
align-items: flex-start;
}
.terminal-prompt {
color: #ffdb58;
margin-right: 8px;
}
.terminal-text {
color: #a9b1d6;
}
.terminal-output {
color: #7aa2f7;
text-align: left;
width: 100%;
}
.skills-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-gap: 0.5rem;
margin-top: 0.25rem;
text-align: left;
}
.skill-item {
padding-right: 1rem;
text-align: left;
}
.terminal-cursor {
display: inline-block;
width: 8px;
height: 16px;
background-color: #7aa2f7;
animation: blink 1s step-start infinite;
vertical-align: middle;
}
@keyframes blink {
0%,
50% {
opacity: 1;
}
51%,
100% {
opacity: 0;
}
}
.typing-line {
display: flex;
align-items: center;
}
.typing {
margin-right: 2px;
}
@media (max-width: 768px) {
.duck-terminal {
width: 100%;
min-width: auto;
}
.skills-grid {
grid-template-columns: repeat(2, 1fr);
}
}
</style>

502
components/Experience.vue Normal file
View File

@ -0,0 +1,502 @@
<template>
<section id="experience" class="experience">
<div class="experience-container">
<h2 class="section-title">Compétences</h2>
<div class="skills-grid">
<div class="skill-card" v-for="(skill, index) in skills" :key="index">
<h3 class="skill-title">{{ skill.title }}</h3>
<div class="skill-tags">
<span class="skill-tag" v-for="(tag, tagIndex) in skill.tags" :key="tagIndex">{{ tag }}</span>
</div>
</div>
</div>
<div class="experience-section">
<h2 class="section-title">Mon Parcours</h2>
<!-- Year filter buttons -->
<div class="filter-buttons">
<button
type="button"
class="filter-button"
:class="{ active: activeFilter === 'all' }"
@click.prevent="setFilter('all')">
Tous
</button>
<button
type="button"
v-for="year in availableYears"
:key="year"
class="filter-button"
:class="{ active: activeFilter === year }"
@click.prevent="setFilter(year)">
{{ year }}
</button>
</div>
<div class="timeline">
<!-- Work Experience Section -->
<div v-if="filteredJobs.length > 0" class="timeline-title">
<h3 class="timeline-heading">Expérience Professionnelle</h3>
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="timeline-icon timeline-icon-work"
>
<path
d="M8 3c.132 0 .263 0 .393 0 7.107.007 10.372 6.167 11.917 11.917.078.287.156.575.234.863"
></path>
<path
d="M20.039 17.39c-.221-.89-.943-1.396-1.871-1.394a1.99 1.99 0 0 0-1.988 1.994v.012a1.998 1.998 0 0 0 2.004 1.998c.464 0 .91-.196 1.244-.548"
></path>
<path
d="M7.05 4.095c-1.815-.407-3.246-.089-4.028 1.007C1.082 7.547 1.603 12.19 4 16c2.492 0 4.623-1.33 6.234-3.219"
></path>
<path d="M11.167 12c1.174-1.525 2.272-3.747 2.272-6.8V3"></path>
<circle cx="16" cy="10" r="1"></circle>
</svg>
</div>
<div v-if="filteredJobs.length > 0" class="timeline-items">
<div
v-for="(job, index) in filteredJobs"
:key="'job-' + index"
class="timeline-card"
>
<div class="card-header">
<div>
<h3 class="card-title">{{ job.title }}</h3>
<p class="card-date">{{ job.period }}</p>
<div class="card-location">
<span>{{ job.location }}</span>
<span v-if="job.type">· {{ job.type }}</span>
</div>
</div>
<div class="card-tags">
<span class="card-tag" v-for="(tag, tagIndex) in job.tags" :key="tagIndex">{{ tag }}</span>
</div>
</div>
<div class="card-content">
<p>{{ job.description }}</p>
</div>
</div>
</div>
<div v-if="filteredJobs.length > 0 && filteredEducation.length > 0" class="section-divider"></div>
<!-- Education Section -->
<div v-if="filteredEducation.length > 0">
<div class="timeline-title">
<h3 class="timeline-heading">Formation</h3>
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="timeline-icon timeline-icon-education"
>
<path
d="M22 12A10 10 0 1 1 12 2a1 1 0 0 1 1 1v1a1 1 0 1 1-2 0V3.07A8 8 0 1 0 19 12h-3a2 2 0 0 0-2 2v4a2 2 0 0 0 2 2h1"
></path>
<path d="M11.8 22h.2a3 3 0 0 0 3-3v-1"></path>
<path
d="M20 19a2 2 0 0 0 2-2v-1a3 3 0 0 0-3-3h-1a2 2 0 0 0-2 2"
></path>
<circle cx="12" cy="12" r="3"></circle>
</svg>
</div>
<div class="timeline-items">
<div
v-for="(edu, index) in filteredEducation"
:key="'edu-' + index"
class="timeline-card education-card"
>
<div class="card-header">
<div>
<h3 class="card-title">{{ edu.title }}</h3>
<p class="card-date">{{ edu.period }}</p>
<div class="card-location">
<span>{{ edu.location }}</span>
</div>
</div>
</div>
<div class="card-content">
<p>{{ edu.description }}</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
</template>
<script>
export default {
name: "Experience",
data() {
return {
activeFilter: 'all',
skills: [
{
title: "Langages & Frameworks",
tags: ["PHP", "Drupal", "Symfony", "Laravel"]
},
{
title: "Systèmes & DevOps",
tags: ["Docker", "Linux", "Git", "CI/CD"]
},
{
title: "Administration & Sécurité",
tags: ["Administration Systèmes", "Sécurité Web", "Monitoring", "SQL"]
}
],
jobs: [
{
title: "Administrateur Systèmes et Réseaux Web | STUDIO BESTIO",
period: "Février 2025 - Aujourd'hui",
location: "Nancy, France",
type: "Indépendant",
tags: ["Docker", "Linux"],
description: "Gestion de l'hébergement, de sauvegardes, de la maintenance et de la sécurité des sites web pour Studiobestio. Administration des serveurs, supervision des performances, gestion de la journalisation, mise en place de solutions de sécurité pour assurer une protection contre les cybermenaces. Garantir le bon fonctionnement, la sécurité et la fiabilité des sites web des clients.",
yearStart: 2025,
yearEnd: 2025
},
{
title: "Développeur PHP | GIE SIMA",
period: "Mai 2023 - Aujourd'hui",
location: "Nancy, France",
tags: ["PHP", "Drupal", "Docker"],
description: "Réecriture et maintenance d'une application pour la vente de contrats de santé ou prévoyance déstinée à des courtiers/conseillers.",
yearStart: 2023,
yearEnd: 2025
},
{
title: "Consultant | DAVIDSON CONSULTING",
period: "Février 2023 - Mai 2023",
location: "Nancy, France",
tags: ["PHP", "Drupal"],
description: "Développement d'applications clients",
yearStart: 2023,
yearEnd: 2023
},
{
title: "Développeur web | GANTOIS INDUSTRIES",
period: "Sept. 2021 - Avr. 2022",
location: "Saint-Dié, France",
type: "Intérimaire",
tags: ["PHP", "Symfony"],
description: "Développement d'applications métier",
yearStart: 2021,
yearEnd: 2022
},
{
title: "Développeur web | GANTOIS INDUSTRIES",
period: "Nov. 2020 - Sept. 2021",
location: "Saint-Dié, France",
type: "Alternance",
tags: ["PHP", "Symfony"],
description: "Développement et maintenance d'une application de contrôle qualité pour les pièces industrielles.",
yearStart: 2020,
yearEnd: 2021
},
{
title: "Développeur Full Stack | 2110 FINANCE",
period: "Juillet 2020",
location: "Saint-Dié, France",
type: "Stage",
tags: ["PHP", "Laravel"],
description: "Application de messagerie interne",
yearStart: 2020,
yearEnd: 2020
}
],
education: [
{
title: "Licence en application web et mobile | Université de Lorraine",
period: "2020 - 2021",
location: "Nancy, France",
description: "Formation en développement d'applications web et mobiles",
yearStart: 2020,
yearEnd: 2021
},
{
title: "DUT informatique | Université de Lorraine",
period: "2018 - 2020",
location: "Nancy, France",
description: "Formation en informatique générale avec spécialisation en développement",
yearStart: 2018,
yearEnd: 2020
},
{
title: "Baccalauréat Scientifique | Lycée Gaston Bachelard",
period: "2015 - 2018",
location: "Bar-sur-Aube, France",
description: "Baccalauréat avec option mathématiques",
yearStart: 2015,
yearEnd: 2018
}
]
};
},
computed: {
availableYears() {
const allYearsSet = new Set();
this.jobs.forEach(job => {
for (let year = job.yearStart; year <= job.yearEnd; year++) {
allYearsSet.add(year);
}
});
this.education.forEach(edu => {
for (let year = edu.yearStart; year <= edu.yearEnd; year++) {
allYearsSet.add(year);
}
});
return [...allYearsSet].sort();
},
filteredJobs() {
if (this.activeFilter === 'all') {
return this.jobs;
}
const filterYear = parseInt(this.activeFilter);
return this.jobs.filter(job => {
return filterYear >= job.yearStart && filterYear <= job.yearEnd;
});
},
// Filter education based on selected year
filteredEducation() {
if (this.activeFilter === 'all') {
return this.education;
}
const filterYear = parseInt(this.activeFilter);
return this.education.filter(edu => {
return filterYear >= edu.yearStart && filterYear <= edu.yearEnd;
});
}
},
methods: {
setFilter(filter) {
this.activeFilter = filter;
}
}
};
</script>
<style scoped>
.experience {
padding: 2rem 0;
scroll-margin-top: 4rem;
}
.experience-container {
max-width: 64rem;
margin: 0 auto;
}
.section-title {
font-size: 1.875rem;
font-weight: 700;
margin-bottom: 2rem;
}
.skills-grid {
display: grid;
grid-template-columns: 1fr;
gap: 1.5rem;
margin-bottom: 3rem;
}
.skill-card {
background-color: white;
padding: 1.5rem;
border-radius: 0.5rem;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.skill-title {
font-size: 1.25rem;
font-weight: 600;
margin-bottom: 1rem;
color: var(--duck-dark-blue);
}
.skill-tags {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.skill-tag {
background-color: var(--duck-yellow);
padding: 0.25rem 0.75rem;
border-radius: 9999px;
font-size: 0.875rem;
font-weight: 500;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
.experience-section {
margin-bottom: 3rem;
}
.filter-buttons {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-bottom: 1.5rem;
}
.filter-button {
padding: 0.5rem 1rem;
border-radius: 0.25rem;
background-color: #f3f4f6;
border: 1px solid #e5e7eb;
color: #4b5563;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
}
.filter-button:hover {
background-color: #e5e7eb;
}
.filter-button.active {
background-color: var(--duck-blue);
color: white;
border-color: var(--duck-blue);
}
.timeline {
margin-top: 2rem;
}
.timeline-title {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 1.5rem;
}
.timeline-heading {
font-size: 1.5rem;
font-weight: 700;
}
.timeline-icon {
width: 2rem;
height: 2rem;
}
.timeline-icon-work {
color: var(--duck-blue);
}
.timeline-icon-education {
color: var(--duck-orange);
}
.timeline-items {
margin-bottom: 2rem;
display: flex;
flex-direction: column;
gap: 1rem;
}
.timeline-card {
border-radius: 0.5rem;
border: 1px solid #e5e7eb;
background-color: white;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
width: 100%;
margin-bottom: 1rem;
border-left: 4px solid var(--duck-blue);
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.timeline-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.education-card {
border-left: 4px solid var(--duck-orange);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
flex-wrap: wrap;
gap: 0.5rem;
padding: 1.5rem;
padding-bottom: 0.5rem;
}
.card-title {
font-size: 1.25rem;
font-weight: 700;
}
.card-date {
font-size: 0.875rem;
color: var(--gray-500);
}
.card-location {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.875rem;
color: var(--gray-600);
margin-top: 0.25rem;
}
.card-tags {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
justify-content: flex-end;
}
.card-tag {
background-color: rgba(34, 139, 34, 0.2);
color: var(--text-color);
font-size: 0.75rem;
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
}
.card-content {
padding: 1.5rem;
padding-top: 0;
}
.card-content p {
color: var(--gray-700);
}
.section-divider {
height: 1px;
width: 100%;
background-color: #e5e7eb;
margin: 2.5rem 0;
}
@media (min-width: 768px) {
.skills-grid {
grid-template-columns: repeat(3, 1fr);
}
}
</style>

174
components/Footer.vue Normal file
View File

@ -0,0 +1,174 @@
<template>
<footer class="footer">
<div class="footer-container">
<div class="footer-left">
<Duck1 class="footer-duck" />
<div>
<h3 class="footer-title">Romaric SIRI</h3>
<p class="footer-subtitle">Développeur PHP | Admin Sys</p>
</div>
</div>
<div class="footer-right">
<p class="footer-text">
Nancy, Grand Est
<a href="tel:0632717245" class="footer-link">06 32 71 72 45</a>
</p>
<p class="footer-text">
<a href="mailto:contact@maric.ro" class="footer-link"
>contact@maric.ro</a
>
</p>
</div>
</div>
<div class="footer-duck-right animate-float">
<Duck1 />
<div class="duck-shadow"></div>
</div>
<div class="footer-duck-left animate-float">
<Duck1 />
<div class="duck-shadow"></div>
</div>
<p class="copyright">© 2025 Romaric SIRI. All rights reserved. 🦆</p>
</footer>
</template>
<script>
import Duck1 from './ducks/Duck1.vue';
export default {
name: "Footer",
components: {
Duck1
}
};
</script>
<style scoped>
.footer {
padding: 2rem 1rem;
background-color: rgba(255, 219, 88, 0.2);
margin-top: 2rem;
position: relative;
overflow: hidden;
}
.footer-container {
max-width: 72rem;
margin: 0 auto;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
}
.footer-left {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 1rem;
}
.footer-duck {
width: 2.5rem;
height: 2.5rem;
}
.footer-title {
font-weight: 700;
}
.footer-subtitle {
font-size: 0.875rem;
color: var(--gray-600);
}
.footer-right {
text-align: center;
}
.footer-text {
font-size: 0.875rem;
color: var(--gray-600);
}
.footer-link {
color: var(--gray-600);
text-decoration: none;
}
.footer-link:hover {
text-decoration: underline;
}
.footer-duck-right {
position: absolute;
bottom: -1.25rem;
right: 1rem;
transform: translateY(0);
opacity: 0.4;
}
.footer-duck-right svg {
width: 4rem;
height: 4rem;
}
.footer-duck-left {
position: absolute;
bottom: -1.25rem;
left: 1rem;
transform: translateY(0);
opacity: 0.3;
animation-delay: 2s;
}
.footer-duck-left svg {
width: 3rem;
height: 3rem;
}
.duck-shadow {
position: absolute;
width: 6rem;
height: 2rem;
background-color: rgba(70, 130, 180, 0.3);
border-radius: 9999px;
bottom: -1rem;
left: 50%;
transform: translateX(-50%);
z-index: -1;
}
.copyright {
text-align: center;
font-size: 0.75rem;
color: var(--gray-500);
margin-top: 2rem;
}
@media (min-width: 768px) {
.footer {
padding: 2rem;
}
.footer-container {
flex-direction: row;
}
.footer-left {
margin-bottom: 0;
}
.footer-right {
text-align: right;
}
.footer-duck-right {
right: 2.5rem;
}
.footer-duck-left {
left: 5rem;
}
}
</style>

348
components/Header.vue Normal file
View File

@ -0,0 +1,348 @@
<template>
<header class="header">
<div class="header-container">
<div class="header-left">
<div class="duck-container">
<Duck1 class="duck-svg animate-float" />
<div class="bubble animate-wave">
<span class="bubble-text">Quack!</span>
</div>
</div>
<div class="header-info">
<h1 class="header-title">Romaric SIRI</h1>
<p class="header-subtitle">
Développeur PHP | Administrateur Systèmes & Réseaux Web
</p>
</div>
</div>
<div class="social-links">
<a href="" class="blog-link">
<span class="blog-text">Blog</span>
</a>
<a href="mailto:contact@maric.ro" title="Email" class="social-link">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="icon"
>
<rect width="20" height="16" x="2" y="4" rx="2"></rect>
<path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"></path>
</svg>
</a>
<a href="tel:0632717245" title="Téléphone" class="social-link">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="icon"
>
<path
d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"
></path>
</svg>
</a>
<a
href="https://linkedin.com/in/romaric-siri-a25949181"
target="_blank"
rel="noopener noreferrer"
title="LinkedIn"
class="social-link"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="icon"
>
<path
d="M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6z"
></path>
<rect width="4" height="12" x="2" y="9"></rect>
<circle cx="4" cy="4" r="2"></circle>
</svg>
</a>
<a
href="https://github.com/"
target="_blank"
rel="noopener noreferrer"
title="GitHub"
class="social-link"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="icon"
>
<path
d="M15 22v-4a4.8 4.8 0 0 0-1-3.5c3 0 6-2 6-5.5.08-1.25-.27-2.48-1-3.5.28-1.15.28-2.35 0-3.5 0 0-1 0-3 1.5-2.64-.5-5.36-.5-8 0C6 2 5 2 5 2c-.3 1.15-.3 2.35 0 3.5A5.403 5.403 0 0 0 4 9c0 3.5 3 5.5 6 5.5-.39.49-.68 1.05-.85 1.65-.17.6-.22 1.23-.15 1.85v4"
></path>
<path d="M9 18c-4.51 2-5-2-7-2"></path>
</svg>
</a>
</div>
</div>
</header>
</template>
<script>
import Duck1 from './ducks/Duck1.vue';
export default {
name: "Header",
components: {
Duck1
}
};
</script>
<style scoped>
.header {
position: relative;
padding: 2.5rem 1rem 1rem;
text-align: center;
}
.header-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin-bottom: 1.5rem;
}
.header-left {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 1rem;
}
.duck-container {
position: relative;
width: 5rem;
height: 5rem;
}
.duck-svg {
width: 5rem;
height: 5rem;
}
.bubble {
padding: 0.5rem 1rem;
position: absolute;
top: 0px;
right: 0px;
background-color: white;
border-radius: 1rem;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
z-index: 10;
}
.bubble:after {
content: "";
position: absolute;
left: 0;
top: 50%;
width: 0;
height: 0;
border: 8px solid transparent;
border-right-color: white;
border-left: 0;
margin-top: -8px;
margin-left: -8px;
}
.bubble-text {
font-size: 0.875rem;
font-weight: 500;
}
.header-info {
text-align: left;
}
.header-title {
font-size: 1.875rem;
font-weight: 700;
color: var(--duck-dark-blue);
}
.header-subtitle {
font-size: 1.125rem;
color: var(--gray-600);
}
.social-links {
display: flex;
align-items: center;
gap: 0.75rem;
}
.header a {
text-decoration: none;
}
.social-link {
display: inline-flex;
align-items: center;
justify-content: center;
width: 2.5rem;
height: 2.5rem;
border-radius: 0.375rem;
border: 1px solid #e5e7eb;
background-color: white;
color: var(--text-color);
transition:
background-color 0.2s,
color 0.2s;
}
.social-link:hover {
background-color: #f3f4f6;
}
.icon {
width: 1rem;
height: 1rem;
}
/* New Blog Link Styles */
.blog-link {
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
border-radius: 2rem;
background: linear-gradient(135deg, var(--duck-blue) 0%, var(--duck-dark-blue) 100%);
color: white;
font-weight: 600;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1), 0 1px 3px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.blog-link::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
transition: 0.5s;
}
.blog-link:hover {
transform: translateY(-2px);
box-shadow: 0 7px 14px rgba(0, 0, 0, 0.1), 0 3px 6px rgba(0, 0, 0, 0.1);
}
.blog-link:hover::before {
left: 100%;
}
.blog-text {
font-size: 0.9rem;
letter-spacing: 0.025em;
}
.blog-icon {
width: 1rem;
height: 1rem;
}
/* Animation for blog link pulsing effect */
@keyframes pulse {
0% {
box-shadow: 0 0 0 0 rgba(var(--duck-blue-rgb), 0.7);
}
70% {
box-shadow: 0 0 0 10px rgba(var(--duck-blue-rgb), 0);
}
100% {
box-shadow: 0 0 0 0 rgba(var(--duck-blue-rgb), 0);
}
}
.animate-float {
animation: float 3s ease-in-out infinite;
}
@keyframes float {
0% {
transform: translateY(0px);
}
50% {
transform: translateY(-10px);
}
100% {
transform: translateY(0px);
}
}
.animate-wave {
animation: wave 1.5s ease-in-out infinite;
}
/* Updated wave animation for fixed positioning */
@keyframes wave {
0% {
transform: translateY(0);
}
50% {
transform: translateY(-5px);
}
100% {
transform: translateY(0);
}
}
@media (min-width: 768px) {
.header {
padding: 2.5rem 2rem 1rem;
}
.header-container {
flex-direction: row;
justify-content: space-between;
}
.header-left {
margin-bottom: 0;
}
/* Make Blog link stand out more on desktop */
.blog-link {
padding: 0.5rem 1.25rem;
}
}
</style>

View File

@ -0,0 +1,34 @@
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 512 512"
:class="svgClass"
>
<ellipse cx="256" cy="256" rx="180" ry="170" fill="#FFDB58" />
<circle cx="360" cy="180" r="90" fill="#FFDB58" />
<circle cx="390" cy="160" r="15" fill="#333" />
<path d="M430 190 L480 180 L430 210 Z" fill="#F97316" />
<path d="M180 370 L150 410 L210 410 Z" fill="#F97316" />
<path d="M300 370 L270 410 L330 410 Z" fill="#F97316" />
<ellipse
cx="200"
cy="250"
rx="60"
ry="40"
fill="#FFE082"
transform="rotate(-10, 200, 250)"
/>
</svg>
</template>
<script>
export default {
name: "DuckSvg",
props: {
svgClass: {
type: String,
default: "",
},
},
};
</script>

View File

5
nuxt.config.ts Normal file
View File

@ -0,0 +1,5 @@
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
compatibilityDate: '2024-11-01',
devtools: { enabled: true }
})

3687
output.txt Normal file

File diff suppressed because it is too large Load Diff

17
package.json Normal file
View File

@ -0,0 +1,17 @@
{
"name": "nuxt-app",
"private": true,
"type": "module",
"scripts": {
"build": "nuxt build",
"dev": "nuxt dev",
"generate": "nuxt generate",
"preview": "nuxt preview",
"postinstall": "nuxt prepare"
},
"dependencies": {
"nuxt": "^3.16.2",
"vue": "^3.5.13",
"vue-router": "^4.5.0"
}
}

292
pages/index.vue Normal file
View File

@ -0,0 +1,292 @@
<template>
<section class="landing">
<div class="landing-container">
<div class="landing-content animate-splash">
<div class="terminal-duck-container">
<div class="terminal-wrapper">
<DuckTerminal />
</div>
<div class="duck-container">
<div class="duck-placeholder">
<img
src="/duck.gif"
alt="Canard dansant"
class="dancing-duck"
@load="duckLoaded = true"
:class="{ 'duck-loaded': duckLoaded }"
@error="() => console.error('Duck image failed to load')"
/>
<div class="duck-loading" v-if="!duckLoaded">
<span class="loading-text">Chargement du canard...</span>
</div>
</div>
</div>
</div>
<div class="landing-buttons">
<button @click="scrollToExperience" class="btn btn-primary">
Voir mon parcours
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="btn-icon"
>
<path d="M12 5v14"></path>
<path d="m19 12-7 7-7-7"></path>
</svg>
</button>
<a href="mailto:contact@maric.ro" class="btn btn-secondary">Me contacter</a>
</div>
</div>
</div>
<div id="experience-section">
<Experience />
</div>
</section>
</template>
<script setup>
import { ref, onMounted } from 'vue';
const duckLoaded = ref(false);
onMounted(() => {
const img = new Image();
img.src = '/duck.gif';
if (img.complete) {
duckLoaded.value = true;
}
img.onload = () => {
duckLoaded.value = true;
};
setTimeout(() => {
duckLoaded.value = true;
}, 3000);
});
const scrollToExperience = () => {
const experienceSection = document.getElementById('experience-section');
if (experienceSection) {
experienceSection.scrollIntoView({ behavior: 'smooth' });
}
};
</script>
<style>
.landing {
padding: 4rem 1rem;
min-height: 80vh;
display: flex;
flex-direction: column;
justify-content: center;
position: relative;
}
.landing-container {
display: flex;
flex-direction: column;
align-items: center;
max-width: 90%;
margin: 0 auto;
z-index: 10;
}
.landing-content {
text-align: center;
width: 100%;
margin-bottom: 2rem;
display: flex;
flex-direction: column;
align-items: center;
}
.terminal-duck-container {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
margin-bottom: 1.5rem;
}
.terminal-wrapper {
position: relative;
z-index: 1;
width: 100%;
}
.duck-container {
margin-top: 1.5rem;
width: 320px;
display: flex;
justify-content: center;
}
.landing-buttons {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 0.75rem;
margin-bottom: 2.5rem;
}
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
font-size: 0.875rem;
font-weight: 500;
height: 2.75rem;
border-radius: 0.375rem;
padding: 0 2rem;
transition: all 0.2s;
cursor: pointer;
text-decoration: none;
}
.btn-primary {
background-color: var(--duck-yellow);
color: black;
border: none;
}
.btn-primary:hover {
background-color: rgba(255, 219, 88, 0.9);
}
.btn-secondary {
background-color: transparent;
border: 1px solid var(--duck-blue);
color: var(--duck-blue);
}
.btn-secondary:hover {
background-color: rgba(70, 130, 180, 0.1);
}
.btn-icon {
width: 1rem;
height: 1rem;
margin-left: 0.5rem;
}
.duck-svg {
width: 5rem;
height: 5rem;
}
.dancing-duck {
width: 320px;
height: 320px;
object-fit: contain;
opacity: 0;
transition: opacity 0.3s ease;
}
.duck-placeholder {
width: 320px;
height: 320px;
position: relative;
border-radius: 0.5rem;
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
}
.duck-loaded {
opacity: 1;
}
.duck-loading {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
border-radius: 0.5rem;
}
.loading-text {
color: var(--duck-yellow);
font-family: "Courier New", monospace;
font-size: 1rem;
animation: duck-bounce 1.5s infinite;
}
@keyframes duck-bounce {
0%,
100% {
transform: translateY(0);
}
50% {
transform: translateY(-10px);
}
}
@media (min-width: 768px) {
.landing-content {
text-align: left;
align-items: flex-start;
}
.terminal-duck-container {
flex-direction: row;
align-items: flex-start;
justify-content: space-between;
}
.terminal-wrapper {
width: 620px;
margin-right: 2rem;
}
.duck-container {
width: 320px;
margin-top: 0;
flex-shrink: 0;
}
.landing-buttons {
justify-content: flex-start;
}
.duck-placeholder {
width: 320px;
height: 320px;
flex-shrink: 0;
}
}
@media (max-width: 767px) {
.terminal-wrapper {
min-width: 100%;
max-width: 100%;
width: 100%;
}
.duck-container,
.duck-placeholder {
width: 280px;
height: 280px;
}
.dancing-duck {
width: 280px;
height: 280px;
}
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

1
public/assets/vue.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>

After

Width:  |  Height:  |  Size: 496 B

BIN
public/duck.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

1
public/robots.txt Normal file
View File

@ -0,0 +1 @@

3
server/tsconfig.json Normal file
View File

@ -0,0 +1,3 @@
{
"extends": "../.nuxt/tsconfig.server.json"
}

4
tsconfig.json Normal file
View File

@ -0,0 +1,4 @@
{
// https://nuxt.com/docs/guide/concepts/typescript
"extends": "./.nuxt/tsconfig.json"
}