feat(sanity): add dynamic studio schemas, certifications, education, and frontend data integration

This commit is contained in:
ssbg04 committed 2026-09-02 23:15:21 +08:00
1 parent cf0cbd9a1f
commit 978711a16a
21 files changed
+14940 -83

No files matched your search

+3 -2
View File
@@ -14,8 +14,9 @@ node_modules/
# testing
/coverage
/scripts
# studio
/Sanity-studio
# studio build & cache (studio code is tracked)
Sanity-studio/.sanity/
Sanity-studio/dist/
theme/
# next.js
+9
View File
@@ -0,0 +1,9 @@
# Sanity Studio ignore rules
node_modules/
.env*
.sanity/
dist/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.DS_Store
+13902
View File
File diff suppressed because it is too large. Load diff
+22
View File
@@ -0,0 +1,22 @@
{
"name": "portfolio-studio",
"private": true,
"version": "1.0.0",
"main": "package.json",
"scripts": {
"dev": "sanity dev",
"build": "sanity build",
"start": "sanity start"
},
"dependencies": {
"@sanity/vision": "^6.3.0",
"react": "^19.2.7",
"react-dom": "^19.2.7",
"sanity": "^6.3.0",
"sanity-plugin-vercel-deploy": "^4.0.0",
"styled-components": "^6.1.15"
},
"devDependencies": {
"typescript": "^5.4.5"
}
}
+8
View File
@@ -0,0 +1,8 @@
import { defineCliConfig } from 'sanity/cli'
export default defineCliConfig({
api: {
projectId: process.env.SANITY_STUDIO_PROJECT_ID || 'rk63yuwi',
dataset: process.env.SANITY_STUDIO_DATASET || 'production'
}
})
+19
View File
@@ -0,0 +1,19 @@
import { defineConfig } from 'sanity'
import { structureTool } from 'sanity/structure'
import { visionTool } from '@sanity/vision'
import { vercelDeployTool } from 'sanity-plugin-vercel-deploy'
import { schemaTypes } from './schemaTypes'
export default defineConfig({
name: 'default',
title: 'Developer Portfolio Studio',
projectId: process.env.SANITY_STUDIO_PROJECT_ID || 'rk63yuwi',
dataset: process.env.SANITY_STUDIO_DATASET || 'production',
plugins: [structureTool(), visionTool(), vercelDeployTool()],
schema: {
types: schemaTypes,
},
})
+118
View File
@@ -0,0 +1,118 @@
import { defineType, defineField } from 'sanity'
export default defineType({
name: 'certification',
title: 'Certification / Credential',
type: 'document',
fields: [
defineField({
name: 'title',
title: 'Certification Title',
type: 'string',
validation: Rule => Rule.required(),
description: 'e.g. Computer Hardware Basics, Introduction to Cybersecurity'
}),
defineField({
name: 'code',
title: 'Badge / Cert Code',
type: 'string',
description: 'e.g. 01, 02, NC2, or Cisco Cert ID'
}),
defineField({
name: 'issuer',
title: 'Issuing Organization',
type: 'string',
validation: Rule => Rule.required(),
description: 'e.g. Cisco, Cisco × OpenEDG, TESDA, Coursera'
}),
defineField({
name: 'category',
title: 'Category',
type: 'string',
options: {
list: [
{ title: 'Cybersecurity', value: 'Cybersecurity' },
{ title: 'Hardware & Systems', value: 'Hardware & Systems' },
{ title: 'Digital Literacy', value: 'Digital Literacy' },
{ title: 'Software Development', value: 'Software Development' },
{ title: 'Cloud & DevOps', value: 'Cloud & DevOps' },
{ title: 'Networking', value: 'Networking' },
{ title: 'Other', value: 'Other' }
]
},
validation: Rule => Rule.required()
}),
defineField({
name: 'badgeImage',
title: 'Badge Image Upload',
type: 'image',
options: {
hotspot: true
},
description: 'Upload PNG or badge asset directly'
}),
defineField({
name: 'badgeImageUrl',
title: 'Badge Image External URL (Fallback)',
type: 'url',
description: 'Direct CDN/Credly image URL (e.g. https://images.credly.com/images/...)'
}),
defineField({
name: 'badgeUrl',
title: 'Verification / Public Badge URL',
type: 'url',
validation: Rule => Rule.required(),
description: 'Public verification link (e.g. Credly badge URL)'
}),
defineField({
name: 'description',
title: 'Description',
type: 'text',
validation: Rule => Rule.required(),
description: 'Overview of competencies, exam scope, and skills covered'
}),
defineField({
name: 'skills',
title: 'Validated Competencies / Skills',
type: 'array',
of: [{ type: 'string' }],
description: 'List of skills demonstrated (e.g. Threat Modeling, Incident Response)'
}),
defineField({
name: 'issueDate',
title: 'Issue Date',
type: 'date',
options: {
dateFormat: 'YYYY-MM-DD'
}
}),
defineField({
name: 'expiryDate',
title: 'Expiration Date (if applicable)',
type: 'date',
options: {
dateFormat: 'YYYY-MM-DD'
}
}),
defineField({
name: 'featured',
title: 'Featured Credential',
type: 'boolean',
initialValue: true,
description: 'Highlight on homepage credentials section'
}),
defineField({
name: 'order',
title: 'Display Order',
type: 'number',
initialValue: 1
})
],
preview: {
select: {
title: 'title',
subtitle: 'issuer',
media: 'badgeImage'
}
}
})
+68
View File
@@ -0,0 +1,68 @@
import { defineType, defineField } from 'sanity'
export default defineType({
name: 'education',
title: 'Education',
type: 'document',
fields: [
defineField({
name: 'institution',
title: 'School / Institution Name',
type: 'string',
validation: Rule => Rule.required(),
description: 'e.g. Laguna State Polytechnic University'
}),
defineField({
name: 'degree',
title: 'Degree / Certificate',
type: 'string',
validation: Rule => Rule.required(),
description: 'e.g. Bachelor of Science in Information Technology'
}),
defineField({
name: 'fieldOfStudy',
title: 'Major / Field of Study',
type: 'string',
description: 'e.g. Major in Software Development'
}),
defineField({
name: 'startDate',
title: 'Start Year / Date',
type: 'string',
validation: Rule => Rule.required(),
description: 'e.g. 2022'
}),
defineField({
name: 'endDate',
title: 'End Year / Date',
type: 'string',
validation: Rule => Rule.required(),
description: 'e.g. 2026 or Present'
}),
defineField({
name: 'location',
title: 'Location',
type: 'string',
description: 'e.g. Laguna, Philippines'
}),
defineField({
name: 'achievements',
title: 'Key Highlights / Coursework / Honors',
type: 'array',
of: [{ type: 'string' }],
description: 'List of bullet points or academic highlights'
}),
defineField({
name: 'order',
title: 'Display Order',
type: 'number',
initialValue: 1
})
],
preview: {
select: {
title: 'degree',
subtitle: 'institution'
}
}
})
+90
View File
@@ -0,0 +1,90 @@
import { defineType, defineField } from 'sanity'
export default defineType({
name: 'experience',
title: 'Experience / Career History',
type: 'document',
fields: [
defineField({
name: 'company',
title: 'Company / Organization Name',
type: 'string',
validation: Rule => Rule.required(),
description: 'e.g. Talisay Integrated School'
}),
defineField({
name: 'role',
title: 'Role / Job Title',
type: 'string',
validation: Rule => Rule.required(),
description: 'e.g. Programmer, Full Stack Developer'
}),
defineField({
name: 'employmentType',
title: 'Employment Type',
type: 'string',
options: {
list: [
{ title: 'Full-time', value: 'Full-time' },
{ title: 'Part-time', value: 'Part-time' },
{ title: 'Internship', value: 'Internship' },
{ title: 'Contract / Freelance', value: 'Contract' }
]
}
}),
defineField({
name: 'location',
title: 'Location',
type: 'string',
description: 'e.g. Laguna, Philippines / Remote'
}),
defineField({
name: 'startDate',
title: 'Start Date',
type: 'string',
validation: Rule => Rule.required(),
description: 'e.g. 2025 or 2025-01'
}),
defineField({
name: 'endDate',
title: 'End Date',
type: 'string',
validation: Rule => Rule.required(),
initialValue: 'Present',
description: 'e.g. Present, 2026-05, etc.'
}),
defineField({
name: 'isCurrent',
title: 'Currently Working Here',
type: 'boolean',
initialValue: true
}),
defineField({
name: 'description',
title: 'Achievements & Responsibilities',
type: 'array',
of: [{ type: 'string' }],
validation: Rule => Rule.required(),
description: 'Bullet points detailing key contributions and systems engineered'
}),
defineField({
name: 'technologies',
title: 'Technologies Used',
type: 'array',
of: [{ type: 'string' }],
description: 'e.g. PHP, MySQL, JavaScript, HTML/CSS'
}),
defineField({
name: 'order',
title: 'Display Order',
type: 'number',
initialValue: 1
})
],
preview: {
select: {
title: 'role',
subtitle: 'company'
}
}
})
+19
View File
@@ -0,0 +1,19 @@
import siteSettings from './siteSettings'
import project from './project'
import skill from './skill'
import certification from './certification'
import experience from './experience'
import education from './education'
import testimonial from './testimonial'
import socialLink from './socialLink'
export const schemaTypes = [
siteSettings,
project,
skill,
certification,
experience,
education,
testimonial,
socialLink
]
+133
View File
@@ -0,0 +1,133 @@
import { defineType, defineField } from 'sanity'
export default defineType({
name: 'project',
title: 'Project',
type: 'document',
fields: [
defineField({
name: 'title',
title: 'Project Title',
type: 'string',
validation: Rule => Rule.required()
}),
defineField({
name: 'slug',
title: 'Slug',
type: 'slug',
options: {
source: 'title',
maxLength: 96
},
validation: Rule => Rule.required()
}),
defineField({
name: 'category',
title: 'Project Category',
type: 'string',
options: {
list: [
{ title: 'Web Application', value: 'Web Application' },
{ title: 'Mobile Application', value: 'Mobile Application' },
{ title: 'System / Record Management', value: 'System / Record Management' },
{ title: 'AI / Machine Learning', value: 'AI / Machine Learning' },
{ title: 'Full Stack', value: 'Full Stack' },
{ title: 'Open Source', value: 'Open Source' },
{ title: 'Tools & Utilities', value: 'Tools & Utilities' }
]
}
}),
defineField({
name: 'status',
title: 'Project Status',
type: 'string',
options: {
list: [
{ title: 'LIVE PRODUCTION', value: 'LIVE PRODUCTION' },
{ title: 'IN DEVELOPMENT', value: 'IN DEVELOPMENT' },
{ title: 'COMPLETED', value: 'COMPLETED' },
{ title: 'PROTOTYPE', value: 'PROTOTYPE' },
{ title: 'ARCHIVED', value: 'ARCHIVED' }
]
},
initialValue: 'LIVE PRODUCTION'
}),
defineField({
name: 'summary',
title: 'Card Summary',
type: 'text',
rows: 2,
validation: Rule => Rule.required(),
description: 'Short 1-2 sentence overview displayed on project cards'
}),
defineField({
name: 'description',
title: 'Detailed Description',
type: 'text',
rows: 5,
validation: Rule => Rule.required(),
description: 'Comprehensive overview for project showcase page'
}),
defineField({
name: 'coverImage',
title: 'Cover / Banner Image',
type: 'image',
options: {
hotspot: true
}
}),
defineField({
name: 'gallery',
title: 'Project Screenshots / Gallery',
type: 'array',
of: [{ type: 'image', options: { hotspot: true } }],
description: 'Additional preview screenshots for the project details page'
}),
defineField({
name: 'technologies',
title: 'Technologies & Frameworks',
type: 'array',
of: [{ type: 'string' }],
validation: Rule => Rule.required(),
description: 'e.g. PHP, MySQL, React, JavaScript, Bootstrap, Tailwind, Node.js'
}),
defineField({
name: 'repositoryUrl',
title: 'Source Code / Repository URL',
type: 'url',
description: 'e.g. https://github.com/ssbg04/repo-name'
}),
defineField({
name: 'liveUrl',
title: 'Live Demo / Production URL',
type: 'url',
description: 'e.g. https://my-app.vercel.app'
}),
defineField({
name: 'featured',
title: 'Featured (Show on Homepage)',
type: 'boolean',
initialValue: true
}),
defineField({
name: 'order',
title: 'Display Order',
type: 'number',
initialValue: 1
}),
defineField({
name: 'publishedAt',
title: 'Published Date / Year',
type: 'string',
initialValue: '2026',
description: 'e.g. 2026 or 2026-05'
})
],
preview: {
select: {
title: 'title',
subtitle: 'category',
media: 'coverImage'
}
}
})
+111
View File
@@ -0,0 +1,111 @@
import { defineType, defineField } from 'sanity'
export default defineType({
name: 'siteSettings',
title: 'Site Settings',
type: 'document',
fields: [
defineField({
name: 'fullName',
title: 'Full Name',
type: 'string',
validation: Rule => Rule.required()
}),
defineField({
name: 'title',
title: 'Professional Title',
type: 'string',
validation: Rule => Rule.required()
}),
defineField({
name: 'valueProposition',
title: 'Value Proposition / Hero Headline',
type: 'text',
validation: Rule => Rule.required(),
description: 'Short introduction displayed on the hero section'
}),
defineField({
name: 'isAvailable',
title: 'Available for New Projects / Roles',
type: 'boolean',
initialValue: true,
description: 'Controls the live pulsating status indicator'
}),
defineField({
name: 'availabilityStatus',
title: 'Availability Status Text',
type: 'string',
initialValue: 'Available for new projects',
description: 'e.g. Available for new projects, Open to Engineering Roles'
}),
defineField({
name: 'biography',
title: 'Biography Paragraphs',
type: 'array',
of: [{ type: 'text', rows: 3 }],
description: 'Paragraphs for the About section.'
}),
defineField({
name: 'heroImage',
title: 'Profile Picture / Hero Image (Day / Primary)',
type: 'image',
options: {
hotspot: true
}
}),
defineField({
name: 'heroImageNight',
title: 'Profile Picture (Night / Dark Mode Optional)',
type: 'image',
options: {
hotspot: true
},
description: 'Optional image to show when dark mode is enabled'
}),
defineField({
name: 'email',
title: 'Contact Email',
type: 'string',
validation: Rule => Rule.required()
}),
defineField({
name: 'location',
title: 'Location',
type: 'string',
validation: Rule => Rule.required(),
description: 'e.g. Laguna, Philippines'
}),
defineField({
name: 'resumeAsset',
title: 'Resume / CV Document Upload (PDF)',
type: 'file',
description: 'Upload your latest CV or Resume directly as a PDF file'
}),
defineField({
name: 'resumeFile',
title: 'Resume File Link (or Static Fallback URL)',
type: 'string',
initialValue: '/CV-Cris-Charles-Garcia.pdf',
description: 'URL or path to fallback resume (e.g. /CV-Cris-Charles-Garcia.pdf or Google Drive link)'
}),
defineField({
name: 'seoTitle',
title: 'Default SEO Title Override',
type: 'string',
description: 'Custom browser tab title prefix'
}),
defineField({
name: 'seoDescription',
title: 'Default SEO Meta Description',
type: 'text',
description: 'Meta description for search engines and social shares'
})
],
preview: {
select: {
title: 'fullName',
subtitle: 'title',
media: 'heroImage'
}
}
})
+59
View File
@@ -0,0 +1,59 @@
import { defineType, defineField } from 'sanity'
export default defineType({
name: 'skill',
title: 'Skill & Technology',
type: 'document',
fields: [
defineField({
name: 'name',
title: 'Skill / Technology Name',
type: 'string',
validation: Rule => Rule.required(),
description: 'e.g. PHP, Node.js, React, MySQL, Flutter, Docker, Git'
}),
defineField({
name: 'category',
title: 'Category',
type: 'string',
options: {
list: [
{ title: 'Frontend', value: 'Frontend' },
{ title: 'Backend', value: 'Backend' },
{ title: 'Mobile', value: 'Mobile' },
{ title: 'Database', value: 'Database' },
{ title: 'DevOps & Cloud', value: 'DevOps' },
{ title: 'Cybersecurity', value: 'Cybersecurity' },
{ title: 'AI/ML', value: 'AI/ML' },
{ title: 'Tools & Workflow', value: 'Tools' }
]
},
validation: Rule => Rule.required()
}),
defineField({
name: 'proficiency',
title: 'Proficiency Percentage (0-100)',
type: 'number',
initialValue: 85,
validation: Rule => Rule.min(0).max(100)
}),
defineField({
name: 'icon',
title: 'Icon Name / SVG Reference',
type: 'string',
description: 'e.g. react, nodejs, mysql, php, flutter'
}),
defineField({
name: 'order',
title: 'Display Order',
type: 'number',
initialValue: 1
})
],
preview: {
select: {
title: 'name',
subtitle: 'category'
}
}
})
+75
View File
@@ -0,0 +1,75 @@
import { defineType, defineField } from 'sanity'
export default defineType({
name: 'socialLink',
title: 'Social Link & Directory Profile',
type: 'document',
fields: [
defineField({
name: 'platform',
title: 'Platform Name',
type: 'string',
validation: Rule => Rule.required(),
description: 'e.g. GitHub, LinkedIn, Facebook, TikTok, Email, X (Twitter), YouTube, Discord'
}),
defineField({
name: 'url',
title: 'Target URL',
type: 'string',
validation: Rule => Rule.required(),
description: 'e.g. https://github.com/ssbg04 or mailto:crischarlesgarcia345@gmail.com'
}),
defineField({
name: 'icon',
title: 'Icon Key / Reference',
type: 'string',
options: {
list: [
{ title: 'GitHub', value: 'github' },
{ title: 'LinkedIn', value: 'linkedin' },
{ title: 'Facebook', value: 'facebook' },
{ title: 'TikTok', value: 'tiktok' },
{ title: 'Email / Mail', value: 'mail' },
{ title: 'X / Twitter', value: 'twitter' },
{ title: 'Messenger', value: 'messenger' },
{ title: 'YouTube', value: 'youtube' },
{ title: 'Instagram', value: 'instagram' },
{ title: 'Discord', value: 'discord' },
{ title: 'Generic Web Link', value: 'link' }
]
},
initialValue: 'link',
description: 'Select platform icon or type key'
}),
defineField({
name: 'order',
title: 'Display Order',
type: 'number',
initialValue: 1
}),
defineField({
name: 'showInHero',
title: 'Show in Hero Section',
type: 'boolean',
initialValue: true
}),
defineField({
name: 'showInFooter',
title: 'Show in Footer Section',
type: 'boolean',
initialValue: true
}),
defineField({
name: 'showInLinksPage',
title: 'Show in /links NFC Directory Page',
type: 'boolean',
initialValue: true
})
],
preview: {
select: {
title: 'platform',
subtitle: 'url'
}
}
})
+62
View File
@@ -0,0 +1,62 @@
import { defineType, defineField } from 'sanity'
export default defineType({
name: 'testimonial',
title: 'Testimonial & Recommendation',
type: 'document',
fields: [
defineField({
name: 'name',
title: 'Person Name',
type: 'string',
validation: Rule => Rule.required()
}),
defineField({
name: 'role',
title: 'Role / Designation',
type: 'string',
validation: Rule => Rule.required(),
description: 'e.g. Lead Engineer, Academic Advisor, Client'
}),
defineField({
name: 'company',
title: 'Company / Organization',
type: 'string',
validation: Rule => Rule.required()
}),
defineField({
name: 'avatar',
title: 'Avatar / Photo',
type: 'image',
options: {
hotspot: true
}
}),
defineField({
name: 'quote',
title: 'Recommendation / Quote Text',
type: 'text',
rows: 4,
validation: Rule => Rule.required()
}),
defineField({
name: 'relationship',
title: 'Context / Project Association',
type: 'string',
description: 'e.g. Collaborated on TIS Management System'
}),
defineField({
name: 'order',
title: 'Display Order',
type: 'number',
initialValue: 1
})
],
preview: {
select: {
title: 'name',
subtitle: 'company',
media: 'avatar'
}
}
})
+18
View File
@@ -0,0 +1,18 @@
{
"compilerOptions": {
"target": "es2020",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx"
},
"include": ["**/*.ts", "**/*.tsx"]
}
+5 -61
View File
@@ -1,71 +1,15 @@
import React, { useState, useEffect } from 'react';
import ScrollReveal from './ScrollReveal';
export interface Certification {
id: string;
code: string;
title: string;
issuer: string;
description: string;
badgeImage: string;
badgeUrl: string;
category: string;
skills: string[];
}
const credlyCerts: Certification[] = [
{
id: 'computer-hardware',
code: '01',
title: 'Computer Hardware Basics',
issuer: 'Cisco',
description: 'Hardware diagnostics, mobile device architectures, component installation, preventative maintenance, and troubleshooting tools.',
badgeImage: 'https://images.credly.com/images/19e742ef-13be-4d26-87ed-ac8f5fd0643c/linkedin_thumb_image.png',
badgeUrl: 'https://www.credly.com/badges/d2f0c176-2e45-4b99-88e2-fa9fb9ff34c6/public_url',
category: 'Hardware & Systems',
skills: ['PC Architecture', 'Component Diagnostics', 'Hardware Assembly', 'System Repair']
},
{
id: 'digital-awareness',
code: '02',
title: 'Digital Awareness',
issuer: 'Cisco × OpenEDG',
description: 'Digital tools ecosystem, online safety protocols, data management, and ethical technology practices in modern computing.',
badgeImage: 'https://images.credly.com/images/29e7c859-4719-4081-a12f-6bdc073a43d2/linkedin_thumb_image.png',
badgeUrl: 'https://www.credly.com/badges/20ec510f-8325-483c-b16e-2ca00ae660ae/public_url',
category: 'Digital Literacy',
skills: ['Digital Privacy', 'Cyber Hygiene', 'Content Ethics', 'Security Best Practices']
},
{
id: 'endpoint-security',
code: '03',
title: 'Endpoint Security',
issuer: 'Cisco',
description: 'Host and OS hardening, endpoint telemetry, malware detection, network segmentation, and defense-in-depth principles.',
badgeImage: 'https://images.credly.com/images/0ca5f542-fb5e-4a22-9b7a-c1a1ce4c3db7/linkedin_thumb_EndpointSecurity.png',
badgeUrl: 'https://www.credly.com/badges/2aae16d4-b16c-474f-a84e-f06c330193cd/public_url',
category: 'Cybersecurity',
skills: ['Endpoint Hardening', 'Threat Telemetry', 'OS Security', 'Mitigation Protocols']
},
{
id: 'intro-cybersecurity',
code: '04',
title: 'Introduction to Cybersecurity',
issuer: 'Cisco',
description: 'Global threat landscape, vulnerability assessment methodologies, incident mitigation, and defensive cybersecurity operations.',
badgeImage: 'https://images.credly.com/images/af8c6b4e-fc31-47c4-8dcb-eb7a2065dc5b/linkedin_thumb_I2CS__1_.png',
badgeUrl: 'https://www.credly.com/badges/adc49a5e-87bb-4917-991d-a28160942c13/public_url',
category: 'Cybersecurity',
skills: ['Threat Modeling', 'Vulnerability Assessment', 'Incident Response', 'Network Defense']
},
];
import { mockCertifications, type Certification } from '../lib/data';
interface Props {
certifications?: Certification[];
extraCerts?: Certification[];
}
export default function Certifications({ extraCerts = [] }: Props) {
const allCerts = [...credlyCerts, ...extraCerts];
export default function Certifications({ certifications, extraCerts = [] }: Props) {
const baseCerts = (certifications && certifications.length > 0) ? certifications : mockCertifications;
const allCerts = [...baseCerts, ...extraCerts];
const [selectedCert, setSelectedCert] = useState<Certification | null>(null);
useEffect(() => {
+210 -15
View File
@@ -6,7 +6,10 @@ export interface Project {
slug: string;
summary: string;
description: string;
category?: string;
status?: string;
coverImage?: string;
gallery?: string[];
technologies: string[];
repositoryUrl?: string;
liveUrl?: string;
@@ -17,26 +20,55 @@ export interface Project {
export interface Skill {
name: string;
category: 'Frontend' | 'Backend' | 'Mobile' | 'Database' | 'DevOps' | 'AI/ML' | 'Tools';
category: 'Frontend' | 'Backend' | 'Mobile' | 'Database' | 'DevOps' | 'AI/ML' | 'Tools' | 'Cybersecurity' | string;
proficiency: number; // 0-100
icon?: string;
}
export interface Certification {
id: string;
code: string;
title: string;
issuer: string;
description: string;
badgeImage: string;
badgeUrl: string;
category: string;
skills: string[];
issueDate?: string;
featured?: boolean;
order?: number;
}
export interface Experience {
company: string;
role: string;
employmentType?: string;
location?: string;
startDate: string;
endDate: string; // "Present" or date
description: string[];
technologies?: string[];
}
export interface Education {
institution: string;
degree: string;
fieldOfStudy?: string;
startDate: string;
endDate: string;
location?: string;
achievements?: string[];
order?: number;
}
export interface Testimonial {
name: string;
role: string;
company: string;
avatar?: string;
quote: string;
relationship?: string;
}
export interface SocialLink {
@@ -44,17 +76,25 @@ export interface SocialLink {
url: string;
icon: string;
order: number;
showInHero?: boolean;
showInFooter?: boolean;
showInLinksPage?: boolean;
}
export interface SiteSettings {
fullName: string;
title: string;
valueProposition: string;
isAvailable?: boolean;
availabilityStatus?: string;
biography?: string[];
heroImage?: string;
heroImageNight?: string;
email: string;
location: string;
resumeFile?: string;
seoTitle?: string;
seoDescription?: string;
}
// ==========================================
@@ -65,6 +105,8 @@ export const mockSiteSettings: SiteSettings = {
fullName: "Cris Charles Garcia",
title: "Software Developer",
valueProposition: "I am an IT student majoring in Software Development based in Laguna, Philippines. I focus on building reliable cross platforms applications with AI assisted.",
isAvailable: true,
availabilityStatus: "Available for new projects",
biography: [
"I am a senior IT student majoring in Software Development. I write code to solve real-world problems.",
"My primary stack includes PHP, React, Node.js, and MySQL. I enjoy database design, front-end development, and bug fixing."
@@ -75,23 +117,84 @@ export const mockSiteSettings: SiteSettings = {
};
export const mockSocialLinks: SocialLink[] = [
{ platform: "GitHub", url: "https://github.com/ssbg04", icon: "github", order: 1 },
{ platform: "Facebook", url: "https://www.facebook.com/kristyarls345/", icon: "facebook", order: 2 },
{ platform: "TikTok", url: "https://www.tiktok.com/@sisibigi", icon: "tiktok", order: 3 },
{ platform: "Email", url: "mailto:crischarlesgarcia345@gmail.com", icon: "mail", order: 4 }
{ platform: "GitHub", url: "https://github.com/ssbg04", icon: "github", order: 1, showInHero: true, showInFooter: true, showInLinksPage: true },
{ platform: "Facebook", url: "https://www.facebook.com/kristyarls345/", icon: "facebook", order: 2, showInHero: true, showInFooter: true, showInLinksPage: true },
{ platform: "TikTok", url: "https://www.tiktok.com/@sisibigi", icon: "tiktok", order: 3, showInHero: true, showInFooter: false, showInLinksPage: true },
{ platform: "Email", url: "mailto:crischarlesgarcia345@gmail.com", icon: "mail", order: 4, showInHero: true, showInFooter: true, showInLinksPage: true }
];
export const mockSkills: Skill[] = [
{ name: "HTML5/CSS3/JS", category: "Frontend", proficiency: 90 },
{ name: "React", category: "Frontend", proficiency: 85 },
{ name: "PHP", category: "Backend", proficiency: 85 },
{ name: "Node.js", category: "Backend", proficiency: 85 },
{ name: "Flutter", category: "Mobile", proficiency: 80 },
{ name: "MySQL", category: "Database", proficiency: 85 }
{ name: "MySQL", category: "Database", proficiency: 85 },
{ name: "Git & GitHub", category: "Tools", proficiency: 90 },
{ name: "Cybersecurity Basics", category: "Cybersecurity", proficiency: 80 }
];
export const mockCertifications: Certification[] = [
{
id: 'computer-hardware',
code: '01',
title: 'Computer Hardware Basics',
issuer: 'Cisco',
description: 'Hardware diagnostics, mobile device architectures, component installation, preventative maintenance, and troubleshooting tools.',
badgeImage: 'https://images.credly.com/images/19e742ef-13be-4d26-87ed-ac8f5fd0643c/linkedin_thumb_image.png',
badgeUrl: 'https://www.credly.com/badges/d2f0c176-2e45-4b99-88e2-fa9fb9ff34c6/public_url',
category: 'Hardware & Systems',
skills: ['PC Architecture', 'Component Diagnostics', 'Hardware Assembly', 'System Repair'],
featured: true,
order: 1
},
{
id: 'digital-awareness',
code: '02',
title: 'Digital Awareness',
issuer: 'Cisco × OpenEDG',
description: 'Digital tools ecosystem, online safety protocols, data management, and ethical technology practices in modern computing.',
badgeImage: 'https://images.credly.com/images/29e7c859-4719-4081-a12f-6bdc073a43d2/linkedin_thumb_image.png',
badgeUrl: 'https://www.credly.com/badges/20ec510f-8325-483c-b16e-2ca00ae660ae/public_url',
category: 'Digital Literacy',
skills: ['Digital Privacy', 'Cyber Hygiene', 'Content Ethics', 'Security Best Practices'],
featured: true,
order: 2
},
{
id: 'endpoint-security',
code: '03',
title: 'Endpoint Security',
issuer: 'Cisco',
description: 'Host and OS hardening, endpoint telemetry, malware detection, network segmentation, and defense-in-depth principles.',
badgeImage: 'https://images.credly.com/images/0ca5f542-fb5e-4a22-9b7a-c1a1ce4c3db7/linkedin_thumb_EndpointSecurity.png',
badgeUrl: 'https://www.credly.com/badges/2aae16d4-b16c-474f-a84e-f06c330193cd/public_url',
category: 'Cybersecurity',
skills: ['Endpoint Hardening', 'Threat Telemetry', 'OS Security', 'Mitigation Protocols'],
featured: true,
order: 3
},
{
id: 'intro-cybersecurity',
code: '04',
title: 'Introduction to Cybersecurity',
issuer: 'Cisco',
description: 'Global threat landscape, vulnerability assessment methodologies, incident mitigation, and defensive cybersecurity operations.',
badgeImage: 'https://images.credly.com/images/af8c6b4e-fc31-47c4-8dcb-eb7a2065dc5b/linkedin_thumb_I2CS__1_.png',
badgeUrl: 'https://www.credly.com/badges/adc49a5e-87bb-4917-991d-a28160942c13/public_url',
category: 'Cybersecurity',
skills: ['Threat Modeling', 'Vulnerability Assessment', 'Incident Response', 'Network Defense'],
featured: true,
order: 4
}
];
export const mockProjects: Project[] = [
{
title: "TIS Record Management System",
slug: "tis-record-management-system",
category: "System / Record Management",
status: "LIVE PRODUCTION",
summary: "Comprehensive record and student management platform engineered for institutional efficiency.",
description: "Designed and built to streamline academic data, attendance records, and student profiles with secure role-based access and intuitive data dashboards.",
coverImage: "https://images.unsplash.com/photo-1551288049-bebda4e38f71?auto=format&fit=crop&w=800&q=80",
@@ -108,6 +211,8 @@ export const mockExperience: Experience[] = [
{
company: "Talisay Integrated School",
role: "Programmer",
employmentType: "Internship",
location: "Laguna, Philippines",
startDate: "2025",
endDate: "Present",
description: [
@@ -118,13 +223,33 @@ export const mockExperience: Experience[] = [
}
];
export const mockEducation: Education[] = [
{
institution: "Laguna State Polytechnic University",
degree: "Bachelor of Science in Information Technology",
fieldOfStudy: "Major in Software Development",
startDate: "2022",
endDate: "2026",
location: "Laguna, Philippines",
achievements: [
"Specialized in full-stack software development, database design, and systems architecture.",
"Developed enterprise records and data management projects."
],
order: 1
}
];
// ==========================================
// Sanity CMS Fetch Logic
// ==========================================
export async function getSiteSettings(): Promise<SiteSettings> {
try {
const settings = await sanityClient.fetch(`*[_type == "siteSettings"][0]`);
if (!sanityClient) return mockSiteSettings;
const settings = await sanityClient.fetch(`*[_type == "siteSettings"][0]{
...,
"resumeAssetUrl": resumeAsset.asset->url
}`);
if (!settings) return mockSiteSettings;
// Handle biography whether stored as array of strings, single string, or block content
@@ -137,17 +262,23 @@ export async function getSiteSettings(): Promise<SiteSettings> {
biography = settings.biography.split('\n\n').filter(Boolean);
}
const resumeResolved = settings.resumeAssetUrl ||
(settings.resumeFile ? (typeof settings.resumeFile === 'string' ? settings.resumeFile : urlFor(settings.resumeFile)) : mockSiteSettings.resumeFile);
return {
fullName: settings.fullName || mockSiteSettings.fullName,
title: settings.title || mockSiteSettings.title,
valueProposition: settings.valueProposition || mockSiteSettings.valueProposition,
isAvailable: settings.isAvailable ?? mockSiteSettings.isAvailable,
availabilityStatus: settings.availabilityStatus || mockSiteSettings.availabilityStatus,
biography: biography.length > 0 ? biography : mockSiteSettings.biography,
heroImage: settings.heroImage ? urlFor(settings.heroImage) : undefined,
heroImageNight: settings.heroImageNight ? urlFor(settings.heroImageNight) : undefined,
email: settings.email || mockSiteSettings.email,
location: settings.location || mockSiteSettings.location,
resumeFile: settings.resumeFile
? (typeof settings.resumeFile === 'string' ? settings.resumeFile : urlFor(settings.resumeFile))
: mockSiteSettings.resumeFile
resumeFile: resumeResolved,
seoTitle: settings.seoTitle,
seoDescription: settings.seoDescription
};
} catch (error) {
console.error('Error fetching siteSettings from Sanity:', error);
@@ -157,13 +288,17 @@ export async function getSiteSettings(): Promise<SiteSettings> {
export async function getSocialLinks(): Promise<SocialLink[]> {
try {
if (!sanityClient) return mockSocialLinks;
const links = await sanityClient.fetch(`*[_type == "socialLink"] | order(coalesce(order, 99) asc)`);
if (!links || links.length === 0) return mockSocialLinks;
return links.map((l: any, idx: number) => ({
platform: l.platform || 'Link',
url: l.url || '#',
icon: l.icon || (l.platform ? l.platform.toLowerCase() : 'link'),
order: typeof l.order === 'number' ? l.order : idx + 1
order: typeof l.order === 'number' ? l.order : idx + 1,
showInHero: l.showInHero ?? true,
showInFooter: l.showInFooter ?? true,
showInLinksPage: l.showInLinksPage ?? true
}));
} catch (error) {
console.error('Error fetching socialLink from Sanity:', error);
@@ -173,7 +308,8 @@ export async function getSocialLinks(): Promise<SocialLink[]> {
export async function getSkills(): Promise<Skill[]> {
try {
const skills = await sanityClient.fetch(`*[_type == "skill"]`);
if (!sanityClient) return mockSkills;
const skills = await sanityClient.fetch(`*[_type == "skill"] | order(coalesce(order, 99) asc)`);
if (!skills || skills.length === 0) return mockSkills;
return skills.map((s: any) => ({
name: s.name || 'Skill',
@@ -187,18 +323,51 @@ export async function getSkills(): Promise<Skill[]> {
}
}
export async function getCertifications(): Promise<Certification[]> {
try {
if (!sanityClient) return mockCertifications;
const certs = await sanityClient.fetch(`*[_type == "certification"] | order(coalesce(order, 99) asc)`);
if (!certs || certs.length === 0) return mockCertifications;
return certs.map((c: any, idx: number) => {
const badgeImg = c.badgeImage ? urlFor(c.badgeImage) : (c.badgeImageUrl || '');
return {
id: c._id || `cert-${idx}`,
code: c.code || (idx + 1 < 10 ? `0${idx + 1}` : `${idx + 1}`),
title: c.title || 'Certification',
issuer: c.issuer || 'Credential Issuer',
description: c.description || '',
badgeImage: badgeImg,
badgeUrl: c.badgeUrl || '#',
category: c.category || 'General',
skills: Array.isArray(c.skills) ? c.skills : [],
issueDate: c.issueDate,
featured: c.featured ?? true,
order: typeof c.order === 'number' ? c.order : idx + 1
};
});
} catch (error) {
console.error('Error fetching certification from Sanity:', error);
return mockCertifications;
}
}
export async function getProjects(): Promise<Project[]> {
try {
if (!sanityClient) return mockProjects;
const projects = await sanityClient.fetch(`*[_type == "project"] | order(coalesce(order, 99) asc)`);
if (!projects || projects.length === 0) return mockProjects;
return projects.map((p: any, idx: number) => {
const slugVal = p.slug?.current || (typeof p.slug === 'string' ? p.slug : (p.title ? p.title.toLowerCase().replace(/[^a-z0-9]+/g, '-') : `project-${idx}`));
const gallery = Array.isArray(p.gallery) ? p.gallery.map((img: any) => urlFor(img)).filter(Boolean) : [];
return {
title: p.title || 'Untitled Project',
slug: slugVal,
summary: p.summary || p.description || '',
description: p.description || p.summary || '',
category: p.category || 'Software Project',
status: p.status || 'LIVE PRODUCTION',
coverImage: p.coverImage ? urlFor(p.coverImage) : undefined,
gallery,
technologies: Array.isArray(p.technologies) ? p.technologies : [],
repositoryUrl: p.repositoryUrl,
liveUrl: p.liveUrl,
@@ -215,7 +384,8 @@ export async function getProjects(): Promise<Project[]> {
export async function getExperience(): Promise<Experience[]> {
try {
const experience = await sanityClient.fetch(`*[_type == "experience"] | order(startDate desc)`);
if (!sanityClient) return mockExperience;
const experience = await sanityClient.fetch(`*[_type == "experience"] | order(coalesce(order, 99) asc, startDate desc)`);
if (!experience || experience.length === 0) return mockExperience;
return experience.map((e: any) => {
let desc: string[] = [];
@@ -227,6 +397,8 @@ export async function getExperience(): Promise<Experience[]> {
return {
company: e.company || 'Company',
role: e.role || 'Developer',
employmentType: e.employmentType,
location: e.location,
startDate: e.startDate ? String(e.startDate) : '2025',
endDate: e.endDate ? String(e.endDate) : 'Present',
description: desc.length > 0 ? desc : [e.company || 'Work history details'],
@@ -239,16 +411,39 @@ export async function getExperience(): Promise<Experience[]> {
}
}
export async function getEducation(): Promise<Education[]> {
try {
if (!sanityClient) return mockEducation;
const education = await sanityClient.fetch(`*[_type == "education"] | order(coalesce(order, 99) asc, startDate desc)`);
if (!education || education.length === 0) return mockEducation;
return education.map((ed: any, idx: number) => ({
institution: ed.institution || 'Institution',
degree: ed.degree || 'Degree',
fieldOfStudy: ed.fieldOfStudy,
startDate: ed.startDate ? String(ed.startDate) : '2022',
endDate: ed.endDate ? String(ed.endDate) : 'Present',
location: ed.location,
achievements: Array.isArray(ed.achievements) ? ed.achievements : [],
order: typeof ed.order === 'number' ? ed.order : idx + 1
}));
} catch (error) {
console.error('Error fetching education from Sanity:', error);
return mockEducation;
}
}
export async function getTestimonials(): Promise<Testimonial[]> {
try {
const testimonials = await sanityClient.fetch(`*[_type == "testimonial"]`);
if (!sanityClient) return [];
const testimonials = await sanityClient.fetch(`*[_type == "testimonial"] | order(coalesce(order, 99) asc)`);
if (!testimonials || testimonials.length === 0) return [];
return testimonials.map((t: any) => ({
name: t.name,
role: t.role,
company: t.company,
avatar: t.avatar ? urlFor(t.avatar) : undefined,
quote: t.quote
quote: t.quote,
relationship: t.relationship
}));
} catch (error) {
console.error('Error fetching testimonial from Sanity:', error);
+3 -2
View File
@@ -5,11 +5,12 @@ import AboutDetails from '../components/About';
import SkillsList from '../components/Skills';
import Certifications from '../components/Certifications';
import Footer from '../components/Footer';
import { getSiteSettings, getExperience, getSkills } from '../lib/data';
import { getSiteSettings, getExperience, getSkills, getCertifications } from '../lib/data';
const settings = await getSiteSettings();
const experience = await getExperience();
const skills = await getSkills();
const certifications = await getCertifications();
---
<Layout title={`About | ${settings.fullName}`} description="Learn more about Cris Charles Garcia's professional history, skills matrix, and developer biography.">
@@ -18,7 +19,7 @@ const skills = await getSkills();
<main class="flex-1 pt-32 pb-24">
<AboutDetails settings={settings} experience={experience} client:load />
<SkillsList skills={skills} client:visible />
<Certifications client:visible />
<Certifications certifications={certifications} client:visible />
</main>
<Footer fullName={settings.fullName} client:load />
+3 -2
View File
@@ -3,9 +3,10 @@ import Layout from '../layouts/Layout.astro';
import Navbar from '../components/Navbar';
import Certifications from '../components/Certifications';
import Footer from '../components/Footer';
import { getSiteSettings } from '../lib/data';
import { getSiteSettings, getCertifications } from '../lib/data';
const settings = await getSiteSettings();
const certifications = await getCertifications();
---
<Layout
@@ -30,7 +31,7 @@ const settings = await getSiteSettings();
</div>
</section>
<Certifications client:visible />
<Certifications certifications={certifications} client:visible />
</main>
<Footer fullName={settings.fullName} client:load />
+3 -1
View File
@@ -14,6 +14,7 @@ import {
getSiteSettings,
getProjects,
getSkills,
getCertifications,
getExperience,
getTestimonials,
getSocialLinks
@@ -26,6 +27,7 @@ const featuredProjects = allProjects.length > 0
? (allProjects.some(p => p.featured) ? allProjects.filter(p => p.featured) : allProjects)
: [];
const skills = await getSkills();
const certifications = await getCertifications();
const experience = await getExperience();
const testimonials = await getTestimonials();
const socialLinks = await getSocialLinks();
@@ -50,7 +52,7 @@ const socialLinks = await getSocialLinks();
<Skills skills={skills} client:visible />
<Certifications client:visible />
<Certifications certifications={certifications} client:visible />
<About settings={settings} experience={experience} client:visible />