feat(sanity): add dynamic studio schemas, certifications, education, and frontend data integration
This commit is contained in:
1 parent
cf0cbd9a1f
commit
978711a16a
21 files changed
+14940
-83
No files matched your search
+3
-2
@@ -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
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
# Sanity Studio ignore rules
|
||||
node_modules/
|
||||
.env*
|
||||
.sanity/
|
||||
dist/
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.DS_Store
|
||||
Generated
+13902
File diff suppressed because it is too large.
Load diff
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
}
|
||||
})
|
||||
@@ -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,
|
||||
},
|
||||
})
|
||||
@@ -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'
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -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'
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -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'
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -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
|
||||
]
|
||||
@@ -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'
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -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'
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -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'
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -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'
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -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'
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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,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 />
|
||||
|
||||
@@ -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 />
|
||||
|
||||
|
||||
Reference in new issue
Block a user