initial
This commit is contained in:
BIN
assets/lain.png
Normal file
BIN
assets/lain.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.1 MiB |
97
css/content.css
Normal file
97
css/content.css
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
/* content.css - Layout, glassmorphism, and content styles */
|
||||||
|
.glass {
|
||||||
|
background: rgba(20, 20, 20, 0.125);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
-webkit-backdrop-filter: blur(8px);
|
||||||
|
border-radius: 16px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||||
|
box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.37),
|
||||||
|
inset 0 1px 0 rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-container {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
max-width: 1200px;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: rgba(0, 0, 0, 0.125);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@keyframes fadeInUp {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(30px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
color: white;
|
||||||
|
background: rgba(0, 0, 0, 0.10);
|
||||||
|
backdrop-filter: blur(32px) saturate(180%) brightness(1.1);
|
||||||
|
-webkit-backdrop-filter: blur(32px) saturate(180%) brightness(1.1);
|
||||||
|
border-radius: 24px;
|
||||||
|
box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.25), inset 0 1px 0 rgba(255,255,255,0.15);
|
||||||
|
padding: 32px 24px;
|
||||||
|
transition: background 0.3s, box-shadow 0.3s;
|
||||||
|
min-width: 26rem;
|
||||||
|
max-width: 26rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-links {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-link {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-tooltip {
|
||||||
|
position: absolute;
|
||||||
|
background: rgba(40, 40, 40, 0.95);
|
||||||
|
color: #fff;
|
||||||
|
padding: 6px 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
font-weight: 500;
|
||||||
|
box-shadow: 0 2px 12px rgba(0,0,0,0.18);
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: opacity 0.3s, transform 0.3s;
|
||||||
|
z-index: 10;
|
||||||
|
white-space: nowrap;
|
||||||
|
width: max-content;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Stylish tooltip for email copy */
|
||||||
|
.email-tooltip {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: -32px;
|
||||||
|
transform: translateX(-50%) scale(1);
|
||||||
|
background: rgba(40, 40, 40, 0.95);
|
||||||
|
color: #fff;
|
||||||
|
padding: 6px 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
font-weight: 500;
|
||||||
|
box-shadow: 0 2px 12px rgba(0,0,0,0.18);
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: opacity 0.3s, transform 0.3s;
|
||||||
|
z-index: 10;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
58
css/matrix.css
Normal file
58
css/matrix.css
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
/* matrix.css - Matrix animation styles */
|
||||||
|
.matrix-canvas {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
z-index: -1;
|
||||||
|
pointer-events: none;
|
||||||
|
overflow: hidden;
|
||||||
|
background: rgba(0, 0, 0, 0.85);
|
||||||
|
background-image: linear-gradient(135deg, rgba(0,0,0,0.9) 0%, rgba(20,20,40,0.7) 100%);
|
||||||
|
filter: brightness(0.55);
|
||||||
|
}
|
||||||
|
|
||||||
|
.matrix-dot {
|
||||||
|
position: absolute;
|
||||||
|
background: rgba(255, 255, 255, 0.8);
|
||||||
|
border-radius: 50%;
|
||||||
|
box-shadow: 0 0 6px rgba(255, 255, 255, 0.4), 0 0 12px rgba(255, 255, 255, 0.2);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-line {
|
||||||
|
position: absolute;
|
||||||
|
height: 2px;
|
||||||
|
background: linear-gradient(90deg,
|
||||||
|
rgba(100, 149, 237, 0) 0%,
|
||||||
|
rgba(138, 43, 226, 0.8) 50%,
|
||||||
|
rgba(100, 149, 237, 0) 100%);
|
||||||
|
transform-origin: left center;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.5s ease-in-out;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: -1;
|
||||||
|
box-shadow: 0 0 4px rgba(138, 43, 226, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-line.active {
|
||||||
|
opacity: 1;
|
||||||
|
animation: connectionPulse 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes connectionPulse {
|
||||||
|
0% {
|
||||||
|
filter: brightness(1);
|
||||||
|
box-shadow: 0 0 4px rgba(138, 43, 226, 0.4);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
filter: brightness(1.5);
|
||||||
|
box-shadow: 0 0 8px rgba(138, 43, 226, 0.6), 0 0 16px rgba(100, 149, 237, 0.3);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
filter: brightness(1);
|
||||||
|
box-shadow: 0 0 4px rgba(138, 43, 226, 0.4);
|
||||||
|
}
|
||||||
|
}
|
153
css/sections/hero.css
Normal file
153
css/sections/hero.css
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
.hero-details {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 32px;
|
||||||
|
align-items: flex-start;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.hero-main-column {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 24px;
|
||||||
|
flex: 1 1 0;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-card {
|
||||||
|
padding: 48px;
|
||||||
|
width: 100%;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 200px 1fr;
|
||||||
|
gap: 40px;
|
||||||
|
align-items: center;
|
||||||
|
animation: fadeInUp 0.8s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
background: linear-gradient(135deg, #fff 0%, rgba(255, 255, 255, 0.8) 100%);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-links {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-link {
|
||||||
|
padding: 12px 20px;
|
||||||
|
background: rgba(10, 18, 40, 0.85);
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 10px;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 15px;
|
||||||
|
position: relative;
|
||||||
|
overflow: visible;
|
||||||
|
box-shadow: 0 2px 16px 0 rgba(31, 38, 135, 0.12);
|
||||||
|
transition: background 0.18s, transform 0.18s;
|
||||||
|
}
|
||||||
|
/* Modern underline effect under text */
|
||||||
|
.social-link span {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
.social-link span::after {
|
||||||
|
content: '';
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 2px;
|
||||||
|
background: linear-gradient(90deg, #fff 0%, #667eea 100%);
|
||||||
|
border-radius: 1px;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
bottom: -4px;
|
||||||
|
opacity: 0.7;
|
||||||
|
transition: height 0.18s, opacity 0.18s;
|
||||||
|
}
|
||||||
|
.social-link:hover {
|
||||||
|
background: rgba(20, 30, 60, 0.95);
|
||||||
|
transform: translateY(-1px) scale(1.03);
|
||||||
|
}
|
||||||
|
.social-link:hover span::after {
|
||||||
|
height: 3px;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.hero-details {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
.hero-card {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.hero-card {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 32px;
|
||||||
|
padding: 32px 24px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
width: 140px;
|
||||||
|
height: 140px;
|
||||||
|
margin: 0 auto;
|
||||||
|
font-size: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-links {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.hero-card {
|
||||||
|
padding: 24px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
width: 120px;
|
||||||
|
height: 120px;
|
||||||
|
font-size: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-links {
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-link {
|
||||||
|
padding: 10px 16px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
}
|
68
css/sections/homelab.css
Normal file
68
css/sections/homelab.css
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
.homelab-section {
|
||||||
|
margin-top: 18px;
|
||||||
|
padding: 32px 24px;
|
||||||
|
background: rgba(20, 20, 30, 0.10);
|
||||||
|
backdrop-filter: blur(24px) saturate(140%) brightness(1.08);
|
||||||
|
-webkit-backdrop-filter: blur(24px) saturate(140%) brightness(1.08);
|
||||||
|
border-radius: 24px;
|
||||||
|
box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.18), inset 0 1px 0 rgba(255,255,255,0.10);
|
||||||
|
min-width: 26rem;
|
||||||
|
max-width: 26rem;
|
||||||
|
align-self: stretch;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 18px;
|
||||||
|
}
|
||||||
|
.homelab-title {
|
||||||
|
font-size: 1.4rem;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
letter-spacing: 0.03em;
|
||||||
|
background: linear-gradient(270deg, #764ba2, #2b2e6c, #764ba2, #2b2e6c);
|
||||||
|
background-size: 400% 400%;
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
animation: homelab-gradient-move 12s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes homelab-gradient-move {
|
||||||
|
0% {
|
||||||
|
background-position: 0% 50%;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
background-position: 100% 50%;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-position: 0% 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.homelab-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.homelab-btn {
|
||||||
|
padding: 12px 28px;
|
||||||
|
background: rgba(10, 18, 40, 0.85);
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 15px;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
text-decoration: none;
|
||||||
|
box-shadow: 0 4px 24px 0 rgba(31, 38, 135, 0.18), 0 1.5px 0 rgba(255,255,255,0.08);
|
||||||
|
transition: transform 0.18s cubic-bezier(0.4, 0, 0.2, 1), background 0.18s;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.homelab-btn:hover {
|
||||||
|
background: rgba(20, 30, 60, 0.95);
|
||||||
|
transform: translateY(-2px) scale(1.04);
|
||||||
|
box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.28);
|
||||||
|
}
|
||||||
|
.homelab-btn:active {
|
||||||
|
transform: translateY(0px) scale(1.0);
|
||||||
|
}
|
78
css/sections/profile.css
Normal file
78
css/sections/profile.css
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
/* Profile Info Section Styles */
|
||||||
|
.profile-info {
|
||||||
|
margin-top: 1.2rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.7rem;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.profile-info-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.6rem;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
background: none;
|
||||||
|
border-radius: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-shadow: none;
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-info-item i {
|
||||||
|
font-size: 1.3rem;
|
||||||
|
color: #4fc3f7;
|
||||||
|
margin-right: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-info-label {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #222;
|
||||||
|
margin-right: 0.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-info-value {
|
||||||
|
color: rgba(255, 255, 255, 0.45);
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-info-value span {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
/* profile.css - Styles for avatar and avatar-container */
|
||||||
|
|
||||||
|
.profile-container {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
width: 160px;
|
||||||
|
height: 160px;
|
||||||
|
border-radius: 24px;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: white;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 2px solid rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: -50%;
|
||||||
|
left: -50%;
|
||||||
|
width: 200%;
|
||||||
|
height: 200%;
|
||||||
|
background: linear-gradient(45deg, transparent 30%, rgba(255, 255, 255, 0.1) 50%, transparent 70%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 24px;
|
||||||
|
}
|
74
css/sections/projects.css
Normal file
74
css/sections/projects.css
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
.projects-section {
|
||||||
|
flex: 0 0 340px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 18px;
|
||||||
|
padding: 32px 24px;
|
||||||
|
background: rgba(20, 20, 30, 0.10);
|
||||||
|
backdrop-filter: blur(24px) saturate(140%) brightness(1.08);
|
||||||
|
-webkit-backdrop-filter: blur(24px) saturate(140%) brightness(1.08);
|
||||||
|
border-radius: 24px;
|
||||||
|
box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.18), inset 0 1px 0 rgba(255,255,255,0.10);
|
||||||
|
min-width: 280px;
|
||||||
|
max-width: 340px;
|
||||||
|
margin-left: 20px;
|
||||||
|
align-self: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.projects-title {
|
||||||
|
font-size: 1.6rem;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
color: #fff;
|
||||||
|
letter-spacing: 0.03em;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #fff 100%);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project {
|
||||||
|
padding: 18px 16px;
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0 2px 12px 0 rgba(31, 38, 135, 0.10);
|
||||||
|
margin-bottom: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
transition: transform 0.18s, box-shadow 0.18s;
|
||||||
|
}
|
||||||
|
.project:nth-child(odd) {
|
||||||
|
background: linear-gradient(135deg, rgba(20, 30, 60, 0.75) 0%, rgba(40, 50, 120, 0.75) 100%);
|
||||||
|
}
|
||||||
|
.project:nth-child(even) {
|
||||||
|
background: linear-gradient(135deg, rgba(10, 10, 10, 0.82) 0%, rgba(30, 30, 30, 0.82) 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-title {
|
||||||
|
font-size: 1.15rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #fff;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
letter-spacing: 0.01em;
|
||||||
|
background: linear-gradient(90deg, #fff 0%, #667eea 100%);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-desc {
|
||||||
|
font-size: 0.98rem;
|
||||||
|
color: rgba(255,255,255,0.85);
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.projects-section {
|
||||||
|
flex: unset;
|
||||||
|
max-width: 100%;
|
||||||
|
margin-left: 0;
|
||||||
|
margin-top: 24px;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
}
|
17
css/style.css
Normal file
17
css/style.css
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
/* style.css - Glassmorphism and layout styles */
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Lexend:wght@100..900&display=swap');
|
||||||
|
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
/* font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; */
|
||||||
|
font-family: "Lexend", sans-serif;
|
||||||
|
background: #000000;
|
||||||
|
min-height: 100vh;
|
||||||
|
overflow-x: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
108
index.html
Normal file
108
index.html
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>ivanch - Portfolio</title>
|
||||||
|
<link rel="stylesheet" href="css/style.css">
|
||||||
|
<link rel="stylesheet" href="css/button.css">
|
||||||
|
<link rel="stylesheet" href="css/matrix.css">
|
||||||
|
<link rel="stylesheet" href="css/content.css">
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="css/sections/profile.css">
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="css/sections/hero.css">
|
||||||
|
<link rel="stylesheet" href="css/sections/projects.css">
|
||||||
|
<link rel="stylesheet" href="css/sections/homelab.css">
|
||||||
|
<!-- Remix Icon CDN for social icons -->
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/remixicon@3.5.0/fonts/remixicon.css" rel="stylesheet">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- Matrix-like background animation -->
|
||||||
|
<div class="matrix-canvas" id="matrixCanvas"></div>
|
||||||
|
|
||||||
|
<!-- Main hero section -->
|
||||||
|
<main class="main-container">
|
||||||
|
<div class="hero-card glass">
|
||||||
|
<div class="profile-container">
|
||||||
|
<div class="avatar">
|
||||||
|
<img src="/assets/lain.png" alt="Avatar" />
|
||||||
|
</div>
|
||||||
|
<div class="profile-info">
|
||||||
|
<div class="profile-info-item">
|
||||||
|
<i class="ri-graduation-cap-fill"></i>
|
||||||
|
<span class="profile-info-value">
|
||||||
|
<span>UTFPR</span>
|
||||||
|
<span>Informations Systems</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="profile-info-item">
|
||||||
|
<i class="ri-building-2-fill"></i>
|
||||||
|
<span class="profile-info-value">
|
||||||
|
<span>EposNow</span>
|
||||||
|
<span>Software Engineer</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="profile-info-item">
|
||||||
|
<i class="ri-book-2-fill"></i>
|
||||||
|
<span class="profile-info-value">5+ years</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="hero-details">
|
||||||
|
<div class="hero-main-column">
|
||||||
|
<div class="content">
|
||||||
|
<h2 class="name">ivanch</h2>
|
||||||
|
<p class="description">Developer and Self Hoster</p>
|
||||||
|
<div class="social-links">
|
||||||
|
<a href="https://www.linkedin.com/in/joseivanch/" class="social-link" target="_blank">
|
||||||
|
<i class="ri-linkedin-fill"></i> <span>LinkedIn</span>
|
||||||
|
</a>
|
||||||
|
<a href="#" class="social-link" id="email-link">
|
||||||
|
<i class="ri-mail-fill"></i> <span>Email</span>
|
||||||
|
</a>
|
||||||
|
<span class="email-tooltip" id="email-tooltip">Copied!</span>
|
||||||
|
<a href="https://github.com/ivanch" class="social-link" target="_blank">
|
||||||
|
<i class="ri-github-fill"></i> <span>GitHub</span>
|
||||||
|
</a>
|
||||||
|
<a href="https://git.ivanch.me/ivanch" class="social-link" target="_blank">
|
||||||
|
<i class="ri-git-repository-fill"></i> <span>Gitea</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="homelab-section">
|
||||||
|
<h3 class="homelab-title">Homelab</h3>
|
||||||
|
<div class="homelab-buttons">
|
||||||
|
<a href="#" class="homelab-btn">Diagram</a>
|
||||||
|
<a href="#" class="homelab-btn">Main Applications</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="projects-section">
|
||||||
|
<h3 class="projects-title">Projects</h3>
|
||||||
|
<div class="project">
|
||||||
|
<div class="project-title">OpenCand</div>
|
||||||
|
<div class="project-desc">Brazilian Candidate Viewer.</div>
|
||||||
|
</div>
|
||||||
|
<div class="project">
|
||||||
|
<div class="project-title">Vision Start</div>
|
||||||
|
<div class="project-desc">Chrome Startpage.</div>
|
||||||
|
</div>
|
||||||
|
<div class="project">
|
||||||
|
<div class="project-title">Kasbot</div>
|
||||||
|
<div class="project-desc">Discord DJ bot.</div>
|
||||||
|
</div>
|
||||||
|
<div class="project">
|
||||||
|
<div class="project-title">Candlebright</div>
|
||||||
|
<div class="project-desc">C++ project game.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<script src="js/matrix.js"></script>
|
||||||
|
<script src="js/social.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
314
js/matrix.js
Normal file
314
js/matrix.js
Normal file
@@ -0,0 +1,314 @@
|
|||||||
|
class MatrixBackground {
|
||||||
|
constructor() {
|
||||||
|
this.canvas = document.getElementById('matrixCanvas');
|
||||||
|
this.dots = [];
|
||||||
|
this.connections = [];
|
||||||
|
this.config = {
|
||||||
|
dotCount: 50,
|
||||||
|
dotSpeed: 0.2, // pixels per frame
|
||||||
|
connectionDuration: { min: 2000, max: 6000 }, // milliseconds
|
||||||
|
connectionChance: 0.0002, // chance per frame per dot pair
|
||||||
|
maxConnections: 8, // maximum simultaneous connections
|
||||||
|
dotSize: 3,
|
||||||
|
colors: {
|
||||||
|
dotDefault: 'rgba(255, 255, 255, 0.8)',
|
||||||
|
dotDefaultGlow: 'rgba(255, 255, 255, 0.4)',
|
||||||
|
dotConnected: 'rgba(100, 149, 237, 0.8)',
|
||||||
|
dotConnectedGlow: 'rgba(100, 149, 237, 0.6)',
|
||||||
|
connection: {
|
||||||
|
start: 'rgba(100, 149, 237, 0)',
|
||||||
|
middle: 'rgba(138, 43, 226, 0.8)',
|
||||||
|
end: 'rgba(100, 149, 237, 0)'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.createDots();
|
||||||
|
this.animate();
|
||||||
|
this.handleResize();
|
||||||
|
|
||||||
|
// Handle window resize
|
||||||
|
window.addEventListener('resize', () => this.handleResize());
|
||||||
|
}
|
||||||
|
|
||||||
|
handleResize() {
|
||||||
|
// Recreate dots with new screen dimensions
|
||||||
|
// Clear previous DOM elements to avoid orphan/sticky dots
|
||||||
|
this.clearCanvas();
|
||||||
|
this.createDots();
|
||||||
|
}
|
||||||
|
|
||||||
|
clearCanvas() {
|
||||||
|
// Remove all dot and connection elements from the canvas
|
||||||
|
try {
|
||||||
|
while (this.canvas.firstChild) {
|
||||||
|
this.canvas.removeChild(this.canvas.firstChild);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
// Reset arrays
|
||||||
|
this.dots = [];
|
||||||
|
this.connections = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
createDots() {
|
||||||
|
this.dots = [];
|
||||||
|
for (let i = 0; i < this.config.dotCount; i++) {
|
||||||
|
this.createDot();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createDot() {
|
||||||
|
const dot = document.createElement('div');
|
||||||
|
dot.className = 'matrix-dot';
|
||||||
|
|
||||||
|
// Random starting position within screen bounds
|
||||||
|
const startX = Math.random() * (window.innerWidth - 20) + 10;
|
||||||
|
const startY = Math.random() * (window.innerHeight - 20) + 10;
|
||||||
|
|
||||||
|
// Random movement direction (flying in all directions)
|
||||||
|
let velocityX = 0, velocityY = 0;
|
||||||
|
let attempts = 0;
|
||||||
|
while ((Math.abs(velocityX) < 0.15 || Math.abs(velocityY) < 0.15) && attempts < 10) {
|
||||||
|
const angle = Math.random() * Math.PI * 2;
|
||||||
|
const speed = this.config.dotSpeed * (0.8 + Math.random() * 0.4); // Ensure minimum speed
|
||||||
|
velocityX = Math.cos(angle) * speed;
|
||||||
|
velocityY = Math.sin(angle) * speed;
|
||||||
|
attempts++;
|
||||||
|
}
|
||||||
|
// Final fallback if still too low
|
||||||
|
if (Math.abs(velocityX) < 0.15) velocityX = velocityX < 0 ? -0.15 : 0.15;
|
||||||
|
if (Math.abs(velocityY) < 0.15) velocityY = velocityY < 0 ? -0.15 : 0.15;
|
||||||
|
const dotData = {
|
||||||
|
element: dot,
|
||||||
|
x: startX,
|
||||||
|
y: startY,
|
||||||
|
vx: velocityX,
|
||||||
|
vy: velocityY,
|
||||||
|
opacity: Math.random() * 0.3 + 0.7,
|
||||||
|
size: this.config.dotSize + Math.random() * 2,
|
||||||
|
isConnected: false,
|
||||||
|
connectionCount: 0,
|
||||||
|
stuckFrames: 0 // Track how long dot is stuck
|
||||||
|
};
|
||||||
|
|
||||||
|
// Set initial styles (white by default)
|
||||||
|
dot.style.left = startX + 'px';
|
||||||
|
dot.style.top = startY + 'px';
|
||||||
|
dot.style.width = dotData.size + 'px';
|
||||||
|
dot.style.height = dotData.size + 'px';
|
||||||
|
dot.style.opacity = dotData.opacity;
|
||||||
|
dot.style.background = this.config.colors.dotDefault;
|
||||||
|
dot.style.boxShadow = `0 0 6px ${this.config.colors.dotDefaultGlow}, 0 0 12px rgba(255, 255, 255, 0.2)`;
|
||||||
|
|
||||||
|
this.canvas.appendChild(dot);
|
||||||
|
this.dots.push(dotData);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateDots() {
|
||||||
|
this.dots.forEach((dot, index) => {
|
||||||
|
// Update position
|
||||||
|
dot.x += dot.vx;
|
||||||
|
dot.y += dot.vy;
|
||||||
|
|
||||||
|
// Track if dot is stuck (velocity very low for several frames)
|
||||||
|
if (Math.abs(dot.vx) < 0.03 && Math.abs(dot.vy) < 0.03) {
|
||||||
|
dot.stuckFrames = (dot.stuckFrames || 0) + 1;
|
||||||
|
} else {
|
||||||
|
dot.stuckFrames = 0;
|
||||||
|
}
|
||||||
|
// If stuck for more than 30 frames, give a strong nudge
|
||||||
|
if (dot.stuckFrames > 30) {
|
||||||
|
const angle = Math.random() * Math.PI * 2;
|
||||||
|
const speed = this.config.dotSpeed * (0.8 + Math.random() * 0.4);
|
||||||
|
dot.vx = Math.cos(angle) * speed;
|
||||||
|
dot.vy = Math.sin(angle) * speed;
|
||||||
|
dot.stuckFrames = 0;
|
||||||
|
} else {
|
||||||
|
// Prevent dots from getting stuck - add small random nudge if velocity is too low
|
||||||
|
if (Math.abs(dot.vx) < 0.05) {
|
||||||
|
dot.vx += (Math.random() - 0.5) * 0.1;
|
||||||
|
}
|
||||||
|
if (Math.abs(dot.vy) < 0.05) {
|
||||||
|
dot.vy += (Math.random() - 0.5) * 0.1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bounce off screen edges (consider dot size so centers stay inside)
|
||||||
|
const maxX = window.innerWidth - dot.size;
|
||||||
|
const maxY = window.innerHeight - dot.size;
|
||||||
|
if (dot.x <= 0 || dot.x >= maxX) {
|
||||||
|
dot.vx = -dot.vx;
|
||||||
|
dot.x = Math.max(0, Math.min(maxX, dot.x));
|
||||||
|
}
|
||||||
|
if (dot.y <= 0 || dot.y >= maxY) {
|
||||||
|
dot.vy = -dot.vy;
|
||||||
|
dot.y = Math.max(0, Math.min(maxY, dot.y));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update DOM element position (top-left)
|
||||||
|
dot.element.style.left = dot.x + 'px';
|
||||||
|
dot.element.style.top = dot.y + 'px';
|
||||||
|
|
||||||
|
// Update dot color based on connection status
|
||||||
|
if (dot.connectionCount > 0 && !dot.isConnected) {
|
||||||
|
dot.isConnected = true;
|
||||||
|
dot.element.style.background = this.config.colors.dotConnected;
|
||||||
|
dot.element.style.boxShadow = `0 0 6px ${this.config.colors.dotConnectedGlow}, 0 0 12px rgba(100, 149, 237, 0.3)`;
|
||||||
|
} else if (dot.connectionCount === 0 && dot.isConnected) {
|
||||||
|
dot.isConnected = false;
|
||||||
|
dot.element.style.background = this.config.colors.dotDefault;
|
||||||
|
dot.element.style.boxShadow = `0 0 6px ${this.config.colors.dotDefaultGlow}, 0 0 12px rgba(255, 255, 255, 0.2)`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
createConnection(dot1, dot2) {
|
||||||
|
if (this.connections.length >= this.config.maxConnections) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const connection = document.createElement('div');
|
||||||
|
connection.className = 'connection-line';
|
||||||
|
|
||||||
|
// Calculate center positions of dots
|
||||||
|
const dot1Rect = dot1.element.getBoundingClientRect();
|
||||||
|
const dot2Rect = dot2.element.getBoundingClientRect();
|
||||||
|
const canvasRect = this.canvas.getBoundingClientRect();
|
||||||
|
const dot1CenterX = dot1Rect.left - canvasRect.left + dot1Rect.width / 2;
|
||||||
|
const dot1CenterY = dot1Rect.top - canvasRect.top + dot1Rect.height / 2;
|
||||||
|
const dot2CenterX = dot2Rect.left - canvasRect.left + dot2Rect.width / 2;
|
||||||
|
const dot2CenterY = dot2Rect.top - canvasRect.top + dot2Rect.height / 2;
|
||||||
|
|
||||||
|
const dx = dot2CenterX - dot1CenterX;
|
||||||
|
const dy = dot2CenterY - dot1CenterY;
|
||||||
|
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||||
|
const angle = Math.atan2(dy, dx) * (180 / Math.PI);
|
||||||
|
|
||||||
|
// Position and style the connection line from center to center
|
||||||
|
const lineHalf = 1;
|
||||||
|
connection.style.left = Math.round(dot1CenterX) + 'px';
|
||||||
|
connection.style.top = Math.round(dot1CenterY - lineHalf) + 'px';
|
||||||
|
connection.style.width = Math.round(distance) + 'px';
|
||||||
|
connection.style.transformOrigin = '0 50%';
|
||||||
|
connection.style.transform = `rotate(${angle}deg)`;
|
||||||
|
connection.style.background = `linear-gradient(90deg,
|
||||||
|
${this.config.colors.connection.start} 0%,
|
||||||
|
${this.config.colors.connection.middle} 50%,
|
||||||
|
${this.config.colors.connection.end} 100%)`;
|
||||||
|
|
||||||
|
this.canvas.appendChild(connection);
|
||||||
|
|
||||||
|
// Fade in
|
||||||
|
setTimeout(() => {
|
||||||
|
connection.classList.add('active');
|
||||||
|
}, 50);
|
||||||
|
|
||||||
|
const connectionData = {
|
||||||
|
element: connection,
|
||||||
|
dot1: dot1,
|
||||||
|
dot2: dot2,
|
||||||
|
startTime: Date.now(),
|
||||||
|
duration: Math.random() * (this.config.connectionDuration.max - this.config.connectionDuration.min) + this.config.connectionDuration.min
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mark dots as connected
|
||||||
|
dot1.connectionCount++;
|
||||||
|
dot2.connectionCount++;
|
||||||
|
|
||||||
|
this.connections.push(connectionData);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateConnections() {
|
||||||
|
this.connections.forEach((connection, index) => {
|
||||||
|
const elapsed = Date.now() - connection.startTime;
|
||||||
|
|
||||||
|
// Update connection position as dots move (using center positions)
|
||||||
|
const dot1Rect = connection.dot1.element.getBoundingClientRect();
|
||||||
|
const dot2Rect = connection.dot2.element.getBoundingClientRect();
|
||||||
|
const canvasRect = this.canvas.getBoundingClientRect();
|
||||||
|
const dot1CenterX = dot1Rect.left - canvasRect.left + dot1Rect.width / 2;
|
||||||
|
const dot1CenterY = dot1Rect.top - canvasRect.top + dot1Rect.height / 2;
|
||||||
|
const dot2CenterX = dot2Rect.left - canvasRect.left + dot2Rect.width / 2;
|
||||||
|
const dot2CenterY = dot2Rect.top - canvasRect.top + dot2Rect.height / 2;
|
||||||
|
|
||||||
|
const dx = dot2CenterX - dot1CenterX;
|
||||||
|
const dy = dot2CenterY - dot1CenterY;
|
||||||
|
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||||
|
const angle = Math.atan2(dy, dx) * (180 / Math.PI);
|
||||||
|
|
||||||
|
const lineHalf = 1;
|
||||||
|
connection.element.style.left = Math.round(dot1CenterX) + 'px';
|
||||||
|
connection.element.style.top = Math.round(dot1CenterY - lineHalf) + 'px';
|
||||||
|
connection.element.style.width = Math.round(distance) + 'px';
|
||||||
|
connection.element.style.transformOrigin = '0 50%';
|
||||||
|
connection.element.style.transform = `rotate(${angle}deg)`;
|
||||||
|
|
||||||
|
// Remove expired connections
|
||||||
|
if (elapsed > connection.duration) {
|
||||||
|
connection.element.classList.remove('active');
|
||||||
|
|
||||||
|
// Mark dots as no longer connected
|
||||||
|
connection.dot1.connectionCount--;
|
||||||
|
connection.dot2.connectionCount--;
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
connection.element.remove();
|
||||||
|
}, 500); // Wait for fade out
|
||||||
|
this.connections.splice(index, 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
tryCreateConnections() {
|
||||||
|
// Randomly try to create connections between nearby dots
|
||||||
|
for (let i = 0; i < this.dots.length; i++) {
|
||||||
|
for (let j = i + 1; j < this.dots.length; j++) {
|
||||||
|
if (Math.random() < this.config.connectionChance) {
|
||||||
|
const dot1 = this.dots[i];
|
||||||
|
const dot2 = this.dots[j];
|
||||||
|
|
||||||
|
// Calculate distance between dot centers
|
||||||
|
const dot1CenterX = dot1.x + dot1.size / 2;
|
||||||
|
const dot1CenterY = dot1.y + dot1.size / 2;
|
||||||
|
const dot2CenterX = dot2.x + dot2.size / 2;
|
||||||
|
const dot2CenterY = dot2.y + dot2.size / 2;
|
||||||
|
|
||||||
|
const dx = dot2CenterX - dot1CenterX;
|
||||||
|
const dy = dot2CenterY - dot1CenterY;
|
||||||
|
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||||
|
|
||||||
|
if (distance < 150 && distance > 30) {
|
||||||
|
// Check if these dots are already connected
|
||||||
|
const alreadyConnected = this.connections.some(conn =>
|
||||||
|
(conn.dot1 === dot1 && conn.dot2 === dot2) ||
|
||||||
|
(conn.dot1 === dot2 && conn.dot2 === dot1)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!alreadyConnected) {
|
||||||
|
this.createConnection(dot1, dot2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
animate() {
|
||||||
|
this.updateDots();
|
||||||
|
this.updateConnections();
|
||||||
|
this.tryCreateConnections();
|
||||||
|
|
||||||
|
requestAnimationFrame(() => this.animate());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize the matrix background when the DOM is loaded
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
new MatrixBackground();
|
||||||
|
});
|
60
js/social.js
Normal file
60
js/social.js
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
|
||||||
|
// Initialize the matrix background when the DOM is loaded
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
new MatrixBackground();
|
||||||
|
|
||||||
|
// Email copy-to-clipboard and tooltip logic
|
||||||
|
const emailLink = document.getElementById('email-link');
|
||||||
|
const emailTooltip = document.getElementById('email-tooltip');
|
||||||
|
const obfuscatedEmail = 'Bg4aC0IJDAAeCBgbCU8AGA0PKQkBAAACQgIGAw=='; // generated through utils/xor-enc.py
|
||||||
|
const xorKey = 'lain';
|
||||||
|
|
||||||
|
function decodeEmail(obf, key) {
|
||||||
|
// Double base64 decode
|
||||||
|
let b64 = atob(obf);
|
||||||
|
// XOR decode
|
||||||
|
let email = '';
|
||||||
|
for (let i = 0; i < b64.length; i++) {
|
||||||
|
email += String.fromCharCode(b64.charCodeAt(i) ^ key.charCodeAt(i % key.length));
|
||||||
|
}
|
||||||
|
return email;
|
||||||
|
}
|
||||||
|
const emailAddress = decodeEmail(obfuscatedEmail, xorKey);
|
||||||
|
|
||||||
|
if (emailLink && emailTooltip) {
|
||||||
|
emailLink.addEventListener('click', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
// Copy email to clipboard
|
||||||
|
if (navigator.clipboard) {
|
||||||
|
navigator.clipboard.writeText(emailAddress);
|
||||||
|
} else {
|
||||||
|
// Fallback for older browsers
|
||||||
|
const textarea = document.createElement('textarea');
|
||||||
|
textarea.value = emailAddress;
|
||||||
|
document.body.appendChild(textarea);
|
||||||
|
textarea.select();
|
||||||
|
document.execCommand('copy');
|
||||||
|
document.body.removeChild(textarea);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Position tooltip above email link
|
||||||
|
const emailRect = emailLink.getBoundingClientRect();
|
||||||
|
const socialLinksRect = emailLink.parentElement.getBoundingClientRect();
|
||||||
|
|
||||||
|
const left = emailRect.left - socialLinksRect.left + (emailRect.width / 2);
|
||||||
|
const top = emailRect.top - socialLinksRect.top - 10;
|
||||||
|
|
||||||
|
emailTooltip.style.left = left + 'px';
|
||||||
|
emailTooltip.style.top = top + 'px';
|
||||||
|
emailTooltip.style.transform = 'translateX(-50%) translateY(-75%)';
|
||||||
|
|
||||||
|
// Show tooltip
|
||||||
|
emailTooltip.style.opacity = '1';
|
||||||
|
|
||||||
|
// Hide after 1.5s
|
||||||
|
setTimeout(() => {
|
||||||
|
emailTooltip.style.opacity = '0';
|
||||||
|
}, 1500);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
27
utils/xor-enc.py
Normal file
27
utils/xor-enc.py
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import base64
|
||||||
|
import sys
|
||||||
|
|
||||||
|
KEY = b"lain"
|
||||||
|
|
||||||
|
def xor_bytes(data: bytes, key: bytes) -> bytes:
|
||||||
|
return bytes(b ^ key[i % len(key)] for i, b in enumerate(data))
|
||||||
|
|
||||||
|
def encrypt(text: str) -> str:
|
||||||
|
data = text.encode("utf-8")
|
||||||
|
xored = xor_bytes(data, KEY)
|
||||||
|
return base64.b64encode(xored).decode("ascii")
|
||||||
|
|
||||||
|
def decrypt(b64text: str) -> str:
|
||||||
|
xored = base64.b64decode(b64text)
|
||||||
|
return xor_bytes(xored, KEY).decode("utf-8")
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if len(sys.argv) > 1:
|
||||||
|
# Use command-line arguments as the text
|
||||||
|
text = " ".join(sys.argv[1:])
|
||||||
|
else:
|
||||||
|
text = input("Text to encrypt: ")
|
||||||
|
print(encrypt(text))
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
Reference in New Issue
Block a user