mirror of
https://github.com/timmypidashev/web.git
synced 2026-04-14 11:03:50 +00:00
Migrate to new astro version; small header/footer/hero fixes; continue work on blog implementation
This commit is contained in:
@@ -8,7 +8,7 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- ./.caddy/Caddyfile.dev:/etc/caddy/Caddyfile:rw
|
- ./.caddy/Caddyfile.dev:/etc/caddy/Caddyfile:rw
|
||||||
networks:
|
networks:
|
||||||
- proxy
|
- web_proxy
|
||||||
depends_on:
|
depends_on:
|
||||||
- web
|
- web
|
||||||
|
|
||||||
@@ -27,9 +27,9 @@ services:
|
|||||||
- ./src/public:/app/public
|
- ./src/public:/app/public
|
||||||
- ./src/src:/app/src
|
- ./src/src:/app/src
|
||||||
networks:
|
networks:
|
||||||
- proxy
|
- web_proxy
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
proxy:
|
web_proxy:
|
||||||
name: proxy
|
name: web_proxy
|
||||||
external: true
|
external: true
|
||||||
|
|||||||
@@ -1,11 +1,166 @@
|
|||||||
import { defineConfig } from "astro/config";
|
import { defineConfig } from "astro/config";
|
||||||
import tailwind from "@astrojs/tailwind";
|
import tailwind from "@astrojs/tailwind";
|
||||||
import react from "@astrojs/react";
|
import react from "@astrojs/react";
|
||||||
|
import mdx from "@astrojs/mdx";
|
||||||
|
import rehypePrettyCode from "rehype-pretty-code";
|
||||||
|
import rehypeSlug from "rehype-slug";
|
||||||
|
|
||||||
|
import sitemap from "@astrojs/sitemap";
|
||||||
|
|
||||||
// https://astro.build/config
|
// https://astro.build/config
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
|
site: "https://timmypidashev.dev",
|
||||||
integrations: [
|
integrations: [
|
||||||
tailwind(),
|
tailwind(),
|
||||||
react()
|
react(),
|
||||||
|
mdx({
|
||||||
|
syntaxHighlight: false,
|
||||||
|
rehypePlugins: [
|
||||||
|
/**
|
||||||
|
* Adds ids to headings
|
||||||
|
*/
|
||||||
|
rehypeSlug,
|
||||||
|
[
|
||||||
|
/**
|
||||||
|
* Enhances code blocks with syntax highlighting, line numbers,
|
||||||
|
* titles, and allows highlighting specific lines and words
|
||||||
|
*/
|
||||||
|
|
||||||
|
rehypePrettyCode,
|
||||||
|
{
|
||||||
|
theme: {
|
||||||
|
"name": "Custom Gruvbox Dark",
|
||||||
|
"type": "dark",
|
||||||
|
"colors": {
|
||||||
|
"editor.background": "#000000",
|
||||||
|
"editor.foreground": "#ebdbb2"
|
||||||
|
},
|
||||||
|
"tokenColors": [
|
||||||
|
{
|
||||||
|
"scope": ["comment", "punctuation.definition.comment"],
|
||||||
|
"settings": {
|
||||||
|
"foreground": "#928374",
|
||||||
|
"fontStyle": "italic"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scope": ["constant", "variable.other.constant"],
|
||||||
|
"settings": {
|
||||||
|
"foreground": "#d3869b"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scope": "variable",
|
||||||
|
"settings": {
|
||||||
|
"foreground": "#ebdbb2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scope": ["keyword", "storage.type", "storage.modifier"],
|
||||||
|
"settings": {
|
||||||
|
"foreground": "#fb4934"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scope": ["string", "punctuation.definition.string"],
|
||||||
|
"settings": {
|
||||||
|
"foreground": "#b8bb26"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scope": ["entity.name.function", "support.function"],
|
||||||
|
"settings": {
|
||||||
|
"foreground": "#b8bb26"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scope": "entity.name.type",
|
||||||
|
"settings": {
|
||||||
|
"foreground": "#fabd2f"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scope": ["entity.name.tag", "punctuation.definition.tag"],
|
||||||
|
"settings": {
|
||||||
|
"foreground": "#83a598"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scope": ["entity.other.attribute-name"],
|
||||||
|
"settings": {
|
||||||
|
"foreground": "#8ec07c"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scope": ["punctuation", "meta.brace"],
|
||||||
|
"settings": {
|
||||||
|
"foreground": "#ebdbb2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scope": "markup.inline.raw",
|
||||||
|
"settings": {
|
||||||
|
"foreground": "#fe8019"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scope": ["markup.heading"],
|
||||||
|
"settings": {
|
||||||
|
"foreground": "#b8bb26",
|
||||||
|
"fontStyle": "bold"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scope": ["markup.bold"],
|
||||||
|
"settings": {
|
||||||
|
"foreground": "#fe8019",
|
||||||
|
"fontStyle": "bold"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scope": ["markup.italic"],
|
||||||
|
"settings": {
|
||||||
|
"foreground": "#fe8019",
|
||||||
|
"fontStyle": "italic"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scope": ["markup.list"],
|
||||||
|
"settings": {
|
||||||
|
"foreground": "#83a598"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scope": ["markup.quote"],
|
||||||
|
"settings": {
|
||||||
|
"foreground": "#928374",
|
||||||
|
"fontStyle": "italic"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scope": ["markup.link"],
|
||||||
|
"settings": {
|
||||||
|
"foreground": "#8ec07c"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scope": "support.class",
|
||||||
|
"settings": {
|
||||||
|
"foreground": "#fabd2f"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scope": "number",
|
||||||
|
"settings": {
|
||||||
|
"foreground": "#d3869b"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
sitemap(),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,18 +8,25 @@
|
|||||||
"preview": "astro preview"
|
"preview": "astro preview"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@astrojs/react": "^2.3.2",
|
"@astrojs/react": "^4.1.1",
|
||||||
"@astrojs/tailwind": "^5.1.2",
|
"@astrojs/tailwind": "^5.1.3",
|
||||||
|
"@tailwindcss/typography": "^0.5.15",
|
||||||
"@types/react": "^18.3.12",
|
"@types/react": "^18.3.12",
|
||||||
"@types/react-dom": "^18.3.1",
|
"@types/react-dom": "^18.3.1",
|
||||||
"astro": "^4.16.7"
|
"astro": "^5.0.9",
|
||||||
|
"tailwindcss": "^3.4.15"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@astrojs/mdx": "^4.0.2",
|
||||||
|
"@astrojs/sitemap": "^3.2.1",
|
||||||
"framer-motion": "^11.11.11",
|
"framer-motion": "^11.11.11",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-responsive": "^10.0.0",
|
"react-responsive": "^10.0.0",
|
||||||
"reading-time": "^1.5.0",
|
"reading-time": "^1.5.0",
|
||||||
|
"rehype-pretty-code": "^0.14.0",
|
||||||
|
"rehype-slug": "^6.0.0",
|
||||||
|
"schema-dts": "^1.1.2",
|
||||||
"typewriter-effect": "^2.21.0"
|
"typewriter-effect": "^2.21.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
1784
src/pnpm-lock.yaml
generated
1784
src/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
63
src/src/components/blog/post-list.tsx
Normal file
63
src/src/components/blog/post-list.tsx
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
type BlogPost = {
|
||||||
|
slug: string;
|
||||||
|
data: {
|
||||||
|
title: string;
|
||||||
|
author: string;
|
||||||
|
date: string;
|
||||||
|
tags: string[];
|
||||||
|
description: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
interface BlogPostListProps {
|
||||||
|
posts: BlogPost[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatDate = (dateString: string) => {
|
||||||
|
return new Date(dateString).toLocaleDateString('en-US', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const BlogPostList = ({ posts }: BlogPostListProps) => {
|
||||||
|
return (
|
||||||
|
<div className="max-w-3xl mx-auto px-4 py-8">
|
||||||
|
<ul className="space-y-8">
|
||||||
|
{posts.map((post) => (
|
||||||
|
<li key={post.slug} className="border-b border-gray-200 pb-8 last:border-b-0">
|
||||||
|
<a
|
||||||
|
href={`/blog/${post.slug}`}
|
||||||
|
className="text-xl font-semibold hover:text-blue-600 transition-colors duration-200"
|
||||||
|
>
|
||||||
|
{post.data.title}
|
||||||
|
</a>
|
||||||
|
<div className="mt-2 text-sm text-gray-600">
|
||||||
|
<span>{post.data.author}</span>
|
||||||
|
<span className="mx-2">•</span>
|
||||||
|
<time dateTime={post.data.date}>
|
||||||
|
{formatDate(post.data.date)}
|
||||||
|
</time>
|
||||||
|
</div>
|
||||||
|
<p className="mt-3 text-gray-700">
|
||||||
|
{post.data.description}
|
||||||
|
</p>
|
||||||
|
<div className="mt-3 space-x-2">
|
||||||
|
{post.data.tags.map((tag) => (
|
||||||
|
<span
|
||||||
|
key={tag}
|
||||||
|
className="text-sm text-gray-600"
|
||||||
|
>
|
||||||
|
#{tag}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -12,7 +12,7 @@ export default function Footer({ fixed = false }) {
|
|||||||
));
|
));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<footer className={`w-full ${fixed ? "fixed bottom-0 left-0 right-0" : ""}`}>
|
<footer className={`w-full font-bold ${fixed ? "fixed bottom-0 left-0 right-0" : ""}`}>
|
||||||
<div className="flex flex-row px-2 py-1 text-lg lg:px-6 lg:py-1.5 lg:text-3xl md:text-2xl justify-between md:justify-center space-x-2 md:space-x-10 lg:space-x-20">
|
<div className="flex flex-row px-2 py-1 text-lg lg:px-6 lg:py-1.5 lg:text-3xl md:text-2xl justify-between md:justify-center space-x-2 md:space-x-10 lg:space-x-20">
|
||||||
{footerLinks}
|
{footerLinks}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export default function Header() {
|
|||||||
));
|
));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className={`fixed top-0 left-0 right-0 transition-transform duration-300 ${
|
<header className={`fixed top-0 left-0 right-0 bg-black font-bold transition-transform duration-300 ${
|
||||||
visible ? "translate-y-0" : "-translate-y-full"
|
visible ? "translate-y-0" : "-translate-y-full"
|
||||||
}`}>
|
}`}>
|
||||||
<div className="flex flex-row pt-1 px-2 text-lg lg:pt-2 lg:text-3xl md:text-2xl items-center justify-between md:justify-center space-x-2 md:space-x-10 lg:space-x-20">
|
<div className="flex flex-row pt-1 px-2 text-lg lg:pt-2 lg:text-3xl md:text-2xl items-center justify-between md:justify-center space-x-2 md:space-x-10 lg:space-x-20">
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ interface HeaderLink {
|
|||||||
|
|
||||||
export const Links: HeaderLink[] = [
|
export const Links: HeaderLink[] = [
|
||||||
{ id: 0, href: "/", label: "Home", color: "text-green" },
|
{ id: 0, href: "/", label: "Home", color: "text-green" },
|
||||||
{ id: 1, href: "about", label: "About", color: "text-yellow" },
|
{ id: 1, href: "/about", label: "About", color: "text-yellow" },
|
||||||
{ id: 2, href: "projects", label: "Projects", color: "text-blue" },
|
{ id: 2, href: "/projects", label: "Projects", color: "text-blue" },
|
||||||
{ id: 3, href: "blog", label: "Blog", color: "text-purple" },
|
{ id: 3, href: "/blog", label: "Blog", color: "text-purple" },
|
||||||
{ id: 4, href: "resume", label: "Resume", color: "text-aqua" }
|
{ id: 4, href: "/resume", label: "Resume", color: "text-aqua" }
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import Typewriter from "typewriter-effect";
|
import Typewriter from "typewriter-effect";
|
||||||
|
|
||||||
|
const html = (strings: TemplateStringsArray, ...values: any[]) => {
|
||||||
|
let result = strings[0];
|
||||||
|
for (let i = 0; i < values.length; i++) {
|
||||||
|
result += values[i] + strings[i + 1];
|
||||||
|
}
|
||||||
|
return result.replace(/\n\s*/g, "").replace(/\s+/g, " ").trim();
|
||||||
|
};
|
||||||
|
|
||||||
interface TypewriterOptions {
|
interface TypewriterOptions {
|
||||||
autoStart: boolean;
|
autoStart: boolean;
|
||||||
loop: boolean;
|
loop: boolean;
|
||||||
@@ -17,22 +25,40 @@ interface TypewriterInstance {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function Hero() {
|
export default function Hero() {
|
||||||
|
const SECTION_1 = html`
|
||||||
|
<span>Hello, I'm</span>
|
||||||
|
<br><div class="mb-4"></div>
|
||||||
|
<span><a href="/about" class="text-aqua hover:underline"><strong>Timothy Pidashev</strong></a></span>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const SECTION_2 = html`
|
||||||
|
<span>I've been turning</span>
|
||||||
|
<br><div class="mb-4"></div>
|
||||||
|
<span><a href="/projects" class="text-green hover:underline"><strong>coffee</strong></a> into
|
||||||
|
<a href="/projects" class="text-yellow hover:underline"><strong>code</strong></a></span>
|
||||||
|
<br><div class="mb-4"></div>
|
||||||
|
<span>since <a href="/about" class="text-blue hover:underline"><strong>2018</strong></a>!</span>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const SECTION_3 = html`
|
||||||
|
<span>Check out my</span>
|
||||||
|
<br><div class="mb-4"></div>
|
||||||
|
<span><a href="/blog" class="text-purple hover:underline"><strong>blog</strong></a>/
|
||||||
|
<a href="/projects" class="text-blue hover:underline"><strong>projects</strong></a> or</span>
|
||||||
|
<br><div class="mb-4"></div>
|
||||||
|
<span><a href="/contact" class="text-green hover:underline"><strong>contact</strong></a> me below!</span>
|
||||||
|
`;
|
||||||
|
|
||||||
const handleInit = (typewriter: TypewriterInstance): void => {
|
const handleInit = (typewriter: TypewriterInstance): void => {
|
||||||
typewriter
|
typewriter
|
||||||
.typeString("<center><span class='inline-block mb-4'>Hello, I'm</span><br><span class='inline-block mb-4'><strong class='text-aqua'>Timothy Pidashev</strong></span></center>")
|
.typeString(SECTION_1)
|
||||||
.pauseFor(2500)
|
.pauseFor(2000)
|
||||||
.deleteAll()
|
.deleteAll()
|
||||||
.start();
|
.typeString(SECTION_2)
|
||||||
|
.pauseFor(2000)
|
||||||
typewriter
|
|
||||||
.typeString("<center><span class='inline-block mb-4'>I've been turning</span><br><span class='inline-block mb-4'><strong class='text-green'>coffee</strong> into <strong class='text-yellow'>code</strong></span><br><span class='inline-block mb-4'>since <strong class='text-blue'>2018</strong>!</span></center>")
|
|
||||||
.pauseFor(2500)
|
|
||||||
.deleteAll()
|
.deleteAll()
|
||||||
.start();
|
.typeString(SECTION_3)
|
||||||
|
.pauseFor(2000)
|
||||||
typewriter
|
|
||||||
.typeString("<center><span class=''>Check out my <strong class='text-purple'>blog</strong> and <strong class='text-aqua'>shop</strong></span><br></span><br><span class=''>or <strong class='text-green'>contact</strong> me below!</span></center>")
|
|
||||||
.pauseFor(2500)
|
|
||||||
.deleteAll()
|
.deleteAll()
|
||||||
.start();
|
.start();
|
||||||
};
|
};
|
||||||
@@ -42,21 +68,17 @@ export default function Hero() {
|
|||||||
loop: true,
|
loop: true,
|
||||||
delay: 50,
|
delay: 50,
|
||||||
deleteSpeed: 800,
|
deleteSpeed: 800,
|
||||||
cursor: ''
|
cursor: '|'
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="flex justify-center items-center min-h-screen">
|
||||||
<div className="flex justify-center items-center h-full font-bold text-4xl">
|
<div className="text-4xl font-bold text-center">
|
||||||
<div className="h-screen flex flex-col items-center justify-center">
|
|
||||||
<div className="flex items-center justify-center relative h-58 overflow-y-auto">
|
|
||||||
<Typewriter
|
<Typewriter
|
||||||
options={typewriterOptions}
|
options={typewriterOptions}
|
||||||
onInit={handleInit}
|
onInit={handleInit}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|||||||
140
src/src/content/blog/generics-with-typescript.mdx
Normal file
140
src/src/content/blog/generics-with-typescript.mdx
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
---
|
||||||
|
title: Generics with TypeScript
|
||||||
|
description: A quick introduction to generics with TypeScript
|
||||||
|
author: Timothy Pidashev
|
||||||
|
tags: [typescript]
|
||||||
|
date: December 25, 2021
|
||||||
|
---
|
||||||
|
|
||||||
|
In this quick post we'll cover what are generics and how to use them with TypeScript. It answers a question I got from a friend, and I thought it could also be useful for others. We'll go through the following points:
|
||||||
|
|
||||||
|
- What are generics
|
||||||
|
- Using generic types
|
||||||
|
- Generics constraints
|
||||||
|
- Generics with functions
|
||||||
|
|
||||||
|
If you're familiar with the concept of code reuse, well it is just that. We can use generics to create reusable patterns for types. It results in flexible types that work with different variations, and also fewer types to maintain as you avoid duplication.
|
||||||
|
|
||||||
|
Code reuse is an important part of building flexible components/functions that serve complex and large systems. Nevertheless, abstraction is hard to get right in general and could cause inflexibility in some cases.
|
||||||
|
|
||||||
|
## What are generics
|
||||||
|
|
||||||
|
You can think of generics as arguments to your types. When a type with generics is used, those generics are passed to it. As we mentioned above, this will allow us to reuse the common patterns of the type declarations.
|
||||||
|
|
||||||
|
Many functions that you're already using could have optional generic types. For example the array methods like `Array.map()` and `Array.reduc()` accept a generic for elements type:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const numbers = [1, 2, 3]
|
||||||
|
numbers.map((n) => n)
|
||||||
|
numbers.map<number>((n) => n) // with a generic <number> type
|
||||||
|
```
|
||||||
|
|
||||||
|
You probably also came across generics while fixing Typescript type errors, without knowing what's going on. We've all been there. Once you learn about generics, you'll see them used everywhere, in the web APIs and third-party libraries.
|
||||||
|
|
||||||
|
Hope that by the end of this post it’ll be more clear to you.
|
||||||
|
|
||||||
|
## Generic types
|
||||||
|
|
||||||
|
We can start by building our first type with generics. Let's say you have two functions `getUser` and `getProduct` that return some data for a user and a product:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
type User = {
|
||||||
|
email: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Product = {
|
||||||
|
id: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserResponse = {
|
||||||
|
data: User
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProductResponse = {
|
||||||
|
data: Product
|
||||||
|
}
|
||||||
|
|
||||||
|
const user: UserResponse = getUser(id)
|
||||||
|
const product: ProductResponse = getProduct(id)
|
||||||
|
```
|
||||||
|
|
||||||
|
You'll notice that the `UserResponse` and `ProductResponse` are kind of the same type declaration, both have `data` key but with different types. You can expect that if a new type is added it will also need to have its own `YetAnotherResponse` type which will result in duplicating same response type over and over again. And in the case you want to make a change across `...Responses` types, good luck with that.
|
||||||
|
|
||||||
|
We can abstract this common pattern, so our `Response` will be a generic type and depending on the `Data` type passed, it will give the corresponding `Response` type.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
type User = {
|
||||||
|
email: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Product = {
|
||||||
|
id: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type GenericResponse<Data> = {
|
||||||
|
data: Data
|
||||||
|
}
|
||||||
|
|
||||||
|
const user: GenericResponse<User> = getUser(id)
|
||||||
|
const product: GenericResponse<Product> = getProduct(id)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Generics constraints
|
||||||
|
|
||||||
|
You might notice that so far the generic type accepts any type. That could be what you need, but sometimes we would want to limit or add constraints to a certain type for the generic type passed.
|
||||||
|
|
||||||
|
For a simple example, we can have an `Input` type that has a `Value` generic type. If we want to constrain the `Value` generic type possibilities to be only a `string` or a `number` type, we can specify that by adding an `extends` keyword after the generic name, then the specific constraints type:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
type Input<Value extends string | number> = Value
|
||||||
|
|
||||||
|
const input: Input<string> = 'text' // works
|
||||||
|
const input: Input<number> = 123456 // works
|
||||||
|
const input: Input<object> = {} // has error
|
||||||
|
```
|
||||||
|
|
||||||
|
## Generics with functions
|
||||||
|
|
||||||
|
Generics are part of type definitions. You just need to know how to annotate functions whether it's a function declaration or an arrow function expression:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
function getInput<Input>(input: Input): Input {
|
||||||
|
return input
|
||||||
|
}
|
||||||
|
|
||||||
|
const getInput = <Input>(input: Input): Input => {
|
||||||
|
return input
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Additional example ⌁
|
||||||
|
|
||||||
|
A few months back I wrote an over simplified version of React Query's `useQuery` hook to replace `useEffect` for data fetching in React. Here you'll find a generic `Query` type:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
type Query<Data> = {
|
||||||
|
loading: boolean
|
||||||
|
error: boolean
|
||||||
|
data: Data
|
||||||
|
}
|
||||||
|
|
||||||
|
type Key = string | string[]
|
||||||
|
type Fetcher<Data> = () => Promise<Data>
|
||||||
|
type Options = { enabled?: boolean; cacheTime?: number }
|
||||||
|
|
||||||
|
const useQuery = <Data>(key: Key, fetcher: Fetcher<Data>, options?: Options): Query<Data> => {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
You can find the entire version of this simplified `useQuery` hook on Render template at [src/hooks/query.ts](https://github.com/oedotme/render/blob/main/src/hooks/query.ts) if you want to take a look on the entire implementation.
|
||||||
|
|
||||||
|
## What's next
|
||||||
|
|
||||||
|
Hope that explained what are generics and how to start using them with TypeScript. You can find more details and examples at [TypeScript generics docs](https://www.typescriptlang.org/docs/handbook/2/generics.html).
|
||||||
|
|
||||||
|
There are very useful [TypeScript utility types](https://www.typescriptlang.org/docs/handbook/utility-types.html), that you'll use generics with. I highly recommend you to check them out, it will help you a lot while using TypeScript in general.
|
||||||
|
|
||||||
|
I would love to hear what you think about this post, feel free to leave a comment on the discussion. If you have questions or got stuck at some point I'll be happy to help.
|
||||||
|
|
||||||
|
Share this post if you find it useful and stay tuned for upcoming posts.
|
||||||
13
src/src/content/config.ts
Normal file
13
src/src/content/config.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { defineCollection, z } from "astro:content";
|
||||||
|
|
||||||
|
export const collections = {
|
||||||
|
blog: defineCollection({
|
||||||
|
schema: z.object({
|
||||||
|
title: z.string(),
|
||||||
|
description: z.string(),
|
||||||
|
author: z.string(),
|
||||||
|
tags: z.array(z.string()),
|
||||||
|
date: z.string(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}
|
||||||
@@ -1,8 +1,5 @@
|
|||||||
---
|
---
|
||||||
const { content } = Astro.props;
|
|
||||||
|
|
||||||
import "@/style/globals.css";
|
import "@/style/globals.css";
|
||||||
|
|
||||||
import Header from "@/components/header";
|
import Header from "@/components/header";
|
||||||
import Footer from "@/components/footer";
|
import Footer from "@/components/footer";
|
||||||
|
|
||||||
@@ -12,20 +9,22 @@ export interface Props {
|
|||||||
permalink: string;
|
permalink: string;
|
||||||
current?: string;
|
current?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { title, description, permalink, current } = Astro.props;
|
const { title, description, permalink, current } = Astro.props;
|
||||||
---
|
---
|
||||||
|
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width" />
|
<meta name="viewport" content="width=device-width" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
<title>{content.title}</title>
|
<title>{title}</title>
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-background text-foreground">
|
<body class="bg-background text-foreground">
|
||||||
<Header client:load />
|
<Header client:load />
|
||||||
<main>
|
<main>
|
||||||
|
<div class="max-w-5xl mx-auto pt-12 px-4 py-8">
|
||||||
<slot />
|
<slot />
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
<Footer client:load />
|
<Footer client:load />
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import Footer from "@/components/footer";
|
|||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width" />
|
<meta name="viewport" content="width=device-width" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
|
<link rel="sitemap" href="/sitemap-index.xml" />
|
||||||
<title>{content.title}</title>
|
<title>{content.title}</title>
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-background text-foreground">
|
<body class="bg-background text-foreground">
|
||||||
|
|||||||
59
src/src/lib/structuredData.ts
Normal file
59
src/src/lib/structuredData.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import { type Article, type Person, type WebSite, type WithContext } from "schema-dts";
|
||||||
|
import type { CollectionEntry } from 'astro:content';
|
||||||
|
|
||||||
|
export const blogWebsite: WithContext<WebSite> = {
|
||||||
|
'@context': 'https://schema.org',
|
||||||
|
'@type': 'WebSite',
|
||||||
|
url: `${import.meta.env.SITE}/blog/`,
|
||||||
|
name: 'Dzmitry Kozhukh blog',
|
||||||
|
description: 'Frontend insights',
|
||||||
|
inLanguage: 'en_US',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const mainWebsite: WithContext<WebSite> = {
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "WebSite",
|
||||||
|
url: import.meta.env.SITE,
|
||||||
|
name: "Timothy Pidashev - Personal website",
|
||||||
|
description: "Timothy Pidashev's contact page, portfolio and blog",
|
||||||
|
inLanguage: "en_US",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const personSchema: WithContext<Person> = {
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "Person",
|
||||||
|
name: "Timothy Pidashev",
|
||||||
|
url: "https://timmypidashev.dev",
|
||||||
|
sameAs: [
|
||||||
|
"https://github.com/timmypidashev",
|
||||||
|
"https://www.linkedin.com/in/timothy-pidashev-4353812b8",
|
||||||
|
],
|
||||||
|
jobTitle: "Software Engineer",
|
||||||
|
worksFor: {
|
||||||
|
"@type": "Organization",
|
||||||
|
name: "Fathers House Christian Center",
|
||||||
|
url: "https://fhccenter.org",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getArticleSchema(post: CollectionEntry<"blog">) {
|
||||||
|
const articleStructuredData: WithContext<Article> = {
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "Article",
|
||||||
|
headline: post.data.title,
|
||||||
|
url: `${import.meta.env.SITE}/blog/${post.slug}/`,
|
||||||
|
description: post.data.excerpt,
|
||||||
|
datePublished: post.data.date.toString(),
|
||||||
|
publisher: {
|
||||||
|
"@type": "Person",
|
||||||
|
name: "Timothy Pidashev",
|
||||||
|
url: import.meta.env.SITE,
|
||||||
|
},
|
||||||
|
author: {
|
||||||
|
"@type": "Person",
|
||||||
|
name: "Timothy Pidashev",
|
||||||
|
url: import.meta.env.SITE,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return articleStructuredData;
|
||||||
|
}
|
||||||
9
src/src/pages/404.astro
Normal file
9
src/src/pages/404.astro
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
import MainLayout from "@/layouts/main.astro";
|
||||||
|
|
||||||
|
const title = "404 Not Found";
|
||||||
|
---
|
||||||
|
|
||||||
|
<MainLayout content={{ title: "404 | Timothy Pidashev" }}>
|
||||||
|
<main>404 not found</main>
|
||||||
|
</MainLayout>
|
||||||
@@ -1,6 +1,17 @@
|
|||||||
---
|
---
|
||||||
import { CollectionEntry, getCollection } from "astro:content";
|
import { CollectionEntry, getCollection } from "astro:content";
|
||||||
import MainLayout from "@/layouts/main.astro";
|
import { Image } from "astro:assets";
|
||||||
|
import BlogLayout from "@/layouts/blog.astro";
|
||||||
|
|
||||||
|
import { Image } from "astro:assets";
|
||||||
|
import { getCollection } from "astro:content";
|
||||||
|
import type { CollectionEntry } from "astro:content";
|
||||||
|
import { getArticleSchema } from "@/lib/structuredData";
|
||||||
|
import { blogWebsite } from "@/lib/structuredData";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
post: CollectionEntry<"blog">;
|
||||||
|
}
|
||||||
|
|
||||||
export async function getStaticPaths() {
|
export async function getStaticPaths() {
|
||||||
const posts = await getCollection("blog");
|
const posts = await getCollection("blog");
|
||||||
@@ -10,20 +21,44 @@ export async function getStaticPaths() {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
type Props = CollectionEntry<"blog">;
|
|
||||||
const post = Astro.props;
|
const post = Astro.props;
|
||||||
const { Content } = await post.render();
|
const { Content } = await post.render();
|
||||||
|
|
||||||
|
const articleStructuredData = getArticleSchema(post);
|
||||||
|
|
||||||
|
const breadcrumbsStructuredData = {
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "BreadcrumbList",
|
||||||
|
itemListElement: [
|
||||||
|
{
|
||||||
|
"@type": "ListItem",
|
||||||
|
position: 1,
|
||||||
|
name: "Blog",
|
||||||
|
item: `${import.meta.env.SITE}/blog/`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"@type": "ListItem",
|
||||||
|
position: 2,
|
||||||
|
name: post.data.title,
|
||||||
|
item: `${import.meta.env.SITE}/blog/${post.slug}/`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const jsonLd = {
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@graph": [articleStructuredData, breadcrumbsStructuredData, blogWebsite],
|
||||||
|
};
|
||||||
---
|
---
|
||||||
|
|
||||||
<MainLayout>
|
<BlogLayout>
|
||||||
<article>
|
<script type="application/ld+json" set:html={JSON.stringify(jsonLd)} />
|
||||||
<h1 class="title">{post.data.title}</h1>
|
<article class="prose">
|
||||||
<p>by <a href={`/authors/${post.data.author.toLowerCase()}/`}>{post.data.author}</a>,
|
<h1 class="text-3xl pt-4">{post.data.title}</h1>
|
||||||
published {post.data.pubDate.toDateString()},
|
<p class="lg:text-2xl sm:text-lg">{post.data.description}</p>
|
||||||
tags: <strong>{post.data.tags.join(", ")}</strong>
|
<p class="text-lg pb-4">{post.data.author} | {post.data.date}</h1>
|
||||||
</p>
|
<hr class="bg-orange" />
|
||||||
<hr />
|
<br />
|
||||||
<Content />
|
<Content />
|
||||||
</article>
|
</article>
|
||||||
</MainLayout>
|
</BlogLayout>
|
||||||
|
|
||||||
|
|||||||
@@ -1,29 +1,16 @@
|
|||||||
---
|
---
|
||||||
import { getCollection } from 'astro:content';
|
import { getCollection } from "astro:content";
|
||||||
import MainLayout from '@/layouts/main.astro';
|
import MainLayout from "@/layouts/main.astro";
|
||||||
|
|
||||||
const posts = (await getCollection('blog', ({ data }) => {
|
import { BlogPostList } from "@/components/blog/post-list";
|
||||||
|
|
||||||
|
const posts = (await getCollection("blog", ({ data }) => {
|
||||||
return data.isDraft !== true;
|
return data.isDraft !== true;
|
||||||
})).sort(
|
})).sort(
|
||||||
(a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf()
|
(a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf()
|
||||||
);
|
);
|
||||||
---
|
---
|
||||||
|
|
||||||
<MainLayout>
|
<MainLayout content={{ title: "Blog | Timothy Pidashev" }}>
|
||||||
<section>
|
<BlogPostList posts={posts} client:load />
|
||||||
<ul>
|
|
||||||
{
|
|
||||||
posts.map((post) => (
|
|
||||||
<li>
|
|
||||||
<a href={`/blog/${post.slug}/`}>{post.data.title}</a>
|
|
||||||
<p>by <a href={`/authors/${post.data.author.toLowerCase()}/`}>{post.data.author}</a>,
|
|
||||||
published {post.data.pubDate.toDateString()},
|
|
||||||
tags: <strong>{post.data.tags.join(", ")}</strong>
|
|
||||||
</p>
|
|
||||||
<p>{post.data.description}</p>
|
|
||||||
</li>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
</ul>
|
|
||||||
</section>
|
|
||||||
</MainLayout>
|
</MainLayout>
|
||||||
|
|||||||
13
src/src/pages/robots.txt.ts
Normal file
13
src/src/pages/robots.txt.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import type { APIRoute } from "astro";
|
||||||
|
|
||||||
|
const getRobotsTxt = (sitemapURL: URL) => `
|
||||||
|
User-agent: *
|
||||||
|
Allow: /
|
||||||
|
|
||||||
|
Sitemap: ${sitemapURL.href}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const GET: APIRoute = ({ site }) => {
|
||||||
|
const sitemapURL = new URL("sitemap-index.xml", site);
|
||||||
|
return new Response(getRobotsTxt(sitemapURL));
|
||||||
|
};
|
||||||
@@ -33,8 +33,154 @@ module.exports = {
|
|||||||
DEFAULT: "#689d6a",
|
DEFAULT: "#689d6a",
|
||||||
bright: "#8ec07c"
|
bright: "#8ec07c"
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
plugins: [],
|
typography: (theme) => ({
|
||||||
|
DEFAULT: {
|
||||||
|
css: {
|
||||||
|
color: theme('colors.foreground'),
|
||||||
|
'--tw-prose-body': theme('colors.foreground'),
|
||||||
|
'--tw-prose-headings': theme('colors.yellow.bright'),
|
||||||
|
'--tw-prose-links': theme('colors.blue.bright'),
|
||||||
|
'--tw-prose-bold': theme('colors.orange.bright'),
|
||||||
|
'--tw-prose-quotes': theme('colors.green.bright'),
|
||||||
|
'--tw-prose-code': theme('colors.purple.bright'),
|
||||||
|
'--tw-prose-hr': theme('colors.foreground'),
|
||||||
|
'--tw-prose-bullets': theme('colors.foreground'),
|
||||||
|
|
||||||
|
// Base text color
|
||||||
|
color: theme('colors.foreground'),
|
||||||
|
|
||||||
|
// Headings
|
||||||
|
h1: {
|
||||||
|
color: theme('colors.yellow.bright'),
|
||||||
|
fontWeight: '700',
|
||||||
|
},
|
||||||
|
h2: {
|
||||||
|
color: theme('colors.yellow.bright'),
|
||||||
|
fontWeight: '600',
|
||||||
|
},
|
||||||
|
h3: {
|
||||||
|
color: theme('colors.yellow.bright'),
|
||||||
|
fontWeight: '600',
|
||||||
|
},
|
||||||
|
h4: {
|
||||||
|
color: theme('colors.yellow.bright'),
|
||||||
|
fontWeight: '600',
|
||||||
|
},
|
||||||
|
|
||||||
|
// Links
|
||||||
|
a: {
|
||||||
|
color: theme('colors.blue.bright'),
|
||||||
|
'&:hover': {
|
||||||
|
color: theme('colors.blue.DEFAULT'),
|
||||||
|
},
|
||||||
|
textDecoration: 'none',
|
||||||
|
borderBottom: `1px solid ${theme('colors.blue.bright')}`,
|
||||||
|
transition: 'all 0.2s ease-in-out',
|
||||||
|
},
|
||||||
|
|
||||||
|
// Bold
|
||||||
|
strong: {
|
||||||
|
color: theme('colors.orange.bright'),
|
||||||
|
fontWeight: '600',
|
||||||
|
},
|
||||||
|
|
||||||
|
// Lists
|
||||||
|
ul: {
|
||||||
|
li: {
|
||||||
|
'&::before': {
|
||||||
|
backgroundColor: theme('colors.foreground'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Blockquotes
|
||||||
|
blockquote: {
|
||||||
|
borderLeftColor: theme('colors.green.bright'),
|
||||||
|
color: theme('colors.green.bright'),
|
||||||
|
fontStyle: 'italic',
|
||||||
|
quotes: '"\\201C""\\201D""\\2018""\\2019"',
|
||||||
|
p: {
|
||||||
|
'&::before': { content: 'none' },
|
||||||
|
'&::after': { content: 'none' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Code
|
||||||
|
code: {
|
||||||
|
color: theme('colors.purple.bright'),
|
||||||
|
backgroundColor: '#282828', // A dark gray that works with black
|
||||||
|
padding: '0.2em 0.4em',
|
||||||
|
borderRadius: '0.25rem',
|
||||||
|
fontWeight: '400',
|
||||||
|
'&::before': {
|
||||||
|
content: '""',
|
||||||
|
},
|
||||||
|
'&::after': {
|
||||||
|
content: '""',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Inline code
|
||||||
|
'code::before': {
|
||||||
|
content: '""',
|
||||||
|
},
|
||||||
|
'code::after': {
|
||||||
|
content: '""',
|
||||||
|
},
|
||||||
|
|
||||||
|
// Pre
|
||||||
|
pre: {
|
||||||
|
backgroundColor: '#282828',
|
||||||
|
color: theme('colors.foreground'),
|
||||||
|
code: {
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
padding: '0',
|
||||||
|
color: 'inherit',
|
||||||
|
fontSize: 'inherit',
|
||||||
|
fontWeight: 'inherit',
|
||||||
|
'&::before': { content: 'none' },
|
||||||
|
'&::after': { content: 'none' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Horizontal rules
|
||||||
|
hr: {
|
||||||
|
borderColor: theme('colors.foreground'),
|
||||||
|
opacity: '0.2',
|
||||||
|
},
|
||||||
|
|
||||||
|
// Table
|
||||||
|
table: {
|
||||||
|
thead: {
|
||||||
|
borderBottomColor: theme('colors.foreground'),
|
||||||
|
th: {
|
||||||
|
color: theme('colors.yellow.bright'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tbody: {
|
||||||
|
tr: {
|
||||||
|
borderBottomColor: theme('colors.foreground'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Images
|
||||||
|
img: {
|
||||||
|
borderRadius: '0.375rem',
|
||||||
|
},
|
||||||
|
|
||||||
|
// Figures
|
||||||
|
figcaption: {
|
||||||
|
color: theme('colors.foreground'),
|
||||||
|
opacity: '0.8',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
require("@tailwindcss/typography"),
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user