🎨 style(new!): add new ui vue.js

Signed-off-by: Abhiraj Roy <157954129+iconized@users.noreply.github.com>
This commit is contained in:
Abhiraj Roy
2024-06-05 04:26:16 +05:30
parent 4878325171
commit 5f4ddc3090
110 changed files with 9442 additions and 0 deletions

99
components/AppFooter.vue Normal file
View File

@@ -0,0 +1,99 @@
<template>
<footer class='flex md:justify-between border-top text-menu-text font-fira_retina'>
<!-- social icons -->
<div class="w-full flex justify-between md:justify-start">
<span id="social-title" class="h-full flex justify-center items-center border-right px-5">
find me in:
</span>
<div id="social-icons" class="flex">
<NuxtLink :to="social.twitter.url + social.twitter.user" target="_blank" class="flex justify-center items-center">
<img src="/icons/social/twitter.svg"/>
</NuxtLink>
<NuxtLink :to="social.facebook.url + social.facebook.user" target="_blank" class="flex justify-center items-center">
<img src="/icons/social/facebook.svg"/>
</NuxtLink>
<NuxtLink :to="social.github.url + social.github.user" target="_blank" class="flex md:hidden justify-center items-center">
<img src="/icons/social/github.svg"/>
</NuxtLink>
</div>
</div>
<!-- github user -->
<NuxtLink :to="social.github.url + social.github.user" target="_blank" class="hidden md:flex items-center px-5 border-left">
@{{ social.github.user }}
<img src="/icons/social/github.svg"/>
</NuxtLink>
</footer>
</template>
<style>
footer {
height: 40px;
min-height: 40px;
font-size: 13px;
}
footer a:hover {
background-color: #1e2d3d74;
}
#social-icons > a {
border-right: 1px solid #1E2D3D;
height: 100%;
width: 50px;
}
#social-icons > a > img {
width: 1.25rem; /* 20px */
height: 1.25rem; /* 20px */
margin: auto;
opacity: 0.4;
}
footer > a > img {
width: 1.25rem; /* 20px */
height: 1.25rem; /* 20px */
margin-left: 0.5rem; /* 8px */
}
#social-icons > a:hover img {
opacity: 1;
}
@media (max-width: 768px) {
#social-title {
border-right: none;
}
#social-icons > a {
border-right: none;
border-left: 1px solid #1E2D3D;
}
#social-icons > a > img {
width: 1.5rem; /* 20px */
height: 1.5rem; /* 20px */
}
}
</style>
<script>
export default {
name: 'AppFooter',
data() {
return {
route: this.$route.path,
}
},
setup() {
return {
social: useRuntimeConfig().dev.contacts.social
}
},
}
</script>

101
components/AppHeader.vue Normal file
View File

@@ -0,0 +1,101 @@
<template>
<header id="navbar" class="w-full hidden lg:flex flex-col">
<nav class="w-full flex justify-between border-bot">
<github-corner url="https://github.com/alexdeploy/developer-portfolio-v2" />
<div class="flex">
<NuxtLink id="nav-logo" to="/">
{{ config.dev.logo_name }}
</NuxtLink>
<NuxtLink id="nav-link" to="/" :class="{ active: isActive('/') }">
_hello
</NuxtLink>
<NuxtLink id="nav-link" to="/about-me" :class="{ active: isActive('/about-me') }">
_about-me
</NuxtLink>
<NuxtLink id="nav-link" to="/projects" :class="{ active: isActive('/projects') }">
_projects
</NuxtLink>
</div>
<NuxtLink id="nav-link-contact" to="/contact-me" :class="{ active: isActive('/contact-me')}">
_contact-me
</NuxtLink>
</nav>
</header>
</template>
<script>
import GithubCorner from './GithubCorner.vue';
export default {
name: 'AppHeader',
components: {
GithubCorner
},
computed: {
// Set active class to current page link
isActive() {
return route => this.$route.path === route;
}
},
setup() {
const config = useRuntimeConfig()
return {
config
}
},
};
</script>
<style>
#nav-link {
border-right: 1px solid #1E2D3D;
@apply text-menu-text font-fira_retina px-6 h-full flex items-center;
}
#nav-link-contact {
border-left: 1px solid #1E2D3D;
@apply text-menu-text font-fira_retina px-6 h-full flex items-center;
}
#nav-link:hover, #nav-link-contact:hover {
background-color: #1e2d3d74;
color: white;
}
#nav-logo {
border-right: 1px solid #1E2D3D;
@apply text-menu-text font-fira_retina px-6 h-full flex items-center;
}
#nav-logo:hover {
background-color: #1e2d3d74;
color: white;
}
#nav-link.router-link-active, #nav-link-contact.router-link-active {
border-bottom: 2px solid #FEA55F;
color: white;
}
#nav-logo.router-link-active {
border-right: 1px solid #1E2D3D;
border-bottom: none;
@apply text-menu-text;
}
#navbar > nav {
height: 45px;
font-size: 13px;
}
</style>

View File

@@ -0,0 +1,72 @@
<template>
<div class="code-container flex font-fira_retina text-menu-text">
<div class="line-numbers lg:flex flex-col w-32 hidden">
<!-- line numbers and asteriscs -->
<div v-for="n in lines" class="grid grid-cols-2 justify-end" :key="n">
<span class="col-span-1 mr-3">{{ n }}</span>
<div v-if="n == 1" class="col-span-1 flex justify-center">/**</div>
<div class="col-span-1 flex justify-center" v-if="n > 1 && n < lines">*</div>
<div class="col-span-1 flex justify-center pl-2" v-if="n == lines">*/</div>
</div>
</div>
<!-- text -->
<div class="text-container">
<p v-html="text"></p>
</div>
</div>
</template>
<script>
export default {
props: {
text: {
type: String,
required: true
}
},
data() {
return {
lines: 0
};
},
mounted() {
this.updateLines();
window.addEventListener("resize", this.updateLines);
window.addEventListener("click", this.updateLines);
},
beforeDestroy() {
window.removeEventListener("resize", this.updateLines);
window.removeEventListener("click", this.updateLines);
},
methods: {
updateLines() {
const textContainer = this.$el.querySelector(".text-container");
const style = window.getComputedStyle(textContainer);
const fontSize = parseInt(style.fontSize);
const lineHeight = parseInt(style.lineHeight);
const maxHeight = textContainer.offsetHeight;
this.lines = Math.ceil(maxHeight / lineHeight) + 1;
}
}
};
</script>
<style>
.code-container {
display: flex;
align-items: flex-start;
}
.line-numbers {
text-align: right;
}
.text-container {
width: 100%;
padding-left: 10px;
word-wrap: break-word;
}
</style>

128
components/ContactForm.vue Normal file
View File

@@ -0,0 +1,128 @@
<template>
<form id="contact-form" class="text-sm">
<div class="flex flex-col">
<label for="name" class="mb-3">_name:</label>
<input type="text" id="name-input" name="name" :placeholder="name" class="p-2 mb-5 placeholder-slate-600" required>
</div>
<div class="flex flex-col">
<label for="email" class="mb-3">_email:</label>
<input type="email" id="email-input" name="email" :placeholder="email" class="p-2 mb-5 placeholder-slate-600" required>
</div>
<div class="flex flex-col">
<label for="message" class="mb-3">_message:</label>
<textarea id="message-input" name="message" :placeholder="message" class="placeholder-slate-600" required></textarea>
</div>
<button id="submit-button" type="submit" class="py-2 px-4">submit-message</button>
</form>
</template>
<script>
export default {
name: 'ContactForm',
props: {
name: {
type: String,
required: true
},
email: {
type: String,
required: true
},
message: {
type: String,
required: true
}
},
mounted() {
document.getElementById("contact-form").addEventListener("submit", function(event) {
event.preventDefault();
const name = document.querySelector('input[name="name"]').value;
const email = document.querySelector('input[name="email"]').value;
const message = document.querySelector('textarea[name="message"]').value;
// Here the code to send the email
});
}
}
</script>
<style>
form {
@apply font-fira_retina text-menu-text
}
input {
background-color: #011221;
border: 2px solid #1E2D3D;
border-radius: 7px;
}
/* Change Autocomplete styles in Chrome*/
input:-webkit-autofill,
input:-webkit-autofill:hover,
input:-webkit-autofill:focus,
textarea:-webkit-autofill,
textarea:-webkit-autofill:hover,
textarea:-webkit-autofill:focus,
select:-webkit-autofill,
select:-webkit-autofill:hover,
select:-webkit-autofill:focus {
-webkit-text-fill-color: rgb(190, 190, 190);
transition: background-color 5000s ease-in-out 0s;
border: 2px solid #607b96;
}
#message-input {
background-color: #011221;
border: 2px solid #1E2D3D;
border-radius: 7px;
resize: none;
height: 150px;
padding: 10px;
}
#submit-button {
@apply font-fira_retina text-white text-sm;
background-color: #1E2D3D;
border-radius: 7px;
margin-top: 20px;
cursor: pointer;
}
#submit-button:hover {
background-color: #263B50;
}
input:focus, #message-input:focus {
outline: none;
transition: none;
border: 2px solid #607b96;
box-shadow: #607b9669 0px 0px 0px 2px;
}
#contact-form {
max-width: 370px;
width: 100%;
}
@media (max-width: 1920px) {
#contact-form {
max-width: 320px;
max-height: 400px;
}
#submit-button {
/* width: 100%; */
font-size: 12px;
}
textarea {
font-size: 13px;
max-height: 130px !important;
}
input {
font-size: 13px;
}
}
</style>

View File

@@ -0,0 +1,170 @@
<template>
<div class="code-container flex font-fira_retina text-menu-text">
<div class="line-numbers lg:flex flex-col w-16 hidden">
<!-- line numbers and asteriscs -->
<div v-for="n in lines" class="grid grid-cols-2 justify-end" :key="n">
<span class="col-span-1 mr-3">{{ n }}</span>
</div>
</div>
<div class="font-fira_retina text-white text-container">
<p>
<span class="tag">
const
</span>
<span class="tag-name">
button
</span>
=
<span class="tag-name">
document.querySelector
<span class="text-menu-text">
(
<span class="text-codeline-link">
'#sendBtn'
</span>
);
</span>
</span>
</p>
<br>
<p class="text-menu-text">
<span class="tag">
const
</span>
<span class="tag-name">
message
</span>
= {
<br> &nbsp;&nbsp;
<span id="name" class="tag-name">
name
</span>
:
<span class="text-codeline-link">"</span>
<span id="name-value" class="text-codeline-link">
{{ name }}
</span>
<span class="text-codeline-link">"</span>
, <br> &nbsp;&nbsp;
<span id="email" class="tag-name">
email
</span>
:
<span class="text-codeline-link">"</span>
<span id="email-value" class="text-codeline-link">
{{ email }}
</span>
<span class="text-codeline-link">"</span>
, <br> &nbsp;&nbsp;
<span id="message" class="tag-name">
message
</span>
:
<span class="text-codeline-link">"</span>
<span id="message-value" class="text-codeline-link">
{{ message }}
</span>
<span class="text-codeline-link">"</span>
, <br> &nbsp;&nbsp;
date:
<span class="text-codeline-link">
"{{ date }}"
</span>
<br>
}
</p>
<br>
<p>
<span class="tag-name">
button.addEventListener
<span class="text-menu-text">
(
<span class="text-codeline-link">
'click'
</span>
), ()
<span class="tag">
=>
</span>
{
<br>
</span>
&nbsp;&nbsp;form.send
<span class="text-menu-text">(</span>
message
<span class="text-menu-text">); <br> })</span>
</span>
</p>
</div>
</div>
</template>
<script>
export default {
data(){
return {
date: new Date().toDateString(),
lines: 0
}
},
props: {
name: String,
email: String,
message: String,
},
mounted() {
this.updateLines();
window.addEventListener("resize", this.updateLines);
window.addEventListener("input", this.updateLines);
window.addEventListener("click", this.updateLines);
},
beforeDestroy() {
window.removeEventListener("resize", this.updateLines);
window.removeEventListener("click", this.updateLines);
window.addEventListener("input", this.updateLines);
},
methods: {
updateLines() {
const textContainer = this.$el.querySelector(".text-container");
const style = window.getComputedStyle(textContainer);
const fontSize = parseInt(style.fontSize);
const lineHeight = parseInt(style.lineHeight);
const maxHeight = textContainer.offsetHeight;
this.lines = Math.ceil(maxHeight / lineHeight);
}
}
}
</script>
<style>
.tag {
color: #C98BDF;
}
.tag-name{
color: #5565E8;
}
.arrow {
color: #F8F8F8;
}
.code-container {
display: flex;
align-items: flex-start;
}
.line-numbers {
text-align: right;
}
.text-container {
width: 100%;
padding-left: 0px;
word-wrap: break-word;
}
</style>

169
components/GistSnippet.vue Normal file
View File

@@ -0,0 +1,169 @@
<template>
<div class="gist mb-5" v-if="dataFetched">
<!-- head info -->
<div class="flex justify-between my-2">
<div class="flex">
<!-- avatar -->
<img :src="gist.owner.avatar_url" alt="" class="w-8 h-8 rounded-full mr-2">
<!-- username & gist date info -->
<div class="flex flex-col">
<a id="username" :href="'https://github.com/' + gist.owner.login" target="_blank" class="font-fira_bold text-purple-text text-xs pb-1 hover:cursor-pointer">
@{{ gist.owner.login }}
</a>
<p class="font-fira_retina text-xs text-menu-text">Created {{ monthsAgo }} months ago</p>
</div>
</div>
<!-- details and stars -->
<div class="flex text-menu-text font-fira_retina text-xs justify-self-end lg:mx-2">
<div class="flex lg:mx-2 hover:cursor-pointer hover:text-white">
<img src="/icons/gist/comments.svg" alt="" class="w-4 h-4 mr-2">
<span @click="showComment(gist.id)">details</span>
</div>
<div class="hidden lg:flex hover:cursor-pointer hover:text-white">
<img src="/icons/gist/star.svg" alt="" class="w-4 h-4 mx-2">
<span class="">stars</span>
</div>
</div>
</div>
<highlightjs class="snippet-container" :code="content"/>
<div :id="'comment' + gist.id" class="flex hidden justify-between text-menu-text font-fira_retina mt-4 pt-4 border-top">
<p id="comment" v-if="comment" class="w-5/6">{{ comment }}</p>
<p v-else class="w-5/6">No comments.</p>
<img src="/icons/close.svg" alt="" class="hover:cursor-pointer" @click="showComment(gist.id)">
</div>
</div>
</template>
<style>
.snippet-container {
background-color: #011221;
padding: 5px;
border-radius: 15px;
border: 1px solid #1E2D3D;
font-size: 12px;
overflow-y: scroll;
overflow-x: scroll;
max-height: 220px;
}
.snippet-container pre {
margin: 0;
overflow: hidden;
width: 100%;
max-height: 220px;
}
.snippet-container code {
white-space: pre-wrap;
max-height: 220px;
width: max-content;
overflow: hidden;
}
.snippet-container::-webkit-scrollbar {
display: none; /* Safari and Chrome */
}
pre code.hljs{
display:block;
/* overflow-x:auto; */
padding:1.5em
}
code.hljs{
padding:3px 5px
}
#comment {
font-size: 14px;
}
#username:hover {
color: #5e6ef2;
}
/* #comment {
} */
.hljs{color:#607B96;background:#011221}.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable.language_{color:#ff7b72}.hljs-title,.hljs-title.class_,.hljs-title.class_.inherited__,.hljs-title.function_{color:#d2a8ff}.hljs-attr,.hljs-attribute,.hljs-literal,.hljs-meta,.hljs-number,.hljs-operator,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-variable{color:#79c0ff}.hljs-meta .hljs-string,.hljs-regexp,.hljs-string{color:#a5d6ff}.hljs-built_in,.hljs-symbol{color:#ffa657}.hljs-code,.hljs-comment,.hljs-formula{color:#8b949e}.hljs-name,.hljs-quote,.hljs-selector-pseudo,.hljs-selector-tag{color:#7ee787}.hljs-subst{color:#c9d1d9}.hljs-section{color:#1f6feb;font-weight:700}.hljs-bullet{color:#f2cc60}.hljs-emphasis{color:#c9d1d9;font-style:italic}.hljs-strong{color:#c9d1d9;font-weight:700}.hljs-addition{color:#aff5b4;background-color:#033a16}.hljs-deletion{color:#ffdcd7;background-color:#67060c}
</style>
<script>
import hljsVuePlugin from "@highlightjs/vue-plugin";
import 'highlight.js/lib/common';
export default {
name: 'GistSnippet',
props: {
id: {
type: String,
required: true
}
},
data(){
return {
gist: null,
monthsAgo: null,
content: null,
language: null,
dataFetched: false,
comment: null
}
},
mounted(){
fetch(`https://api.github.com/gists/${this.id}`)
.then(response => response.json())
.then(data => this.setValues(data))
},
methods: {
async setValues(gist) {
this.gist = gist
this.monthsAgo = this.setMonths(gist.created_at)
this.content = this.setSnippet(gist)
this.language = Object.values(gist.files)[0].language
this.dataFetched = true
this.comment = await this.setComments(gist.comments_url)
},
setMonths(date) {
let now = new Date()
let gistDate = new Date(date)
let diff = now.getTime() - gistDate.getTime()
let days = Math.floor(diff / (1000 * 3600 * 24))
let months = Math.floor(days / 30)
return months
},
setSnippet(gist) {
let snippet = Object.values(gist.files)[0].content // Object.values(gist.files)[0].filename.content
return snippet
},
async setComments(comments_url){
let response = await fetch(comments_url)
let data = await response.json()
try{
let body = data[0].body
return body
} catch {
console.log(`no comments found on ${comments_url}`)
}
},
showComment(id) {
let comment = document.getElementById('comment' + id)
comment.classList.toggle('hidden')
}
},
components: {
highlightjs: hljsVuePlugin.component
}
}
</script>

View File

@@ -0,0 +1,64 @@
<template>
<a :href="url" class="github-corner" target="_blank" aria-label="View source on Github">
<svg width="80" height="80" viewBox="0 0 250 250" aria-hidden="true">
<path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path>
<path d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2"
fill="currentColor" style="transform-origin: 130px 106px;" class="octo-arm"></path>
<path d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z"
fill="currentColor" class="octo-body"></path>
</svg>
</a>
</template>
<script>
export default {
name: 'GithubCorner',
props: {
url: {
type: String,
default: ''
}
}
}
</script>
<style>
/* ----------------------------------------------
* GitHub Corners
* w: https://github.com/tholman/github-corners
* ---------------------------------------------- */
.github-corner {
fill: #071511;
color: #43D9AD;
position: absolute;
top: 0;
border: 0;
right: 0;
}
.github-corner:hover .octo-arm {
animation: octocat-wave 560ms ease-in-out;
}
@keyframes octocat-wave {
0%,
100% {
transform: rotate(0);
}
20%,
60% {
transform: rotate(-25deg);
}
40%,
80% {
transform: rotate(10deg);
}
}
@media (max-width: 500px) {
.github-corner {
display: none;
}
}
</style>

98
components/MobileMenu.vue Normal file
View File

@@ -0,0 +1,98 @@
<template>
<div id="mobile-menu" class="w-full z-10 lg:hidden">
<!-- header -->
<div id="mobile-header" class="w-full h-16 flex justify-between items-center">
<NuxtLink class="text-menu-text font-fira_retina flex h-full items-center mx-5" to="/" @click="goHome()">
{{ config.dev.logo_name }}
</NuxtLink>
<img src="/icons/burger.svg" v-if="!menuOpen" @click="toggleMobileMenu()" class="w-5 h-5 mx-5 my-auto"/>
<img src="/icons/burger-close.svg" v-else @click="toggleMobileMenu()" name="icon-park-outline:close" class="w-5 h-5 mx-5 my-auto"/>
</div>
<!-- mobile menu -->
<div id="menu" class="bg-mobile-menu-blue z-10 hidden">
<NuxtLink id="nav-link-mobile" to="/" :class="{ active: isActive('/') }" @click="toggleMobileMenu()">
_hello
</NuxtLink>
<NuxtLink id="nav-link-mobile" to="/about-me" :class="{ active: isActive('/about-me') }" @click="toggleMobileMenu()">
_about-me
</NuxtLink>
<NuxtLink id="nav-link-mobile" to="/projects" :class="{ active: isActive('/projects') }" @click="toggleMobileMenu()">
_projects
</NuxtLink>
<NuxtLink id="nav-link-mobile" to="/contact-me" :class="{ active: isActive('/contact-me') }" @click="toggleMobileMenu()">
_contact-me
</NuxtLink>
</div>
</div>
</template>
<script>
export default {
data(){
return {
menuOpen: false
}
},
setup() {
const config = useRuntimeConfig()
return {
config
}
},
methods: {
toggleMobileMenu(){
this.menuOpen ? this.menuOpen = false : this.menuOpen = true
const menu = document.getElementById('menu');
menu.classList.toggle('hidden')
const page = document.getElementsByTagName('main')[0];
// Hide / show section
if (page.style.display === 'none') {
page.style.display = 'flex';
} else {
page.style.display = 'none';
}
},
goHome() {
const menu = document.getElementById('menu');
if(!menu.classList.contains('hidden')){
menu.classList.toggle('hidden')
document.getElementsByTagName('main')[0].style.display = 'flex';
this.menuOpen ? this.menuOpen = false : this.menuOpen = true
}
}
},
computed: {
// Set active class to current page link
isActive() {
return route => this.$route.path === route;
},
}
}
</script>
<style>
#mobile-header {
border-bottom: 1px solid #1E2D3D;
}
#nav-link-mobile {
border-bottom: 1px solid #1E2D3D;
@apply text-menu-text font-fira_retina px-6 py-4 flex items-center;
}
#nav-link-mobile.active {
color: white
}
</style>

View File

@@ -0,0 +1,80 @@
<template>
<div id="project" :key="key" class="lg:mx-5">
<span class="flex text-sm my-3">
<h3 v-if="index == null" class="text-purplefy font-fira_bold mr-3">Project {{ key + 1 }}</h3>
<h3 v-else class="text-purplefy font-fira_bold mr-3">Project {{ index + 1 }}</h3>
<h4 class="font-fira_retina text-menu-text"> // {{ project.title }}</h4>
</span>
<div id="project-card" class="flex flex-col">
<div id="window">
<div class="absolute flex right-3 top-3">
<img v-for="tech in project.tech" :key="tech" :src="'/icons/techs/filled/' + tech + '.svg'" alt="" class="w-6 h-6 mx-1 hover:opacity-75">
</div>
<img id="showcase" :src="project.img" alt="" class="">
</div>
<div class="pb-8 pt-6 px-6 border-top">
<p class="text-menu-text font-fira_retina text-sm mb-5">
{{ project.description }}
</p>
<a id="view-button" :href="project.url" target="_blank" class="text-white font-fira_retina py-2 px-4 w-fit text-xs rounded-lg">
view-project
</a>
</div>
</div>
</div>
</template>
<script setup>
const { project, key, index } = defineProps(['project', 'key', 'index'])
</script>
<style scoped>
#project {
min-width: 400px;
margin-bottom: 5px;
}
#project-card {
border: 1px solid #1E2D3D;
background-color: #011221;
border-radius: 15px;
max-width: 400px;
}
#window {
max-height: 120px;
position: relative;
overflow: hidden;
}
#showcase {
border-top-right-radius: 15px;
border-top-left-radius: 15px;
}
@media (max-width: 768px) {
#project {
min-width: 100%;
}
}
@media (min-width: 768px) {
#project {
width: 100%;
min-width: 100%;
padding-inline: 5px;
}
}
@media (min-width: 1350px) {
#project {
width: 100%;
min-width: 100%;
padding-inline: 20px;
}
}
</style>

24
components/README.md Normal file
View File

@@ -0,0 +1,24 @@
# `components/` [Directory](https://nuxt.com/docs/getting-started/views#components)
Most components are reusable pieces of the user interface, like buttons and menus. In Nuxt, you can create these components in the `components/` directory, and they will be automatically available across your application without having to explicitly import them.
*app.vue*
````html
<template>
<div>
<h1>Welcome to the homepage</h1>
<AppAlert>
This is an auto-imported component.
</AppAlert>
</div>
</template>
````
*components/AppAlert.vue*
````html
<template>
<span>
<slot />
</span>
</template>
````

580
components/SnakeGame.vue Normal file
View File

@@ -0,0 +1,580 @@
<template>
<div id="console">
<!-- bolts -->
<img id="corner" src="/icons/console/bolt-up-left.svg" alt="" class="absolute top-2 left-2 opacity-70">
<img id="corner" src="/icons/console/bolt-up-right.svg" alt="" class="absolute top-2 right-2 opacity-70">
<img id="corner" src="/icons/console/bolt-down-left.svg" alt="" class="absolute bottom-2 left-2 opacity-70">
<img id="corner" src="/icons/console/bolt-down-right.svg" alt="" class="absolute bottom-2 right-2 opacity-70">
<!-- Game Screen -->
<div id="game-screen" ref="gameScreen"></div>
<button id="start-button" class="font-fira_retina" @click="startGame">start-game</button>
<!-- Game Over -->
<div id="game-over" class="hidden">
<span class="font-fira_retina text-greenfy bg-bluefy-dark h-12 flex items-center justify-center">GAME OVER!</span>
<button class="font-fira_retina text-menu-text text-sm flex items-center justify-center w-full py-6 hover:text-white" @click="startAgain">start-again</button>
</div>
<div id="congrats" class="hidden">
<span class="font-fira_retina text-greenfy bg-bluefy-dark h-12 flex items-center justify-center">WELL DONE!</span>
<button class="font-fira_retina text-menu-text text-sm flex items-center justify-center w-full py-6 hover:text-white" @click="startAgain">play-again</button>
</div>
<div id="console-menu" class="h-full flex flex-col items-end justify-between">
<div>
<div id="instructions" class="font-fira_retina text-sm text-white">
<p>// use your keyboard</p>
<p>// arrows to play</p>
<div id="buttons" class="w-full flex flex-col items-center gap-1 pt-5">
<button id="console-button" class="button-up" @click="move('up')">
<img src="/icons/console/arrow-button.svg" alt="">
</button>
<div class="grid grid-cols-3 gap-1">
<button id="console-button" class="button-left" @click="move('left')">
<img src="/icons/console/arrow-button.svg" alt="" class="-rotate-90">
</button>
<button id="console-button" class="button-down" @click="move('down')">
<img src="/icons/console/arrow-button.svg" alt="" class="rotate-180">
</button>
<button id="console-button" class="button-right" @click="move('right')">
<img src="/icons/console/arrow-button.svg" alt="" class="rotate-90">
</button>
</div>
</div>
</div>
<!-- score board -->
<div id="score-board" class="w-full flex flex-col pl-5">
<p class="font-fira_retina text-white pt-5">// food left</p>
<div id="score" class="grid grid-cols-5 gap-5 justify-items-center pt-5 w-fit">
<div class="food"></div>
<div class="food"></div>
<div class="food"></div>
<div class="food"></div>
<div class="food"></div>
<div class="food"></div>
<div class="food"></div>
<div class="food"></div>
<div class="food"></div>
<div class="food"></div>
</div>
</div>
</div>
<!-- skip -->
<NuxtLink id="skip-btn" to="/about-me" class="font-fira_retina flex hover:bg-white/20">
skip
</NuxtLink>
</div>
</div>
</template>
<script>
export default {
data() {
return {
score: 0,
gameInterval: null,
gameStarted: false,
gameOver: false,
food: { x: 10, y: 5 },
snake: [
{ x: 10, y: 12 },
{ x: 10, y: 13 },
{ x: 10, y: 14 },
{ x: 10, y: 15 },
{ x: 10, y: 16 },
{ x: 10, y: 17 },
{ x: 10, y: 18 },
{ x: 11, y: 18 },
{ x: 12, y: 18 },
{ x: 13, y: 18 },
{ x: 14, y: 18 },
{ x: 15, y: 18 },
{ x: 15, y: 19 },
{ x: 15, y: 20 },
{ x: 15, y: 21 },
{ x: 15, y: 22 },
{ x: 15, y: 23 },
{ x: 15, y: 24 },
],
direction: "up",
};
},
methods: {
startGame() {
// hide start button
document.getElementById("start-button").style.display = "none";
// start game
this.gameStarted = true;
this.gameInterval = setInterval(this.moveSnake, 50);
},
startAgain() {
// Mostrar botón de start-game
document.getElementById("start-button").style.display = "block";
// Ocultar game over
document.getElementById("game-over").style.display = "none";
document.getElementById("congrats").style.display = "none";
// reiniciar datos del juego
this.gameStarted = false;
this.gameOver = false;
this.restartScore();
this.food = {
x: 10,
y: 5
};
this.snake = [
{ x: 10, y: 12 },
{ x: 10, y: 13 },
{ x: 10, y: 14 },
{ x: 10, y: 15 },
{ x: 10, y: 16 },
{ x: 10, y: 17 },
{ x: 10, y: 18 },
{ x: 11, y: 18 },
{ x: 12, y: 18 },
{ x: 13, y: 18 },
{ x: 14, y: 18 },
{ x: 15, y: 18 },
{ x: 15, y: 19 },
{ x: 15, y: 20 },
{ x: 15, y: 21 },
{ x: 15, y: 22 },
{ x: 15, y: 23 },
{ x: 15, y: 24 },
],
this.direction = "up";
// limpiar intervalo de juego
clearInterval(this.gameInterval);
this.render();
},
// ... resto del código
moveSnake() {
let newX = this.snake[0].x;
let newY = this.snake[0].y;
switch (this.direction) {
case "up":
newY--;
break;
case "down":
newY++;
break;
case "left":
newX--;
break;
case "right":
newX++;
break;
}
// check if snake dont leave from game window
// and check if snake dont eat itself
if (
newX >= 0 &&
newX < 24 &&
newY >= 0 &&
newY < 40 &&
!this.snake.find(
snakeCell => snakeCell.x === newX && snakeCell.y === newY
)
) {
/* snake move next cell */
this.snake.unshift({ x: newX, y: newY });
/* check snake next cell is food */
if (newX === this.food.x && newY === this.food.y) {
// add score
this.score++;
// add food to score board
const scoreFoods = document.getElementsByClassName("food");
scoreFoods[this.score - 1].style.opacity = 1;
// check if score is 10 (max score)
if(this.score === 10) {
// move snake head to food (fix snake head position at end)
this.snake.unshift({ x: newX, y: newY }); // move head
this.food = { x: null, y: null } // remove food
clearInterval(this.gameInterval); // stop game
document.getElementById('congrats').style.display = 'block' // show congrats
this.gameOver = true; // game over
this.gameStarted = false; // stop game
} else {
// create new food
this.food = {
x: Math.floor(Math.random() * 24),
y: Math.floor(Math.random() * 40)
};
}
} else {
// if next cell is not food: snake pop last cell
this.snake.pop();
}
} else {
// GAME OVER: if snake leave from game window or eat itself
clearInterval(this.gameInterval);
document.getElementById('game-over').style.display = 'block'
this.gameStarted = false;
this.gameOver = true;
}
this.render();
},
render() {
let gameScreen = this.$refs.gameScreen;
gameScreen.innerHTML = "";
// responsive cell screen
// (this.$refs.gameScreen.offsetWidth / 20) + "px";
/* const widthCells = window.innerWidth > 1536 ? 24 : 20; */
const cellSize = window.innerWidth > 1536 ? "10px" : "8px";
// eje y
for (let i = 0; i < 40; i++) {
// exe x
for (let j = 0; j < 24; j++) {
/* cell style */
let cell = document.createElement("div");
cell.classList.add("cell");
cell.style.width = cellSize
cell.style.height = cellSize
cell.style.display = "flex";
cell.style.flexShrink = 0;
cell.classList.add("black");
/* Food cell style */
if (j === this.food.x && i === this.food.y) {
cell.style.backgroundColor = "#43D9AD";
cell.style.borderRadius = "50%";
cell.style.boxShadow = "0 0 10px #43D9AD";
}
/* Estilo de la serpiente a medida que va crediendo */
let snakeCell = this.snake.find(
snakeCell => snakeCell.x === j && snakeCell.y === i
);
if (snakeCell) {
cell.style.backgroundColor = "#43D9AD";
cell.style.opacity = 1 - (this.snake.indexOf(snakeCell) / this.snake.length);
cell.classList.add("green");
}
/* Estilo de la cabeza */
if (snakeCell && this.snake.indexOf(snakeCell) === 0) {
let headRadius = "5px";
if (this.direction === "up") {
cell.style.borderTopLeftRadius = headRadius;
cell.style.borderTopRightRadius = headRadius;
}
if (this.direction === "down") {
cell.style.borderBottomLeftRadius = headRadius;
cell.style.borderBottomRightRadius = headRadius;
}
if (this.direction === "left") {
cell.style.borderTopLeftRadius = headRadius;
cell.style.borderBottomLeftRadius = headRadius;
}
if (this.direction === "right") {
cell.style.borderTopRightRadius = headRadius;
cell.style.borderBottomRightRadius = headRadius;
}
}
gameScreen.appendChild(cell);
}
}
},
restartScore(){
this.score = 0;
const scoreFoods = document.getElementsByClassName("food");
for (let i = 0; i < scoreFoods.length; i++) {
scoreFoods[i].style.opacity = 0.3;
}
},
move(direction){
switch (direction) {
case "up":
if (this.direction !== "down") {
this.direction = "up";
}
break;
case "down":
if (this.direction !== "up") {
this.direction = "down";
}
break;
case "left":
if (this.direction !== "right") {
this.direction = "left";
}
break;
case "right":
if (this.direction !== "left") {
this.direction = "right";
}
break;
}
}
},
mounted() {
document.addEventListener("keydown", event => {
if (this.gameStarted) {
switch (event.keyCode) {
case 37:
if (this.direction !== "right") {
this.direction = "left";
}
break;
case 38:
if (this.direction !== "down") {
this.direction = "up";
}
break;
case 39:
if (this.direction !== "left") {
this.direction = "right";
}
break;
case 40:
if (this.direction !== "up") {
this.direction = "down";
}
break;
}
} else {
switch (event.keyCode) {
case 32:
if(this.gameOver){
this.startAgain();
}else {
this.startGame();
}
break;
}
}
});
/* window.innerWidth < 1536 ? cellSize = 8 : cellSize = 10; */
/* this.food = {
x: Math.floor(Math.random() * 24),
y: Math.floor(Math.random() * 40)
}; */
window.onresize = () => {
this.render();
};
this.render();
}
};
</script>
<style>
#console {
width: 530px;
height: 475px;
border: 1px solid black;
display: flex;
align-items: center;
justify-content: space-between;
background: linear-gradient(to bottom, rgba(35, 123, 109, 1), rgba(67, 217, 173, 0.13));
border-radius: 10px;
padding: 30px;
position: relative;
}
#game-screen {
width: 240px;
height: 400px;
border-radius: 10px;
background-color: rgba(1, 22, 39, 0.84);
display: flex;
flex-wrap: wrap;
box-shadow: inset 0 0 10px #00000071;
}
#start-button {
padding-inline: 16px;
padding-block: 8px;
border-radius: 10px;
border: 1px solid black;
background-color: #FEA55F;
color: black;
cursor: pointer;
position: absolute;
bottom: 20%;
left: 17%;
font-size: 0.875rem; /* 14px */
line-height: 1.25rem; /* 20px */
}
#start-button:hover {
background-color: rgb(255, 178, 119);
}
#console-menu{
height: 400px;
}
#console-button {
background-color: #010C15;
border-radius: 10px;
display: flex;
justify-content: center;
align-items: center;
width: 50px;
height: 30px;
}
#console-button:hover {
background-color: #010c15d8;
box-shadow: #43D9AD 0 0 10px;
}
#instructions {
background-color: rgba(1, 20, 35, 0.19);
border-radius: 7px;
padding: 10px;
}
.food {
background-color: #43D9AD;
border-radius: 50%;
box-shadow: 0 0 10px #43D9AD;
width: 8px;
height: 8px;
opacity: 0.3;
}
#game-over, #congrats {
position: absolute;
bottom: 12%;
color: #43D9AD;
width: 240px;
}
#game-over, #congrats > span {
font-size: 1.5rem; /* 24px */
line-height: 2rem; /* 32px */
}
#corner {
width: 24px;
height: 24px;
}
#skip-btn{
font-size: 14px;
color: white;
padding-inline: 16px;
padding-block: 8px;
border: 2px solid white;
border-radius: 0.5rem; /* 8px */
}
@media (min-width: 1024px) and (max-width: 1536px) {
#game-screen {
width: 192px;
height: 320px;
}
#console {
width: 420px;
height: 370px;
padding: 24px;
}
#start-button {
padding-inline: 12px;
padding-block: 6px;
border-radius: 8px;
bottom: 20%;
left: 17%;
font-size: 0.75rem; /* 14px */
line-height: 1rem; /* 20px */
}
#console-menu{
height: 320px;
}
#instructions {
font-size: 12px;
}
#console-button {
width: 40px;
height: 25px;
border-radius: 6px;
}
#score-board {
font-size: 12px;
}
.food {
width: 6px;
height: 6px;
}
#game-over, #congrats {
position: absolute;
bottom: 10%;
color: #43D9AD;
width: 192px;
}
#game-over, #congrats > span {
font-size: 1.125rem; /* 18px */
line-height: 1.75rem; /* 28px */
}
#corner {
width: 20px;
height: 20px;
}
#skip-btn{
font-size: 12px;
padding-inline: 12px;
padding-block: 6px;
border: 2px solid white;
border-radius: 0.5rem; /* 8px */
}
}
</style>