first
75
README.md
Normal 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
@ -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
After Width: | Height: | Size: 130 KiB |
323
components/DuckTerminal.vue
Normal 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
@ -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
@ -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
@ -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>
|
34
components/ducks/Duck1.vue
Normal 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>
|
0
components/ducks/Duck2.vue
Normal file
5
nuxt.config.ts
Normal 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
17
package.json
Normal 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
@ -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>
|
BIN
public/assets/duck-dance.gif
Normal file
After Width: | Height: | Size: 2.0 MiB |
BIN
public/assets/duck-dance2.gif
Normal file
After Width: | Height: | Size: 2.8 MiB |
BIN
public/assets/duck-dance3.gif
Normal file
After Width: | Height: | Size: 2.0 MiB |
1
public/assets/vue.svg
Normal 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
After Width: | Height: | Size: 130 KiB |
BIN
public/favicon.ico
Normal file
After Width: | Height: | Size: 4.2 KiB |
1
public/robots.txt
Normal file
@ -0,0 +1 @@
|
||||
|
3
server/tsconfig.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "../.nuxt/tsconfig.server.json"
|
||||
}
|
4
tsconfig.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
// https://nuxt.com/docs/guide/concepts/typescript
|
||||
"extends": "./.nuxt/tsconfig.json"
|
||||
}
|