diff --git a/app/Home.module.css b/app/Home.module.css index 85a3b2f..a0f642b 100644 --- a/app/Home.module.css +++ b/app/Home.module.css @@ -13,16 +13,13 @@ isolation: isolate; overflow: hidden; color: var(--ocean-text); - background: - radial-gradient(circle at 18% 4%, oklch(0.76 0.1 206 / 18%), transparent 28rem), - radial-gradient(circle at 84% 22%, oklch(0.5 0.12 226 / 12%), transparent 32rem), - linear-gradient( - 180deg, - var(--ocean-surface) 0%, - var(--ocean-mid) 25%, - var(--ocean-deep) 58%, - var(--ocean-abyss) 100% - ); + background: linear-gradient( + 180deg, + var(--ocean-surface) 0%, + var(--ocean-mid) 25%, + var(--ocean-deep) 58%, + var(--ocean-abyss) 100% + ); } .lightRaysLayer { @@ -38,29 +35,6 @@ position: relative; z-index: 1; padding-block: clamp(3.5rem, 7vw, 6rem); - border-top: 1px solid var(--ocean-border); -} - -.introBand { - background: - linear-gradient(90deg, oklch(0.42 0.095 220 / 18%), transparent 55%), - linear-gradient(180deg, oklch(0.73 0.07 205 / 9%), transparent 70%); -} - -.projectsBand { - background: linear-gradient(90deg, transparent, oklch(0.26 0.075 248 / 24%) 50%, transparent); -} - -.blogBand { - background: - radial-gradient(circle at 12% 12%, oklch(0.33 0.08 236 / 16%), transparent 22rem), - linear-gradient(180deg, oklch(0.08 0.03 262 / 6%), transparent 58%); -} - -.contactBand { - background: - radial-gradient(ellipse at 52% 100%, oklch(0.14 0.04 250 / 26%), transparent 32rem), - linear-gradient(180deg, transparent, oklch(0.045 0.022 265 / 36%)); } .split, @@ -193,6 +167,8 @@ --lure-y: 35.4%; --ghost-x: 50%; --ghost-y: 50%; + --angler-reveal-duration: 820ms; + --angler-reveal-ease: cubic-bezier(0.22, 1, 0.36, 1); position: relative; display: grid; min-height: clamp(16rem, 26vw, 20rem); @@ -278,7 +254,7 @@ width: min(14rem, 48vw); aspect-ratio: 1086 / 1448; transform: translate(calc(50% - var(--lure-x)), calc(50% - var(--lure-y))); - transition: transform 180ms ease; + transition: transform var(--angler-reveal-duration) var(--angler-reveal-ease); } .anglerImageFrame::before, @@ -303,7 +279,7 @@ radial-gradient(circle, oklch(0.82 0.12 205 / 18%), transparent 62%); filter: blur(0.16rem); mix-blend-mode: screen; - transition: opacity 180ms ease; + transition: opacity var(--angler-reveal-duration) var(--angler-reveal-ease); } .anglerImageFrame::after { @@ -335,8 +311,8 @@ opacity: 0; pointer-events: none; transition: - opacity 180ms ease, - filter 180ms ease; + opacity var(--angler-reveal-duration) var(--angler-reveal-ease), + filter var(--angler-reveal-duration) var(--angler-reveal-ease); } .anglerContact:hover .anglerImage, @@ -419,9 +395,9 @@ opacity: 0; transform: translateY(0.75rem); transition: - opacity 180ms ease, - transform 180ms ease, - visibility 180ms ease; + opacity var(--angler-reveal-duration) var(--angler-reveal-ease), + transform var(--angler-reveal-duration) var(--angler-reveal-ease), + visibility var(--angler-reveal-duration) var(--angler-reveal-ease); } .anglerContact:hover .contactLinks, diff --git a/app/about/About.module.css b/app/about/About.module.css new file mode 100644 index 0000000..08f7531 --- /dev/null +++ b/app/about/About.module.css @@ -0,0 +1,380 @@ +.page { + padding-block: 0 var(--section-space); +} + +.hero { + position: relative; + display: grid; + min-height: clamp(17.5rem, 32vw, 25rem); + margin-bottom: 2rem; + overflow: hidden; + background-color: black; +} + +.hero::after { + position: absolute; + inset: 0; + z-index: 1; + content: ''; + background: + linear-gradient(90deg, rgb(0 0 0 / 72%), rgb(0 0 0 / 34%) 46%, rgb(0 0 0 / 16%)), + linear-gradient(180deg, rgb(0 0 0 / 18%), transparent 38%, rgb(0 0 0 / 22%)); +} + +.heroMedia { + position: absolute; + inset-block: 0; + left: 50%; + width: min(100%, calc(var(--page-width) + var(--space-page) * 2)); + overflow: hidden; + transform: translateX(-50%); +} + +.heroMedia::after { + position: absolute; + inset: 0; + z-index: 1; + pointer-events: none; + content: ''; + background: + linear-gradient(90deg, black, transparent 9%, transparent 91%, black), + linear-gradient(180deg, black, transparent 14%, transparent 86%, black); +} + +.heroMedia img { + position: relative; + z-index: 0; + width: 100%; + height: 100%; + object-fit: cover; + object-position: center; +} + +.heroContent { + position: relative; + z-index: 2; + display: grid; + gap: 0.95rem; + align-content: center; + justify-items: start; + isolation: isolate; + padding-block: clamp(2rem, 5vw, 4rem); + text-align: left; +} + +.heroContent::before { + position: absolute; + inset-block: clamp(1.2rem, 3vw, 2.4rem); + left: var(--space-page); + z-index: 0; + width: min(58rem, calc(100% - var(--space-page) * 2)); + content: ''; + background: linear-gradient(90deg, rgb(0 0 0 / 70%), rgb(0 0 0 / 48%) 68%, transparent); +} + +.heroContent > * { + position: relative; + z-index: 1; +} + +.hero .kicker { + color: oklch(0.8 0.17 210); +} + +.kicker { + margin-bottom: 0; + color: var(--primary); + font-family: var(--font-geist-mono), monospace; + font-size: 0.76rem; + font-weight: 720; + letter-spacing: 0.12em; + text-transform: uppercase; +} + +.hero h1 { + max-width: 18ch; + margin-bottom: 0; + color: white; + font-size: clamp(1.9rem, 4vw, 3.2rem); + line-height: 1.02; + text-shadow: 0 0.25rem 1.4rem rgb(0 0 0 / 72%); + text-wrap: balance; +} + +.introCopy { + display: grid; + max-width: 52rem; + gap: 0.85rem; +} + +.introCopy p { + margin-bottom: 0; + color: var(--muted-foreground); + font-size: clamp(1rem, 1.4vw, 1.12rem); + line-height: 1.8; +} + +.hero .introCopy p { + color: oklch(0.88 0.025 220); + text-shadow: 0 0.18rem 1rem rgb(0 0 0 / 76%); +} + +.content { + display: grid; + gap: clamp(2.8rem, 6vw, 5rem); +} + +.timelineSection { + position: relative; + display: grid; + grid-template-columns: minmax(12rem, 0.42fr) minmax(0, 1fr); + gap: clamp(1.5rem, 5vw, 4rem); + align-items: start; +} + +.sectionIntro { + display: grid; + gap: 0.75rem; +} + +.timeline { + position: relative; + display: grid; + gap: 1.35rem; + padding: 0; + margin: 0; + list-style: none; +} + +.timeline::before { + position: absolute; + top: 0.45rem; + bottom: 0.45rem; + left: 12rem; + width: 1px; + content: ''; + background: linear-gradient(180deg, transparent, var(--border-strong), transparent); +} + +.timelineItem { + position: relative; + display: grid; + grid-template-columns: 10.8rem minmax(0, 1fr); + gap: 3rem; + align-items: start; +} + +.timelineItemWithMedia { + cursor: default; +} + +.timelineItem::before { + position: absolute; + top: 0.34rem; + left: 11.75rem; + z-index: 1; + width: 0.55rem; + height: 0.55rem; + content: ''; + background: var(--accent); + border: 2px solid var(--background); + border-radius: 999px; + box-shadow: 0 0 0 0.35rem color-mix(in oklch, var(--accent) 20%, transparent); +} + +.timelineYear { + color: var(--faint-foreground); + font-family: var(--font-geist-mono), monospace; + font-size: 0.78rem; + font-weight: 700; + line-height: 1.25; + transition: + color 180ms ease, + text-shadow 180ms ease, + transform 420ms cubic-bezier(0.22, 1, 0.36, 1); + transform-origin: right center; + white-space: nowrap; +} + +.timelineItem:hover .timelineYear, +.timelineItem:focus-within .timelineYear, +.timelineItemMediaOpen .timelineYear { + color: var(--foreground); + text-shadow: 0 0 1rem color-mix(in oklch, var(--accent) 62%, transparent); + transform: scale(1.04); +} + +.timelineCard { + position: relative; + padding: 0; + transition: transform 420ms cubic-bezier(0.22, 1, 0.36, 1); + transform-origin: left top; +} + +.timelineItem:hover .timelineCard, +.timelineItem:focus-within .timelineCard { + transform: scale(1.015); +} + +.timelineCard h3 { + margin-bottom: 0; + font-size: 1.05rem; + line-height: 1.25; +} + +.timelineCard h3 a { + display: inline-flex; + gap: 0.42rem; + align-items: baseline; + color: inherit; + text-decoration: none; + text-underline-offset: 0.24em; + transition: + color 180ms ease, + text-decoration-color 180ms ease; +} + +.timelineCard h3 a:hover, +.timelineCard h3 a:focus-visible { + color: var(--primary); + text-decoration: underline; + text-decoration-color: color-mix(in oklch, var(--primary) 70%, transparent); +} + +.timelineCard h3 a:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 0.22rem; + border-radius: 0.2rem; +} + +.timelineTitleLinkIcon { + display: inline-grid; + width: 1.05rem; + height: 1.05rem; + flex: 0 0 auto; + color: var(--primary); + font-size: 0.76rem; + line-height: 1; + background: color-mix(in oklch, var(--primary) 14%, transparent); + border: 1px solid color-mix(in oklch, var(--primary) 32%, transparent); + border-radius: 999px; + place-items: center; + transform: translateY(-0.06rem); +} + +.timelineCard p { + margin-top: 0.65rem; + margin-bottom: 0; + color: var(--muted-foreground); + line-height: 1.7; +} + +.timelineMediaPreview { + position: absolute; + top: 50%; + right: calc(100% + clamp(1.5rem, 5vw, 4rem)); + z-index: 20; + width: 200px; + height: 200px; + overflow: hidden; + pointer-events: none; + background: color-mix(in oklch, var(--surface) 88%, transparent); + border: 1px solid color-mix(in oklch, var(--border-strong) 74%, transparent); + border-radius: var(--radius-md); + box-shadow: 0 1.6rem 4rem color-mix(in oklch, var(--background) 72%, transparent); + opacity: 0; + backdrop-filter: blur(18px) saturate(130%); + transform: translateY(-50%) translateX(-0.7rem) scale(0.98); + transition: + opacity 180ms ease, + transform 260ms cubic-bezier(0.22, 1, 0.36, 1); +} + +.timelineItemMediaOpen .timelineMediaPreview { + opacity: 1; + pointer-events: auto; + transform: translateY(-50%) translateX(0) scale(1); +} + +.timelineMediaPreview img { + width: 100%; + height: 100%; + object-fit: cover; + object-position: center; +} + +.timelineMediaPreview .timelineMediaImageContain { + object-fit: contain; + padding: 0.55rem; + background: color-mix(in oklch, var(--surface-raised) 78%, transparent); +} + +.timelineMediaArrow { + position: absolute; + top: 50%; + z-index: 1; + display: grid; + width: 1.85rem; + height: 1.85rem; + padding: 0; + color: var(--foreground); + cursor: pointer; + background: color-mix(in oklch, var(--background) 54%, transparent); + border: 1px solid color-mix(in oklch, var(--border-strong) 70%, transparent); + border-radius: 999px; + box-shadow: 0 0.5rem 1.2rem var(--shadow-color); + transform: translateY(-50%); + place-items: center; + backdrop-filter: blur(10px); +} + +.timelineMediaArrow span { + transform: translateY(-0.05rem); + font-size: 1.25rem; + line-height: 1; +} + +.timelineMediaArrowPrevious { + left: 0.45rem; +} + +.timelineMediaArrowNext { + right: 0.45rem; +} + +.timelineMediaArrow:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 0.16rem; +} + +@media (max-width: 56rem) { + .timelineSection { + grid-template-columns: 1fr; + } +} + +@media (max-width: 56.0625rem) { + .timelineMediaPreview { + display: none; + } +} + +@media (max-width: 34rem) { + .timeline::before { + left: 0.28rem; + } + + .timelineItem { + grid-template-columns: 1fr; + gap: 0.55rem; + padding-left: 1.6rem; + } + + .timelineItem::before { + left: 0; + } + + .timelineYear { + line-height: 1.2; + } +} diff --git a/app/about/TimelineItem.tsx b/app/about/TimelineItem.tsx new file mode 100644 index 0000000..3e0e3f8 --- /dev/null +++ b/app/about/TimelineItem.tsx @@ -0,0 +1,109 @@ +'use client' + +import { useEffect, useId, useRef, useState } from 'react' +import TimelineMediaCarousel from './TimelineMediaCarousel' +import styles from './About.module.css' + +type TimelineMedia = { + src: string + alt: string + width: number + height: number + fit?: 'cover' | 'contain' +} + +type TimelineItemData = { + year: string + title: string + titleHref?: string + description: string + media?: TimelineMedia[] +} + +type TimelineItemProps = { + item: TimelineItemData +} + +export default function TimelineItem({ item }: TimelineItemProps) { + const [mediaOpen, setMediaOpen] = useState(false) + const itemId = useId() + const itemRef = useRef(null) + const hasMedia = Boolean(item.media?.length) + + function activateItem() { + window.dispatchEvent(new CustomEvent('about-media-preview-open', { detail: itemId })) + + if (hasMedia) { + setMediaOpen(true) + } + } + + useEffect(() => { + function closeWhenAnotherPreviewOpens(event: Event) { + if ((event as CustomEvent).detail !== itemId) { + setMediaOpen(false) + } + } + + window.addEventListener('about-media-preview-open', closeWhenAnotherPreviewOpens) + + return () => { + window.removeEventListener('about-media-preview-open', closeWhenAnotherPreviewOpens) + } + }, [itemId]) + + useEffect(() => { + if (!mediaOpen) { + return + } + + function closeOnOutsidePointerDown(event: PointerEvent) { + if (!itemRef.current?.contains(event.target as Node)) { + setMediaOpen(false) + } + } + + function closeOnEscape(event: KeyboardEvent) { + if (event.key === 'Escape') { + setMediaOpen(false) + } + } + + document.addEventListener('pointerdown', closeOnOutsidePointerDown) + document.addEventListener('keydown', closeOnEscape) + + return () => { + document.removeEventListener('pointerdown', closeOnOutsidePointerDown) + document.removeEventListener('keydown', closeOnEscape) + } + }, [mediaOpen]) + + return ( +
  • +
    {item.year}
    +
    +

    + {item.titleHref ? ( + + {item.title} + + + ) : ( + item.title + )} +

    +

    {item.description}

    +
    + {item.media ? : null} +
  • + ) +} diff --git a/app/about/TimelineMediaCarousel.tsx b/app/about/TimelineMediaCarousel.tsx new file mode 100644 index 0000000..7875035 --- /dev/null +++ b/app/about/TimelineMediaCarousel.tsx @@ -0,0 +1,68 @@ +'use client' + +import Image from 'next/image' +import { useState } from 'react' +import styles from './About.module.css' + +type TimelineMedia = { + src: string + alt: string + width: number + height: number + fit?: 'cover' | 'contain' +} + +type TimelineMediaCarouselProps = { + images: TimelineMedia[] + title: string +} + +export default function TimelineMediaCarousel({ images, title }: TimelineMediaCarouselProps) { + const [activeIndex, setActiveIndex] = useState(0) + const activeImage = images[activeIndex] + const hasMultipleImages = images.length > 1 + + function showPrevious() { + setActiveIndex((currentIndex) => (currentIndex === 0 ? images.length - 1 : currentIndex - 1)) + } + + function showNext() { + setActiveIndex((currentIndex) => (currentIndex === images.length - 1 ? 0 : currentIndex + 1)) + } + + return ( +
    + {hasMultipleImages ? ( + + ) : null} + {activeImage.alt} + {hasMultipleImages ? ( + + ) : null} +
    + ) +} diff --git a/app/about/page.tsx b/app/about/page.tsx index ac45c76..59d3eca 100644 --- a/app/about/page.tsx +++ b/app/about/page.tsx @@ -1,10 +1,202 @@ import type { Metadata } from 'next' +import TimelineItem from './TimelineItem' +import PageContainer from '../components/PageContainer' +import styles from './About.module.css' export const metadata: Metadata = { title: 'About', - description: 'About Alper.', + description: 'Background, technical interests, and learning focus for Alper Makcan.', } +type TimelineMedia = { + src: string + alt: string + width: number + height: number + fit?: 'cover' | 'contain' +} + +type TimelineEntry = { + year: string + title: string + titleHref?: string + description: string + media?: TimelineMedia[] +} + +const timeline: TimelineEntry[] = [ + { + year: '2022', + title: 'Built computer science foundations with CS50', + description: + "Followed Harvard's CS50 coursework to build a stronger base in programming, algorithms, and how software problems are approached.", + media: [ + { + src: '/about/cs50-harvard.webp', + alt: 'Harvard CS50 course certificate', + width: 800, + height: 400, + fit: 'contain', + }, + ], + }, + { + year: 'Jul 2023', + title: 'Graduated from Yildiz Technical University', + description: + 'Completed a degree in Naval Architecture and Marine Engineering, carrying forward an engineering mindset shaped by systems, constraints, and practical problem solving.', + media: [ + { + src: '/about/ytu-gemi.webp', + alt: 'Yildiz Technical University naval architecture graduation', + width: 447, + height: 447, + }, + ], + }, + { + year: 'Aug - Nov 2023', + title: 'Worked as a project engineer at HAT-SAN Shipyard', + description: + 'Worked in a project engineering role, gaining experience with coordination, technical documentation, and the discipline of delivering within a real engineering environment.', + media: [ + { src: '/about/hatsan-1.webp', alt: 'HAT-SAN Shipyard project view', width: 577, height: 635 }, + { src: '/about/hatsan-2.webp', alt: 'HAT-SAN Shipyard engineering work', width: 690, height: 507 }, + { src: '/about/hatsan-3.webp', alt: 'HAT-SAN Shipyard vessel detail', width: 428, height: 675 }, + ], + }, + { + year: 'Early 2024', + title: 'Returned to core computer science topics', + description: + 'Before specializing in web development, studied introductory computer science, computer architecture, and programming language principles through courses by Prof. Dr. Kemal Bicakci and Oguz Ergin.', + }, + { + year: 'Sep - Feb 2025', + title: 'Moved into full-stack web development', + description: + 'Started progressing seriously in web development through the Workintech full-stack program, working across frontend, backend, and practical product-oriented exercises.', + }, + { + year: 'Mar 2025', + title: 'Joined a JavaScript crash course', + titleHref: 'https://youtube.com/playlist?list=PL9pDl_Oth4cqVnLrf5DCK4a_HhoAEhV4a&si=enr22xt20aehCWmd', + description: + 'Participated in Armagan Amcalar and Women Techmakers Berlin JavaScript Crash Course, reinforcing modern JavaScript fundamentals through focused practice.', + }, + { + year: 'Apr 2025', + title: 'Continued with nimble.dev JavaScript learning', + titleHref: 'https://www.nimble.dev/', + description: + "Followed Armagan Amcalar's nimble.dev JavaScript course and continued learning from the nimble broadcast archive to deepen language and engineering judgment.", + }, + { + year: 'Jun 2025', + title: 'Followed The Odin Project full-stack roadmap', + titleHref: 'https://www.theodinproject.com/', + description: + 'Worked through The Odin Project full-stack path, with a focus on React for frontend work and Node.js for backend fundamentals.', + }, + { + year: 'Sep 2025', + title: 'Studied OOP through Deitel', + description: + "Used Deitel's C How to Program, including its C++ material, as an entry point for object-oriented programming principles and lower-level programming practice.", + media: [ + { + src: '/about/deitel-c-how-to-program.webp', + alt: "Deitel's C How to Program book cover", + width: 762, + height: 1000, + fit: 'contain', + }, + ], + }, + { + year: 'Jan 2026', + title: 'Earned AWS Certified Cloud Practitioner', + titleHref: 'https://cp.certmetrics.com/amazon/en/public/verify/credential/b3252ef5acc94a51aae33f481006a62c', + description: + 'Validated cloud fundamentals, AWS service awareness, pricing, security basics, and shared responsibility concepts.', + media: [{ src: '/about/aws-clf.webp', alt: 'AWS Certified Cloud Practitioner badge', width: 600, height: 600 }], + }, + { + year: 'Mar 2026', + title: 'Earned AWS Solutions Architect - Associate', + titleHref: 'https://cp.certmetrics.com/amazon/en/public/verify/credential/fdb23858c0614516b39d854942286c38', + description: + 'Built and validated a deeper understanding of AWS architecture, resilient system design, networking, storage, compute, and cost-aware cloud decisions.', + media: [ + { + src: '/about/aws-saa.webp', + alt: 'AWS Certified Solutions Architect - Associate badge', + width: 600, + height: 600, + }, + ], + }, + { + year: 'Apr 2026', + title: 'Built the n11 + Patika developed project', + titleHref: 'https://github.com/replakcan/n11_patika_final', + description: + 'Joined the n11 and Patika Java Spring Boot bootcamp, gaining hands-on experience with microservice architecture and related backend patterns.', + }, + { + year: 'Feb - May 2026', + title: 'Contributed to the LearnOps education platform', + titleHref: 'https://github.com/Bilgisayar-Kavramlari-Toplulugu/project-learnops', + description: + 'Contributed to frontend, backend, and DevOps issues for the LearnOps education platform project within the Bilgisayar Kavramlari Toplulugu community.', + }, + { + year: 'Today', + title: 'Adding primary sources to practical learning', + description: + 'After meeting many design patterns in microservice practice, started pairing hands-on work with books such as Clean Architecture, Design Patterns, and Concepts of Programming Languages.', + }, +] + export default function AboutPage() { - return null + return ( +
    +
    + + +

    About

    +

    Small signals, long paths.

    +
    +

    + I like the idea that small technical habits can carry a history. The number of blank lines someone leaves + around an indentation style, or whether they pronounce SQL as “sequel”, can sometimes hint at the books, + communities, tools, and curiosities that shaped the way they learned. +

    +

    + I do not think these details are trivia. They are small signals of how much a person has cared to ask + where things come from, and how seriously they try to understand the culture behind the craft. That is why + I find it worth sharing my own path through computer science, programming, web development, and software + design below. +

    +
    +
    +
    + + +
    +
    +

    Journey

    +
    +
      + {timeline.map((item) => ( + + ))} +
    +
    +
    +
    + ) } diff --git a/app/blog/Blog.module.css b/app/blog/Blog.module.css new file mode 100644 index 0000000..78493e7 --- /dev/null +++ b/app/blog/Blog.module.css @@ -0,0 +1,95 @@ +.page { + padding-block: clamp(3rem, 7vw, 6rem) var(--section-space); +} + +.content { + display: grid; +} + +.placeholder { + position: relative; + display: grid; + min-height: clamp(24rem, 52vw, 36rem); + grid-template-columns: minmax(0, 1fr) minmax(14rem, 0.45fr); + gap: clamp(2rem, 6vw, 5rem); + align-items: center; + overflow: hidden; + isolation: isolate; +} + +.placeholder::before { + position: absolute; + inset: 12% 0 auto 8%; + z-index: -1; + width: min(34rem, 72vw); + height: min(34rem, 72vw); + content: ''; + background: radial-gradient(circle, color-mix(in oklch, var(--primary) 18%, transparent), transparent 64%); + filter: blur(1.6rem); +} + +.kicker { + margin-bottom: 0.85rem; + color: var(--primary); + font-family: var(--font-geist-mono), monospace; + font-size: 0.76rem; + font-weight: 720; + letter-spacing: 0.12em; + text-transform: uppercase; +} + +.placeholder h1 { + max-width: 12ch; + margin-bottom: 1.15rem; + font-size: clamp(2.35rem, 6vw, 5.4rem); + line-height: 0.96; + text-wrap: balance; +} + +.placeholder p:last-child { + max-width: 42rem; + margin-bottom: 0; + color: var(--muted-foreground); + font-size: clamp(1rem, 1.5vw, 1.15rem); + line-height: 1.8; +} + +.statusPanel { + display: grid; + width: min(100%, 20rem); + gap: 0.85rem; + justify-self: end; + padding: 1rem; + background: color-mix(in oklch, var(--surface) 82%, transparent); + border: 1px solid color-mix(in oklch, var(--border-strong) 68%, transparent); + border-radius: var(--radius-md); + box-shadow: 0 1rem 2.4rem var(--shadow-color); +} + +.statusPanel span { + display: block; + height: clamp(3.2rem, 7vw, 4.6rem); + background: + linear-gradient(90deg, color-mix(in oklch, var(--primary) 18%, transparent), transparent 46%), + color-mix(in oklch, var(--surface-raised) 78%, transparent); + border: 1px dashed color-mix(in oklch, var(--border-strong) 74%, transparent); + border-radius: var(--radius-sm); +} + +.statusPanel span:nth-child(2) { + width: 82%; +} + +.statusPanel span:nth-child(3) { + width: 62%; +} + +@media (max-width: 48rem) { + .placeholder { + grid-template-columns: 1fr; + } + + .statusPanel { + justify-self: start; + } +} diff --git a/app/blog/page.tsx b/app/blog/page.tsx index d9fcab2..83e13bc 100644 --- a/app/blog/page.tsx +++ b/app/blog/page.tsx @@ -1,10 +1,32 @@ import type { Metadata } from 'next' +import PageContainer from '../components/PageContainer' +import styles from './Blog.module.css' export const metadata: Metadata = { title: 'Blog', - description: 'Writing by Alper.', + description: 'Draft notes and writing by Alper Makcan.', } export default function BlogPage() { - return null + return ( +
    + +
    +
    +

    Blog

    +

    Notes are being assembled.

    +

    + This section is reserved for technical notes, project writeups, and small ideas worth keeping. The first + drafts are still on the workbench. +

    +
    + +
    +
    +
    + ) } diff --git a/app/components/AnglerSocialReveal.tsx b/app/components/AnglerSocialReveal.tsx index b576ac8..6effc11 100644 --- a/app/components/AnglerSocialReveal.tsx +++ b/app/components/AnglerSocialReveal.tsx @@ -89,7 +89,7 @@ export default function AnglerSocialReveal() {
    -
    +
    - { - event.currentTarget.style.display = 'none' - }} - />

    Based in Istanbul · Building for the web

    diff --git a/app/components/LightRays.tsx b/app/components/LightRays.tsx index 50ed101..bad64c1 100644 --- a/app/components/LightRays.tsx +++ b/app/components/LightRays.tsx @@ -164,7 +164,7 @@ function usePrefersReducedMotion() { const [prefersReducedMotion, setPrefersReducedMotion] = useState(false) useEffect(() => { - const media = window.matchMedia('(prefers-reduced-motion: reduce)') + const media = globalThis.matchMedia('(prefers-reduced-motion: reduce)') const update = () => setPrefersReducedMotion(media.matches) update() @@ -204,7 +204,7 @@ export default function LightRays({ if (prefersReducedMotion || !containerRef.current) return const container = containerRef.current - const renderer = new Renderer({ dpr: Math.min(window.devicePixelRatio, 2), alpha: true }) + const renderer = new Renderer({ dpr: Math.min(globalThis.devicePixelRatio, 2), alpha: true }) rendererRef.current = renderer const gl = renderer.gl @@ -240,7 +240,7 @@ export default function LightRays({ const resize = () => { const { clientWidth, clientHeight } = container - const dpr = Math.min(window.devicePixelRatio, 2) + const dpr = Math.min(globalThis.devicePixelRatio, 2) renderer.dpr = dpr renderer.setSize(clientWidth, clientHeight) @@ -267,12 +267,12 @@ export default function LightRays({ } resize() - window.addEventListener('resize', resize) + globalThis.addEventListener('resize', resize) animationRef.current = requestAnimationFrame(render) return () => { if (animationRef.current) cancelAnimationFrame(animationRef.current) - window.removeEventListener('resize', resize) + globalThis.removeEventListener('resize', resize) try { renderer.gl.getExtension('WEBGL_lose_context')?.loseContext() @@ -354,9 +354,9 @@ export default function LightRays({ } } - window.addEventListener('mousemove', onMouseMove) + globalThis.addEventListener('mousemove', onMouseMove) - return () => window.removeEventListener('mousemove', onMouseMove) + return () => globalThis.removeEventListener('mousemove', onMouseMove) }, [followMouse]) return ( diff --git a/app/components/Parallax.module.css b/app/components/Parallax.module.css index 5ac053c..fdbbc51 100644 --- a/app/components/Parallax.module.css +++ b/app/components/Parallax.module.css @@ -5,10 +5,19 @@ width: 100%; min-height: 100dvh; overflow: hidden; - background-image: url('/images/parallax/layer-1.jpeg'); - background-repeat: no-repeat; - background-position: top center; - background-size: 100% auto; +} + +.baseLayer { + position: absolute; + z-index: 0; + inset: 0; + pointer-events: none; +} + +.baseImage { + display: block; + width: 100%; + height: auto; } .wrapper::after { @@ -34,12 +43,18 @@ bottom: -10rem; } -.layer:nth-child(1) { - background-image: url('/images/parallax/layer-2.svg'); +.layer:nth-of-type(1) { + background-image: image-set( + url('/images/parallax/layer-2.webp') type('image/webp'), + url('/images/parallax/layer-2.svg') type('image/svg+xml') + ); } -.layer:nth-child(2) { - background-image: url('/images/parallax/layer-3.svg'); +.layer:nth-of-type(2) { + background-image: image-set( + url('/images/parallax/layer-3.webp') type('image/webp'), + url('/images/parallax/layer-3.svg') type('image/svg+xml') + ); } @media (max-width: 71.875rem) { @@ -49,7 +64,16 @@ } @media (max-width: 48rem) { - .wrapper, + .baseImage { + position: absolute; + top: 0; + left: 50%; + width: auto; + max-width: none; + height: 100%; + transform: translateX(-50%); + } + .layer { background-size: auto 100%; } diff --git a/app/components/ParallaxScene.tsx b/app/components/ParallaxScene.tsx index 475b8bf..95c4f6e 100644 --- a/app/components/ParallaxScene.tsx +++ b/app/components/ParallaxScene.tsx @@ -67,6 +67,23 @@ export default function ParallaxScene() { return (
    + + + + diff --git a/app/components/ScrollReveal.tsx b/app/components/ScrollReveal.tsx index 2ef3e41..f1c20b3 100644 --- a/app/components/ScrollReveal.tsx +++ b/app/components/ScrollReveal.tsx @@ -24,7 +24,7 @@ function usePrefersReducedMotion() { const prefersReducedMotionRef = useRef(false) useEffect(() => { - const media = window.matchMedia('(prefers-reduced-motion: reduce)') + const media = globalThis.matchMedia('(prefers-reduced-motion: reduce)') const update = () => { prefersReducedMotionRef.current = media.matches } diff --git a/app/contact/Contact.module.css b/app/contact/Contact.module.css new file mode 100644 index 0000000..d85b74b --- /dev/null +++ b/app/contact/Contact.module.css @@ -0,0 +1,325 @@ +.page { + --radar-duration: 7.2s; + min-height: calc(100svh - var(--header-height)); + color: oklch(0.94 0.025 150); + background: black; +} + +.radarField { + position: relative; + min-height: calc(100svh - var(--header-height)); + overflow: hidden; + isolation: isolate; + background: + radial-gradient(circle at 50% 50%, oklch(0.24 0.1 155 / 28%), transparent 18rem), + radial-gradient(circle at 18% 70%, oklch(0.18 0.08 150 / 24%), transparent 20rem), + linear-gradient(180deg, oklch(0.045 0.025 155), oklch(0.018 0.018 155) 62%, black); +} + +.radarField::before, +.radarField::after { + position: absolute; + inset: 0; + z-index: -1; + content: ''; + pointer-events: none; +} + +.radarField::before { + background: + linear-gradient(90deg, oklch(0.74 0.18 150 / 7%) 1px, transparent 1px), + linear-gradient(180deg, oklch(0.74 0.18 150 / 7%) 1px, transparent 1px); + background-size: clamp(2.2rem, 5vw, 4.5rem) clamp(2.2rem, 5vw, 4.5rem); + mask-image: radial-gradient(circle at 50% 50%, black, transparent 72%); +} + +.radarField::after { + background: + radial-gradient( + circle at 50% 50%, + transparent 0 9.5rem, + oklch(0.76 0.19 150 / 13%) 9.56rem 9.62rem, + transparent 9.68rem + ), + radial-gradient( + circle at 50% 50%, + transparent 0 17rem, + oklch(0.76 0.19 150 / 12%) 17.06rem 17.12rem, + transparent 17.18rem + ), + radial-gradient( + circle at 50% 50%, + transparent 0 25rem, + oklch(0.76 0.19 150 / 10%) 25.06rem 25.12rem, + transparent 25.18rem + ), + linear-gradient(90deg, transparent calc(50% - 1px), oklch(0.76 0.19 150 / 13%) 50%, transparent calc(50% + 1px)), + linear-gradient(180deg, transparent calc(50% - 1px), oklch(0.76 0.19 150 / 13%) 50%, transparent calc(50% + 1px)); + opacity: 0.86; +} + +.radarGrid { + position: absolute; + top: 50%; + left: 50%; + z-index: 0; + width: max(132vmax, 54rem); + aspect-ratio: 1; + border: 1px solid oklch(0.72 0.19 150 / 18%); + border-radius: 50%; + box-shadow: + inset 0 0 4rem oklch(0.48 0.16 150 / 12%), + 0 0 5rem oklch(0.48 0.16 150 / 14%); + transform: translate(-50%, -50%); +} + +.radarSweep { + position: absolute; + top: 50%; + left: 50%; + z-index: 1; + width: max(150vmax, 64rem); + aspect-ratio: 1; + border-radius: 50%; + background: conic-gradient( + from -90deg, + oklch(0.78 0.2 150 / 0%) 0deg, + oklch(0.78 0.2 150 / 4%) 12deg, + oklch(0.78 0.2 150 / 28%) 28deg, + oklch(0.78 0.2 150 / 0%) 54deg, + oklch(0.78 0.2 150 / 0%) 360deg + ); + mix-blend-mode: screen; + transform: translate(-50%, -50%); + animation: radarSpin var(--radar-duration) linear infinite; +} + +.radarCenter { + position: absolute; + top: 50%; + left: 50%; + z-index: 2; + width: 0.7rem; + height: 0.7rem; + background: oklch(0.82 0.2 150); + border-radius: 50%; + box-shadow: + 0 0 0 0.45rem oklch(0.72 0.18 150 / 16%), + 0 0 2rem oklch(0.72 0.18 150 / 72%); + transform: translate(-50%, -50%); +} + +.nodes { + position: absolute; + inset: 0; + z-index: 5; +} + +.contactNode { + position: absolute; + top: var(--y); + left: var(--x); + display: inline-flex; + align-items: center; + gap: 0.8rem; + max-width: min(18rem, 42vw); + color: inherit; + text-decoration: none; + transform: translate(-50%, -50%); +} + +.contactNode:focus-visible { + outline: 3px solid oklch(0.82 0.2 150); + outline-offset: 0.65rem; + border-radius: 999px; +} + +.nodeDot { + position: relative; + display: block; + width: 0.9rem; + height: 0.9rem; + flex: 0 0 auto; + background: oklch(0.64 0.25 28); + border: 2px solid oklch(0.94 0.08 42); + border-radius: 50%; + box-shadow: + 0 0 0 0.24rem oklch(0.62 0.25 28 / 18%), + 0 0 1.2rem oklch(0.62 0.25 28 / 78%); + animation: nodeDotEncounter var(--radar-duration) linear infinite; + animation-delay: var(--pulse-delay); +} + +.nodeDot::before, +.nodeDot::after { + position: absolute; + inset: -0.5rem; + content: ''; + border: 1px solid oklch(0.68 0.25 28 / 74%); + border-radius: 50%; + opacity: 0; + transform: scale(0.25); + animation: contactPing var(--radar-duration) ease-out infinite; + animation-delay: calc(var(--pulse-delay) + 0.08s); +} + +.nodeDot::after { + animation-delay: calc(var(--pulse-delay) + 0.26s); +} + +.nodeLabel { + display: grid; + gap: 0.28rem; + min-width: 0; + padding: 0.64rem 0.76rem; + background: rgb(0 0 0 / 54%); + border: 1px solid oklch(0.75 0.18 150 / 18%); + border-radius: 0.4rem; + box-shadow: 0 1rem 2.5rem rgb(0 0 0 / 42%); + backdrop-filter: blur(10px) saturate(130%); + animation: nodeLabelEncounter var(--radar-duration) linear infinite; + animation-delay: var(--pulse-delay); +} + +.nodeTitle { + display: inline-flex; + gap: 0.35rem; + align-items: center; + color: white; + font-weight: 780; + line-height: 1.1; +} + +.nodeTitle svg { + width: 0.95rem; + height: 0.95rem; + color: oklch(0.82 0.2 150); + stroke-width: 2.2; +} + +.nodeDetail { + color: oklch(0.76 0.035 155); + font-size: 0.82rem; + line-height: 1.45; +} + +.contactNode:hover .nodeLabel, +.contactNode:focus-visible .nodeLabel { + border-color: oklch(0.78 0.2 150 / 44%); +} + +@keyframes radarSpin { + to { + transform: translate(-50%, -50%) rotate(1turn); + } +} + +@keyframes contactPing { + 0% { + opacity: 0; + transform: scale(0.3); + } + + 4% { + opacity: 0.88; + } + + 18% { + opacity: 0; + transform: scale(4.2); + } + + 100% { + opacity: 0; + transform: scale(4.2); + } +} + +@keyframes nodeDotEncounter { + 0%, + 7%, + 100% { + background: oklch(0.64 0.25 28); + border-color: oklch(0.94 0.08 42); + box-shadow: + 0 0 0 0.24rem oklch(0.62 0.25 28 / 18%), + 0 0 1.2rem oklch(0.62 0.25 28 / 78%); + transform: scale(1); + } + + 1.8% { + background: oklch(0.9 0.22 145); + border-color: white; + box-shadow: + 0 0 0 0.35rem oklch(0.82 0.2 150 / 28%), + 0 0 1.1rem oklch(0.86 0.22 150 / 94%), + 0 0 3.4rem oklch(0.74 0.2 150 / 76%); + transform: scale(1.42); + } +} + +@keyframes nodeLabelEncounter { + 0%, + 8%, + 100% { + border-color: oklch(0.75 0.18 150 / 18%); + box-shadow: 0 1rem 2.5rem rgb(0 0 0 / 42%); + } + + 2.2% { + border-color: oklch(0.84 0.2 150 / 68%); + box-shadow: + 0 1rem 2.5rem rgb(0 0 0 / 42%), + 0 0 1.4rem oklch(0.76 0.19 150 / 42%); + } +} + +@media (prefers-reduced-motion: reduce) { + .radarSweep, + .nodeDot, + .nodeLabel, + .nodeDot::before, + .nodeDot::after { + animation: none; + } +} + +@media (max-width: 46rem) { + .radarField { + min-height: calc(100svh - var(--header-height) + 8rem); + } + + .contactNode { + max-width: min(15rem, 58vw); + gap: 0.55rem; + } + + .nodeLabel { + padding: 0.52rem 0.6rem; + } + + .nodeDetail { + display: none; + } +} + +@media (max-width: 34rem) { + .contactNode { + max-width: 48vw; + } + + .contactNode:nth-child(1) { + top: 58%; + left: 27%; + } + + .contactNode:nth-child(2) { + top: 43%; + left: 70%; + } + + .contactNode:nth-child(3) { + top: 78%; + left: 54%; + } +} diff --git a/app/contact/page.tsx b/app/contact/page.tsx index 7aa548a..7788ce3 100644 --- a/app/contact/page.tsx +++ b/app/contact/page.tsx @@ -1,10 +1,87 @@ import type { Metadata } from 'next' +import type { CSSProperties } from 'react' +import { MoveUpRight } from 'lucide-react' +import { socialLinks } from '../data/site' +import styles from './Contact.module.css' export const metadata: Metadata = { title: 'Contact', - description: 'Contact Alper.', + description: 'Ways to reach Alper Makcan.', } +type RadarNodeStyle = CSSProperties & { + '--x': string + '--y': string + '--pulse-delay': string +} + +const contactLinks = [ + { + label: 'Email', + href: socialLinks.find((link) => link.label === 'Email')?.href ?? 'mailto:alper.makcan@gmail.com', + detail: 'For project notes, collaboration ideas, or a plain hello.', + x: '22%', + y: '66%', + pulseDelay: '6s', + }, + { + label: 'GitHub', + href: socialLinks.find((link) => link.label === 'GitHub')?.href ?? 'https://github.com/replakcan', + detail: 'Code, experiments, and repository work in progress.', + x: '71%', + y: '35%', + pulseDelay: '2.3s', + }, + { + label: 'LinkedIn', + href: socialLinks.find((link) => link.label === 'LinkedIn')?.href ?? 'https://www.linkedin.com/in/alpermutluakcan/', + detail: 'Professional background and public profile.', + x: '48%', + y: '78%', + pulseDelay: '4.9s', + }, +] + export default function ContactPage() { - return null + return ( +
    +
    +

    + Contact +

    +
    +
    + ) } diff --git a/app/data/projects.ts b/app/data/projects.ts new file mode 100644 index 0000000..d054d50 --- /dev/null +++ b/app/data/projects.ts @@ -0,0 +1,78 @@ +export type ProjectStatus = 'live' | 'in-progress' | 'archived' + +export type ProjectLink = { + readonly label: string + readonly href: string + readonly kind: 'demo' | 'repository' | 'case-study' | 'external' +} + +export type Project = { + readonly slug: string + readonly title: string + readonly description: string + readonly year: string + readonly status: ProjectStatus + readonly role: string + readonly techStack: readonly string[] + readonly links?: readonly ProjectLink[] + readonly featured?: boolean +} + +export const projects = [ + { + slug: 'alper-dev', + title: 'alper.dev', + description: + 'Personal portfolio site built as a static Next.js app with animated page sections, CSS Modules, and deployable static output.', + year: '2026', + status: 'in-progress', + role: 'Design and development', + techStack: ['Next.js', 'React', 'TypeScript', 'CSS Modules', 'Static export'], + links: [ + { + label: 'Source', + href: 'https://github.com/replakcan/alper-dev', + kind: 'repository', + }, + ], + featured: true, + }, + { + slug: 'n11-patika-final', + title: 'n11 + Patika final project', + description: + 'Java Spring Boot bootcamp project focused on backend service design, API structure, and practical microservice architecture patterns.', + year: '2026', + status: 'archived', + role: 'Backend developer', + techStack: ['Java', 'Spring Boot', 'Microservices', 'REST APIs'], + links: [ + { + label: 'Repository', + href: 'https://github.com/replakcan/n11_patika_final', + kind: 'repository', + }, + ], + featured: true, + }, + { + slug: 'learnops', + title: 'LearnOps education platform', + description: + 'Community education platform contribution spanning frontend, backend, and DevOps issues for a collaborative learning product.', + year: '2026', + status: 'in-progress', + role: 'Contributor', + techStack: ['React', 'Backend APIs', 'DevOps', 'GitHub collaboration'], + links: [ + { + label: 'Repository', + href: 'https://github.com/Bilgisayar-Kavramlari-Toplulugu/project-learnops', + kind: 'repository', + }, + ], + featured: true, + }, +] satisfies readonly Project[] + +export const featuredProjects = projects.filter((project) => project.featured) diff --git a/app/loading.tsx b/app/loading.tsx index 8b05fd1..f15322a 100644 --- a/app/loading.tsx +++ b/app/loading.tsx @@ -1,5 +1,3 @@ -import AlperLoader from './components/AlperLoader' - export default function Loading() { - return + return null } diff --git a/app/page.tsx b/app/page.tsx index ad309dc..9ca3211 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -49,7 +49,7 @@ export default function Home() { noiseAmount={0.08} distortion={0.08} /> -
    +

    About

    @@ -91,7 +91,7 @@ export default function Home() {
    -
    +
    @@ -119,7 +119,7 @@ export default function Home() {
    -
    +

    Blog

    @@ -137,7 +137,7 @@ export default function Home() {
    -
    +

    Contact

    diff --git a/app/projects/Projects.module.css b/app/projects/Projects.module.css new file mode 100644 index 0000000..417d1e3 --- /dev/null +++ b/app/projects/Projects.module.css @@ -0,0 +1,176 @@ +.page { + padding-block: clamp(3rem, 7vw, 6rem) var(--section-space); +} + +.content { + display: grid; + gap: clamp(2.2rem, 5vw, 4rem); +} + +.header { + max-width: 52rem; +} + +.kicker { + margin-bottom: 0.85rem; + color: var(--primary); + font-family: var(--font-geist-mono), monospace; + font-size: 0.76rem; + font-weight: 720; + letter-spacing: 0.12em; + text-transform: uppercase; +} + +.header h1 { + max-width: 13ch; + margin-bottom: 1.15rem; + font-size: clamp(2.35rem, 6vw, 5.4rem); + line-height: 0.96; + text-wrap: balance; +} + +.header p:last-child { + margin-bottom: 0; + color: var(--muted-foreground); + font-size: clamp(1rem, 1.5vw, 1.15rem); + line-height: 1.8; +} + +.grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 1rem; +} + +.card { + display: grid; + min-height: 24rem; + align-content: start; + padding: clamp(1rem, 2vw, 1.35rem); + background: color-mix(in oklch, var(--surface) 88%, transparent); + border: 1px solid color-mix(in oklch, var(--border-strong) 68%, transparent); + border-radius: var(--radius-md); + box-shadow: 0 1rem 2.4rem var(--shadow-color); +} + +.cardHeader { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 1rem; +} + +.meta { + margin-bottom: 0.55rem; + color: var(--faint-foreground); + font-family: var(--font-geist-mono), monospace; + font-size: 0.7rem; + font-weight: 700; + line-height: 1.35; + text-transform: uppercase; +} + +.card h2 { + margin-bottom: 0; + font-size: clamp(1.24rem, 2vw, 1.55rem); + line-height: 1.08; + text-wrap: balance; +} + +.status { + flex: 0 0 auto; + padding: 0.3rem 0.48rem; + color: var(--primary-strong); + font-family: var(--font-geist-mono), monospace; + font-size: 0.64rem; + font-weight: 760; + line-height: 1; + white-space: nowrap; + background: color-mix(in oklch, var(--primary) 11%, transparent); + border: 1px solid color-mix(in oklch, var(--primary) 24%, transparent); + border-radius: 999px; +} + +.description { + margin-top: 1.25rem; + margin-bottom: 0; + color: var(--muted-foreground); + line-height: 1.7; +} + +.techStack { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + padding: 0; + margin: 1.4rem 0 0; + list-style: none; +} + +.techStack li { + padding: 0.42rem 0.58rem; + color: var(--muted-foreground); + font-family: var(--font-geist-mono), monospace; + font-size: 0.68rem; + background: color-mix(in oklch, var(--surface-raised) 72%, transparent); + border: 1px solid color-mix(in oklch, var(--border) 72%, transparent); + border-radius: 999px; +} + +.links { + display: flex; + flex-wrap: wrap; + gap: 0.7rem; + margin-top: auto; + padding-top: 1.5rem; +} + +.links a { + display: inline-flex; + gap: 0.42rem; + align-items: center; + min-height: 2.35rem; + padding-inline: 0.82rem; + color: var(--foreground); + font-weight: 720; + text-decoration: none; + background: color-mix(in oklch, var(--primary) 9%, transparent); + border: 1px solid color-mix(in oklch, var(--primary) 22%, transparent); + border-radius: var(--radius-sm); +} + +.links a:hover { + color: var(--primary-strong); + border-color: color-mix(in oklch, var(--primary) 42%, transparent); +} + +.links svg { + width: 0.95rem; + height: 0.95rem; + flex: 0 0 auto; + stroke-width: 2.2; +} + +@media (max-width: 64rem) { + .grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + +@media (max-width: 42rem) { + .grid { + grid-template-columns: 1fr; + } + + .card { + min-height: 0; + } + + .cardHeader { + flex-direction: column; + } + + .status { + white-space: normal; + } +} diff --git a/app/projects/page.tsx b/app/projects/page.tsx index 8b04dae..e5a5115 100644 --- a/app/projects/page.tsx +++ b/app/projects/page.tsx @@ -1,10 +1,62 @@ import type { Metadata } from 'next' +import { ArrowUpRight } from 'lucide-react' +import PageContainer from '../components/PageContainer' +import { projects } from '../data/projects' +import styles from './Projects.module.css' export const metadata: Metadata = { title: 'Projects', - description: 'Projects by Alper.', + description: 'Selected software projects by Alper Makcan.', } export default function ProjectsPage() { - return null + return ( +
    + +
    +

    Projects

    +

    Selected work, experiments, and practical builds.

    +

    + A small local index of projects I want to keep visible: what they are, what they use, and where to inspect + the work when a public link exists. +

    +
    + +
    + {projects.map((project) => ( +
    +
    +
    +

    + {project.year} / {project.role} +

    +

    {project.title}

    +
    + {project.status} +
    + +

    {project.description}

    + +
      + {project.techStack.map((tech) => ( +
    • {tech}
    • + ))} +
    + + {project.links?.length ? ( +
    + {project.links.map((link) => ( + + {link.label} + + ))} +
    + ) : null} +
    + ))} +
    +
    +
    + ) } diff --git a/public/about/aws-clf.png b/public/about/aws-clf.png new file mode 100644 index 0000000..619ae96 Binary files /dev/null and b/public/about/aws-clf.png differ diff --git a/public/about/aws-clf.webp b/public/about/aws-clf.webp new file mode 100644 index 0000000..d1cbf44 Binary files /dev/null and b/public/about/aws-clf.webp differ diff --git a/public/about/aws-saa.webp b/public/about/aws-saa.webp new file mode 100644 index 0000000..6a768c3 Binary files /dev/null and b/public/about/aws-saa.webp differ diff --git a/public/about/aws-saapng.png b/public/about/aws-saapng.png new file mode 100644 index 0000000..4fee5b9 Binary files /dev/null and b/public/about/aws-saapng.png differ diff --git a/public/about/cs50-harvard.png b/public/about/cs50-harvard.png new file mode 100644 index 0000000..3519db3 Binary files /dev/null and b/public/about/cs50-harvard.png differ diff --git a/public/about/cs50-harvard.webp b/public/about/cs50-harvard.webp new file mode 100644 index 0000000..32c7689 Binary files /dev/null and b/public/about/cs50-harvard.webp differ diff --git a/public/about/deitel-c-how-to-program.jpg b/public/about/deitel-c-how-to-program.jpg new file mode 100644 index 0000000..1db297e Binary files /dev/null and b/public/about/deitel-c-how-to-program.jpg differ diff --git a/public/about/deitel-c-how-to-program.webp b/public/about/deitel-c-how-to-program.webp new file mode 100644 index 0000000..c6a6b97 Binary files /dev/null and b/public/about/deitel-c-how-to-program.webp differ diff --git a/public/about/drei-glaser.jpeg b/public/about/drei-glaser.jpeg new file mode 100644 index 0000000..129e732 Binary files /dev/null and b/public/about/drei-glaser.jpeg differ diff --git a/public/about/drei-glaser.webp b/public/about/drei-glaser.webp new file mode 100644 index 0000000..e956d0a Binary files /dev/null and b/public/about/drei-glaser.webp differ diff --git a/public/about/hatsan-1.png b/public/about/hatsan-1.png new file mode 100644 index 0000000..f3d82ea Binary files /dev/null and b/public/about/hatsan-1.png differ diff --git a/public/about/hatsan-1.webp b/public/about/hatsan-1.webp new file mode 100644 index 0000000..579b565 Binary files /dev/null and b/public/about/hatsan-1.webp differ diff --git a/public/about/hatsan-2.png b/public/about/hatsan-2.png new file mode 100644 index 0000000..e98e5a0 Binary files /dev/null and b/public/about/hatsan-2.png differ diff --git a/public/about/hatsan-2.webp b/public/about/hatsan-2.webp new file mode 100644 index 0000000..e941bd3 Binary files /dev/null and b/public/about/hatsan-2.webp differ diff --git a/public/about/hatsan-3.png b/public/about/hatsan-3.png new file mode 100644 index 0000000..5c6ff82 Binary files /dev/null and b/public/about/hatsan-3.png differ diff --git a/public/about/hatsan-3.webp b/public/about/hatsan-3.webp new file mode 100644 index 0000000..1caed28 Binary files /dev/null and b/public/about/hatsan-3.webp differ diff --git a/public/about/ytu-gemi.webp b/public/about/ytu-gemi.webp new file mode 100644 index 0000000..059c2c9 Binary files /dev/null and b/public/about/ytu-gemi.webp differ diff --git "a/public/about/yt\303\274-gemi.jpeg" "b/public/about/yt\303\274-gemi.jpeg" new file mode 100644 index 0000000..19cb72c Binary files /dev/null and "b/public/about/yt\303\274-gemi.jpeg" differ diff --git a/public/contact/angler_fish.webp b/public/contact/angler_fish.webp new file mode 100644 index 0000000..a40a0b7 Binary files /dev/null and b/public/contact/angler_fish.webp differ diff --git a/public/images/parallax/layer-1-1280.webp b/public/images/parallax/layer-1-1280.webp new file mode 100644 index 0000000..a2062bc Binary files /dev/null and b/public/images/parallax/layer-1-1280.webp differ diff --git a/public/images/parallax/layer-1-1672.webp b/public/images/parallax/layer-1-1672.webp new file mode 100644 index 0000000..27b3c62 Binary files /dev/null and b/public/images/parallax/layer-1-1672.webp differ diff --git a/public/images/parallax/layer-1-960.webp b/public/images/parallax/layer-1-960.webp new file mode 100644 index 0000000..5d593d6 Binary files /dev/null and b/public/images/parallax/layer-1-960.webp differ diff --git a/public/images/parallax/layer-2.webp b/public/images/parallax/layer-2.webp new file mode 100644 index 0000000..dd35d28 Binary files /dev/null and b/public/images/parallax/layer-2.webp differ diff --git a/public/images/parallax/layer-3.webp b/public/images/parallax/layer-3.webp new file mode 100644 index 0000000..2b33960 Binary files /dev/null and b/public/images/parallax/layer-3.webp differ