Files
Portfolio/src/app/(main)/page.tsx
T

414 lines
20 KiB
TypeScript

import { supabase } from "@/lib/supabase";
import ScrollReveal from "@/components/ScrollReveal";
import Link from "next/link";
export default async function Home() {
const recaptchaSiteKey = process.env.RECAPTCHA_SITE_KEY || "";
// Check if Supabase is actually configured
const isDummy = (process.env.NEXT_PUBLIC_SUPABASE_URL || 'https://dummy.supabase.co') === 'https://dummy.supabase.co';
let settingsRows = null;
let techStackRows = null;
let projectRows = null;
let certRows = null;
let articleRows = null;
if (!isDummy) {
// Fetch Settings
const resSettings = await supabase.from('settings').select('*').eq('id', 1).single();
settingsRows = resSettings.data;
// Fetch Tech Stack
const resTech = await supabase.from('techstack').select('tech_name').order('id', { ascending: true });
techStackRows = resTech.data;
// Fetch Projects (Limit 3)
const resProj = await supabase.from('projects').select('*').order('id', { ascending: false }).limit(3);
projectRows = resProj.data;
// Fetch Certifications (Limit 3)
const resCert = await supabase.from('certifications').select('*').order('id', { ascending: false }).limit(3);
certRows = resCert.data;
// Fetch Articles (Limit 3)
const resArt = await supabase.from('articles').select('*').order('id', { ascending: false }).limit(3);
articleRows = resArt.data;
}
const settings = settingsRows || {
about_text_1: 'I am a senior IT student majoring in Software Development. I write code to solve real-world problems.',
about_text_2: 'My primary stack includes PHP, React, Node.js, and MySQL. I enjoy database design, front-end development, and bug fixing.',
years_coding: '3+',
projects_count: '10+',
certifications_count: '6+',
tiktok_url: 'https://www.tiktok.com/@crischarlesgarcia',
facebook_url: 'https://facebook.com/kristyarls345',
instagram_url: 'https://instagram.com/crischarlesgarcia',
linkedin_url: 'https://www.linkedin.com/in/cris-charles-garcia-187415303',
github_url: 'https://github.com/ssbg04'
};
const techStack = techStackRows ? techStackRows.map(r => r.tech_name) : [];
return (
<>
{/* ===== HERO ===== */}
<ScrollReveal>
<header className="hero">
<div className="hero-text">
<div className="hero-badge">
<span className="dot"></span>
Available for work
</div>
<span style={{ display: "block", color: "var(--text-muted)", fontSize: "1rem", fontWeight: 500, marginBottom: "10px" }}>
Hi, I am Cris Charles Garcia.
</span>
<h1>Software <span className="highlight">Developer</span>.</h1>
<p className="subtitle">
I am an IT student majoring in Software Development based in Laguna, Philippines. I focus on building reliable web applications.
</p>
<div className="hero-btns">
<Link href="#projects" className="btn primary" id="btn-view-projects">My Projects ↗</Link>
<Link href="#contact" className="btn secondary" id="btn-get-in-touch">Contact Me</Link>
</div>
</div>
<div className="hero-img-container">
<div className="img-ring"></div>
<img src="/assets/img/pp-night.webp" alt="Cris Charles Garcia" className="profile-pic img-night-normal" />
<img src="/assets/img/pp-night-shy.webp" alt="Cris Charles Garcia" className="profile-pic img-night-hover" />
<img src="/assets/img/pp-day.webp" alt="Cris Charles Garcia" className="profile-pic img-day-normal" />
<img src="/assets/img/pp-day-shy.webp" alt="Cris Charles Garcia" className="profile-pic img-day-hover" />
<div className="glow-effect"></div>
</div>
</header>
</ScrollReveal>
<div className="section-divider"></div>
{/* ===== ABOUT ===== */}
<ScrollReveal>
<section id="about" className="section-pad">
<div className="section-header">
<span className="section-label">About Me</span>
<h2 className="section-title">About Me</h2>
</div>
<div className="about-content">
<div className="about-text">
<p>{settings.about_text_1}</p>
<p>{settings.about_text_2}</p>
</div>
<div className="tech-stack">
<h3>Tech Stack</h3>
<div className="tags">
{techStack.length > 0 ? (
techStack.map((tech, index) => (
<span key={index}>{tech}</span>
))
) : (
<p>No tech stack found.</p>
)}
</div>
</div>
</div>
<div className="stats-row">
<div className="stat-item">
<div className="stat-number">{settings.years_coding}</div>
<div className="stat-label">Years Coding</div>
</div>
<div className="stat-item">
<div className="stat-number">{settings.projects_count}</div>
<div className="stat-label">Projects Built</div>
</div>
<div className="stat-item">
<div className="stat-number">{settings.certifications_count}</div>
<div className="stat-label">Certifications</div>
</div>
</div>
</section>
</ScrollReveal>
<div className="section-divider"></div>
{/* ===== PROJECTS ===== */}
<ScrollReveal>
<section id="projects" className="section-pad">
<div className="section-header">
<span className="section-label">Projects</span>
<h2 className="section-title">Recent Projects</h2>
</div>
<div className="project-grid">
{projectRows && projectRows.length > 0 ? projectRows.map((project, index) => (
<a href={project.url} target="_blank" rel="noopener noreferrer" className="project-card" key={index}>
<div className="card-img">
<img src={project.img_url || undefined} alt={project.title} loading="lazy" />
<div className="card-img-overlay"></div>
</div>
<div className="card-content">
<div className="card-header">
<h3>{project.title}</h3>
<span className="arrow-icon">↗</span>
</div>
<p>{project.des}</p>
<div className="stack-badges">
{project.techstack ? project.techstack.split(',').map((tag: string) => tag.trim()).map((tag: string, i: number) => (
<span className="stack-badge" key={i}>{tag}</span>
)) : null}
</div>
</div>
</a>
)) : <p style={{ color: "var(--text-muted)", gridColumn: "1/-1", textAlign: "center", padding: "40px 0" }}>No projects available.</p>}
</div>
<div className="view-all-container">
<Link href="/projects" className="btn secondary" id="btn-all-projects">All Projects →</Link>
</div>
</section>
</ScrollReveal>
<div className="section-divider"></div>
{/* ===== CERTIFICATIONS ===== */}
<ScrollReveal>
<section id="certificates" className="section-pad">
<div className="section-header">
<span className="section-label">Certifications</span>
<h2 className="section-title">My Certifications</h2>
</div>
<div className="cert-grid">
{certRows && certRows.length > 0 ? certRows.map((cert, index) => (
<div className="cert-card" key={index}>
<div className="cert-icon">{cert.icon || "🏆"}</div>
<div className="cert-info">
<h3>{cert.title}</h3>
<p className="issuer">{cert.issuer}</p>
<span className="date">Issued {cert.date_issued}</span>
</div>
</div>
)) : <p style={{ color: "var(--text-muted)", gridColumn: "1/-1", textAlign: "center", padding: "40px 0" }}>No certifications available.</p>}
</div>
<div className="view-all-container">
<Link href="/certifications" className="btn secondary" id="btn-all-certs">All Certifications →</Link>
</div>
</section>
</ScrollReveal>
<div className="section-divider"></div>
{/* ===== BLOG ===== */}
<ScrollReveal>
<section id="blog" className="section-pad">
<div className="section-header">
<span className="section-label">Articles</span>
<h2 className="section-title">Latest Articles</h2>
</div>
<div className="blog-grid">
{articleRows && articleRows.length > 0 ? articleRows.map((article, index) => (
<article className="blog-card" key={index}>
<div className="blog-meta">
<span className="blog-date">{article.date_published}</span>
{article.tag && <span className="blog-tag">{article.tag}</span>}
</div>
<h3 className="blog-title">{article.title}</h3>
<p className="blog-excerpt">{article.excerpt}</p>
<a href={article.url} target="_blank" rel="noopener noreferrer" className="read-more">Read Article →</a>
</article>
)) : <p style={{ color: "var(--text-muted)", gridColumn: "1/-1", textAlign: "center", padding: "40px 0" }}>No articles available.</p>}
</div>
<div className="view-all-container">
<Link href="/blog" className="btn secondary" id="btn-all-articles">All Articles →</Link>
</div>
</section>
</ScrollReveal>
<div className="section-divider"></div>
{/* ===== TOAST NOTIFICATION ===== */}
<div className="toast-backdrop" id="toast-backdrop" />
<div className="toast-container" id="toast-container" role="alert" aria-live="polite">
<div className="toast" id="toast-box">
<div className="toast-icon" id="toast-icon"></div>
<div className="toast-body">
<div className="toast-title" id="toast-title"></div>
<div className="toast-msg" id="toast-msg"></div>
</div>
<button className="toast-close" id="toast-close" aria-label="Close">&times;</button>
</div>
</div>
{/* ===== CONTACT ===== */}
<ScrollReveal>
<section id="contact" className="section-pad">
<div className="section-header" style={{ textAlign: "center" }}>
<span className="section-label">Contact</span>
<h2 className="section-title">Get in Touch</h2>
</div>
<div className="contact-container">
<p>Feel free to reach out if you have a project idea, a job opportunity, or any questions.</p>
<div className="social-links">
{settings.github_url && (
<a href={settings.github_url} target="_blank" className="social-btn" aria-label="GitHub">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"></path></svg>
</a>
)}
{settings.linkedin_url && (
<a href={settings.linkedin_url} target="_blank" className="social-btn" aria-label="LinkedIn">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><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 x="2" y="9" width="4" height="12"></rect><circle cx="4" cy="4" r="2"></circle></svg>
</a>
)}
{settings.facebook_url && (
<a href={settings.facebook_url} target="_blank" className="social-btn" aria-label="Facebook">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M18 2h-3a5 5 0 0 0-5 5v3H7v4h3v8h4v-8h3l1-4h-4V7a1 1 0 0 1 1-1h3z"></path></svg>
</a>
)}
{settings.instagram_url && (
<a href={settings.instagram_url} target="_blank" className="social-btn" aria-label="Instagram">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="2" y="2" width="20" height="20" rx="5" ry="5"></rect><path d="M16 11.37A4 4 0 1 1 12.63 8 4 4 0 0 1 16 11.37z"></path><line x1="17.5" y1="6.5" x2="17.51" y2="6.5"></line></svg>
</a>
)}
{settings.tiktok_url && (
<a href={settings.tiktok_url} target="_blank" className="social-btn" aria-label="TikTok">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M9 12a4 4 0 1 0 4 4V4a5 5 0 0 0 5 5"></path></svg>
</a>
)}
</div>
<form className="email-form" id="contact-form">
<input type="text" name="name" id="form-name" placeholder="Name" required />
<input type="email" name="email" id="form-email" placeholder="Your Email" required />
<input type="text" name="subject" id="form-subject" placeholder="Subject" required />
<textarea name="message" id="form-message" rows={5} placeholder="Message" required></textarea>
{/* Compact reCAPTCHA badge */}
<div className="recaptcha-compact" title="Protected by reCAPTCHA Enterprise">
<svg className="recaptcha-shield" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M32 4L8 14v18c0 14 10.7 26.4 24 29 13.3-2.6 24-15 24-29V14L32 4z" fill="#4285F4" opacity="0.8"/>
<path d="M28 38l-8-8 2.8-2.8L28 32.4l13.2-13.2L44 22 28 38z" fill="white"/>
</svg>
<div className="recaptcha-text">
<span className="recaptcha-protected">Protected by reCAPTCHA</span>
<span className="recaptcha-links">
<a href="https://policies.google.com/privacy" target="_blank" rel="noopener">Privacy</a>
{" · "}
<a href="https://policies.google.com/terms" target="_blank" rel="noopener">Terms</a>
</span>
</div>
<div className="recaptcha-logo">re<br/>CAPTCHA</div>
</div>
{/* reCAPTCHA v3 Enterprise — invisible */}
<script src={`https://www.google.com/recaptcha/enterprise.js?render=${recaptchaSiteKey}`} async defer></script>
<script dangerouslySetInnerHTML={{ __html: `
(function() {
function showToast(type, title, msg) {
var container = document.getElementById('toast-container');
var backdrop = document.getElementById('toast-backdrop');
var box = document.getElementById('toast-box');
var icon = document.getElementById('toast-icon');
var titleEl = document.getElementById('toast-title');
var msgEl = document.getElementById('toast-msg');
var closeBtn = document.getElementById('toast-close');
box.className = 'toast toast-' + type;
icon.textContent = type === 'success' ? '✓' : '✕';
titleEl.textContent = title;
msgEl.textContent = msg || '';
container.classList.add('toast-visible');
backdrop.classList.add('toast-visible');
var autoClose;
function dismiss() {
container.classList.remove('toast-visible');
backdrop.classList.remove('toast-visible');
clearTimeout(autoClose);
}
closeBtn.onclick = dismiss;
backdrop.onclick = dismiss;
autoClose = setTimeout(dismiss, type === 'success' ? 4000 : 6000);
}
// Check URL params for status on load
document.addEventListener('DOMContentLoaded', function() {
var params = new URLSearchParams(window.location.search);
var status = params.get('status');
var msg = params.get('msg');
if (status === 'success') {
showToast('success', 'Message Sent!', 'Thank you for reaching out. I will get back to you soon.');
history.replaceState(null, '', window.location.pathname + window.location.hash);
} else if (status === 'error') {
showToast('error', 'Something went wrong', msg || 'Please try again later.');
history.replaceState(null, '', window.location.pathname + window.location.hash);
}
var form = document.getElementById('contact-form');
if (!form) return;
form.addEventListener('submit', function(event) {
event.preventDefault();
var btn = document.getElementById('btn-send-message');
var originalText = btn.innerText;
btn.innerText = 'Verifying...';
btn.disabled = true;
grecaptcha.enterprise.ready(async function() {
try {
const token = await grecaptcha.enterprise.execute('${recaptchaSiteKey}', {action: 'submit'});
const verifyRes = await fetch('/api/contact', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token: token })
});
const verifyData = await verifyRes.json();
if (!verifyData.success) {
btn.innerText = originalText;
btn.disabled = false;
showToast('error', 'Verification Failed', verifyData.msg);
return;
}
btn.innerText = 'Sending...';
const formData = new FormData(form);
formData.append('access_key', '${process.env.WEB3FORMS_ACCESS_KEY}');
formData.append('from_name', 'Portfolio Contact Form');
const web3Res = await fetch('https://api.web3forms.com/submit', {
method: 'POST',
body: formData
});
const web3Data = await web3Res.json();
btn.innerText = originalText;
btn.disabled = false;
if (web3Data.success) {
form.reset();
showToast('success', 'Message Sent!', 'Thank you for reaching out. I will get back to you soon.');
} else {
showToast('error', 'Something went wrong', 'The email service rejected the request. Please try again.');
}
} catch(err) {
btn.innerText = originalText;
btn.disabled = false;
showToast('error', 'Unexpected Error', 'reCAPTCHA failed to load. Please refresh and try again.');
}
});
});
});
})();
`}} />
<button type="submit" className="btn primary full-width" id="btn-send-message">Send Message</button>
</form>
</div>
</section>
</ScrollReveal>
</>
);
}