first commit
This commit is contained in:
24
app/components/Certs.tsx
Executable file
24
app/components/Certs.tsx
Executable file
@@ -0,0 +1,24 @@
|
||||
import Image from "next/image";
|
||||
import styles from "./components.module.css"
|
||||
|
||||
export default function Projects() {
|
||||
return (
|
||||
<div className={styles.window}>
|
||||
<div className={styles.folder}>
|
||||
<div className={styles.navFolder}>
|
||||
<div className={styles.buttonContainer}>
|
||||
<a href="/"style={{backgroundColor: "#FE4A45"}} className={styles.ball}>
|
||||
</a>
|
||||
<div style={{backgroundColor: "#FDBE05"}} className={styles.ball}>
|
||||
</div>
|
||||
<div style={{backgroundColor: "#05D02C"}} className={styles.ball}>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.folderContent}>
|
||||
<a>No projects yet...</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
25
app/components/Clock.tsx
Executable file
25
app/components/Clock.tsx
Executable file
@@ -0,0 +1,25 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export function Clock() {
|
||||
const [time, setTime] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
const updateTime = () => {
|
||||
const now = new Date();
|
||||
setTime(
|
||||
now.toLocaleTimeString([], {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
updateTime();
|
||||
const interval = setInterval(updateTime, 1000);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
return <span>{time}</span>;
|
||||
}
|
||||
185
app/components/Contact.tsx
Executable file
185
app/components/Contact.tsx
Executable file
@@ -0,0 +1,185 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import styles from "./components.module.css";
|
||||
import Script from "next/script";
|
||||
|
||||
|
||||
export default function Contact() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [status, setStatus] = useState<boolean | null>(null);
|
||||
const [captchaToken, setCaptchaToken] = useState<string | null>(null);
|
||||
const [pendingForm, setPendingForm] =
|
||||
useState<HTMLFormElement | null>(null);
|
||||
|
||||
|
||||
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
|
||||
e.preventDefault();
|
||||
|
||||
if (!captchaToken) {
|
||||
setPendingForm(e.currentTarget);
|
||||
// @ts-ignore
|
||||
window.grecaptcha.execute();
|
||||
return;
|
||||
}
|
||||
|
||||
await sendForm(e.currentTarget);
|
||||
}
|
||||
|
||||
async function sendForm(form: HTMLFormElement) {
|
||||
setLoading(true);
|
||||
setStatus(null);
|
||||
|
||||
const data = {
|
||||
name: "Website Contact",
|
||||
email: (form.elements.namedItem("from") as HTMLInputElement).value,
|
||||
subject: (form.elements.namedItem("subject") as HTMLInputElement).value,
|
||||
message: (form.elements.namedItem("message") as HTMLTextAreaElement).value,
|
||||
token: captchaToken,
|
||||
};
|
||||
|
||||
try {
|
||||
const res = await fetch("/api/contact", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
|
||||
if (!res.ok) throw new Error();
|
||||
|
||||
setStatus(true);
|
||||
form.reset();
|
||||
} catch {
|
||||
setStatus(false);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setCaptchaToken(null);
|
||||
// @ts-ignore
|
||||
window.grecaptcha.reset();
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (captchaToken && pendingForm) {
|
||||
sendForm(pendingForm);
|
||||
setPendingForm(null);
|
||||
}
|
||||
}, [captchaToken]);
|
||||
|
||||
useEffect(() => {
|
||||
if (status === true) {
|
||||
const timer = setTimeout(() => {
|
||||
setStatus(null);
|
||||
}, 3_000);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [status]);
|
||||
|
||||
useEffect(() => {
|
||||
(window as any).onRecaptchaSuccess = (token: string) => {
|
||||
setCaptchaToken(token);
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
||||
const icon =
|
||||
status === null
|
||||
? "/img/icons/send.png"
|
||||
: status === true
|
||||
? "/img/icons/check.png"
|
||||
: "/img/icons/error.png";
|
||||
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<div className={styles.cwindow}>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.cbuttonContainer}>
|
||||
<a href="/" style={{ backgroundColor: "#FE4A45" }} className={styles.ball} />
|
||||
<div style={{ backgroundColor: "#FDBE05" }} className={styles.ball} />
|
||||
<div style={{ backgroundColor: "#05D02C" }} className={styles.ball} />
|
||||
<Script
|
||||
src="https://www.google.com/recaptcha/api.js"
|
||||
strategy="afterInteractive"
|
||||
/>
|
||||
<div
|
||||
className="g-recaptcha"
|
||||
data-sitekey={process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY}
|
||||
data-size="invisible"
|
||||
data-callback="onRecaptchaSuccess"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
form="contact-form"
|
||||
disabled={loading || pendingForm !== null || status === true}
|
||||
style={{
|
||||
marginLeft: "2vh",
|
||||
backgroundColor: "transparent",
|
||||
border: "none",
|
||||
cursor: "pointer",
|
||||
height: "2vh",
|
||||
opacity: loading ? 0.5 : 1,
|
||||
}}
|
||||
><img
|
||||
src={icon}
|
||||
style={{ height: "2.5vh", filter: "invert(1)" }}
|
||||
alt="Send"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className={styles.mContent}>
|
||||
<div className={styles.form}>
|
||||
<form
|
||||
id="contact-form"
|
||||
onSubmit={handleSubmit}
|
||||
style={{ width: "100%", textAlign: "center" }}
|
||||
>
|
||||
<div style={{ display: "flex" }}>
|
||||
<span>To:</span>
|
||||
<input
|
||||
type="email"
|
||||
placeholder="contact@4l3ks.com"
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ display: "flex" }}>
|
||||
<span>Cc:</span>
|
||||
<input type="email" disabled />
|
||||
</div>
|
||||
|
||||
<div style={{ display: "flex" }}>
|
||||
<span>Subject:</span>
|
||||
<input
|
||||
name="subject"
|
||||
type="text"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ display: "flex" }}>
|
||||
<span>From:</span>
|
||||
<input
|
||||
name="from"
|
||||
type="email"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ display: "flex" }}>
|
||||
<textarea
|
||||
name="message"
|
||||
required
|
||||
placeholder=""
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
51
app/components/Info.tsx
Executable file
51
app/components/Info.tsx
Executable file
@@ -0,0 +1,51 @@
|
||||
import Image from "next/image";
|
||||
import styles from "./components.module.css"
|
||||
|
||||
export default function Info() {
|
||||
return (
|
||||
<div className={styles.iwindow}>
|
||||
<div className={styles.icontent}>
|
||||
<div className={styles.buttonContainer}>
|
||||
<a href="/"style={{backgroundColor: "#FE4A45"}} className={styles.ball}>
|
||||
</a>
|
||||
<div style={{backgroundColor: "#575757ef"}} className={styles.ball}>
|
||||
</div>
|
||||
<div style={{backgroundColor: "#575757ef"}} className={styles.ball}>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.info}>
|
||||
<img src="/img/pfp.png" style={{height: "16vh", borderRadius: "50%"}}></img>
|
||||
<h1>Aleksandar Petrov</h1>
|
||||
<a>Web & App Developer</a>
|
||||
<div className={styles.fullInfo}>
|
||||
<div className={styles.infoBoxes}>
|
||||
<b>Age:</b>
|
||||
<a>21</a>
|
||||
</div>
|
||||
<div className={styles.infoBoxes}>
|
||||
<b>Languages:</b>
|
||||
<a>English, Bulgarian, German, Spanish, Catalan</a>
|
||||
</div>
|
||||
<div className={styles.infoBoxes}>
|
||||
<b>Location:</b>
|
||||
<a>EU/Remote</a>
|
||||
</div>
|
||||
<div className={styles.infoBoxes}>
|
||||
<b>Status:</b>
|
||||
<a>Working</a>
|
||||
</div>
|
||||
<div className={styles.infoBoxes}>
|
||||
<b>Experience:</b>
|
||||
<a>1+ Year</a>
|
||||
</div>
|
||||
<div className={styles.infoBoxes}>
|
||||
<b>Academic Degree:</b>
|
||||
<a>{'Computer Science (In progress)'}</a>
|
||||
</div>
|
||||
<button className={styles.infoBtn}>More Info...</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
21
app/components/Nav.tsx
Executable file
21
app/components/Nav.tsx
Executable file
@@ -0,0 +1,21 @@
|
||||
import Image from "next/image";
|
||||
import styles from "./components.module.css"
|
||||
|
||||
export default function Nav() {
|
||||
return (
|
||||
<div className={styles.nav}>
|
||||
<a className={styles.icon}>
|
||||
<img src="/img/icons/folder.png" style={{height: "14vh"}}></img>
|
||||
Projects
|
||||
</a>
|
||||
<a className={styles.icon}>
|
||||
<img src="/img/icons/info.png" style={{height: "14vh"}}></img>
|
||||
Info
|
||||
</a>
|
||||
<a className={styles.icon}>
|
||||
<img src="/img/icons/contact.png" style={{height: "14vh"}}></img>
|
||||
Contact
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
24
app/components/Projects.tsx
Executable file
24
app/components/Projects.tsx
Executable file
@@ -0,0 +1,24 @@
|
||||
import Image from "next/image";
|
||||
import styles from "./components.module.css"
|
||||
|
||||
export default function Projects() {
|
||||
return (
|
||||
<div className={styles.window}>
|
||||
<div className={styles.folder}>
|
||||
<div className={styles.navFolder}>
|
||||
<div className={styles.buttonContainer}>
|
||||
<a href="/"style={{backgroundColor: "#FE4A45"}} className={styles.ball}>
|
||||
</a>
|
||||
<div style={{backgroundColor: "#FDBE05"}} className={styles.ball}>
|
||||
</div>
|
||||
<div style={{backgroundColor: "#05D02C"}} className={styles.ball}>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.folderContent}>
|
||||
<a>No projects yet...</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
244
app/components/components.module.css
Executable file
244
app/components/components.module.css
Executable file
@@ -0,0 +1,244 @@
|
||||
.nav {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
flex-direction: column;
|
||||
gap: 6vh;
|
||||
}
|
||||
.icon {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
.nav {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
flex-direction: row;
|
||||
gap: 2vh;
|
||||
margin-top: 10vh
|
||||
}
|
||||
}
|
||||
/*Projects*/
|
||||
.window{
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
flex-direction: column;
|
||||
gap: 4vh;
|
||||
}
|
||||
.folder {
|
||||
background-color: rgba(56, 56, 56, 0.7);
|
||||
width: 70%;
|
||||
height: 70%;
|
||||
border-radius: 2vh;
|
||||
display: flex;
|
||||
}
|
||||
.navFolder {
|
||||
|
||||
height: 100%;
|
||||
width: 25%;
|
||||
}
|
||||
.buttonContainer {
|
||||
padding: 2vh;
|
||||
display: flex;
|
||||
gap: 0.7vh;
|
||||
height: auto;
|
||||
width: 100%;
|
||||
}
|
||||
.ball {
|
||||
border-radius: 50%;
|
||||
height: 1.5vh;
|
||||
width: 1.5vh;
|
||||
}
|
||||
.folderContent {
|
||||
background-color: #231B2C;
|
||||
width: 100%;
|
||||
border-left: solid black 2px;
|
||||
border-top-right-radius: 2vh;
|
||||
border-bottom-right-radius: 2vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.folderContent a {
|
||||
color: rgb(230, 230, 230);
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
.folder {
|
||||
width: 100%;
|
||||
height: 70%;
|
||||
}
|
||||
}
|
||||
/*Info*/
|
||||
.iwindow{
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
flex-direction: column;
|
||||
gap: 4vh;
|
||||
}
|
||||
.icontent {
|
||||
background-color: #231b2cf1;
|
||||
width: 30%;
|
||||
height: 70%;
|
||||
border-radius: 2vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.info {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 1vh;
|
||||
flex-direction: column;
|
||||
}
|
||||
.info h1 {
|
||||
color: rgb(230, 230, 230);
|
||||
margin-bottom:0;
|
||||
}
|
||||
.info a {
|
||||
color: rgb(148, 148, 148);
|
||||
}
|
||||
.info b {
|
||||
color: rgb(230, 230, 230);
|
||||
}
|
||||
.fullInfo {
|
||||
width: 100%;
|
||||
margin-top: 2vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 0.1vh;
|
||||
}
|
||||
.infoBoxes {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 80%;
|
||||
gap: 2vh;
|
||||
}
|
||||
.infoBoxes a {
|
||||
text-align: left;
|
||||
flex: 1;
|
||||
font-size: 1.6vh;
|
||||
}
|
||||
.infoBoxes b {
|
||||
text-align: right;
|
||||
flex: 1;
|
||||
}
|
||||
.infoBtn {
|
||||
margin-top: 2vh;
|
||||
background-color: #a0a0a0e3;
|
||||
color: white;
|
||||
outline: none;
|
||||
border: none;
|
||||
height: 2vh;
|
||||
width: 10vh;
|
||||
font-size: 1.4vh;
|
||||
border-radius: 0.5vh;
|
||||
}
|
||||
@media (max-width: 1300px) {
|
||||
.icontent{
|
||||
margin-top: 5vh;
|
||||
width: 100%;
|
||||
height: 80%;
|
||||
}
|
||||
}
|
||||
/*Contact*/
|
||||
.cwindow {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
flex-direction: column;
|
||||
gap: 4vh;
|
||||
}
|
||||
.content {
|
||||
background-color: rgba(56, 56, 56, 0.7);
|
||||
width: 70%;
|
||||
height: 70%;
|
||||
border-radius: 2vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.cbuttonContainer {
|
||||
padding: 3vh 2vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.7vh;
|
||||
background-color: rgba(40, 39, 41, 0.6);
|
||||
border-top-left-radius: 2vh;
|
||||
border-top-right-radius: 2vh;
|
||||
}
|
||||
.mContent {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
.form {
|
||||
width: 90%;
|
||||
justify-content: center;
|
||||
display: flex;
|
||||
}
|
||||
.form input{
|
||||
width: 99%;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
border-bottom: solid rgba(40, 39, 41, 0.6) 2px;
|
||||
font-size: 1.6vh;
|
||||
padding: 1vh 0;
|
||||
color: white;
|
||||
}
|
||||
.form input::placeholder{
|
||||
color: rgb(211, 211, 211)
|
||||
}
|
||||
.form input:focus {
|
||||
outline: none;
|
||||
}
|
||||
.form textarea{
|
||||
width: 99%;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
color: white;
|
||||
font-size: 1.6vh;
|
||||
padding: 1vh 0;
|
||||
margin-top: 1vh;
|
||||
height: 30vh;
|
||||
}
|
||||
.form textarea:focus {
|
||||
border: solid rgba(40, 39, 41, 0.6) 1px;
|
||||
outline: none;
|
||||
}
|
||||
.form span{
|
||||
border-bottom: solid rgba(40, 39, 41, 0.6) 2px;
|
||||
font-size: 1.6vh;
|
||||
padding-right: 2vh;
|
||||
color: gray;
|
||||
padding-top: 1vh;
|
||||
padding-bottom: 1vh;
|
||||
}
|
||||
.icon {
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
.content {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user