new file: .eslintrc.json

new file:   .github/workflows/ci.yml
	new file:   .gitignore
	new file:   .prettierignore
	new file:   .storybook/main.ts
	new file:   .storybook/preview.tsx
	new file:   LICENSE.md
	new file:   README.md
	new file:   app/[...slug]/not-found.tsx
	new file:   app/[...slug]/page.tsx
	new file:   app/layout.tsx
	new file:   app/loading.tsx
	new file:   app/og/route.tsx
	new file:   app/page.tsx
	new file:   app/posts/[...slug]/not-found.tsx
	new file:   app/posts/[...slug]/page.tsx
	new file:   app/posts/page.tsx
	new file:   components/analytics.tsx
	new file:   components/blog-title.tsx
	new file:   components/button.tsx
	new file:   components/callout.tsx
	new file:   components/comments.tsx
main
joker 2 years ago
commit 81bde603a6
  1. 10
      .eslintrc.json
  2. 43
      .github/workflows/ci.yml
  3. 38
      .gitignore
  4. 38
      .prettierignore
  5. 17
      .storybook/main.ts
  6. 66
      .storybook/preview.tsx
  7. 21
      LICENSE.md
  8. 14
      README.md
  9. 15
      app/[...slug]/not-found.tsx
  10. 64
      app/[...slug]/page.tsx
  11. 117
      app/layout.tsx
  12. 9
      app/loading.tsx
  13. 122
      app/og/route.tsx
  14. 55
      app/page.tsx
  15. 22
      app/posts/[...slug]/not-found.tsx
  16. 76
      app/posts/[...slug]/page.tsx
  17. 42
      app/posts/page.tsx
  18. 7
      components/analytics.tsx
  19. 45
      components/blog-title.tsx
  20. 27
      components/button.tsx
  21. 63
      components/callout.tsx
  22. 83
      components/code-block.tsx
  23. 27
      components/comments.tsx
  24. 18
      components/font-style-provider.tsx
  25. 153
      components/footer.tsx
  26. 48
      components/header.tsx
  27. 123
      components/hero-section.tsx
  28. 86
      components/mdx-components.tsx
  29. 20
      components/mdx-content.tsx
  30. 29
      components/mdx-styles.tsx
  31. 39
      components/navigation-bar.tsx
  32. 88
      components/page-controls.tsx
  33. 73
      components/post-card.tsx
  34. 65
      components/post-intro.tsx
  35. 40
      components/post-paginator.tsx
  36. 25
      components/post-tags.tsx
  37. 69
      components/search-input.tsx
  38. 76
      components/search-results.tsx
  39. 42
      components/search-tags.tsx
  40. 84
      components/search.tsx
  41. 25
      components/stories/blog-title.stories.tsx
  42. 30
      components/stories/button.stories.tsx
  43. 69
      components/stories/callout.stories.tsx
  44. 426
      components/stories/code-block.stories.tsx
  45. 7
      components/stories/decorators/center.tsx
  46. 3
      components/stories/decorators/index.ts
  47. 11
      components/stories/decorators/markdown.tsx
  48. 7
      components/stories/decorators/padding.tsx
  49. 22
      components/stories/footer.stories.tsx
  50. 43
      components/stories/header.stories.tsx
  51. 15
      components/stories/hero-section.stories.tsx
  52. 40
      components/stories/post-card.stories.tsx
  53. 21
      components/stories/post-intro.stories.tsx
  54. 69
      components/stories/post-paginator.stories.tsx
  55. 28
      components/stories/post-tags.stories.tsx
  56. 35
      components/stories/search.stories.tsx
  57. 39
      components/stories/table-of-contents.stories.tsx
  58. 42
      components/stories/tooltip.stories.tsx
  59. 37
      components/table-of-contents.tsx
  60. 139
      components/toolbar.tsx
  61. 20
      components/tooltip.tsx
  62. 67
      config/index.js
  63. 135
      config/types.ts
  64. 26
      content/pages/about.mdx
  65. 651
      content/posts/olimp/practice-test-10-11-robotics.mdx
  66. 504
      content/posts/olimp/practice-test-5-6-robotics.mdx
  67. 170
      contentlayer.config.ts
  68. 59
      lib/datetime.test.ts
  69. 107
      lib/datetime.ts
  70. 36
      lib/search.test.ts
  71. 67
      lib/search.ts
  72. 7
      lib/utils.ts
  73. 7
      next-sitemap.config.js
  74. 10
      next.config.js
  75. 23542
      package-lock.json
  76. 86
      package.json
  77. 12536
      pnpm-lock.yaml
  78. 6
      postcss.config.js
  79. 38
      prettier.config.js
  80. BIN
      public/android-chrome-192x192.png
  81. BIN
      public/android-chrome-512x512.png
  82. BIN
      public/apple-touch-icon.png
  83. BIN
      public/assets/RedHatDisplay-Bold.ttf
  84. BIN
      public/assets/RedHatDisplay-Regular.ttf
  85. BIN
      public/assets/RedHatDisplay-SemiBold.ttf
  86. BIN
      public/favicon-16x16.png
  87. BIN
      public/favicon-32x32.png
  88. BIN
      public/favicon.ico
  89. BIN
      public/images/computer-vision/lane-detection/first-frame.webp
  90. BIN
      public/images/computer-vision/lane-detection/frame-with-lane-markings.webp
  91. BIN
      public/images/computer-vision/lane-detection/lines-cropped.webp
  92. BIN
      public/images/computer-vision/lane-detection/lines-too-long.webp
  93. BIN
      public/images/computer-vision/lane-detection/polar-coordinates.webp
  94. BIN
      public/images/computer-vision/lane-detection/polar-to-cartesian.webp
  95. BIN
      public/images/computer-vision/lane-detection/preprocessing-steps.webp
  96. BIN
      public/images/computer-vision/lane-detection/trapezoid-mask.webp
  97. BIN
      public/images/rust/fizzbuzz/extended.webp
  98. BIN
      public/images/web-dev/mdx-nextjs-13/frontmatter.webp
  99. BIN
      public/images/web-dev/mdx-nextjs-13/meme.webp
  100. BIN
      public/images/web-dev/mdx-nextjs-13/result.webp
  101. Some files were not shown because too many files have changed in this diff Show More

@ -0,0 +1,10 @@
{
"extends": ["next/core-web-vitals", "plugin:storybook/recommended"],
"plugins": ["eslint-plugin-tailwindcss"],
"rules": {
"tailwindcss/classnames-order": ["error", { "callees": ["cn"] }],
"tailwindcss/no-custom-classname": ["error", { "callees": ["cn"] }],
"tailwindcss/no-contradicting-classname": ["error", { "callees": ["cn"] }],
"tailwindcss/enforces-shorthand": ["error", { "callees": ["cn"] }]
}
}

@ -0,0 +1,43 @@
name: 'CI'
on:
pull_request:
push:
branches: ['prod']
workflow_dispatch:
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: ['ubuntu-latest']
node-version: ['16.x']
steps:
- name: 'Checkout repository'
uses: actions/checkout@v3
- name: 'Use Node.js v${{ matrix.node-version }}'
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
- name: 'Install pnpm'
uses: pnpm/action-setup@v2
with:
version: 6.0.2
- name: 'Install Dependencies'
run: pnpm install
- name: 'Build Contentlayer'
run: NODE_ENV=production npx contentlayer build
- name: 'Run Type Checks'
run: npx tsc --noEmit
- name: 'Run Lint and Format Checks'
run: pnpm run style:all
- name: 'Run Tests'
run: pnpm run test

38
.gitignore vendored

@ -0,0 +1,38 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
.vscode
.contentlayer
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

@ -0,0 +1,38 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
.vscode
.contentlayer
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

@ -0,0 +1,17 @@
import { type StorybookConfig } from '@storybook/nextjs';
const config: StorybookConfig = {
stories: ['../components/stories/**/*.stories.@(js|jsx|ts|tsx)'],
addons: [
'@storybook/addon-links',
'@storybook/addon-essentials',
'@storybook/addon-interactions',
'storybook-tailwind-dark-mode',
],
framework: {
name: '@storybook/nextjs',
options: {},
},
};
export default config;

@ -0,0 +1,66 @@
import '@/styles/globals.css';
import 'react-tooltip/dist/react-tooltip.css';
import { Red_Hat_Display } from 'next/font/google';
import type { Decorator, Parameters } from '@storybook/react';
import type { GlobalTypes } from '@storybook/types';
import { cn } from '@/lib/utils';
const fontSans = Red_Hat_Display({
subsets: ['latin'],
variable: '--font-red-hat',
});
export const parameters: Parameters = {
nextjs: { appDirectory: true },
layout: 'fullscreen',
actions: { argTypesRegex: '^on[A-Z].*' },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
};
export const globalTypes: GlobalTypes = {
darkMode: {
type: 'boolean',
defaultValue: false,
},
};
export const decorators: Decorator[] = [
(Story) => (
<div
className="font-sans"
style={
{ '--font-red-hat': fontSans.style.fontFamily } as React.CSSProperties
}
>
<div className="grid min-h-screen grid-cols-1 grid-rows-1 bg-slate-200 dark:bg-slate-700 sm:grid-cols-layout">
<div className="col-span-1 min-h-screen bg-slate-200 dark:bg-slate-700 sm:col-start-2">
<Story />
</div>
<div // left column
className={cn(
'col-span-1 col-start-1 row-span-3 row-start-1 hidden bg-gradient-to-r sm:block',
'from-slate-300 via-slate-400 to-slate-500',
'dark:from-slate-800 dark:via-slate-700 dark:to-slate-600',
)}
>
<div className="invisible h-full w-full bg-gradient-to-l from-rose-50 to-slate-700 opacity-25 dark:visible" />
</div>
<div // right column
className={cn(
'col-span-1 col-start-3 row-span-3 row-start-1 hidden bg-gradient-to-l sm:block',
'from-slate-300 via-slate-400 to-slate-500',
'dark:from-slate-800 dark:via-slate-700 dark:to-slate-600',
)}
>
<div className="invisible h-full w-full bg-gradient-to-r from-rose-50 to-slate-700 opacity-25 dark:visible" />
</div>
</div>
</div>
),
];

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 Kfir Fitousi
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

@ -0,0 +1,14 @@
[![‹kfir/blog›](https://user-images.githubusercontent.com/37262772/213866538-70e7024e-64f5-4bd8-ba92-af803e6169e7.png)](https://blog.kfirfitousi.com)
![Build Status](https://img.shields.io/github/deployments/kfirfitousi/blog/Production%20%E2%80%93%20blog?label=build&logo=vercel&style=for-the-badge)
![Website Status](https://img.shields.io/website?down_color=lightgrey&logo=vercel&style=for-the-badge&url=https%3A%2F%2Fblog.kfirfitousi.com)
![CI Status](https://img.shields.io/github/actions/workflow/status/kfirfitousi/blog/ci.yml?branch=main&label=CI&logo=github&style=for-the-badge)
![License](https://img.shields.io/github/license/kfirfitousi/blog?color=blue&style=for-the-badge)
Blog built with Next.js 13, TypeScript, Contentlayer, Zustand and TailwindCSS.
- 🔥 Using latest Next.js 13 features including the `/app` directory, SEO & Metadata, `next/font`, and React 18's Server Components
- 🎛 Customizable reading experience - light/dark mode, serif/sans-serif, and font size
- 🧩 MDX plugins and custom components
- 💬 Comment sections using [Giscus](https://giscus.app/)
- ⚡ Vercel OG image generation at the Edge
- 📖 Storybook v7 integration (published to [story.blog.kfirfitousi.com](https://story.blog.kfirfitousi.com))

@ -0,0 +1,15 @@
import { Home, XCircle } from 'lucide-react';
import { Button } from '@/components/button';
export default function NotFound() {
return (
<div className="flex h-full flex-col items-center justify-center space-y-4">
<XCircle className="h-24 w-24 text-slate-700 dark:text-rose-100" />
<h2 className="text-3xl font-bold text-slate-700 dark:text-rose-50">
Page not found
</h2>
<Button href="/" label="Home" icon={<Home className="h-4 w-4" />} />
</div>
);
}

@ -0,0 +1,64 @@
import { notFound } from 'next/navigation';
import { type Metadata } from 'next/types';
import { allPages } from 'contentlayer/generated';
import { blogConfig } from '@/config';
import { MDXContent } from '@/components/mdx-content';
type PageProps = {
params: {
slug: string[];
};
};
export async function generateStaticParams(): Promise<PageProps['params'][]> {
return allPages.map(({ slug }) => ({
slug: slug.split('/'),
}));
}
export function generateMetadata({ params }: PageProps): Metadata {
const { title, description, url } = allPages.find(
({ slug }) => slug === params.slug.join('/'),
) || {
title: 'Page Not Found',
description: 'Page Not Found',
url: '/',
};
const ogImage = {
url: `${blogConfig.url}/og?title=${title}`,
};
return {
title,
description,
openGraph: {
type: 'website',
url: `${blogConfig.url}${url}`,
title,
description,
images: [ogImage],
},
twitter: {
title,
description,
images: ogImage,
card: 'summary_large_image',
},
};
}
export default async function Page({ params }: PageProps) {
const page = allPages.find(({ slug }) => slug === params.slug.join('/'));
if (!page) {
notFound();
}
return (
<div className="h-full px-8">
<MDXContent code={page.body.code} />
</div>
);
}

@ -0,0 +1,117 @@
import '@/styles/globals.css';
import 'react-tooltip/dist/react-tooltip.css';
import { Newsreader, Red_Hat_Display } from 'next/font/google';
import { type Metadata } from 'next/types';
import { allPosts } from 'contentlayer/generated';
import { blogConfig } from '@/config';
import { Analytics } from '@/components/analytics';
import { FontStyleProvider } from '@/components/font-style-provider';
import { Footer } from '@/components/footer';
import { Header } from '@/components/header';
import { Search } from '@/components/search';
import { cn } from '@/lib/utils';
const fontSans = Red_Hat_Display({
subsets: ['latin'],
variable: '--font-red-hat',
});
const fontSerif = Newsreader({
subsets: ['latin'],
variable: '--font-newsreader',
});
type RootLayoutProps = {
children: React.ReactNode;
};
export const metadata: Metadata = {
title: {
default: blogConfig.title,
template: `${blogConfig.title} | %s`,
},
openGraph: {
title: {
default: blogConfig.title,
template: `${blogConfig.title} | %s`,
},
},
twitter: {
title: {
default: blogConfig.title,
template: `${blogConfig.title} | %s`,
},
},
robots: {
index: true,
follow: true,
},
icons: [
{
rel: 'apple-touch-icon',
sizes: '180x180',
url: '/apple-touch-icon.png',
},
{
rel: 'icon',
type: 'image/png',
sizes: '32x32',
url: '/favicon-32x32.png',
},
{
rel: 'icon',
type: 'image/png',
sizes: '16x16',
url: '/favicon-16x16.png',
},
],
};
export default function RootLayout({ children }: RootLayoutProps) {
return (
<html
lang="ru"
className={cn(
'scroll-pt-16 overflow-auto overscroll-none',
fontSans.variable,
fontSerif.variable,
)}
>
<head />
<body className="grid min-h-screen grid-cols-1 grid-rows-layout bg-slate-200 dark:bg-slate-700 sm:grid-cols-layout">
<FontStyleProvider>
<section className="sticky top-0 z-30 col-span-1 row-span-1 row-start-1 h-full self-start sm:col-start-2">
<Header />
</section>
<main className="col-span-1 row-start-2 sm:col-start-2">
{children}
</main>
<section className="col-span-3 row-span-1 row-start-3 sm:col-span-1 sm:col-start-2">
<Footer />
</section>
<div // left column
className={cn(
'col-span-1 col-start-1 row-span-3 row-start-1 hidden bg-gradient-to-r sm:block',
'from-slate-300 via-slate-400 to-slate-500',
'dark:from-slate-800 dark:via-slate-700 dark:to-slate-600',
)}
>
<div className="invisible h-full w-full bg-gradient-to-l from-rose-50 to-slate-700 opacity-25 dark:visible" />
</div>
<div // right column
className={cn(
'col-span-1 col-start-3 row-span-3 row-start-1 hidden bg-gradient-to-l sm:block',
'from-slate-300 via-slate-400 to-slate-500',
'dark:from-slate-800 dark:via-slate-700 dark:to-slate-600',
)}
>
<div className="invisible h-full w-full bg-gradient-to-r from-rose-50 to-slate-700 opacity-25 dark:visible" />
</div>
<Search posts={allPosts} />
<Analytics />
</FontStyleProvider>
</body>
</html>
);
}

@ -0,0 +1,9 @@
import { Loader2 } from 'lucide-react';
export default function Loading() {
return (
<div className="flex h-full flex-col items-center justify-center">
<Loader2 className="h-12 w-12 animate-spin text-slate-700 dark:text-rose-50" />
</div>
);
}

@ -0,0 +1,122 @@
import { NextRequest } from 'next/server';
import { ImageResponse } from '@vercel/og';
import colors from 'tailwindcss/colors';
import { blogConfig } from '@/config';
export const runtime = 'edge';
const fontRegular = fetch(
new URL('../../public/assets/RedHatDisplay-Regular.ttf', import.meta.url),
).then((res) => res.arrayBuffer());
const fontSemiBold = fetch(
new URL('../../public/assets/RedHatDisplay-SemiBold.ttf', import.meta.url),
).then((res) => res.arrayBuffer());
const fontBold = fetch(
new URL('../../public/assets/RedHatDisplay-Bold.ttf', import.meta.url),
).then((res) => res.arrayBuffer());
export async function GET(req: NextRequest) {
const fontRegularData = await fontRegular;
const fontSemiBoldData = await fontSemiBold;
const fontBoldData = await fontBold;
const accentColor = blogConfig.theme?.accentColor?.light || colors.rose[700];
try {
const { searchParams } = new URL(req.url);
// ?title=<title>&subtitle=<subtitle>
const hasTitle = searchParams.has('title');
const title = hasTitle && searchParams.get('title')?.slice(0, 100);
const hasSubtitle = searchParams.has('subtitle');
const subtitle = hasSubtitle && searchParams.get('subtitle')?.slice(0, 100);
return new ImageResponse(
(
<div tw="h-full w-full flex flex-col items-center justify-center bg-slate-300">
<div
tw="flex flex-row items-center text-7xl"
style={{
marginBottom: title ? '8rem' : '0rem',
fontFamily: 'Red Hat Display',
}}
>
{blogConfig.titleParts && (
<div style={{ color: accentColor }}></div>
)}
<h1
tw="mx-0.5 font-semibold text-center text-slate-800"
style={{
fontFamily: 'Red Hat Display Bold',
}}
>
{blogConfig.titleParts ? (
<>
{blogConfig.titleParts[0]}
<span
tw="px-px font-light"
style={{
fontFamily: 'Red Hat Display',
color: accentColor,
}}
>
/
</span>
{blogConfig.titleParts[1]}
</>
) : (
blogConfig.title
)}
</h1>
{blogConfig.titleParts && (
<div style={{ color: accentColor }}></div>
)}
</div>
<div
tw="text-5xl text-slate-800 text-center mb-4"
style={{ fontFamily: 'Red Hat Display SemiBold' }}
>
{title}
</div>
<div
tw="text-2xl text-slate-800 text-center"
style={{ fontFamily: 'Red Hat Display' }}
>
{subtitle}
</div>
</div>
),
{
width: 1200,
height: 630,
fonts: [
{
name: 'Red Hat Display',
data: fontRegularData,
style: 'normal',
},
{
name: 'Red Hat Display SemiBold',
data: fontSemiBoldData,
style: 'normal',
},
{
name: 'Red Hat Display Bold',
data: fontBoldData,
style: 'normal',
},
],
},
);
} catch (e: any) {
console.log(`${e.message}`);
return new Response('Failed to generate the image', {
status: 500,
});
}
}

@ -0,0 +1,55 @@
import { type Metadata } from 'next/types';
import { allPosts } from 'contentlayer/generated';
import { compareDesc } from 'date-fns';
import { FileText } from 'lucide-react';
import { blogConfig } from '@/config';
import { Button } from '@/components/button';
import { HeroSection } from '@/components/hero-section';
import { PostCard } from '@/components/post-card';
const { title, description } = blogConfig.pages.home;
const ogImage = {
url: `${blogConfig.url}/og`,
};
export const metadata: Metadata = {
title,
description,
openGraph: {
type: 'website',
url: blogConfig.url,
title,
description,
images: [ogImage],
},
twitter: {
description,
images: ogImage,
card: 'summary_large_image',
},
};
export default function Home() {
const latestPosts = allPosts
.sort((a, b) => compareDesc(new Date(a.date), new Date(b.date)))
.slice(0, 3);
return (
<div className="flex h-full flex-col space-y-4 px-6 pb-12 sm:px-12">
<HeroSection />
<section className="flex w-full flex-col space-y-4">
{latestPosts.map((post) => (
<PostCard key={post._id} post={post} />
))}
<Button
href="/posts"
label="Все статьи"
className="place-self-end"
icon={<FileText className="h-4 w-4" />}
/>
</section>
</div>
);
}

@ -0,0 +1,22 @@
import { FileQuestion, FileText, Home } from 'lucide-react';
import { Button } from '@/components/button';
export default function NotFound() {
return (
<div className="flex h-full flex-col items-center justify-center space-y-4">
<FileQuestion className="h-24 w-24 text-slate-700 dark:text-rose-100" />
<h2 className="text-3xl font-bold text-slate-700 dark:text-rose-50">
Post not found
</h2>
<div className="flex flex-row space-x-2">
<Button
href="/posts"
label="All Posts"
icon={<FileText className="h-4 w-4" />}
/>
<Button href="/" label="Home" icon={<Home className="h-4 w-4" />} />
</div>
</div>
);
}

@ -0,0 +1,76 @@
import { notFound } from 'next/navigation';
import { type Metadata } from 'next/types';
import { allPosts } from 'contentlayer/generated';
import { blogConfig } from '@/config';
import { Comments } from '@/components/comments';
import { MDXContent } from '@/components/mdx-content';
import { PostIntro } from '@/components/post-intro';
type PostPageProps = {
params: {
slug: string[];
};
};
export async function generateStaticParams(): Promise<
PostPageProps['params'][]
> {
return allPosts.map(({ slug }) => ({
slug: slug.split('/'),
}));
}
export function generateMetadata({ params }: PostPageProps): Metadata {
const { title, excerpt, url, date } = allPosts.find(
({ slug }) => slug === params.slug.join('/'),
) || {
title: 'Post Not Found',
excerpt: null,
url: '/posts',
date: new Date().toISOString(),
};
const ogImage = {
url: `${blogConfig.url}/og?title=${title}&subtitle=${excerpt ?? ''}`,
};
const description = excerpt ?? 'Post Not Found';
return {
title,
description,
openGraph: {
type: 'article',
url: `${blogConfig.url}${url}`,
title,
description,
publishedTime: date,
images: [ogImage],
},
twitter: {
title,
description,
images: ogImage,
card: 'summary_large_image',
},
};
}
export default function PostPage({ params }: PostPageProps) {
const post = allPosts.find(({ slug }) => slug === params.slug.join('/'));
if (!post) {
notFound();
}
return (
<article className="h-full px-8">
<PostIntro title={post.title} date={post.date} tags={post.tags} />
<MDXContent code={post.body.code} />
{
//<Comments />
}
</article>
);
}

@ -0,0 +1,42 @@
import { type Metadata } from 'next/types';
import { allPosts } from 'contentlayer/generated';
import { compareDesc } from 'date-fns';
import { blogConfig } from '@/config';
import { PostPaginator } from '@/components/post-paginator';
const { url, title, description } = blogConfig.pages.posts;
const ogImage = {
url: `${blogConfig.url}/og?title=${title}`,
};
export const metadata: Metadata = {
title,
description,
openGraph: {
type: 'website',
url: `${blogConfig.url}${url}`,
title,
description,
images: [ogImage],
},
twitter: {
title,
description,
images: ogImage,
card: 'summary_large_image',
},
};
export default function PostsPage() {
const posts = allPosts.sort((a, b) =>
compareDesc(new Date(a.date), new Date(b.date)),
);
return (
<div className="h-full px-6 pb-12 sm:px-12">
<PostPaginator posts={posts} />
</div>
);
}

@ -0,0 +1,7 @@
'use client';
import { Analytics as VercelAnalytics } from '@vercel/analytics/react';
export function Analytics() {
return <VercelAnalytics />;
}

@ -0,0 +1,45 @@
'use client';
import { blogConfig } from '@/config';
import { cn } from '@/lib/utils';
type BlogTitleProps = {
className?: string;
};
export function BlogTitle({ className }: BlogTitleProps) {
return (
<div
className={cn(
'inline-flex w-full items-center justify-center',
className,
)}
>
{blogConfig.titleParts && (
<div className="text-accent dark:text-accent-dark"></div>
)}
<h1
className={cn(
'mx-0.5 whitespace-nowrap text-center font-semibold drop-shadow-sm',
'text-slate-800 hover:text-accent',
'dark:text-rose-50 dark:hover:text-accent-dark',
)}
>
{blogConfig.titleParts ? (
<>
{blogConfig.titleParts[0]}
<span className="px-px font-light text-accent dark:text-accent-dark">
/
</span>
{blogConfig.titleParts[1]}
</>
) : (
blogConfig.title
)}
</h1>
{blogConfig.titleParts && (
<div className="text-accent dark:text-accent-dark"></div>
)}
</div>
);
}

@ -0,0 +1,27 @@
import Link from 'next/link';
import { cn } from '@/lib/utils';
type ButtonProps = {
href: string;
label: string;
className?: string;
icon?: React.ReactNode;
};
export function Button({ href, label, className, icon }: ButtonProps) {
return (
<Link href={href} className={className}>
<button
className={cn(
'flex flex-row items-center space-x-2 rounded p-2 font-semibold shadow-xl hover:outline',
'bg-slate-700 text-slate-200 hover:bg-transparent hover:text-slate-700 hover:outline-slate-700',
'dark:bg-rose-50 dark:text-slate-800 dark:hover:bg-transparent dark:hover:text-rose-50 dark:hover:outline-rose-50',
)}
>
{icon && <span aria-hidden>{icon}</span>}
<span className="text-sm">{label}</span>
</button>
</Link>
);
}

@ -0,0 +1,63 @@
import {
AlertCircle,
AlertOctagon,
AlertTriangle,
Lightbulb,
LucideIcon,
} from 'lucide-react';
import { cn } from '@/lib/utils';
type CalloutProps = {
type: 'update' | 'note' | 'warning' | 'important';
children: React.ReactNode;
};
const Icons: Record<CalloutProps['type'], LucideIcon> = {
note: AlertCircle,
warning: AlertTriangle,
update: Lightbulb,
important: AlertOctagon,
};
export const Callout = ({ type, children }: CalloutProps) => {
const Icon = Icons[type];
return (
<div
className={cn(
'relative my-6 rounded-md p-2 px-8',
type === 'update' &&
'bg-slate-100 text-slate-600 dark:bg-slate-600 dark:text-slate-200',
type === 'note' &&
'bg-cyan-50/50 text-cyan-700 dark:bg-cyan-200/10 dark:text-cyan-100',
type === 'warning' &&
'bg-amber-50/50 text-amber-700 dark:bg-amber-300/10 dark:text-amber-500',
type === 'important' &&
'bg-red-50/50 text-rose-700 dark:bg-rose-400/10 dark:text-rose-300',
)}
>
<div
className={cn(
'absolute -top-3 left-3 h-6 w-fit rounded-md p-2',
'flex flex-row items-center justify-center space-x-1',
type === 'update' &&
'bg-slate-600 text-slate-300 dark:bg-slate-300 dark:text-slate-600',
type === 'note' &&
'bg-cyan-700 text-cyan-50 dark:bg-cyan-100 dark:text-cyan-900',
type === 'warning' &&
'bg-amber-700 text-amber-50 dark:bg-amber-500 dark:text-amber-900',
type === 'important' &&
'bg-rose-700 text-rose-50 dark:bg-rose-300 dark:text-rose-900',
)}
>
<Icon className="h-4 w-4" aria-hidden />
<div className="text-sm">
{type.charAt(0).toUpperCase()}
{type.slice(1)}
</div>
</div>
{children}
</div>
);
};

@ -0,0 +1,83 @@
'use client';
import { useState } from 'react';
import { Check, Copy } from 'lucide-react';
type CodeBlockProps = {
children: React.ReactNode;
};
export function CodeBlock({ children }: CodeBlockProps) {
const [showCopy, setShowCopy] = useState(false);
const [isCopied, setIsCopied] = useState(false);
const copy = async () => {
await navigator.clipboard.writeText(
extractText(children as React.ReactElement),
);
setIsCopied(true);
setTimeout(() => setIsCopied(false), 2000);
};
return (
<pre
className="relative mx-auto max-w-3xl"
onMouseEnter={() => setShowCopy(true)}
onMouseLeave={() => setShowCopy(false)}
onFocus={() => setShowCopy(true)}
onBlur={() => setShowCopy(false)}
>
{showCopy && (
<button
className="absolute right-2 top-2 flex h-7 w-7 items-center justify-center rounded bg-white dark:bg-slate-800"
onClick={copy}
disabled={isCopied}
>
{isCopied ? (
<Check
className="h-6 w-6 animate-pulse text-accent dark:text-accent-dark"
aria-label="Copied"
/>
) : (
<Copy
className="h-6 w-6 text-slate-300 hover:text-accent dark:text-slate-600 dark:hover:text-accent-dark"
aria-label="Copy code"
/>
)}
</button>
)}
{children}
</pre>
);
}
/**
* Extracts the text from a ReactElement
*/
const extractText = (element: React.ReactElement | string): string => {
// If the element is a string, return it
if (typeof element === 'string') {
return element;
}
// If the element is a ReactElement, check if it has children
// If the children is a single string, return it
if (typeof element.props.children === 'string') {
return element.props.children;
}
// If the children is an array, map over it and extract the text
if (Array.isArray(element.props.children)) {
return (element.props.children as (React.ReactElement | string)[])
.map((child) => extractText(child))
.join('');
}
// If the children is an object (ReactElement), extract the text from it recursively
if (typeof element.props.children === 'object') {
return extractText(element.props.children);
}
return '';
};

@ -0,0 +1,27 @@
'use client';
import Giscus from '@giscus/react';
import { useThemeStore } from '@/stores/theme-store';
import { blogConfig } from '@/config';
export function Comments() {
const isDark = useThemeStore((state) => state.isDark);
const theme = isDark
? blogConfig.giscus.theme?.dark || 'dark_dimmed'
: blogConfig.giscus.theme?.light || 'light';
return (
<section className="mx-auto max-w-2xl">
<Giscus
lang="ru"
loading="lazy"
reactionsEnabled="1"
inputPosition="bottom"
{...blogConfig.giscus}
theme={theme}
/>
</section>
);
}

@ -0,0 +1,18 @@
'use client';
import { useThemeStore } from '@/stores/theme-store';
import { cn } from '@/lib/utils';
type FontStyleProviderProps = {
children: React.ReactNode;
};
export function FontStyleProvider({ children }: FontStyleProviderProps) {
const isSerif = useThemeStore((state) => state.isSerif);
return (
<div className={cn('contents', isSerif ? 'font-serif' : 'font-sans')}>
{children}
</div>
);
}

@ -0,0 +1,153 @@
'use client';
import {
AtSign,
Copyright,
Github,
Linkedin,
Pizza,
Twitter,
} from 'lucide-react';
import { Icon24LogoVkOutline } from '@vkontakte/icons';
import { Icon24LocationOutline } from '@vkontakte/icons';
import { blogConfig } from '@/config';
import { Tooltip } from '@/components/tooltip';
export function Footer() {
const { footerLinks } = blogConfig;
return (
<footer className="relative flex h-full w-full flex-col items-center justify-center space-y-4">
<div className="flex flex-row flex-wrap justify-center gap-4 max-xs:px-16">
{footerLinks?.github && (
<a
href={footerLinks.github}
target="_blank"
rel="noopener noreferrer"
>
<Github
className="icon-base"
data-tooltip-content="Мой GitHub"
data-tooltip-id="footer-tooltip"
aria-label="Мой GitHub "
/>
</a>
)}
{footerLinks?.twitter && (
<a
href={footerLinks.twitter}
target="_blank"
rel="noopener noreferrer"
>
<Icon24LogoVkOutline
className="icon-base"
data-tooltip-content="Мой VK Профиль"
data-tooltip-id="footer-tooltip"
aria-label="Мой VK Профиль"
/>
</a>
)}
{footerLinks?.linkedin && (
<a
href={footerLinks.linkedin}
target="_blank"
rel="noopener noreferrer"
>
<Icon24LocationOutline
className="icon-base"
data-tooltip-content="Мой Telegramm"
data-tooltip-id="footer-tooltip"
aria-label="Мой Telegramm"
/>
</a>
)}
{footerLinks?.email && (
<a href={`mailto:${footerLinks.email}`}>
<AtSign
className="icon-base"
data-tooltip-content="Мой Email"
data-tooltip-id="footer-tooltip"
aria-label="Мой Email"
/>
</a>
)}
{footerLinks?.storybook && (
<a
href={footerLinks.storybook}
target="_blank"
rel="noopener noreferrer"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24px"
height="24px"
viewBox="-4 -4 40 40"
role="img"
fill="none"
stroke="currentColor"
strokeWidth="2.6"
className="icon-base"
data-tooltip-content="Storybook"
data-tooltip-id="footer-tooltip"
aria-label="Storybook"
>
<path d="M21.786 0.318l-0.161 3.615c-0.005 0.203 0.229 0.328 0.391 0.203l1.411-1.068 1.198 0.932c0.156 0.104 0.365 0 0.375-0.188l-0.135-3.677 1.776-0.135c0.922-0.063 1.708 0.672 1.708 1.599v28.802c0 0.917-0.766 1.646-1.682 1.599l-21.469-0.958c-0.833-0.036-1.505-0.708-1.531-1.547l-1-26.401c-0.052-0.885 0.62-1.646 1.505-1.693l17.599-1.109zM17.693 12.401c0 0.625 4.214 0.318 4.786-0.109 0-4.266-2.292-6.521-6.479-6.521-4.198 0-6.531 2.297-6.531 5.724 0 5.932 8 6.036 8 9.276 0 0.938-0.427 1.469-1.401 1.469-1.281 0-1.802-0.651-1.734-2.88 0-0.479-4.865-0.641-5.026 0-0.359 5.375 2.974 6.932 6.797 6.932 3.724 0 6.63-1.984 6.63-5.573 0-6.359-8.135-6.188-8.135-9.333 0-1.292 0.964-1.464 1.505-1.464 0.604 0 1.667 0.094 1.589 2.49z" />
</svg>
</a>
)}
{footerLinks?.buyMeAPizza && (
<a
href={footerLinks.buyMeAPizza}
target="_blank"
rel="noopener noreferrer"
>
<Pizza
id="pizza"
className="icon-base"
data-tooltip-content="Buy me a pizza"
data-tooltip-id="footer-tooltip"
aria-label="Buy me a pizza"
/>
</a>
)}
<Tooltip id="footer-tooltip" />
</div>
<div className="flex h-6 flex-row items-center justify-center space-x-1 text-slate-600 dark:text-slate-300">
<Copyright className="h-4 w-4" aria-label="Copyright" />
<span className="text-xs xs:text-sm">2023 · {blogConfig.author}</span>
</div>
<button
className="absolute bottom-4 left-8 h-full w-fit"
onClick={() => {
document.body.scrollTop = 0;
document.documentElement.scrollTop = 0;
}}
>
<svg
id="scroll-to-top"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="48"
viewBox="0 0 24 48"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="icon-base h-12"
viewTarget="0 0 24 48"
aria-label="Scroll to top"
data-tooltip-content="Scroll to top"
>
<line x1="12" y1="38" x2="12" y2="5"></line>
<polyline points="5 12 12 5 19 12"></polyline>
</svg>
<Tooltip anchorSelect="#scroll-to-top" place="right" />
</button>
</footer>
);
}

@ -0,0 +1,48 @@
'use client';
import { useEffect, useRef, useState } from 'react';
import { useSelectedLayoutSegments } from 'next/navigation';
import { blogConfig } from '@/config';
import { NavigationBar } from '@/components/navigation-bar';
import { Toolbar } from '@/components/toolbar';
import { cn } from '@/lib/utils';
export function Header() {
const [scrollTop, setScrollTop] = useState(0);
const headerRef = useRef<HTMLDivElement>(null);
const layoutSegment = useSelectedLayoutSegments();
const isPostPage =
layoutSegment[0] === blogConfig.pages.posts.url.substring(1) &&
!!layoutSegment[1];
useEffect(() => {
// sync scroll position with state
setScrollTop(document.documentElement.scrollTop);
// update state on scroll
const handleScroll = () => {
setScrollTop(document.documentElement.scrollTop);
};
document.addEventListener('scroll', handleScroll);
return () => document.removeEventListener('scroll', handleScroll);
}, []);
return (
<header
ref={headerRef}
className={cn(
headerRef.current && scrollTop > headerRef.current.clientHeight
? 'border-b border-b-slate-300 bg-slate-500/20 py-2 dark:border-b-slate-600'
: 'bg-transparent py-8',
'flex flex-row items-center justify-between px-4 xs:px-8',
'transition-[padding,background-color] duration-300 ease-in-out',
'text-slate-700 backdrop-blur dark:text-rose-50',
)}
>
<NavigationBar className="flex-grow mix-blend-color-dodge max-xs:mr-2" />
<Toolbar fontControls={isPostPage} className="ml-auto" />
</header>
);
}

@ -0,0 +1,123 @@
'use client';
import { useReducer } from 'react';
import GraphemeSplitter from 'grapheme-splitter';
import { Pause, Play } from 'lucide-react';
import Typist from 'react-typist-component';
import Balancer from 'react-wrap-balancer';
import { blogConfig } from '@/config';
import { cn } from '@/lib/utils';
type TypingState = {
titleDone: boolean;
subtitleDone: boolean;
isPaused: boolean;
};
type TypingAction =
| { type: 'togglePause' }
| { type: 'setDone'; payload: 'title' | 'subtitle' };
const reducer = (state: TypingState, action: TypingAction) => {
switch (action.type) {
case 'togglePause':
return {
...state,
isPaused: !state.isPaused,
};
case 'setDone':
return {
...state,
[`${action.payload}Done`]: true,
};
}
};
const splitter = (str: string) => new GraphemeSplitter().splitGraphemes(str);
export function HeroSection() {
const [{ titleDone, subtitleDone, isPaused }, dispatch] = useReducer(
reducer,
{
titleDone: false,
subtitleDone: false,
isPaused: false,
},
);
return (
<section
className={cn(
'flex flex-col items-center justify-center space-y-2',
'relative h-40 w-full rounded-md px-4 shadow-lg',
'bg-slate-300 dark:bg-slate-800/50',
)}
>
<Typist
typingDelay={100}
splitter={splitter}
pause={isPaused}
onTypingDone={() => dispatch({ type: 'setDone', payload: 'title' })}
>
<h1 className="block w-full text-center text-3xl font-bold text-slate-800 dark:text-rose-50 xs:text-4xl sm:text-5xl">
<Balancer>
Добро пожаловать в мой блог
<span className="ml-2 inline-block origin-[70%_70%] animate-wave">
👋
</span>
</Balancer>
</h1>
</Typist>
<p className="text-center text-lg text-slate-800 dark:text-rose-50 xs:text-2xl">
{titleDone && (
<Typist
typingDelay={100}
startDelay={1000}
pause={isPaused}
onTypingDone={() => {
dispatch({ type: 'setDone', payload: 'subtitle' });
}}
>
Я пишу про {' '}
</Typist>
)}
{subtitleDone && (
<Typist typingDelay={100} backspaceDelay={75} pause={isPaused} loop>
{blogConfig.topics.map((topic) => (
<span key={topic} className="font-semibold">
{topic}
<Typist.Delay ms={1000} />
<Typist.Backspace count={topic.length} />
</span>
))}
</Typist>
)}
</p>
<button
className="absolute right-3 top-1"
onClick={() => dispatch({ type: 'togglePause' })}
>
{isPaused ? (
<Play
className={cn(
'h-4 w-4',
'text-slate-400/50 hover:text-accent',
'dark:text-rose-50/20 dark:hover:text-accent-dark',
)}
aria-label="Запустить анимацию"
/>
) : (
<Pause
className={cn(
'h-4 w-4',
'text-slate-400/50 hover:text-accent',
'dark:text-rose-50/20 dark:hover:text-accent-dark',
)}
aria-label="Остановить анимацию"
/>
)}
</button>
</section>
);
}

@ -0,0 +1,86 @@
import Image from 'next/image';
import Link from 'next/link';
import Balancer from 'react-wrap-balancer';
import { Callout } from '@/components/callout';
import { CodeBlock } from '@/components/code-block';
import { TableOfContents } from '@/components/table-of-contents';
import { cn } from '@/lib/utils';
/**
* Use <Link> for internal links and <a> for external links and anchors
* and open external links in a new tab
*/
function a({ href, children }: React.HTMLProps<HTMLAnchorElement>) {
if (href && href.startsWith('/')) {
return <Link href={href}>{children}</Link>;
}
if (href && href.startsWith('#')) {
return <a href={href}>{children}</a>;
}
return (
<a href={href} target="_blank" rel="noopener noreferrer">
{children}
</a>
);
}
/**
* Use div instead of p elements since p elements have restrictions on what
* elements can be nested inside them
*/
function p(props: React.HTMLProps<HTMLParagraphElement>) {
return <div className={cn('my-4', props.className)} {...props} />;
}
/**
* Image component that uses next/image, with optional caption and width/height
* Example usage: \!\[alt text {{ w: 600, h: 300, cap: "caption text" }}](/path/to/image)
*/
function img({ src, alt }: React.HTMLProps<HTMLImageElement>) {
const _alt = (alt?.split('{')[0].trim() ?? alt) || '';
const props = alt?.split('{')[1];
const width = parseInt(props?.match(/w:\s*(\d+)/)?.[1] ?? '700');
const height = parseInt(props?.match(/h:\s*(\d+)/)?.[1] ?? '400');
const caption = props?.match(/cap:\s*"(.*?)"/)?.[1];
return (
<figure
className="mx-auto mb-6 mt-3 flex h-fit w-fit flex-col rounded bg-slate-300/20 dark:bg-rose-50/25"
aria-label={_alt}
>
<Image
src={src || ''}
alt={_alt}
width={width}
height={height}
className={cn('rounded', caption && 'rounded-b-none')}
/>
{caption && (
<figcaption
className={cn(
'm-0 rounded-b-[3px] px-6 py-1 text-center',
'bg-slate-300/50 text-slate-700',
'dark:bg-rose-50/5 dark:text-rose-50',
)}
style={{
maxWidth: width,
}}
>
<Balancer>{caption}</Balancer>
</figcaption>
)}
</figure>
);
}
/**
* Code block component with copy button
*/
function pre({ children }: React.HTMLProps<HTMLPreElement>) {
return <CodeBlock>{children}</CodeBlock>;
}
export const MDXComponents = { a, p, img, pre, TableOfContents, Callout };

@ -0,0 +1,20 @@
'use client';
import { useMDXComponent } from 'next-contentlayer/hooks';
import { MDXComponents } from '@/components/mdx-components';
import { MDXStyles } from '@/components/mdx-styles';
type MDXContentProps = {
code: string;
};
export function MDXContent({ code }: MDXContentProps) {
const Component = useMDXComponent(code);
return (
<MDXStyles>
<Component components={MDXComponents} />
</MDXStyles>
);
}

@ -0,0 +1,29 @@
'use client';
import '@/styles/markdown.css';
import { useThemeStore } from '@/stores/theme-store';
import { cn } from '@/lib/utils';
type MDXStylesProps = {
children: React.ReactNode;
};
export function MDXStyles({ children }: MDXStylesProps) {
const fontSize = useThemeStore((state) => state.fontSize);
return (
<section
className={cn(
'prose-' + fontSize,
'prose prose-slate max-w-none py-8 dark:prose-invert dark:text-rose-50',
'prose-headings:drop-shadow-sm dark:prose-headings:text-rose-50',
'prose-ul:my-4 prose-li:my-0 prose-li:marker:text-slate-600 dark:prose-li:marker:text-slate-400',
'prose-a:text-accent prose-a:no-underline hover:prose-a:underline dark:prose-a:text-accent-dark',
'prose-blockquote:border-l-slate-800 dark:prose-blockquote:border-l-slate-300 dark:prose-blockquote:text-rose-50',
'prose-hr:border-slate-700 dark:prose-hr:border-slate-300',
)}
>
{children}
</section>
);
}

@ -0,0 +1,39 @@
import Link from 'next/link';
import { BlogTitle } from '@/components/blog-title';
import { cn } from '@/lib/utils';
import { allPages } from '../.contentlayer/generated';
type NavigationBarProps = {
className?: string;
};
export function NavigationBar({ className }: NavigationBarProps) {
return (
<nav
className={cn(
'flex h-8 flex-row items-center space-x-2 max-xs:text-sm sm:space-x-4',
className,
)}
>
<Link href="/">
<BlogTitle />
</Link>
<Link
href="/posts"
className="font-semibold hover:text-accent dark:hover:text-accent-dark"
>
Статьи
</Link>
{allPages.map((page) => (
<Link
href={page.url}
key={page._id}
className="font-semibold hover:text-accent dark:hover:text-accent-dark"
>
{page.title}
</Link>
))}
</nav>
);
}

@ -0,0 +1,88 @@
import { ArrowLeft, ArrowRight } from 'lucide-react';
import { cn } from '@/lib/utils';
type PageControlsProps = {
currentPage: number;
lastPage: number;
setCurrentPage: (page: number) => void;
bottom?: boolean;
};
export function PageControls({
currentPage,
lastPage,
bottom = false,
setCurrentPage,
}: PageControlsProps) {
return (
<div className="grid w-full grid-cols-[6rem,1fr,6rem] items-center justify-between">
<button
onClick={() => {
setCurrentPage(currentPage - 1);
if (bottom) {
document.body.scrollTop = 0;
document.documentElement.scrollTop = 0;
}
}}
disabled={currentPage === 1}
className={cn(
'flex h-6 flex-row items-center space-x-1 justify-self-start',
bottom ? 'place-self-start' : 'place-self-end',
'text-slate-600 enabled:hover:text-accent disabled:text-slate-400',
'dark:text-slate-300 dark:enabled:hover:text-accent-dark dark:disabled:text-slate-500',
)}
>
<ArrowLeft aria-label="Previous page" />
<span>Прошлые</span>
</button>
<div className="flex flex-row flex-wrap justify-center space-x-2 px-2 text-2xl">
{Array.from({ length: currentPage }, (_, i) => (
<span
key={i}
className={cn(
currentPage === i + 1
? 'leading-3'
: currentPage === i + 2
? 'leading-4'
: 'leading-5',
'text-slate-800 dark:text-slate-300',
)}
>
</span>
))}
{Array.from({ length: lastPage - currentPage }, (_, i) => (
<span
key={i}
className={cn(
i === 0 ? 'leading-4' : 'leading-5',
'text-slate-400 dark:text-slate-500',
)}
>
</span>
))}
</div>
<button
onClick={() => {
setCurrentPage(currentPage + 1);
if (bottom) {
document.body.scrollTop = 0;
document.documentElement.scrollTop = 0;
}
}}
disabled={currentPage === lastPage}
className={cn(
'flex h-6 flex-row items-center justify-end space-x-1 justify-self-end',
bottom ? 'place-self-start' : 'place-self-end',
'text-slate-600 enabled:hover:text-accent disabled:text-slate-400',
'dark:text-slate-300 dark:enabled:hover:text-accent-dark dark:disabled:text-slate-500',
)}
>
<span className="h-full">Новые</span>
<ArrowRight aria-label="Next page" />
</button>
</div>
);
}

@ -0,0 +1,73 @@
'use client';
import Link from 'next/link';
import { type Post } from 'contentlayer/generated';
import { Calendar } from 'lucide-react';
import Balancer from 'react-wrap-balancer';
import { PostTags } from '@/components/post-tags';
import { formatDateTime } from '@/lib/datetime';
import { cn } from '@/lib/utils';
type PostCardProps = {
post: Post;
};
export function PostCard({ post }: PostCardProps) {
const publishedDate = formatDateTime(post.date);
return (
<Link
href={post.url}
className={cn(
'group relative flex h-fit w-full',
'transition-transform duration-300 ease-in-out hover:scale-[1.02]',
)}
aria-label={post.title}
>
<article
className={cn(
'flex h-fit w-full flex-col space-y-4 rounded',
'relative z-10 m-0.5 py-3 pl-10 pr-6 shadow-lg hover:shadow-xl',
'bg-slate-100/95 dark:bg-slate-600/90',
)}
>
<div className="flex flex-col space-y-2">
<h2 className="text-2xl font-bold leading-normal text-slate-800 dark:text-rose-50 sm:text-3xl">
<Balancer>
{post.title}
{publishedDate.isFresh && (
<>
{' '}
<sup className="text-base font-semibold text-accent dark:text-accent-dark">
Новый
</sup>
</>
)}
</Balancer>
</h2>
<p className="text-slate-700 dark:text-rose-50">
<Balancer>{post.excerpt}</Balancer>
</p>
<p className="inline-flex items-center space-x-1 text-slate-600/90 dark:text-rose-50/80">
<Calendar className="h-4 w-4 self-baseline" aria-hidden />
<span className="text-sm">
Публикация {publishedDate.asString}{' '}
<span className="opacity-95 max-xs:hidden">
· {publishedDate.asRelativeTimeString}
</span>
</span>
</p>
</div>
<PostTags tags={post.tags} className="text-sm sm:text-xs" />
</article>
<div
className={cn(
'absolute inset-0 z-20 my-auto h-[calc(100%_-_0.25rem)] w-4 rounded-l',
'group-hover:animate-border group-focus:animate-border-fast',
'bg-slate-700 dark:bg-rose-50',
)}
/>
</Link>
);
}

@ -0,0 +1,65 @@
'use client';
import Balancer from 'react-wrap-balancer';
import { useThemeStore } from '@/stores/theme-store';
import { PostTags } from '@/components/post-tags';
import { formatDateTime } from '@/lib/datetime';
import { cn } from '@/lib/utils';
type PostIntroProps = {
title: string;
date: string;
tags: string[];
};
export function PostIntro({ title, date, tags }: PostIntroProps) {
const fontSize = useThemeStore((state) => state.fontSize);
const publishedDate = formatDateTime(date);
return (
<section className="flex flex-col space-y-4 sm:p-3">
<h1
className={cn(
'font-bold text-slate-800 drop-shadow-sm dark:text-rose-50',
fontSize === 'sm' && 'text-xl sm:text-2xl md:text-3xl',
fontSize === 'base' && 'text-2xl sm:text-3xl md:text-4xl',
fontSize === 'lg' && 'text-3xl sm:text-4xl md:text-5xl',
fontSize === 'xl' && 'text-4xl sm:text-5xl md:text-6xl',
fontSize === '2xl' && 'text-5xl sm:text-6xl md:text-7xl',
)}
>
<Balancer>{title}</Balancer>
</h1>
<div
className={cn(
'text-slate-700 dark:text-rose-50',
fontSize === 'sm' && 'text-xs sm:text-sm',
fontSize === 'base' && 'text-sm sm:text-base',
fontSize === 'lg' && 'text-base sm:text-lg',
fontSize === 'xl' && 'text-lg sm:text-xl',
fontSize === '2xl' && 'text-xl sm:text-2xl',
)}
>
<span>Published </span>
<time dateTime={publishedDate.asISOString}>
{publishedDate.asString}{' '}
</time>
<div className="inline-block text-slate-600 dark:text-rose-50/60">
{' '}
· {publishedDate.asRelativeTimeString}
</div>
</div>
<PostTags
tags={tags}
className={cn(
fontSize === 'sm' && 'text-xs',
fontSize === 'base' && 'text-xs',
fontSize === 'lg' && 'text-sm',
fontSize === 'xl' && 'text-base',
fontSize === '2xl' && 'text-lg',
)}
/>
</section>
);
}

@ -0,0 +1,40 @@
'use client';
import { useState } from 'react';
import { type Post } from 'contentlayer/generated';
import { PageControls } from '@/components/page-controls';
import { PostCard } from '@/components/post-card';
type PostPaginatorProps = {
posts: Post[];
postsPerPage?: number;
};
export function PostPaginator({ posts, postsPerPage = 5 }: PostPaginatorProps) {
const [currentPage, setCurrentPage] = useState(1);
const lastPage = Math.ceil(posts.length / postsPerPage);
return (
<section className="flex h-full w-full flex-col space-y-4">
<PageControls
setCurrentPage={setCurrentPage}
currentPage={currentPage}
lastPage={lastPage}
/>
<div className="flex h-full w-full flex-col space-y-4">
{posts
.slice((currentPage - 1) * postsPerPage, currentPage * postsPerPage)
.map((post) => (
<PostCard key={post._id} post={post} />
))}
</div>
<PageControls
setCurrentPage={setCurrentPage}
currentPage={currentPage}
lastPage={lastPage}
bottom
/>
</section>
);
}

@ -0,0 +1,25 @@
import { cn } from '@/lib/utils';
interface PostTagsProps {
tags: string[];
className?: string;
}
export function PostTags({ tags, className }: PostTagsProps) {
return (
<div className={cn('flex flex-row flex-wrap gap-1', className)}>
{tags.map((tag) => (
<span
key={tag}
className={cn(
'w-fit whitespace-nowrap rounded px-2 py-1',
'bg-slate-700 text-slate-200',
'dark:bg-rose-50 dark:text-slate-700',
)}
>
{tag}
</span>
))}
</div>
);
}

@ -0,0 +1,69 @@
'use client';
import { X } from 'lucide-react';
import { useSearchStore } from '@/stores/search-store';
import { cn } from '@/lib/utils';
type SearchInputProps = {
hasResults: boolean;
};
export function SearchInput({ hasResults }: SearchInputProps) {
const query = useSearchStore((state) => state.query);
const setQuery = useSearchStore((state) => state.setQuery);
return (
<>
<input
type="text"
autoFocus
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder={
placeholders[Math.floor(Math.random() * placeholders.length)]
}
className={cn(
'w-full rounded border px-2 placeholder:opacity-50',
hasResults ? 'sm:text-2xl' : 'sm:text-4xl',
'border-slate-400 bg-slate-100 text-slate-700',
'dark:border-slate-500 dark:bg-slate-700 dark:text-rose-50',
)}
aria-label="Search Posts"
/>
{query && (
<button
onClick={() => setQuery('')}
className={cn(
'absolute right-14 top-[1.1rem]',
hasResults ? 'sm:top-5' : 'sm:top-7',
)}
>
<X
className="icon-base text-slate-500 dark:text-slate-400"
aria-label="Clear"
/>
</button>
)}
</>
);
}
const placeholders = [
'What are you looking for?',
'Something need doing?',
'Looking for something specific?',
'Looking for something special?',
'Ah, I have just the thing for you.',
'What brings you here?',
'Whatcha lookin for?',
"You need somethin'?",
'I got what you need!',
'Yeah, what do you want?',
'What do you require?',
'I have exactly what you need.',
'What can I get for ya today?',
'May you find what you seek.',
"I hope you'll find something useful!",
'Feel free to browse.',
];

@ -0,0 +1,76 @@
'use client';
import Link from 'next/link';
import { type Post } from 'contentlayer/generated';
import Balancer from 'react-wrap-balancer';
import { useSearchStore } from '@/stores/search-store';
import { formatDateTime } from '@/lib/datetime';
import { cn } from '@/lib/utils';
type SearchResultsProps = {
query: string;
results: Post[];
};
export function SearchResults({ query, results }: SearchResultsProps) {
const toggleSearch = useSearchStore((state) => state.toggleSearch);
return (
<ul className="flex flex-col overflow-scroll">
{results.map((post) => {
const publishedDate = formatDateTime(post.date);
return (
<li
key={post.slug}
className={cn(
'rounded p-2 transition-none sm:px-8',
'even:bg-slate-400/30 hover:bg-slate-500/50',
'dark:even:bg-slate-700/60 dark:hover:bg-slate-400/40',
)}
>
<Link
href={post.url}
onClick={toggleSearch}
className="flex h-fit flex-col"
>
<span className="font-semibold text-slate-800 dark:text-rose-50 sm:text-xl">
<Balancer>{highlightSearchQuery(query, post.title)}</Balancer>
</span>
<span className="text-sm text-slate-700 dark:text-rose-50 sm:text-base">
{highlightSearchQuery(query, post.excerpt)}
</span>
<span className="text-sm text-slate-600 dark:text-slate-300">
{publishedDate.asString} · {publishedDate.asRelativeTimeString}
</span>
</Link>
</li>
);
})}
</ul>
);
}
/**
* Highlights the search query in the text by wrapping it in a span with the
* font-extrabold class.
* @param query The search query
* @param text The text to highlight
* @returns The text with the query highlighted
*/
function highlightSearchQuery(query: string, text: string) {
const sanitizedQuery = query.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
return text.split(new RegExp(`(${sanitizedQuery})`, 'gi')).map((part, i) => (
<span
key={i}
className={
part.toLowerCase() === query.toLowerCase()
? 'font-extrabold'
: undefined
}
>
{part}
</span>
));
}

@ -0,0 +1,42 @@
'use client';
import { useMemo } from 'react';
import { type Post } from 'contentlayer/generated';
import { useSearchStore } from '@/stores/search-store';
import { getTagsWithCount } from '@/lib/search';
import { cn } from '@/lib/utils';
type SearchTagsProps = {
posts: Post[];
className?: string;
};
export function SearchTags({ posts, className }: SearchTagsProps) {
const tagsWithCounts = useMemo(() => getTagsWithCount(posts), [posts]);
const setQuery = useSearchStore((state) => state.setQuery);
return (
<div
className={cn(
'flex h-fit flex-row flex-wrap items-center justify-center space-x-4 space-y-1 text-sm',
className,
)}
>
{tagsWithCounts.map(([tag, count]) => (
<button
key={tag}
onClick={() => setQuery(tag)}
className="flex w-fit flex-row items-baseline space-x-0.5"
>
<span className="text-slate-600 hover:text-accent dark:text-slate-300 dark:hover:text-accent-dark">
#{tag}
</span>
<span className="font-mono text-xs text-slate-600 dark:text-slate-300">
({count})
</span>
</button>
))}
</div>
);
}

@ -0,0 +1,84 @@
'use client';
import { useEffect, useMemo } from 'react';
import { type Post } from 'contentlayer/generated';
import { ChevronUp } from 'lucide-react';
import { shallow } from 'zustand/shallow';
import { useSearchStore } from '@/stores/search-store';
import { SearchInput } from '@/components/search-input';
import { SearchResults } from '@/components/search-results';
import { SearchTags } from '@/components/search-tags';
import { searchPosts } from '@/lib/search';
import { cn } from '@/lib/utils';
type SearchProps = {
posts: Post[];
};
export function Search({ posts }: SearchProps) {
const { query, isSearching, toggleSearch } = useSearchStore(
(state) => ({
query: state.query,
isSearching: state.isSearching,
toggleSearch: state.toggleSearch,
}),
shallow,
);
const results = useMemo(() => searchPosts(query, posts), [query, posts]);
useEffect(() => {
// toggle search on cmd+k or ctrl+k
const handleKeyDown = (e: KeyboardEvent) => {
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
e.preventDefault();
toggleSearch();
}
};
document.addEventListener('keydown', handleKeyDown);
return () => document.removeEventListener('keydown', handleKeyDown);
}, [toggleSearch]);
if (!isSearching) return null;
return (
<section
className={cn(
'fixed left-1/2 top-1/2 z-50 h-fit max-h-[80vh] w-5/6 max-w-3xl -translate-x-1/2 -translate-y-1/2',
'flex flex-col rounded-md border-2 p-4 backdrop-blur-md',
'border-slate-400 bg-slate-200/40',
'dark:border-slate-500 dark:bg-slate-600/80',
)}
>
<div className="mb-2 flex h-fit flex-row items-center">
<SearchInput hasResults={results.length > 0} />
<button onClick={toggleSearch}>
<ChevronUp
className="icon-base ml-2 text-slate-400 dark:text-slate-400"
aria-label="Close Search"
/>
</button>
</div>
<SearchResults query={query} results={results} />
{results.length > 0 && (
<hr className="my-2 border-slate-400 dark:border-slate-500 max-xs:hidden" />
)}
<SearchTags
posts={posts}
className={cn(
'my-2',
results.length > 0 ? 'max-xs:hidden sm:text-base' : 'sm:text-lg',
)}
/>
<div className="absolute bottom-1 left-2 text-xs text-slate-600 dark:text-slate-200 max-sm:hidden">
Toggle with +K or Ctrl+K
</div>
</section>
);
}

@ -0,0 +1,25 @@
import type { Meta, StoryObj } from '@storybook/react';
import { BlogTitle } from '@/components/blog-title';
import { Center } from './decorators';
const meta: Meta<typeof BlogTitle> = {
title: 'Blog Title',
component: BlogTitle,
decorators: [Center],
};
export default meta;
type Story = StoryObj<typeof BlogTitle>;
export const Big: Story = {
args: {
className: 'text-6xl',
},
};
export const Small: Story = {
args: {
className: 'text-lg',
},
};

@ -0,0 +1,30 @@
import type { Meta, StoryObj } from '@storybook/react';
import { FileText } from 'lucide-react';
import { Button } from '@/components/button';
import { Center } from './decorators';
const meta: Meta<typeof Button> = {
title: 'Button',
component: Button,
decorators: [Center],
args: {
href: '#',
},
};
export default meta;
type Story = StoryObj<typeof Button>;
export const WithIcon: Story = {
args: {
label: 'Все статьи',
icon: <FileText className="h-4 w-4" />,
},
};
export const WithoutIcon: Story = {
args: {
label: 'Click Me',
},
};

@ -0,0 +1,69 @@
import type { Meta, StoryObj } from '@storybook/react';
import { Callout } from '@/components/callout';
import { Markdown, Padding } from './decorators';
const meta: Meta<typeof Callout> = {
title: 'Callout',
component: Callout,
decorators: [Markdown, Padding],
};
export default meta;
type Story = StoryObj<typeof Callout>;
export const Update: Story = {
args: {
type: 'update',
children: (
<div className="my-4">
This is an update callout. Lorem ipsum dolor sit amet consectetur
adipisicing elit. Reiciendis quas quasi adipisci ex laudantium neque
officiis veritatis eligendi tempore modi dignissimos sed, saepe facilis
totam, natus odit, incidunt perferendis accusamus.
</div>
),
},
};
export const Note: Story = {
args: {
type: 'note',
children: (
<div className="my-4">
This is a note callout. Lorem ipsum dolor sit amet consectetur
adipisicing elit. Reiciendis quas quasi adipisci ex laudantium neque
officiis veritatis eligendi tempore modi dignissimos sed, saepe facilis
totam, natus odit, incidunt perferendis accusamus.
</div>
),
},
};
export const Warning: Story = {
args: {
type: 'warning',
children: (
<div className="my-4">
This is a warning callout. Lorem ipsum dolor sit amet consectetur
adipisicing elit. Reiciendis quas quasi adipisci ex laudantium neque
officiis veritatis eligendi tempore modi dignissimos sed, saepe facilis
totam, natus odit, incidunt perferendis accusamus.
</div>
),
},
};
export const Important: Story = {
args: {
type: 'important',
children: (
<div className="my-4">
This is an important callout. Lorem ipsum dolor sit amet consectetur
adipisicing elit. Reiciendis quas quasi adipisci ex laudantium neque
officiis veritatis eligendi tempore modi dignissimos sed, saepe facilis
totam, natus odit, incidunt perferendis accusamus.
</div>
),
},
};

@ -0,0 +1,426 @@
import type { Meta, StoryObj } from '@storybook/react';
import { CodeBlock } from '@/components/code-block';
import { Markdown, Padding } from './decorators';
const meta: Meta<typeof CodeBlock> = {
title: 'Code Block',
component: CodeBlock,
render: (args) => (
<CodeBlock {...args}>
<CodeLight />
<CodeDark />
</CodeBlock>
),
decorators: [
(Story) => (
<div className="w-full">
<Story />
</div>
),
Markdown,
Padding,
],
};
export default meta;
type Story = StoryObj<typeof CodeBlock>;
export const Normal: Story = {};
export const WithTitle: Story = {
decorators: [
(Story) => (
<>
<div data-rehype-pretty-code-title="">/src/components/parallax.tsx</div>
<Story />
</>
),
],
};
/** Generated by rehype-pretty-code */
const CodeLight = () => (
<code data-language="tsx" data-theme="light">
<span className="line">
<span style={{ color: '#D73A49' }}>export</span>
<span style={{ color: '#24292E' }}> </span>
<span style={{ color: '#D73A49' }}>default</span>
<span style={{ color: '#24292E' }}> </span>
<span style={{ color: '#D73A49' }}>function</span>
<span style={{ color: '#24292E' }}> </span>
<span style={{ color: '#6F42C1' }}>Parallax</span>
<span style={{ color: '#24292E' }}>() {'{'}</span>
</span>
<span className="line">
<span style={{ color: '#24292E' }}></span>
<span style={{ color: '#D73A49' }}>const</span>
<span style={{ color: '#24292E' }}> </span>
<span style={{ color: '#005CC5' }}>ref</span>
<span style={{ color: '#24292E' }}> </span>
<span style={{ color: '#D73A49' }}>=</span>
<span style={{ color: '#24292E' }}> </span>
<span style={{ color: '#6F42C1' }}>useRef</span>
<span style={{ color: '#24292E' }}>&lt;</span>
<span style={{ color: '#6F42C1' }}>HTMLDivElement</span>
<span style={{ color: '#24292E' }}>&gt;(</span>
<span style={{ color: '#005CC5' }}>null</span>
<span style={{ color: '#24292E' }}>);</span>
</span>
<span className="line"> </span>
<span className="line">
<span style={{ color: '#24292E' }}></span>
<span style={{ color: '#D73A49' }}>const</span>
<span style={{ color: '#24292E' }}> {'{'} </span>
<span style={{ color: '#005CC5' }}>scrollYProgress</span>
<span style={{ color: '#24292E' }}> {'}'} </span>
<span style={{ color: '#D73A49' }}>=</span>
<span style={{ color: '#24292E' }}> </span>
<span style={{ color: '#6F42C1' }}>useScroll</span>
<span style={{ color: '#24292E' }}>
({'{'} target: ref {'}'});
</span>
</span>
<span className="line"> </span>
<span className="line">
<span style={{ color: '#24292E' }}></span>
<span style={{ color: '#D73A49' }}>const</span>
<span style={{ color: '#24292E' }}> </span>
<span style={{ color: '#005CC5' }}>yYellow</span>
<span style={{ color: '#24292E' }}> </span>
<span style={{ color: '#D73A49' }}>=</span>
<span style={{ color: '#24292E' }}> </span>
<span style={{ color: '#6F42C1' }}>useParallax</span>
<span style={{ color: '#24292E' }}>(scrollYProgress, </span>
<span style={{ color: '#005CC5' }}>300</span>
<span style={{ color: '#24292E' }}>);</span>
</span>
<span className="line">
<span style={{ color: '#24292E' }}></span>
<span style={{ color: '#D73A49' }}>const</span>
<span style={{ color: '#24292E' }}> </span>
<span style={{ color: '#005CC5' }}>yGreen</span>
<span style={{ color: '#24292E' }}> </span>
<span style={{ color: '#D73A49' }}>=</span>
<span style={{ color: '#24292E' }}> </span>
<span style={{ color: '#6F42C1' }}>useParallax</span>
<span style={{ color: '#24292E' }}>(scrollYProgress, </span>
<span style={{ color: '#005CC5' }}>600</span>
<span style={{ color: '#24292E' }}>);</span>
</span>
<span className="line"> </span>
<span className="line">
<span style={{ color: '#24292E' }}></span>
<span style={{ color: '#D73A49' }}>return</span>
<span style={{ color: '#24292E' }}> (</span>
</span>
<span className="line">
<span style={{ color: '#24292E' }}> &lt;</span>
<span style={{ color: '#005CC5' }}>motion.div</span>
<span style={{ color: '#24292E' }}> </span>
<span style={{ color: '#6F42C1' }}>ref</span>
<span style={{ color: '#D73A49' }}>=</span>
<span style={{ color: '#24292E' }}>
{'{'}ref{'}'}{' '}
</span>
<span style={{ color: '#6F42C1' }}>className</span>
<span style={{ color: '#D73A49' }}>=</span>
<span style={{ color: '#032F62' }}>
&quot;flex h-1/2 w-2/3 flex-row gap-2&quot;
</span>
<span style={{ color: '#24292E' }}>&gt;</span>
</span>
<span className="line">
<span style={{ color: '#24292E' }}> &lt;</span>
<span style={{ color: '#005CC5' }}>motion.div</span>
<span style={{ color: '#24292E' }}> </span>
<span style={{ color: '#6F42C1' }}>className</span>
<span style={{ color: '#D73A49' }}>=</span>
<span style={{ color: '#032F62' }}>&quot;w-1/3 bg-rose-600&quot;</span>
<span style={{ color: '#24292E' }}> /&gt;</span>
</span>
<span className="line">
<span style={{ color: '#24292E' }}> &lt;</span>
<span style={{ color: '#005CC5' }}>motion.div</span>
</span>
<span className="line">
<span style={{ color: '#24292E' }}> </span>
<span style={{ color: '#6F42C1' }}>className</span>
<span style={{ color: '#D73A49' }}>=</span>
<span style={{ color: '#032F62' }}>&quot;w-1/3 bg-amber-600&quot;</span>
</span>
<span className="line">
<span style={{ color: '#24292E' }}> </span>
<span style={{ color: '#6F42C1' }}>initial</span>
<span style={{ color: '#D73A49' }}>=</span>
<span style={{ color: '#24292E' }}>
{'{'}
{'{'} y:{' '}
</span>
<span style={{ color: '#005CC5' }}>0</span>
<span style={{ color: '#24292E' }}>
{' '}
{'}'}
{'}'}
</span>
</span>
<span className="line">
<span style={{ color: '#24292E' }}> </span>
<span style={{ color: '#6F42C1' }}>style</span>
<span style={{ color: '#D73A49' }}>=</span>
<span style={{ color: '#24292E' }}>
{'{'}
{'{'} y: yYellow {'}'}
{'}'}
</span>
</span>
<span className="line">
<span style={{ color: '#24292E' }}> /&gt;</span>
</span>
<span className="line">
<span style={{ color: '#24292E' }}> &lt;</span>
<span style={{ color: '#005CC5' }}>motion.div</span>
</span>
<span className="line">
<span style={{ color: '#24292E' }}> </span>
<span style={{ color: '#6F42C1' }}>className</span>
<span style={{ color: '#D73A49' }}>=</span>
<span style={{ color: '#032F62' }}>&quot;w-1/3 bg-emerald-600&quot;</span>
</span>
<span className="line">
<span style={{ color: '#24292E' }}> </span>
<span style={{ color: '#6F42C1' }}>initial</span>
<span style={{ color: '#D73A49' }}>=</span>
<span style={{ color: '#24292E' }}>
{'{'}
{'{'} y:{' '}
</span>
<span style={{ color: '#005CC5' }}>0</span>
<span style={{ color: '#24292E' }}>
{' '}
{'}'}
{'}'}
</span>
</span>
<span className="line">
<span style={{ color: '#24292E' }}> </span>
<span style={{ color: '#6F42C1' }}>style</span>
<span style={{ color: '#D73A49' }}>=</span>
<span style={{ color: '#24292E' }}>
{'{'}
{'{'} y: yGreen {'}'}
{'}'}
</span>
</span>
<span className="line">
<span style={{ color: '#24292E' }}> /&gt;</span>
</span>
<span className="line">
<span style={{ color: '#24292E' }}> &lt;/</span>
<span style={{ color: '#005CC5' }}>motion.div</span>
<span style={{ color: '#24292E' }}>&gt;</span>
</span>
<span className="line">
<span style={{ color: '#24292E' }}> );</span>
</span>
<span className="line">
<span style={{ color: '#24292E' }}>{'}'}</span>
</span>
</code>
);
/** Generated by rehype-pretty-code */
const CodeDark = () => (
<code data-language="tsx" data-theme="dark">
<span className="line">
<span style={{ color: 'rgb(249, 117, 131)' }}>export</span>
<span style={{ color: 'rgb(225, 228, 232)' }}> </span>
<span style={{ color: 'rgb(249, 117, 131)' }}>default</span>
<span style={{ color: 'rgb(225, 228, 232)' }}> </span>
<span style={{ color: 'rgb(249, 117, 131)' }}>function</span>
<span style={{ color: 'rgb(225, 228, 232)' }}> </span>
<span style={{ color: 'rgb(179, 146, 240)' }}>Parallax</span>
<span style={{ color: 'rgb(225, 228, 232)' }}>() {'{'}</span>
</span>
<span className="line">
<span style={{ color: 'rgb(225, 228, 232)' }}> </span>
<span style={{ color: 'rgb(249, 117, 131)' }}>const</span>
<span style={{ color: 'rgb(225, 228, 232)' }}> </span>
<span style={{ color: 'rgb(121, 184, 255)' }}>ref</span>
<span style={{ color: 'rgb(225, 228, 232)' }}> </span>
<span style={{ color: 'rgb(249, 117, 131)' }}>=</span>
<span style={{ color: 'rgb(225, 228, 232)' }}> </span>
<span style={{ color: 'rgb(179, 146, 240)' }}>useRef</span>
<span style={{ color: 'rgb(225, 228, 232)' }}>&lt;</span>
<span style={{ color: 'rgb(179, 146, 240)' }}>HTMLDivElement</span>
<span style={{ color: 'rgb(225, 228, 232)' }}>&gt;(</span>
<span style={{ color: 'rgb(121, 184, 255)' }}>null</span>
<span style={{ color: 'rgb(225, 228, 232)' }}>);</span>
</span>
<span className="line"> </span>
<span className="line">
<span style={{ color: 'rgb(225, 228, 232)' }}></span>
<span style={{ color: 'rgb(249, 117, 131)' }}>const</span>
<span style={{ color: 'rgb(225, 228, 232)' }}> {'{'} </span>
<span style={{ color: 'rgb(121, 184, 255)' }}>scrollYProgress</span>
<span style={{ color: 'rgb(225, 228, 232)' }}> {'}'} </span>
<span style={{ color: 'rgb(249, 117, 131)' }}>=</span>
<span style={{ color: 'rgb(225, 228, 232)' }}> </span>
<span style={{ color: 'rgb(179, 146, 240)' }}>useScroll</span>
<span style={{ color: 'rgb(225, 228, 232)' }}>
({'{'} target: ref {'}'});
</span>
</span>
<span className="line"> </span>
<span className="line">
<span style={{ color: 'rgb(225, 228, 232)' }}></span>
<span style={{ color: 'rgb(249, 117, 131)' }}>const</span>
<span style={{ color: 'rgb(225, 228, 232)' }}> </span>
<span style={{ color: 'rgb(121, 184, 255)' }}>yYellow</span>
<span style={{ color: 'rgb(225, 228, 232)' }}> </span>
<span style={{ color: 'rgb(249, 117, 131)' }}>=</span>
<span style={{ color: 'rgb(225, 228, 232)' }}> </span>
<span style={{ color: 'rgb(179, 146, 240)' }}>useParallax</span>
<span style={{ color: 'rgb(225, 228, 232)' }}>(scrollYProgress, </span>
<span style={{ color: 'rgb(121, 184, 255)' }}>300</span>
<span style={{ color: 'rgb(225, 228, 232)' }}>);</span>
</span>
<span className="line">
<span style={{ color: 'rgb(225, 228, 232)' }}></span>
<span style={{ color: 'rgb(249, 117, 131)' }}>const</span>
<span style={{ color: 'rgb(225, 228, 232)' }}> </span>
<span style={{ color: 'rgb(121, 184, 255)' }}>yGreen</span>
<span style={{ color: 'rgb(225, 228, 232)' }}> </span>
<span style={{ color: 'rgb(249, 117, 131)' }}>=</span>
<span style={{ color: 'rgb(225, 228, 232)' }}> </span>
<span style={{ color: 'rgb(179, 146, 240)' }}>useParallax</span>
<span style={{ color: 'rgb(225, 228, 232)' }}>(scrollYProgress, </span>
<span style={{ color: 'rgb(121, 184, 255)' }}>600</span>
<span style={{ color: 'rgb(225, 228, 232)' }}>);</span>
</span>
<span className="line"> </span>
<span className="line">
<span style={{ color: 'rgb(225, 228, 232)' }}></span>
<span style={{ color: 'rgb(249, 117, 131)' }}>return</span>
<span style={{ color: 'rgb(225, 228, 232)' }}> (</span>
</span>
<span className="line">
<span style={{ color: 'rgb(225, 228, 232)' }}> &lt;</span>
<span style={{ color: 'rgb(121, 184, 255)' }}>motion.div</span>
<span style={{ color: 'rgb(225, 228, 232)' }}> </span>
<span style={{ color: 'rgb(179, 146, 240)' }}>ref</span>
<span style={{ color: 'rgb(249, 117, 131)' }}>=</span>
<span style={{ color: 'rgb(225, 228, 232)' }}>
{'{'}ref{'}'}{' '}
</span>
<span style={{ color: 'rgb(179, 146, 240)' }}>className</span>
<span style={{ color: 'rgb(249, 117, 131)' }}>=</span>
<span style={{ color: 'rgb(158, 203, 255)' }}>
&quot;flex h-1/2 w-2/3 flex-row gap-2&quot;
</span>
<span style={{ color: 'rgb(225, 228, 232)' }}>&gt;</span>
</span>
<span className="line">
<span style={{ color: 'rgb(225, 228, 232)' }}> &lt;</span>
<span style={{ color: 'rgb(121, 184, 255)' }}>motion.div</span>
<span style={{ color: 'rgb(225, 228, 232)' }}> </span>
<span style={{ color: 'rgb(179, 146, 240)' }}>className</span>
<span style={{ color: 'rgb(249, 117, 131)' }}>=</span>
<span style={{ color: 'rgb(158, 203, 255)' }}>
&quot;w-1/3 bg-rose-600&quot;
</span>
<span style={{ color: 'rgb(225, 228, 232)' }}> /&gt;</span>
</span>
<span className="line">
<span style={{ color: 'rgb(225, 228, 232)' }}> &lt;</span>
<span style={{ color: 'rgb(121, 184, 255)' }}>motion.div</span>
</span>
<span className="line">
<span style={{ color: 'rgb(225, 228, 232)' }}> </span>
<span style={{ color: 'rgb(179, 146, 240)' }}>className</span>
<span style={{ color: 'rgb(249, 117, 131)' }}>=</span>
<span style={{ color: 'rgb(158, 203, 255)' }}>
&quot;w-1/3 bg-amber-600&quot;
</span>
</span>
<span className="line">
<span style={{ color: 'rgb(225, 228, 232)' }}> </span>
<span style={{ color: 'rgb(179, 146, 240)' }}>initial</span>
<span style={{ color: 'rgb(249, 117, 131)' }}>=</span>
<span style={{ color: 'rgb(225, 228, 232)' }}>
{'{'}
{'{'} y:{' '}
</span>
<span style={{ color: 'rgb(121, 184, 255)' }}>0</span>
<span style={{ color: 'rgb(225, 228, 232)' }}>
{' '}
{'}'}
{'}'}
</span>
</span>
<span className="line">
<span style={{ color: 'rgb(225, 228, 232)' }}> </span>
<span style={{ color: 'rgb(179, 146, 240)' }}>style</span>
<span style={{ color: 'rgb(249, 117, 131)' }}>=</span>
<span style={{ color: 'rgb(225, 228, 232)' }}>
{'{'}
{'{'} y: yYellow {'}'}
{'}'}
</span>
</span>
<span className="line">
<span style={{ color: 'rgb(225, 228, 232)' }}> /&gt;</span>
</span>
<span className="line">
<span style={{ color: 'rgb(225, 228, 232)' }}> &lt;</span>
<span style={{ color: 'rgb(121, 184, 255)' }}>motion.div</span>
</span>
<span className="line">
<span style={{ color: 'rgb(225, 228, 232)' }}> </span>
<span style={{ color: 'rgb(179, 146, 240)' }}>className</span>
<span style={{ color: 'rgb(249, 117, 131)' }}>=</span>
<span style={{ color: 'rgb(158, 203, 255)' }}>
&quot;w-1/3 bg-emerald-600&quot;
</span>
</span>
<span className="line">
<span style={{ color: 'rgb(225, 228, 232)' }}> </span>
<span style={{ color: 'rgb(179, 146, 240)' }}>initial</span>
<span style={{ color: 'rgb(249, 117, 131)' }}>=</span>
<span style={{ color: 'rgb(225, 228, 232)' }}>
{'{'}
{'{'} y:{' '}
</span>
<span style={{ color: 'rgb(121, 184, 255)' }}>0</span>
<span style={{ color: 'rgb(225, 228, 232)' }}>
{' '}
{'}'}
{'}'}
</span>
</span>
<span className="line">
<span style={{ color: 'rgb(225, 228, 232)' }}> </span>
<span style={{ color: 'rgb(179, 146, 240)' }}>style</span>
<span style={{ color: 'rgb(249, 117, 131)' }}>=</span>
<span style={{ color: 'rgb(225, 228, 232)' }}>
{'{'}
{'{'} y: yGreen {'}'}
{'}'}
</span>
</span>
<span className="line">
<span style={{ color: 'rgb(225, 228, 232)' }}> /&gt;</span>
</span>
<span className="line">
<span style={{ color: 'rgb(225, 228, 232)' }}> &lt;/</span>
<span style={{ color: 'rgb(121, 184, 255)' }}>motion.div</span>
<span style={{ color: 'rgb(225, 228, 232)' }}>&gt;</span>
</span>
<span className="line">
<span style={{ color: 'rgb(225, 228, 232)' }}> );</span>
</span>
<span className="line">
<span style={{ color: 'rgb(225, 228, 232)' }}>{'}'}</span>
</span>
</code>
);

@ -0,0 +1,7 @@
import { type StoryFn } from '@storybook/react';
export const Center = (Story: StoryFn) => (
<div className="flex h-full w-full items-center justify-center">
<Story />
</div>
);

@ -0,0 +1,3 @@
export * from './center';
export * from './padding';
export * from './markdown';

@ -0,0 +1,11 @@
import { type StoryFn } from '@storybook/react';
import { MDXStyles } from '@/components/mdx-styles';
export const Markdown = (Story: StoryFn) => {
return (
<MDXStyles>
<Story />
</MDXStyles>
);
};

@ -0,0 +1,7 @@
import { type StoryFn } from '@storybook/react';
export const Padding = (Story: StoryFn) => (
<div className="h-full p-6 sm:p-12">
<Story />
</div>
);

@ -0,0 +1,22 @@
import type { Meta, StoryObj } from '@storybook/react';
import { Footer } from '@/components/footer';
const meta: Meta<typeof Footer> = {
title: 'Footer',
component: Footer,
decorators: [
(Story) => (
<div className="flex h-screen flex-col">
<div className="mt-auto h-32">
<Story />
</div>
</div>
),
],
};
export default meta;
type Story = StoryObj<typeof Footer>;
export const Normal: Story = {};

@ -0,0 +1,43 @@
import type { Meta, StoryObj } from '@storybook/react';
import { Header } from '@/components/header';
const meta: Meta<typeof Header> = {
title: 'Header',
component: Header,
decorators: [
(Story) => (
<div className="relative min-h-[200vh]">
<div className="sticky top-0 h-24 w-full">
<Story />
</div>
<div className="flex h-[calc(100vh-6rem)] flex-col items-center justify-center text-3xl text-slate-300 dark:text-slate-600">
&nbsp;&nbsp;Scroll Down&nbsp;&nbsp;
</div>
</div>
),
],
};
export default meta;
type Story = StoryObj<typeof Header>;
export const PostPage: Story = {
parameters: {
nextjs: {
navigation: {
segments: ['posts', 'examples', 'example-post'],
},
},
},
};
export const Normal: Story = {
parameters: {
nextjs: {
navigation: {
segments: [''],
},
},
},
};

@ -0,0 +1,15 @@
import type { Meta, StoryObj } from '@storybook/react';
import { HeroSection } from '@/components/hero-section';
import { Padding } from './decorators';
const meta: Meta<typeof HeroSection> = {
title: 'Hero Section',
component: HeroSection,
decorators: [Padding],
};
export default meta;
type Story = StoryObj<typeof HeroSection>;
export const Normal: Story = {};

@ -0,0 +1,40 @@
import type { Post } from 'contentlayer/generated';
import type { Meta, StoryObj } from '@storybook/react';
import { PostCard } from '@/components/post-card';
import { Center, Padding } from './decorators';
const meta: Meta<typeof PostCard> = {
title: 'Post Card',
component: PostCard,
decorators: [Center, Padding],
};
export default meta;
type Story = StoryObj<typeof PostCard>;
export const Normal: Story = {
args: {
post: {
title: 'Example Post',
excerpt: 'This is an example post.',
date: '2022-01-01',
tags: ['example', 'post', 'tags'],
url: '/posts/example-post',
slug: 'posts/example-post',
} as Post,
},
};
export const FreshPost: Story = {
args: {
post: {
title: 'Example Post',
excerpt: 'This is an example post.',
date: new Date().toISOString(),
tags: ['example', 'post', 'tags'],
url: '/posts/example-post',
slug: 'posts/example-post',
} as Post,
},
};

@ -0,0 +1,21 @@
import type { Meta, StoryObj } from '@storybook/react';
import { PostIntro } from '@/components/post-intro';
import { Padding } from './decorators';
const meta: Meta<typeof PostIntro> = {
title: 'Post Intro',
component: PostIntro,
decorators: [Padding],
};
export default meta;
type Story = StoryObj<typeof PostIntro>;
export const Normal: Story = {
args: {
title: 'Example Post',
date: '2022-01-01',
tags: ['example', 'post', 'storybook'],
},
};

@ -0,0 +1,69 @@
import type { Post } from 'contentlayer/generated';
import type { Meta, StoryObj } from '@storybook/react';
import { PostPaginator } from '@/components/post-paginator';
import { Padding } from './decorators';
const posts = Array.from({ length: 100 }, (_, index) => ({
title: `Post ${index + 1}`,
excerpt: `This is post ${index + 1}`,
date: '2022-01-01',
tags: ['example', 'post', 'tags'],
url: `/posts/post-${index + 1}`,
slug: `posts/post-${index + 1}`,
})) as Post[];
const meta: Meta<typeof PostPaginator> = {
title: 'Post Paginator',
component: PostPaginator,
decorators: [Padding],
args: {
postsPerPage: 5,
},
};
export default meta;
type Story = StoryObj<typeof PostPaginator>;
export const TenPages: Story = {
args: {
posts: posts.slice(0, 50),
},
};
export const ThreePages: Story = {
args: {
posts: posts.slice(0, 15),
},
};
export const TwoPages: Story = {
args: {
posts: posts.slice(0, 10),
},
};
export const OnePage: Story = {
args: {
posts: posts.slice(0, 5),
},
};
export const TwentyPages: Story = {
args: {
posts,
},
};
export const OnePost: Story = {
args: {
posts: posts.slice(0, 1),
},
};
export const TenPerPage: Story = {
args: {
posts,
postsPerPage: 10,
},
};

@ -0,0 +1,28 @@
import type { Meta, StoryObj } from '@storybook/react';
import { PostTags } from '@/components/post-tags';
import { Center, Padding } from './decorators';
const meta: Meta<typeof PostTags> = {
title: 'Post Tags',
component: PostTags,
decorators: [Center, Padding],
};
export default meta;
type Story = StoryObj<typeof PostTags>;
export const Normal: Story = {
args: {
tags: [
'example',
'post',
'tags',
'test',
'storybook',
'nextjs',
'react',
'typescript',
],
},
};

@ -0,0 +1,35 @@
import type { Post } from 'contentlayer/generated';
import type { Meta, StoryObj } from '@storybook/react';
import { useSearchStore } from '@/stores/search-store';
import { Search } from '@/components/search';
const posts = Array.from({ length: 50 }, (_, index) => ({
title: `Example Post ${index + 1}`,
excerpt: 'This is an example post.',
date: '2022-01-01',
tags: ['example', 'post', 'test', 'storybook', `tag${(index + 1) % 10}`],
url: `/posts/post-${index + 1}`,
slug: `posts/post-${index + 1}`,
body: {
raw: `Post ${index + 1} body`,
},
})) as Post[];
const meta: Meta<typeof Search> = {
title: 'Search',
component: Search,
decorators: [
(Story) => {
useSearchStore((state) => state.toggleSearch)();
return <Story />;
},
],
};
export default meta;
type Story = StoryObj<typeof Search>;
export const Normal: Story = {
args: { posts },
};

@ -0,0 +1,39 @@
import type { Meta, StoryObj } from '@storybook/react';
import { TableOfContents } from '@/components/table-of-contents';
import { Markdown, Padding } from './decorators';
const meta: Meta<typeof TableOfContents> = {
title: 'Table of Contents',
component: TableOfContents,
decorators: [Markdown, Padding],
args: {
children: (
<ul>
<li>
<a href="#heading-1">
Lorem ipsum dolor sit amet consectetur adipisicing elit
</a>
</li>
<li>
<a href="#heading-2">
Quos ex voluptates debitis accusamus laborum fugit aut
</a>
</li>
<li>
<a href="#heading-3">Quo suscipit aspernatur dicta beatae?</a>
</li>
<li>
<a href="#heading-4">
Lorem ipsum dolor sit amet consectetur adipisicing elit
</a>
</li>
</ul>
),
},
};
export default meta;
type Story = StoryObj<typeof TableOfContents>;
export const Normal: Story = {};

@ -0,0 +1,42 @@
import type { Meta, StoryObj } from '@storybook/react';
import { Github } from 'lucide-react';
import { Tooltip } from '@/components/tooltip';
import { Center } from './decorators';
const meta: Meta<typeof Tooltip> = {
title: 'Tooltip',
component: Tooltip,
render: (args) => (
<>
<Github
id="github"
className="icon-base"
data-tooltip-content={args.content}
data-tooltip-id="tooltip"
aria-label={args.content}
/>
<Tooltip id="tooltip" {...args} />
</>
),
decorators: [Center],
args: {
content: 'My GitHub profile',
},
argTypes: {
place: {
control: false,
},
},
};
export default meta;
type Story = StoryObj<typeof Tooltip>;
export const Top: Story = { args: { place: 'top' } };
export const Bottom: Story = { args: { place: 'bottom' } };
export const Left: Story = { args: { place: 'left' } };
export const Right: Story = { args: { place: 'right' } };

@ -0,0 +1,37 @@
import { useState } from 'react';
import { Bookmark, ChevronDown } from 'lucide-react';
import { cn } from '@/lib/utils';
type TableOfContentsProps = {
children: React.ReactNode;
};
export function TableOfContents({ children }: TableOfContentsProps) {
const [isOpen, setIsOpen] = useState(false);
return (
<section className="mt-8 flex w-full flex-col rounded bg-slate-300/50 dark:bg-slate-600/50 sm:w-fit">
<button
className={cn(
'flex flex-row items-center rounded p-2 font-bold',
isOpen && 'rounded-b-none',
'bg-slate-300 text-slate-700',
'dark:bg-slate-600 dark:text-slate-200',
)}
onClick={() => setIsOpen((prev) => !prev)}
aria-label="Toggle Table of Contents"
>
<Bookmark className="mr-1 h-5 w-5" />
<span className="mr-6">Содержание</span>
<ChevronDown
className={cn(
'ml-auto h-6 w-6 transition-transform duration-300 ease-in-out',
isOpen && 'rotate-180',
)}
/>
</button>
{isOpen && <div className="p-2 pr-6">{children}</div>}
</section>
);
}

@ -0,0 +1,139 @@
'use client';
import { MinusSquare, Moon, PlusSquare, Search, Sun, Type } from 'lucide-react';
import { shallow } from 'zustand/shallow';
import { useSearchStore } from '@/stores/search-store';
import { useThemeStore } from '@/stores/theme-store';
import { Tooltip } from '@/components/tooltip';
import { cn } from '@/lib/utils';
type ToolbarProps = {
fontControls: boolean;
className?: string;
};
export function Toolbar({ fontControls, className }: ToolbarProps) {
const toggleSearch = useSearchStore((state) => state.toggleSearch);
const isSearching = useSearchStore((state) => state.isSearching);
const {
isDark,
isSerif,
isFontSizeMin,
isFontSizeMax,
toggleDark,
toggleSerif,
increaseFontSize,
decreaseFontSize,
} = useThemeStore(
(state) => ({
isDark: state.isDark,
isSerif: state.isSerif,
isFontSizeMin: state.isFontSizeMin,
isFontSizeMax: state.isFontSizeMax,
toggleDark: state.toggleDark,
toggleSerif: state.toggleSerif,
increaseFontSize: state.increaseFontSize,
decreaseFontSize: state.decreaseFontSize,
}),
shallow,
);
const toggleDarkAndApply = () => {
toggleDark();
document.querySelector('html')?.classList.toggle('dark');
};
return (
<div
className={cn(
'flex h-8 w-fit flex-row items-center justify-end space-x-1',
className,
)}
>
<button onClick={toggleSearch}>
<Search
id="search"
className={cn(
'icon-base h-5 w-5 mix-blend-color-dodge xs:h-6 xs:w-6',
isSearching && 'text-accent/60 dark:text-accent-dark/80',
)}
aria-label="Search Posts"
data-tooltip-content="Search Posts"
data-tooltip-id="toolbar-tooltip"
/>
</button>
{fontControls && (
<>
<button
onClick={decreaseFontSize}
disabled={isFontSizeMin}
className="group"
>
<MinusSquare
id="decrease-font-size"
className={cn(
'icon-base h-5 w-5 mix-blend-color-dodge xs:h-6 xs:w-6',
'group-disabled:text-accent/60 dark:group-disabled:text-accent-dark/80',
)}
aria-label="Decrease font size"
data-tooltip-content="Decrease font size"
data-tooltip-id="toolbar-tooltip"
/>
</button>
<button
onClick={increaseFontSize}
disabled={isFontSizeMax}
className="group"
>
<PlusSquare
id="increase-font-size"
className={cn(
'icon-base h-5 w-5 mix-blend-color-dodge xs:h-6 xs:w-6',
'group-disabled:text-accent/60 dark:group-disabled:text-accent-dark/80',
)}
aria-label="Increase font size"
data-tooltip-content="Increase font size"
data-tooltip-id="toolbar-tooltip"
/>
</button>
</>
)}
<button onClick={toggleSerif}>
<Type
id="serif"
className={cn(
'icon-base ml-auto h-5 w-5 mix-blend-color-dodge xs:h-6 xs:w-6',
isSerif && 'text-accent/60 dark:text-accent-dark/80',
)}
data-tooltip-content="Toggle serif font"
data-tooltip-id="toolbar-tooltip"
aria-label="Toggle serif font"
/>
</button>
<button
onClick={toggleDarkAndApply}
id="theme-toggle"
data-tooltip-content={`Switch to ${isDark ? 'light' : 'dark'} mode`}
data-tooltip-id="toolbar-tooltip"
>
{isDark ? (
<Moon
className="icon-base h-5 w-5 mix-blend-color-dodge xs:h-6 xs:w-6"
aria-label="Switch to light mode"
/>
) : (
<Sun
className="icon-base h-5 w-5 mix-blend-color-dodge xs:h-6 xs:w-6"
aria-label="Switch to dark mode"
/>
)}
</button>
<Tooltip id="toolbar-tooltip" />
</div>
);
}

@ -0,0 +1,20 @@
import {
Tooltip as ReactTooltip,
type ITooltip as TooltipProps,
} from 'react-tooltip';
import { cn } from '@/lib/utils';
export function Tooltip(props: TooltipProps) {
return (
<ReactTooltip
className={cn(
'hidden text-sm xs:block',
'!bg-slate-700 !text-slate-200',
'dark:!bg-slate-200 dark:!text-slate-700',
props.className,
)}
{...props}
/>
);
}

@ -0,0 +1,67 @@
// @ts-check
/**
* The blog's configuration. Start here after cloning the repo.
* Hovering over the properties (in an editor like VSCode) will provide additional information about them.
*/
/** @type {import('./types').BlogConfig} */
const blogConfig = {
url: 'https://edu.krasnikov.pro',
title: '‹edu/блог›',
titleParts: ['edu', 'blog'],
author: 'Красников Павел',
pages: {
home: {
title: 'Блог Красникова Павла',
description:
"Меня зовут Павел, и я преподаватель пишу про робототехнику, программирование, информатику..",
},
posts: {
url: '/posts',
title: 'Posts',
description:
"Все мои записи в блоге. Я пишу о робототехнике и других темах, которые меня интересуют",
},
},
theme: {
accentColor: {
light: '#be123c',
dark: '#fda4af',
},
codeBlockTheme: {
light: 'github-light',
dark: 'github-dark',
},
},
giscus: {
repo: 'kfirfitousi/blg',
repoId: 'R_kgDOI',
category: 'Comments',
categoryId: 'DIC_kwIcM7JM4CTdK0',
mapping: 'title',
theme: {
light: 'light',
dark: 'dark_dimmed',
},
},
footerLinks: {
twitter: 'https://vk.com/krasnikov_pro',
github: 'https://git.krasnikov.pro',
linkedin: 'https://www.linkedin.com/in/kfirp',
email: 'crapsh@gmail.com',
storybook: '',
buyMeAPizza: '',
},
topics: [
'Робототехнику',
'Программирование',
'Информатику',
'Python',
'LEGO EV3',
'VEX IQ',
'mBlock',
],
};
module.exports = { blogConfig };

@ -0,0 +1,135 @@
import type { GiscusProps, Theme as GiscusTheme } from '@giscus/react';
import type { Theme as ShikiTheme } from 'shiki';
/**
* This type represents the configuration of the blog.
* It should not be edited, unless you wish to add additional configurations.
*/
export type BlogConfig = {
/**
* The URL of the blog.
* You can use localhost while developing, but make sure to change it to the actual URL when deploying.
* Should not include a trailing slash.
* @example 'https://blog.kfirfitousi.com'
* @example 'http://localhost:3000'
*/
url: string;
/**
* The title of the blog. Visible in the header and the OG image.
* Also used as the prefix for the title of each page.
*/
title: string;
/**
* If you want to keep the styling of the title (i.e xxx/yyy),
* pass the two parts separately.
* @example ['kfir', 'blog']
*/
titleParts?: [string, string];
/**
* The name of the blog's author. Visible in the footer.
*/
author: string;
/**
* The two main pages of the blog, the home page and the posts page.
* To add a markdown page (e.g. about, contact, etc.),
* simply add a new file to the `content/pages` directory.
*/
pages: {
home: {
/**
* The title of the home page (the part after the pipe).
* When omitted, only the blog's name is used (without the pipe).
* @example 'Home' --> '‹blog/name› | Home'
* @example undefined --> '‹blog/name›'
*/
title?: string;
/**
* The description of the home page.
*/
description: string;
};
posts: {
/**
* The URL of the posts page.
* If you want to use a different URL,
* make sure to rename the `app/posts` directory to match.
*/
url: `/${string}`;
/**
* The title of the posts page (the part after the pipe).
*/
title: string;
/**
* The description of the posts page.
*/
description: string;
};
};
/**
* Customize the blog's theme.
*/
theme?: {
/**
* The accent color to use.
*/
accentColor?: {
/**
* @default #be123c // rose-700
*/
light?: `#${string}`;
/**
* @default #fda4af // rose-300
*/
dark?: `#${string}`;
};
/**
* The themes to use for the code blocks.
* Must be a valid {@link ShikiTheme}.
*/
codeBlockTheme?: {
/**
* @default 'github-light'
*/
light: ShikiTheme;
/**
* @default 'github-dark'
*/
dark: ShikiTheme;
};
};
/**
* Giscus comment sections configuration.
* @see https://giscus.app
*/
giscus: Omit<GiscusProps, 'theme'> & {
/**
* The themes to use in the Giscus comment sections.
* Must be a valid {@link GiscusTheme}.
*/
theme?: {
/**
* @default 'light'
*/
light?: GiscusTheme;
/**
* @default 'dark_dimmed'
*/
dark?: GiscusTheme;
};
};
/**
* Footer links.
*/
footerLinks?: {
twitter?: string;
github?: string;
linkedin?: string;
email?: string;
storybook?: string;
buyMeAPizza?: string;
};
/**
* Topics to show in the hero section typing animation.
*/
topics: string[];
};

@ -0,0 +1,26 @@
---
title: Обо мне
description: I'm a Full Stack Developer experienced with TypeScript, React and Next.js.
---
```ts title="Pavel.ts"
type Pavle = Наставник & Учитель & Тренер;
const pavel: pavel = {
name: 'Павел Красников',
location: 'Краснодар',
occupation: {
title: 'Преподователь робототехники, информатики',
institution: 'МАОУ СОШ 103 г.Краснодар',
},
languages: ['TypeScript', 'JavaScript', 'Python', 'C++/C'],
technologies: {
frontEnd: ['React', 'Svelte'],
backEnd: ['Node.js', 'Express.js'],
fullStack: ['Next.js', 'SvelteKit'],
stateManagement: ['Zustand', 'React-Query'],
testing: ['Vitest', 'Jest', 'Playwright', 'Storybook'],
css: ['TailwindCSS'],
},
};
```

@ -0,0 +1,651 @@
---
title: Олимпиада по робототехники школьный этап 10 - 11 класc. Пробный вариант.
excerpt: Пробный вариант по за 10 - 11 класс 2023-24 год.
date: '2023-09-06'
tags:
- Робототехника
- Олимпиада
---
Перед Вами пробный варинат олимпиадных задний по профилю технология в правление робототехника. К каждому вопросу в олимпиаде идет 5 разных вопросов по схожей теме.
<TableOfContents>
- [Вопрос № 1](#вопрос--1)
- [Вопрос № 2](#вопрос--2)
- [Вопрос № 3](#вопрос--3)
- [Вопрос № 4](#вопрос--4)
- [Вопрос № 5](#вопрос--5)
- [Вопрос № 6](#вопрос--6)
- [Вопрос № 7](#вопрос--7)
- [Вопрос № 8](#вопрос--8)
- [Вопрос № 9](#вопрос--9)
- [Вопрос № 10](#вопрос--10)
- [Вопрос № 12](#вопрос--12)
- [Вопрос № 13](#вопрос--13)
- [Вопрос № 14](#вопрос--14)
- [Вопрос № 15](#вопрос--15)
</TableOfContents>
## Вопрос № 1
1.1 Какой тип дисплея наиболее распространен в современных смартфонах?
- а. OLED
- б. CRT
- в. Plasma
- г. LCD
1.2 Какая компания разработала первый персональный компьютер?
- a. IBM
- b. Microsoft
- c. Apple
- d. Intel
1.3 Какой вид памяти используется в большинстве USB-накопителей?
- a RAM
- b. ROM
- c. Flash
- d. SRAM
1.4. Какие виды беспроводных связей поддерживает современный смартфон?
- a. 3G, 4G, 5G
- b. AM, FM, SW
- c. USB, HDMI, Ethernet
- d. VHS, Betamax, DVD
1.5. Какой тип аккумулятора наиболее распространен в портативных ноутбуках и смартфонах?
- a. Литий-ионный
- b.. Никель-кадмиевый
- c. Свинцово-кислотный
- d. Алкалиновый
## Вопрос № 2
2.1 Это устройство используется для замера температуры внутри печи. Какое устройство это?
- a. Термометр
- b. Телефон
- c. Телевизор
- d. Фотоаппарат
2.2 Это устройство обеспечивает связь между компьютерами внутри одного офиса. Какое устройство это?
- a. Микроволновка
- b. Маршрутизатор
- c. Смартфон
- d. Магнитофон
2.3 Это устройство используется для управления высотой полета воздушных шаров. Какое устройство это?
- a. Палка для селфи
- b. Радио
- c. Шарикоподшипник
- d. Баллон с гелием
2.4 Это устройство применяется для измерения скорости движения автомобилей на дорогах. Какое устройство это?
- a. Микроволновая печь
- b. Велосипед
- c. Радар
- d. Гитара
2.5. Это устройство позволяет записывать звук и воспроизводить его. Какое устройство это?
- a. Фотоаппарат
- b. Магнитофон
- c. Смартфон
- d. Велосипед
## Вопрос № 3
3.1 Какое из следующих утверждений верно относительно равнопеременного движения роботов А и Б?
- a. Робот А всегда будет двигаться быстрее.
- b. Робот Б всегда будет двигаться быстрее.
- c. Роботы А и Б будут двигаться с одинаковой скоростью.
- d. Скорость движения роботов будет зависеть от их амплитуды.
3.2 Если фазы равнопеременного движения роботов А и Б различаются на 180 градусов, как изменится их относительное положение со временем?
- a. Робот А и Б всегда будут находиться в одной точке.
- b. Робот А и Б будут двигаться в противоположных направлениях.
- c. Робот А и Б будут двигаться в одном направлении.
- d. Относительное положение роботов не изменится.
3.3 Если амплитуда движения робота А увеличивается со временем, как это повлияет на его относительное положение по сравнению с роботом Б?
- a. Робот А будет приближаться к роботу Б.
- b. Робот А будет удаляться от робота Б.
- c. Относительное положение роботов не изменится.
- d. Ответ зависит от разницы в фазах движения.
3.4 Если фаза движения робота А опережает фазу робота Б на 90 градусов, как это повлияет на их относительное положение?
- a. Робот А всегда будет опережать робота Б на 90 градусов.
- b. Робот Б всегда будет опережать робота А на 90 градусов.
- c. Роботы А и Б будут находиться в одной точке.
- d. Ответ зависит от амплитуд движения роботов.
3.5 Если оба робота имеют одинаковую амплитуду и фазу движения, как изменится их относительное положение со временем?
- a. Робот А всегда будет опережать робота Б.
- b. Робот Б всегда будет опережать робота А.
- c. Роботы А и Б будут двигаться с одинаковой скоростью в одном направлении.
- d. Относительное положение роботов не изменится.
## Вопрос № 4
4.1 Какой источник энергии становится все более популярным в мировой энергетике?
- a. Уголь
- b. Нефть
- c. Ветряная энергия
- d. Гидроэнергия
4.2 Какое технологическое средство способствует увеличению эффективности производства энергии?
- a. Угольная печь
- b. Усовершенствованный трансформатор
- c. Солнечные панели
- d. Паровая машина
4.3 Какая стратегия по сокращению выбросов парниковых газов стала важной для развития мировой энергетики?
- a. Увеличение использования угля
- b. Расширение производства нефти
- c. Переход на альтернативные источники энергии
- d. Усовершенствование производства газа
4.4 Какая область энергетики представляет собой наибольший потенциал для роста в ближайшие десятилетия?
- a. Энергия ветра
- b. Энергия урана
- c. Энергия нефти
- d. Энергия угля
4.5 Какие меры мировое сообщество предпринимает для увеличения энергетической эффективности?
- a. Субсидирование нефтяных компаний
- b. Внедрение стандартов энергосбережения
- c. Увеличение добычи природного газа
- d. Усовершенствование транспортных средств с низким расходом топлива
## Вопрос № 5
5.1 Какие преимущества приносит наука в развитие технологий и производства?
- a. Улучшение качества продукции
- b. Снижение затрат на производство
- c. Увеличение прибыли компаний
- d. Увеличение числа рабочих мест
5.2 Какие области науки наиболее активно влияют на развитие технологий?
- a. Биология и медицина
- b. Физика и химия
- c. Гуманитарные науки
- d. Социальные науки
5.3. Какие факторы способствуют успешному внедрению научных исследований в производство?
- a. Государственная поддержка научных проектов
- b. Инновационная культура в организации
- c. Высокий уровень конкуренции на рынке
- d. Отсутствие патентных прав на научные разработки
5.4. Какие вызовы могут возникнуть при интеграции науки и производства?
- a. Этические дилеммы
- b. Увеличение экологического воздействия
- c. Сокращение прибыли компаний
- d. Отсутствие интереса со стороны научных исследователей
5.5 Какие перспективы видите в будущем для сотрудничества науки и производства?
- a. Более эффективное использование ресурсов
- b. Развитие более экологичных технологий
- c. Увеличение зависимости от иностранных технологий
- d. Сокращение роли науки в производстве
## Вопрос № 6
6.1 Робот движется равномерно по окружности диаметром 30 см. Скорость робота составляет 10 см/сек. Сколько времени ему потребуется для одного полного оборота вокруг окружности? Выберите ближайший вариант.
- a. 12 секунд
- b. 15 секунд
- c. 18 секунд
- d. 20 секунд
6.2 Робот движется равномерно по окружности диаметром 15 см. Скорость робота составляет 20 см/сек. Сколько времени ему потребуется для одного полного оборота вокруг окружности? Выберите ближайший вариант.
- a. 5 секунд
- b. 7.5 секунд
- c. 10 секунд
- d. 12 секунд
6.3 Робот движется равномерно по окружности диаметром 25 см. Скорость робота составляет 18 см/сек. Сколько времени ему потребуется для одного полного оборота вокруг окружности? Выберите ближайший вариант.
- a. 8 секунд
- b. 10 секунд
- c. 12 секунд
- d. 15 секунд
6.4. Робот движется равномерно по окружности диаметром 40 см. Скорость робота составляет 12 см/сек. Сколько времени ему потребуется для одного полного оборота вокруг окружности? Выберите ближайший вариант.
- a. 10 секунд
- b. 15 секунд
- c. 20 секунд
- d. 25 секунд
6.5. Робот движется равномерно по окружности диаметром 18 см. Скорость робота составляет 25 см/сек. Сколько времени ему потребуется для одного полного оборота вокруг окружности? Выберите ближайший вариант.
- a. 4.5 секунд
- b. 5 секунд
- c. 6 секунд
- d. 7.2 секунды
## Вопрос № 7
7.1 Андрей собрал редуктор с ведущей шестерёнкой на 12 зубьев и ведомой на 60 зубьев. Между ними находится две паразитные шестерёнки: одна на 30 зубов и другая на 15 зубов. Если ведущая ось вращается со скоростью 40 оборотов в секунду, то какая будет скорость ведомой оси?
7.2 Юлия создала механизм с ведущей шестерёнкой на 10 зубьев и ведомой на 50 зубьев. Между ними установлена паразитная шестерёнка на 20 зубов. Если скорость ведущей оси составляет 60 оборотов в минуту, то какова будет скорость ведомой оси?
7.3 Петр собрал редуктор, в котором ведущая шестерёнка имеет 16 зубьев, а ведомая - 64 зуба. Между ними находится паразитная шестерёнка на 32 зуба. Если скорость ведущей оси составляет 80 оборотов в минуту, то какая будет скорость ведомой оси?
7.4 Олег собирает редуктор с ведущей шестерёнкой на 18 зубьев и ведомой на 72 зуба. Между ними устанавливается паразитная шестерёнка на 36 зубов. Если ведущая ось вращается со скоростью 90 оборотов в минуту, то какова будет скорость ведомой оси?
7.5 Мария создала механизм с ведущей шестерёнкой на 14 зубьев и ведомой на 70 зубов. Между ними находится паразитная шестерёнка на 28 зубов. Если скорость ведущей оси составляет 56 оборотов в минуту, то какая будет скорость ведомой оси?
## Вопрос № 8
8.1 Какова временная сложность алгоритма сортировки "Слиянием" для массива из N элементов?
- a. O(N)
- b. O(N*log(N))
- c. O(N^2)
- d. O(log(N))
8.2 Какова пространственная сложность алгоритма быстрой сортировки (Quick Sort. для массива из N элементов)?
- a. O(N)
- b. O(log(N))
- c. O(N*log(N))
- d. O(1)
8.3 Какова временная сложность линейного поиска в массиве из N элементов?
- a. O(N)
- b. O(log(N))
- c. O(N*log(N))
- d. O(1.
8.4 Какова пространственная сложность алгоритма обратного хода (Backtracking. при решении задачи коммивояжера для N городов)?
- a. O(N)
- b. O(N)
- c. O(log(N))
- d. O(1)
8.5 Какова временная сложность алгоритма поиска наименьшего общего предка (LCA. в бинарном дереве с N узлами)?
- a. O(N)
- b. O(log(N))
- c. O(N*log(N)))
- d. O(1)
## Вопрос № 9
9.1 Какая оптимизация может снизить количество рекурсивных вызовов в быстрой сортировке?
- a. Использование хвостовой рекурсии
- b. Увеличение максимальной глубины рекурсии
- c. Применение сортировки слиянием вместо быстрой сортировки
- d. Уменьшение размера входного массива
9.2 Какая структура данных может помочь улучшить производительность быстрой сортировки?
- a. Стек
- b. Очередь
- c. Связанный список
- d. Двоичное дерево
9.3 Какие случаи данных лучше всего подходят для быстрой сортировки?
- a. Массивы с уникальными значениями
- b. Массивы с большим количеством повторяющихся элементов
- c. Массивы, уже отсортированные по возрастанию
- d. Массивы, отсортированные в обратном порядке
9.4 Какая оптимизация может уменьшить использование дополнительной памяти в быстрой сортировке?
- a. Использование рекурсии вместо итерации
- b. Выделение большего объема памяти заранее
- c. Использование встроенных функций сортировки
- d. Минимизация использования дополнительных структур данных
9.5 Какой выбор опорного элемента может повысить эффективность быстрой сортировки?
- a. Случайный выбор элемента
- b. Всегда выбирать первый элемент массива
- c. Всегда выбирать средний элемент массива
- d. Всегда выбирать последний элемент массива
## Вопрос № 10
10.1 Дистанционное управление роботом: Робот двигался по лабиринту в течение 30 минут с постоянной скоростью 2 м/с. Определите, сколько метров прошел робот.
10.2 Исследование роботом под водой: Робот-подводник преодолел 5 километров и сделал 200 оборотов винта. Какое расстояние он преодолел за один оборот винта?
10.3 Скорость движения робота-марсохода: Робот-марсоход проехал 10 километров за 2 часа. Определите его скорость в м/с.
10.4 Программирование движения робота: Робот двигался со скоростью 1 м/с и должен был пройти расстояние 500 метров. Определите, сколько времени затратил робот на выполнение задачи.
10.5 Скорость манипулятора робота: Робот-манипулятор перемещал свой манипулятор с одной точки в другую со скоростью 0,5 м/с. Определите, сколько времени потребовалось роботу для выполнения этой задачи.
## Вопрос 12
12.1 Андрей собрал редуктор с ведущей шестерёнкой на 12 зубьев и ведомой на 60 зубьев. Между ними находится две паразитные шестерёнки: одна на 30 зубов и другая на 15 зубов. Если ведущая ось вращается со скоростью 40 оборотов в секунду, то какая будет скорость ведомой оси?
12.2 Юлия создала механизм с ведущей шестерёнкой на 10 зубьев и ведомой на 50 зубьев. Между ними установлена паразитная шестерёнка на 20 зубов. Если скорость ведущей оси составляет 60 оборотов в минуту, то какова будет скорость ведомой оси?
12.3 Петр собрал редуктор, в котором ведущая шестерёнка имеет 16 зубьев, а ведомая - 64 зуба. Между ними находится паразитная шестерёнка на 32 зуба. Если скорость ведущей оси составляет 80 оборотов в минуту, то какая будет скорость ведомой оси?
12.4 Олег собирает редуктор с ведущей шестерёнкой на 18 зубьев и ведомой на 72 зуба. Между ними устанавливается паразитная шестерёнка на 36 зубов. Если ведущая ось вращается со скоростью 90 оборотов в минуту, то какова будет скорость ведомой оси?
12.5 Мария создала механизм с ведущей шестерёнкой на 14 зубьев и ведомой на 70 зубов. Между ними находится паразитная шестерёнка на 28 зубов. Если скорость ведущей оси составляет 56 оборотов в минуту, то какая будет скорость ведомой оси?
## Вопрос 13
13.1 Два автомобиля движутся навстречу друг другу по одной и той же дороге. Первый автомобиль стартует из пункта А, а второй из пункта Б, находящегося на 60 километров дальше по дороге. Первый автомобиль движется со скоростью 80 км/ч, а второй - со скоростью 60 км/ч. Через какое время они встретятся?
13.2 Два пловца стартуют с противоположных концов бассейна одновременно. Первый пловец может проплывать 2 м/с, а второй - 1.5 м/с. Бассейн имеет длину 50 метров. Через какое время они встретятся впервые?
13.3 Два поезда отправляются из разных городов навстречу друг другу. Первый поезд отправляется со скоростью 100 км/ч, а второй - со скоростью 120 км/ч. Расстояние между городами составляет 600 километров. В каком месте находятся поезда, когда они встречаются?
13.4 Два часовых начинают свою смену одновременно. Первый часовой работает 4 часа, а второй - 6 часов. Сколько времени прошло, когда они снова встретятся?
13.5 Два спутника обращаются вокруг Земли на разных высотах. Первый спутник находится на высоте 500 километров, а второй - на высоте 800 километров. Один оборот первого спутника занимает 90 минут, а второго - 120 минут. Через сколько времени спутники будут находиться над одной и той же точкой Земли впервые?
## Вопрос 14
14.1 Робот находится в верхнем левом углу сетки 3x3 и должен добраться до нижнего правого угла. Сколько существует различных путей для робота, перемещающегося только вниз и вправо, чтобы достичь цели?
14.2 Робот находится в верхнем левом углу сетки 4x4 и должен добраться до нижнего правого угла. Сколько существует различных путей для робота, перемещающегося только вниз и вправо, чтобы достичь цели?
14.3 Робот находится в верхнем левом углу сетки 6x6 и должен добраться до нижнего правого угла. Сколько существует различных путей для робота, перемещающегося только вниз и вправо, чтобы достичь цели?
14.4 Робот находится в верхнем левом углу сетки 2x2 и должен добраться до нижнего правого угла. Сколько существует различных путей для робота, перемещающегося только вниз и вправо, чтобы достичь цели?
14.5 Робот находится в верхнем левом углу сетки 7x7 и должен добраться до нижнего правого угла. Сколько существует различных путей для робота, перемещающегося только вниз и вправо, чтобы достичь цели?
## Вопрос 15
15.1 Робот-марафонец начинает движение из точки A и двигается по пятиугольному маршруту, состоящему из 5 сторон по 200 метров каждая. Если диаметр колеса робота составляет 40 см, сколько оборотов каждого колеса робота он сделает, чтобы вернуться в точку A?
15.2 Робот-сборщик прямоугольных блоков двигается по пятиугольному маршруту, где каждая сторона имеет длину 150 мм. Робот собирает блоки, стоящие на каждой стороне маршрута. Если радиус колеса робота составляет 75 мм, сколько оборотов каждого колеса робота он сделает, чтобы собрать все блоки и вернуться в исходную точку?
15.3. Робот-исследователь двигается по пятиугольной траектории для сбора данных о местности. Расстояние между центрами колес составляет 20 см, и каждая сторона пятиугольника имеет длину 250 метров. Если радиус колеса робота равен 60 мм, сколько оборотов каждого колеса робота он сделает во время исследования?
15.4 Робот-газонокосилка движется по пятиугольной траектории, чтобы подстричь траву на газоне. Каждая сторона маршрута имеет длину 180 метров. Если диаметр колеса робота составляет 50 мм, сколько оборотов каждого колеса робота он сделает, чтобы подстричь всю траву и вернуться в начальную точку?
15.5 Робот-курьер доставляет посылки по пятиугольной траектории. Каждая сторона маршрута имеет длину 120 метров. Если радиус колеса робота составляет 45 мм, сколько оборотов каждого колеса робота он сделает, чтобы доставить все посылки и вернуться в отправную точку?
# Ответы на вопросы
## Вопрос № 1
1.1 Какой тип дисплея наиболее распространен в современных смартфонах?
- Ответ: a) OLED
1.2 Какая компания разработала первый персональный компьютер?
- Ответ: a) IBM
1.3 Какой вид памяти используется в большинстве USB-накопителей?
- Ответ: c) Flash
1.4 Какие виды беспроводных связей поддерживает современный смартфон?
- Ответ: a) 3G, 4G, 5G
1.5 Какой тип аккумулятора наиболее распространен в портативных ноутбуках и смартфонах?
- Ответ: a) Литий-ионный
## Вопрос № 2
2.1 Это устройство используется для замера температуры внутри печи. Какое устройство это?
- Ответ: a. Термометр
2.2 Это устройство обеспечивает связь между компьютерами внутри одного офиса. Какое устройство это?
- Ответ: b. Маршрутизатор
2.3 Это устройство используется для управления высотой полета воздушных шаров. Какое устройство это?
- Ответ: b. Радио
2.4 Это устройство применяется для измерения скорости движения автомобилей на дорогах. Какое устройство это?
- Ответ: c. Радар
2.5 Это устройство позволяет записывать звук и воспроизводить его. Какое устройство это?
- Ответ: b. Магнитофон
## Вопрос № 3
Для решения этих задач, давайте разберемся с основными концепциями равнопеременного движения и его влиянием на относительное положение роботов.
3.1 Какое из следующих утверждений верно относительно равнопеременного движения роботов А и Б?
- Правильный ответ: c) Роботы А и Б будут двигаться с одинаковой скоростью.
Роботы, начинающие равнопеременное движение одновременно, но с разными фазами и имеющие разные амплитуды, будут двигаться с одинаковой скоростью. Однако их положение в пространстве будет меняться из-за разных фаз и амплитуд, но они будут двигаться с одинаковой скоростью.
3.2 Второй вопрос: Если фазы равнопеременного движения роботов А и Б различаются на 180 градусов, как изменится их относительное положение со временем?
- Правильный ответ: b) Робот А и Б будут двигаться в противоположных направлениях.
Разница в фазах на 180 градусов означает, что один робот движется в одном направлении, а другой - в противоположном направлении. Они будут двигаться в противоположных направлениях.
3.3 Третий вопрос: Если амплитуда движения робота А увеличивается со временем, как это повлияет на его относительное положение по сравнению с роботом Б?
- Правильный ответ: a) Робот А будет приближаться к роботу Б.
Увеличение амплитуды движения робота А означает, что его максимальное удаление от исходной точки будет больше. Это приведет к тому, что он будет приближаться к роботу Б, который имеет постоянную амплитуду.
3.4 Четвертый вопрос: Если фаза движения робота А опережает фазу робота Б на 90 градусов, как это повлияет на их относительное положение?
- Правильный ответ: c) Роботы А и Б будут находиться в одной точке.
Разница в фазах на 90 градусов означает, что роботы будут находиться в разных точках на кривой движения, но их суммарное движение будет таким, что они будут находиться в одной точке одновременно.
3.5 Пятый вопрос: Если оба робота имеют одинаковую амплитуду и фазу движения, как изменится их относительное положение со временем?
- Правильный ответ: d) Относительное положение роботов не изменится.
Если оба робота имеют одинаковую амплитуду и фазу движения, то их относительное положение будет оставаться неизменным со временем, так как они будут двигаться синхронно и оставаться на одинаковом расстоянии друг от друга.
## Вопрос № 4
4.1 Какой источник энергии становится все более популярным в мировой энергетике?
- Ответ: c) Ветряная энергия
4.2 Какое технологическое средство способствует увеличению эффективности производства энергии?
- Ответ: c) Солнечные панели
4.3 Какая стратегия по сокращению выбросов парниковых газов стала важной для развития мировой энергетики?
- Ответ: c) Переход на альтернативные источники энергии
4.4 Какая область энергетики представляет собой наибольший потенциал для роста в ближайшие десятилетия?
- Ответ: a) Энергия ветра
4.5 Какие меры мировое сообщество предпринимает для увеличения энергетической эффективности?
- Ответ: b) Внедрение стандартов энергосбережения
## Вопрос №5
5.1 Какие преимущества приносит наука в развитие технологий и производства?
- Ответ: Все перечисленные варианты. Наука способствует улучшению качества продукции, снижению затрат, увеличению прибыли компаний и созданию новых рабочих мест.
5.2 Какие области науки наиболее активно влияют на развитие технологий?
- Ответ: a) Биология и медицина, b) Физика и химия. Эти области науки играют ключевую роль в развитии технологий.
5.3 Какие факторы способствуют успешному внедрению научных исследований в производство?
- Ответ: a) Государственная поддержка научных проектов и b) Инновационная культура в организации. Эти факторы способствуют успешному внедрению научных исследований в производство.
5.4 Какие вызовы могут возникнуть при интеграции науки и производства?
- Ответ: a) Этические дилеммы и b) Увеличение экологического воздействия. При интеграции науки и производства могут возникать этические и экологические проблемы.
5.5 Какие перспективы видите в будущем для сотрудничества науки и производства?
- Ответ: a) Более эффективное использование ресурсов и b) Развитие более экологичных технологий. Сотрудничество науки и производства может привести к более эффективному использованию ресурсов и развитию экологичных технологий.
## Вопрос № 6
Для решения каждого из этих вопросов мы можем использовать формулу для вычисления времени, которое требуется роботу для одного полного оборота вокруг окружности. Формула для вычисления времени (t) на один оборот в данном случае будет выглядеть так:
t=2πrv,t=v2πr,
где:
- tt - время для одного оборота,
- ππ (пи) - математическая константа, приближенное значение которой около 3.14159,
- rr - радиус окружности (половина диаметра),
- vv - скорость робота.
Давайте рассмотрим каждый из вопросов:
6.1 Робот движется по окружности диаметром 30 см (r=15 смr=15 см) со скоростью 10 см/сек (v=10 см/секv=10 см/сек):
t=2⋅3.14159⋅15 см10 см/сек=94.247 см10 см/сек=9.4247 сек.t=10 см/сек2⋅3.14159⋅15 см=10 см/сек94.247 см=9.4247 сек.
- Ответ: ближайший вариант - 10 секунд (вариант "c").
6.2 Робот движется по окружности диаметром 15 см (r=7.5 смr=7.5 см) со скоростью 20 см/сек (v=20 см/секv=20 см/сек):
t=2⋅3.14159⋅7.5 см20 см/сек=47.123 см20 см/сек=2.35615 сек.t=20 см/сек2⋅3.14159⋅7.5 см=20 см/сек47.123 см=2.35615 сек.
- Ответ: ближайший вариант - 2.5 секунды (вариант "b").
6.3 Робот движется по окружности диаметром 25 см (r=12.5 смr=12.5 см) со скоростью 18 см/сек (v=18 см/секv=18 см/сек):
t=2⋅3.14159⋅12.5 см18 см/сек=78.54 см18 см/сек=4.3633 сек.t=18 см/сек2⋅3.14159⋅12.5 см=18 см/сек78.54 см=4.3633 сек.
- Ответ: ближайший вариант - 4.5 секунды (вариант "a").
6.4 Робот движется по окружности диаметром 40 см (r=20 смr=20 см) со скоростью 12 см/сек (v=12 см/секv=12 см/сек):
t=2⋅3.14159⋅20 см12 см/сек=125.663 см12 см/сек=10.472 сек.t=12 см/сек2⋅3.14159⋅20 см=12 см/сек125.663 см=10.472 сек.
- Ответ: ближайший вариант - 10 секунд (вариант "a").
6.5 Робот движется по окружности диаметром 18 см (r=9 смr=9 см) со скоростью 25 см/сек (v=25 см/секv=25 см/сек):
t=2⋅3.14159⋅9 см25 см/сек=56.548 см25 см/сек=2.26192 сек.t=25 см/сек2⋅3.14159⋅9 см=25 см/сек56.548 см=2.26192 сек.
- Ответ: ближайший вариант - 2.5 секунды (вариант "b").
## Вопрос № 7
Задача 7.1:
- Ведущая шестерёнка имеет 12 зубьев, ведомая - 60 зубов, и есть две паразитные шестерёнки: одна на 30 зубов, и другая на 15 зубов.
- Сначала вычислим передаточное отношение редуктора: передаточное отношение = (число зубьев на ведущей шестерёнке) / (число зубьев на ведомой шестерёнке) = 12 / 60 = 1/5.
- Теперь умножим скорость вращения ведущей оси (40 оборотов в секунду) на передаточное отношение, чтобы найти скорость ведомой оси: скорость ведомой оси = (скорость ведущей оси) * (передаточное отношение) = 40 * (1/5) = 8 оборотов в секунду.
Задача 7.2:
- Ведущая шестерёнка имеет 10 зубьев, ведомая - 50 зубов, и есть одна паразитная шестерёнка на 20 зубов.
- Передаточное отношение редуктора = 10 / 50 = 1/5.
- Скорость ведомой оси = (скорость ведущей оси) * (передаточное отношение) = 60 * (1/5) = 12 оборотов в минуту.
Задача 7.3:
- Ведущая шестерёнка имеет 16 зубьев, ведомая - 64 зуба, и есть одна паразитная шестерёнка на 32 зуба.
- Передаточное отношение редуктора = 16 / 64 = 1/4.
- Скорость ведомой оси = (скорость ведущей оси) * (передаточное отношение) = 80 * (1/4) = 20 оборотов в минуту.
Задача 7.4:
- Ведущая шестерёнка имеет 18 зубьев, ведомая - 72 зуба, и есть одна паразитная шестерёнка на 36 зубов.
- Передаточное отношение редуктора = 18 / 72 = 1/4.
- Скорость ведомой оси = (скорость ведущей оси) * (передаточное отношение) = 90 * (1/4) = 22.5 оборотов в минуту.
Задача 7.5:
- Ведущая шестерёнка имеет 14 зубьев, ведомая - 70 зубов, и есть одна паразитная шестерёнка на 28 зубов.
- Передаточное отношение редуктора = 14 / 70 = 1/5.
- Скорость ведомой оси = (скорость ведущей оси) * (передаточное отношение) = 56 * (1/5) = 11.2 оборотов в минуту.
## Вопрос № 8
1. Какова временная сложность алгоритма сортировки "Слиянием" для массива из N элементов?
- Ответ: b) O(N*log(N))
2. Какова пространственная сложность алгоритма быстрой сортировки (Quick Sort) для массива из N элементов?
- Ответ: d) O(1)
3. Какова временная сложность линейного поиска в массиве из N элементов?
- Ответ: a) O(N)
4. Какова пространственная сложность алгоритма обратного хода (Backtracking) при решении задачи коммивояжера для N городов?
- Ответ: b) O(N!)
5. Какова временная сложность алгоритма поиска наименьшего общего предка (LCA) в бинарном дереве с N узлами?
- Ответ: b) O(log(N))
## Вопрос № 9
9.1 Какая оптимизация может снизить количество рекурсивных вызовов в быстрой сортировке?
- Ответ: a) Использование хвостовой рекурсии
9.2 Какая структура данных может помочь улучшить производительность быстрой сортировки?
- Ответ: a) Стек
9.3 Какие случаи данных лучше всего подходят для быстрой сортировки?
- Ответ: a) Массивы с уникальными значениями
9.4 Какая оптимизация может уменьшить использование дополнительной памяти в быстрой сортировке?
- Ответ: c) Использование встроенных функций сортировки
9.5 Какой выбор опорного элемента может повысить эффективность быстрой сортировки?
- Ответ: a) Случайный выбор элемента
## Вопрос № 10
10.1 Дистанционное управление роботом: Робот двигался в течение 30 минут с постоянной скоростью 2 м/с. Для определения расстояния, которое он прошел, умножим время на скорость: Расстояние = Время × Скорость = 30 минут × (2 м/с) = 60 метров. Таким образом, робот прошел 60 метров.
10.2 Исследование роботом под водой: Робот-подводник преодолел 5 километров и сделал 200 оборотов винта. Чтобы найти расстояние, которое он преодолел за один оборот винта, разделим общее расстояние на количество оборотов: Расстояние за один оборот винта = 5 километров / 200 = 0,025 километра = 25 метров.
10.3 Скорость движения робота-марсохода: Робот-марсоход проехал 10 километров за 2 часа. Для определения его скорости, разделим расстояние на время: Скорость = Расстояние / Время = 10 километров / 2 часа = 5 километров в час. Для перевода в метры в секунду, умножим на 1000 (1 км = 1000 м) и разделим на 3600 (1 час = 3600 секунд): Скорость = (5 километров * 1000 м/км) / (2 часа * 3600 с/час) ≈ 1,39 м/с.
10.4 Программирование движения робота: Робот двигался со скоростью 1 м/с и должен был пройти расстояние 500 метров. Для определения времени, затраченного на выполнение задачи, разделим расстояние на скорость: Время = Расстояние / Скорость = 500 метров / 1 м/с = 500 секунд = 8 минут и 20 секунд.
10.5 Скорость манипулятора робота: Робот-манипулятор перемещал свой манипулятор с одной точки в другую со скоростью 0,5 м/с. Если нам не дано конкретное расстояние, то мы не можем определить время, затраченное на выполнение задачи, без дополнительной информации о расстоянии, которое нужно преодолеть.
## Вопрос 12
Задача 12.1:
- Ведущая шестерёнка имеет 12 зубьев, ведомая - 60 зубов, и есть две паразитные шестерёнки: одна на 30 зубов, и другая на 15 зубов.
- Сначала вычислим передаточное отношение редуктора: передаточное отношение = (число зубьев на ведущей шестерёнке) / (число зубьев на ведомой шестерёнке) = 12 / 60 = 1/5.
- Теперь умножим скорость вращения ведущей оси (40 оборотов в секунду) на передаточное отношение, чтобы найти скорость ведомой оси: скорость ведомой оси = (скорость ведущей оси) * (передаточное отношение) = 40 * (1/5) = 8 оборотов в секунду.
Задача 12.2:
- Ведущая шестерёнка имеет 10 зубьев, ведомая - 50 зубов, и есть одна паразитная шестерёнка на 20 зубов.
- Передаточное отношение редуктора = 10 / 50 = 1/5.
- Скорость ведомой оси = (скорость ведущей оси) * (передаточное отношение) = 60 * (1/5) = 12 оборотов в минуту.
Задача 12.3:
- Ведущая шестерёнка имеет 16 зубьев, ведомая - 64 зуба, и есть одна паразитная шестерёнка на 32 зуба.
- Передаточное отношение редуктора = 16 / 64 = 1/4.
- Скорость ведомой оси = (скорость ведущей оси) * (передаточное отношение) = 80 * (1/4) = 20 оборотов в минуту.
Задача 12.4:
- Ведущая шестерёнка имеет 18 зубьев, ведомая - 72 зуба, и есть одна паразитная шестерёнка на 36 зубов.
- Передаточное отношение редуктора = 18 / 72 = 1/4.
- Скорость ведомой оси = (скорость ведущей оси) * (передаточное отношение) = 90 * (1/4) = 22.5 оборотов в минуту.
Задача 12.5:
- Ведущая шестерёнка имеет 14 зубьев, ведомая - 70 зубов, и есть одна паразитная шестерёнка на 28 зубов.
- Передаточное отношение редуктора = 14 / 70 = 1/5.
- Скорость ведомой оси = (скорость ведущей оси) * (передаточное отношение) = 56 * (1/5) = 11.2 оборотов в минуту.
## Вопрос 13.
13.1 Для определения времени встречи автомобилей можно воспользоваться формулой:
- Время = Расстояние / (Скорость1 + Скорость2)
- В данном случае, расстояние между пунктами А и Б составляет 60 километров, скорость первого автомобиля - 80 км/ч, а скорость второго - 60 км/ч.
- Время = 60 км / (80 км/ч + 60 км/ч) = 60 км / 140 км/ч = 3/7 часа.
- Переведем время в минуты: (3/7) * 60 = 25.71 минута.
- Таким образом, автомобили встретятся через примерно 25.71 минуту.
13.2 Для определения времени встречи пловцов в бассейне можно использовать формулу:
- Время = Расстояние / (Скорость1 + Скорость2)
- В данном случае, расстояние в бассейне составляет 50 метров, первый пловец плывет со скоростью 2 м/с, а второй - 1.5 м/с.
- Время = 50 м / (2 м/с + 1.5 м/с) = 50 м / 3.5 м/с = 14.29 секунд.
- Таким образом, пловцы встретятся через примерно 14.29 секунд.
13.3 Для определения места встречи поездов можно использовать формулу:
- Место = (Скорость1 * Время) + (Скорость2 * Время)
- В данном случае, первый поезд движется со скоростью 100 км/ч, второй - 120 км/ч, и расстояние между городами составляет 600 километров.
- Пусть Место будет расстоянием между городами, а Время - время встречи.
- 600 км = (100 км/ч * Время) + (120 км/ч * Время)
- 600 км = (100 + 120) км/ч * Время
- 600 км = 220 км/ч * Время
- Время = 600 км / 220 км/ч ≈ 2.73 часа.
- Теперь можно найти расстояние от начала первого города до места встречи:
- Место = 100 км/ч * 2.73 часа ≈ 273 километра от начала первого города.
- Таким образом, поезда встретятся примерно через 2.73 часа, и это произойдет примерно 273 километра от начала первого города.
13.4. Для определения времени встречи часовых можно воспользоваться наименьшим общим кратным их смен:
- НОК(4, 6) = 12 часов.
- Часовые встретятся через каждые 12 часов работы, так как это наименьшее общее кратное их смен.
13.5 Для определения времени встречи спутников над одной и той же точкой Земли можно воспользоваться формулой:
- Время = (Период1 * Период2) / |Период1 - Период2|
- В данном случае, первый спутник имеет период обращения 90 минут, а второй - 120 минут.
- Время = (90 мин * 120 мин) / |90 мин - 120 мин| = (10800 мин) / (30 мин) = 360 минут.
- Таким образом, спутники будут находиться над одной и той же точкой Земли каждые 360 минут, или 6 часов.
## Вопрос № 14
14.1. Робот находится в верхнем левом углу сетки 3x3 и должен добраться до нижнего правого угла. Существует 6 различных путей для робота. Путь можно представить в виде последовательности шагов "вниз" (D) и "вправо" (R):
o DDRR
o DRDR
o RDRD
o RRDD
o DRRD
o RDDR
14.2 Робот находится в верхнем левом углу сетки 4x4 и должен добраться до нижнего правого угла. Существует 20 различных путей для робота.
14.3 Робот находится в верхнем левом угле сетки 6x6 и должен добраться до нижнего правого угла. Существует 924 различных пути для робота. Эту задачу можно решить с помощью биномиальных коэффициентов, где C(n, k) представляет количество путей для перемещения из верхнего левого угла в нижний правый угол сетки n x n, используя только движения вниз и вправо. В данном случае, C(12, 6) = 924.
14.4 Робот находится в верхнем левом угле сетки 2x2 и должен добраться до нижнего правого угла. Существует 2 различных пути для робота.
14.5 Робот находится в верхнем левом угле сетки 7x7 и должен добраться до нижнего правого угла. Существует 3432 различных пути для робота. Эту задачу также можно решить с помощью биномиальных коэффициентов, где C(14, 7) = 3432.
## Вопрос № 15
15.1 Решение для задачи №1: Для определения количества оборотов, которые каждое колесо робота сделает, чтобы вернуться в точку A, нам нужно сначала найти общее расстояние, которое робот должен проехать.
Общее расстояние = 5 сторон * 200 метров = 1000 метров.
Теперь мы можем использовать формулу для расчета количества оборотов:
n = k / L,
где k - общее расстояние (1000 м), L - длина одного оборота одного колеса (π * d).
L = π * 0.4 м (диаметр колеса в метрах) = 1.256 м.
Теперь можем вычислить количество оборотов:
n = 1000 м / 1.256 м = 796.18 оборотов.
Ответ: Каждое колесо робота сделает примерно 796.18 оборотов, чтобы вернуться в точку A.
15.2 Решение для задачи №2: Аналогично, сначала найдем общее расстояние:
Общее расстояние = 5 сторон * 150 мм = 750 мм = 0.75 метра.
Теперь используем формулу для расчета количества оборотов:
L = π * 0.75 м (диаметр колеса в метрах) = 2.356 м.
n = 0.75 м / 2.356 м = 0.318 оборотов.
Ответ: Каждое колесо робота сделает примерно 0.318 оборота, чтобы собрать все блоки и вернуться в исходную точку.
15.3. Решение для задачи №3: Снова найдем общее расстояние:
Общее расстояние = 5 сторон * 250 мм = 1250 мм = 1.25 метра.
Теперь используем формулу для расчета количества оборотов:
L = π * 0.6 м (радиус колеса в метрах) = 1.884 м.
n = 1.25 м / 1.884 м = 0.664 оборотов.
Ответ: Каждое колесо робота сделает примерно 0.664 оборота во время исследования.
15.4 Решение для задачи №4: Найдем общее расстояние:
Общее расстояние = 5 сторон * 180 мм = 900 мм = 0.9 метра.
L = π * 0.5 м (диаметр колеса в метрах) = 1.57 м.
n = 0.9 м / 1.57 м = 0.573 оборота.
Ответ: Каждое колесо робота сделает примерно 0.573 оборота, чтобы подстричь всю траву и вернуться в начальную точку.
15.5. Решение для задачи №5: Найдем общее расстояние:
Общее расстояние = 5 сторон * 120 мм = 600 мм = 0.6 метра.
L = π * 0.45 м (радиус колеса в метрах) = 1.413 м.
n = 0.6 м / 1.413 м = 0.425 оборотов.
Ответ: Каждое колесо робота сделает примерно 0.425 оборота, чтобы доставить все посылки и вернуться в отправную точку.

@ -0,0 +1,504 @@
---
title: Олимпиада по робототехники школьный этап 5 - 6 класc. Пробный вариант.
excerpt: Пробный вариант по за 5 - 6 класс 2023-24 год.
date: '2023-09-06'
tags:
- Робототехника
- Олимпиада
---
Перед Вами пробный варинат олимпиадных задний по профилю технология в правление робототехника. К каждому вопросу в олимпиаде идет 5 разных вопросов по схожей теме.
<TableOfContents>
- [Вопрос № 1](#вопрос--1)
- [Вопрос № 2](#вопрос--2)
- [Вопрос № 3](#вопрос--3)
- [Вопрос № 4](#вопрос--4)
- [Вопрос № 5](#вопрос--5)
- [Вопрос № 6](#вопрос--6)
- [Вопрос № 7](#вопрос--7)
- [Вопрос № 8](#вопрос--8)
- [Вопрос № 9](#вопрос--9)
- [Вопрос № 10](#вопрос--10)
- [Вопрос № 12 и 13](#вопрос--12-13)
- [Вопрос № 15](#вопрос--15)
</TableOfContents>
## Вопрос № 1
1.1 Какой тип роботов спроектирован для выполнения задач в водной среде?
- a. Робот-подводник
- b. Робот-полет
- c. Робот-грузовик
- d. Робот-гуманоид
1.2 Какой робот предназначен для исследования других планет?
- a. Робот-уборщик
- b. Робот-пароварка
- c. Робот-марсоход
- d. Робот-парикмахер
1.3 Какой тип роботов используется для выполнения задач в медицине?
- a. Робот-пылесос
- b. Робот-хирург
- c. Робот-повар
- d. Робот-дальнобойщик
1.4 Какие роботы обычно используются в автопроме для сборки автомобилей?
- a. Робот-массажист
- b. Робот-архитектор
- c. Робот-сварщик
- d. Робот-пилот
1.5 Какие роботы могут быть использованы для доставки почты и товаров в крупных городах?
- a. Робот-кулинар
- b. Робот-медсестра
- c. Робот-почтальон
- d. Робот-уборщик
## Вопрос № 2
2.1 Это устройство используется для замера температуры внутри печи. Какое устройство это?
- a. Термометр
- b. Телефон
- c. Телевизор
- d. Фотоаппарат
2.2 Это устройство обеспечивает связь между компьютерами внутри одного офиса. Какое устройство это?
- a. Микроволновка
- b. Маршрутизатор
- c. Смартфон
- d. Магнитофон
2.3 Это устройство используется для управления высотой полета воздушных шаров. Какое устройство это?
- a. Палка для селфи
- b. Радио
- c. Шарикоподшипник
- d. Баллон с гелием
2.4 Это устройство применяется для измерения скорости движения автомобилей на дорогах. Какое устройство это?
- a. Микроволновая печь
- b. Велосипед
- c. Радар
- d. Гитара
2.5. Это устройство позволяет записывать звук и воспроизводить его. Какое устройство это?
- a. Фотоаппарат
- b. Магнитофон
- c. Смартфон
- d. Велосипед
## Задание № 3
3.1 Какое название передачи используется для передачи движения с помощью зубчатых колес?
- a. Ременная передача
- b. Цепная передача
- c. Зубчатая передача
- d. Червячная передача
3.2 Какую передачу часто используют для передачи движения велосипедов?
- a. Ременная передача
- b. Цепная передача
- c. Зубчатая передача
- d. Червячная передача
3.3 Какая передача обычно используется для переключения скоростей в автомобилях с механической коробкой передач?
- a. Ременная передача
- b. Цепная передача
- c. Зубчатая передача
- d. Червячная передача
3.4 Какой тип передачи характеризуется использованием червячного винта?
- a. Ременная передача
- b. Цепная передача
- c. Зубчатая передача
- d. Червячная передача
3.5 Какой вид передачи применяется для передачи движения между параллельными осями с помощью зубчатых колес?
- a. Ременная передача
- b. Цепная передача
- c. Зубчатая передача
- d. Червячная передача
## Задание № 4
4.1 Наташа решила сделать подарок своей бабушке на день рождения. Она хочет сшить две одинаковые подушки и для этого ей нужно купить ткань. Один метр ткани стоит 250 рублей. Сколько рублей Наташа должна потратить на ткань, чтобы сшить две подушки, если каждая подушка требует 1.5 метра ткани?
4.2 Макс решил приготовить себе завтрак и для этого ему нужно купить два яйца. Он пошел в магазин и увидел, что цена за одно яйцо составляет 5 рублей. Сколько рублей Макс должен заплатить, чтобы купить два яйца?
4.3 Аня решила купить новые карандаши для школы. Она хочет купить 12 карандашей, и каждый карандаш стоит 15 рублей. Сколько рублей Аня должна заплатить за все карандаши?
4.4 Денис хочет подарить своему другу набор красок для художественных работ. Набор стоит 500 рублей, и Денис решил купить два таких набора. Сколько рублей Денис должен заплатить за оба набора?
4.5 Лена решила купить новую книгу. Книга стоит 350 рублей. У нее уже есть 200 рублей, и она хочет узнать, сколько ей еще нужно денег, чтобы купить эту книгу.
## Задание № 5
5.1 Коля нарисовал прямоугольник со сторонами 560 мм и 320 мм. Найдите его периметр в дециметрах.
5.2 Анна изготовила прямоугольный столешницу размерами 1.2 метра в длину и 80 см в ширину. Каков будет периметр этой столешницы в дециметрах?
5.3 Дан прямоугольный бассейн с размерами 4 метра в длину и 2.5 метра в ширину. Определите периметр бассейна в дециметрах.
5.4 Максим создал картину в форме прямоугольника, длиной 90 см и шириной 60 см. Найдите периметр его картины в дециметрах.
5.5 Оля построила грядку для сада, имеющую форму прямоугольника. Её длина составляет 5.6 метра, а ширина - 2.3 метра. Чему равен периметр грядки в дециметрах?
## Задание № 6
6.1 Велосипедист крутит педали. Какой род рычага используется в механизме передачи движения от педалей к заднему колесу?
6.2 Дверь, прикрепленная к стене, открывается и закрывается. Какой род рычага применяется в механизме, который позволяет легко управлять дверью?
6.3 Автомобиль имеет рулевое управление. Какой тип рычага используется в рулевой колонке, чтобы водитель мог поворачивать автомобиль?
6.4 Кухонный блендер использует мотор для вращения ножей внутри. Какой род рычага используется в механизме передачи движения от мотора к ножам блендера?
6.5 Люди используют щипцы для захвата и поднятия предметов. Какой тип рычага присутствует в механизме щипцов, который позволяет усилить приложенную силу и сжать их?
## Задание № 7
7.1 Андрей собрал редуктор с ведущей шестерёнкой на 12 зубьев и ведомой на 60 зубьев. Между ними находится две паразитные шестерёнки: одна на 30 зубов и другая на 15 зубов. Если ведущая ось вращается со скоростью 40 оборотов в секунду, то какая будет скорость ведомой оси?
7.2 Юлия создала механизм с ведущей шестерёнкой на 10 зубьев и ведомой на 50 зубьев. Между ними установлена паразитная шестерёнка на 20 зубов. Если скорость ведущей оси составляет 60 оборотов в минуту, то какова будет скорость ведомой оси?
7.3 Петр собрал редуктор, в котором ведущая шестерёнка имеет 16 зубьев, а ведомая - 64 зуба. Между ними находится паразитная шестерёнка на 32 зуба. Если скорость ведущей оси составляет 80 оборотов в минуту, то какая будет скорость ведомой оси?
7.4 Олег собирает редуктор с ведущей шестерёнкой на 18 зубьев и ведомой на 72 зуба. Между ними устанавливается паразитная шестерёнка на 36 зубов. Если ведущая ось вращается со скоростью 90 оборотов в минуту, то какова будет скорость ведомой оси?
7.5 Мария создала механизм с ведущей шестерёнкой на 14 зубьев и ведомой на 70 зубов. Между ними находится паразитная шестерёнка на 28 зубов. Если скорость ведущей оси составляет 56 оборотов в минуту, то какая будет скорость ведомой оси?
## Задание № 8
8.1 Какая функция у современных мобильных телефонов позволяет им автоматически подстраивать яркость экрана в зависимости от окружающей освещенности?
- a. Голосовое управление
- b. Автоматическая яркость
- c. Селфи-режим
- d. Вибрационная обратная связь
8.2 Какая технология позволяет автомобилям самостоятельно удерживать полосу движения на дороге и адаптироваться к скорости других транспортных средств?
- a. Гироскопическая стабилизация
- b. Автопилот
- c. Газовый двигатель
- d. Система кондиционирования
8.3 Какая функция в современных кофемашинах автоматически подстраивает степень помола кофейных зерен в зависимости от выбранного типа кофе?
- a. Режим капучино
- b. Система самоочистки
- c. Регулировка водяной температуры
- d. Автоматический помол
8.4 Какая технология позволяет смарт-телевизорам автоматически регулировать качество изображения в зависимости от содержания и освещения в комнате?
- a. Голосовое управление
- b. Автоматическая подсветка
- c. Технология HDR
- d. Система управления звуком
8.5 Какая функция в смарт-доме позволяет автоматически управлять освещением, открывать и закрывать жалюзи в зависимости от времени суток и погодных условий?
- a. Система безопасности
- b. Умный домофон
- c. Автоматическое управление климатом
- d. Умное управление освещением
## Задание № 9
9.1 Какую функцию могут выполнять роботы в космической индустрии?
- a. Орбитальные ремонты и обслуживание
- b. Ингаляция атмосферы Марса
- c. Проектирование ракетных двигателей
- d. Гастрономическое обслуживание астронавтов
9.2 Какой тип роботов используется для исследования поверхности других планет?
- a. Дроны
- b. Археологические роботы
- c. Швейцарские ножи
- d. Роботы-уборщики
9.3 В каких операциях на МКС могут участвовать роботы?
- a. Медицинские операции
- b. Ремонт оборудования
- c. Готовка пищи
- d. Сборка космических кораблей
9.4 Для чего используются роботы-манипуляторы в космической индустрии?
- a. Для выполнения сложных монтажных и ремонтных работ
- b. Для обучения астронавтов танцевать
- c. Для выращивания растений на Марсе
- d. Для массажа астронавтов
9.5 Какие преимущества предоставляют роботы в космической исследовательской миссии?
- a. Уменьшение риска для человека
- b. Поддержка астронавтов в деловых переговорах с инопланетянами
- c. Подготовка астронавтов к конкурсу красоты
- d. Увеличение количества космических котов на борту
## Задание № 10
10.1 Робот оснащён двумя отдельно управляемыми колёсами, радиус каждого из колёс робота равен 35 мм. Левым колесом управляет мотор А, правым колесом управляет мотор Б. Робот проезжает прямолинейный участок трассы, длина которого равна 4 м 80 см. Определите, сколько оборотов совершили моторы за время проезда робота по прямолинейному участку трассы. При расчётах примите число π=3,14. В ответ запишите число оборотов, результат округлите до целого.
10.2 У робота есть два колеса, радиус каждого колеса составляет 50 мм. Левым колесом управляет мотор X, правым колесом управляет мотор Y. Робот проезжает прямолинейный участок трассы, длина которого равна 6 метрам. Определите, сколько оборотов сделали моторы за время проезда робота по данному участку. При расчетах используйте число π=3,14 и округлите результат до ближайшего целого.
10.3 Робот имеет два колеса, радиус каждого из них равен 30 мм. Левым колесом управляет мотор P, правым колесом управляет мотор Q. Робот движется по прямолинейному участку дороги, длина которого составляет 3 м 75 см. Определите, сколько оборотов выполнили моторы P и Q во время движения робота по данному участку. При расчетах используйте число π=3,14 и округлите результат до целого числа.
10.4 Двухколесный робот оснащен колесами с радиусом 25 мм. Левым колесом управляет мотор M1, правым - мотор M2. Робот движется по прямолинейному участку дороги, длина которого равна 7 метрам. Определите, сколько оборотов сделали моторы M1 и M2 во время проезда робота по данному участку. Используйте число π=3,14 и округлите ответ до ближайшего целого числа.
10.5 У робота есть два колеса с радиусом 20 мм. Левым колесом управляет мотор A, правым колесом управляет мотор B. Робот движется по прямолинейному участку трассы, длина которого составляет 6 метров и 30 сантиметров. Определите, сколько оборотов совершили моторы A и B за время проезда робота по данному участку. Используйте значение π=3,14 и округлите результат до целого числа.
## Вопрос № 12 и 13
12.1 В механизме соединены две зубчатых шестерёнки с 16 и 24 зубьями. Какое передаточное соотношение между ними?
- a. 1:1
- b. 2:1
- c. 3:2
- d. 2:3
12.2 На изображении видно две зубчатых шестерёнки с 8 и 40 зубьями. Какое передаточное соотношение соответствует этим шестерёнкам?
- a. 1:5
- b. 5:1
- c. 1:8
- d. 8:1
12.3 В механизме используются шестерёнки с 12 и 20 зубьями. Какое передаточное соотношение существует между ними?
- a. 3:2
- b. 2:3
- c. 5:3
- d. 3:5
12.4. На рисунке изображены две зубчатых шестерёнки с 24 и 36 зубьями. Какое передаточное соотношение между этими шестерёнками?
- a. 3:2
- b. 2:3
- c. 4:3
- d. 3:4
12.5. Какое передаточное соотношение зубьев существует между шестерёнками с 16 и 40 зубьями?
- a. 2:5
- b. 5:2
- c. 1:4
- d. 4:1
## Вопрос № 15
15.1 Робот-пылесос движется по квадратной комнате размером 4 метра на 4 метра. Робот начинает движение из угла комнаты и должен по очереди очистить каждую сторону комнаты, двигаясь вдоль стен. Робот имеет радиус 20 см и двигается со скоростью 0.5 м/с. Сколько времени роботу потребуется, чтобы очистить всю комнату?
15.2 Робот-курьер должен доставить посылку из точки A в точку B, которые находятся на расстоянии 2 километров друг от друга. Робот двигается со скоростью 1 м/с. Сколько времени потребуется роботу для доставки посылки?
15.3 Робот-грузовик движется по шоссе и должен проехать расстояние 300 километров. Радиус колес грузовика составляет 40 см. Сколько оборотов совершит колесо грузовика, чтобы пройти это расстояние?
15.4 Робот-садовник должен перемешать землю в саду, который имеет форму круга с радиусом 5 метров. Робот имеет рабочий инструмент, закрепленный в центре, и вращает его с угловой скоростью 2 радиана в секунду. Сколько времени потребуется роботу, чтобы перемешать всю землю в саду?
15.5 Робот-подводник должен исследовать дно океана на глубине 100 метров. Робот двигается со скоростью 0.2 м/с и начинает свое путешествие с поверхности воды. Сколько времени потребуется роботу, чтобы достичь дна океана?
# Ответы на вопросы
## Вопрос 1.
1.1 Какой тип роботов спроектирован для выполнения задач в водной среде?
- Ответ: a) Робот-подводник
1.2 Какой робот предназначен для исследования других планет?
- Ответ: c) Робот-марсоход
1.3 Какой тип роботов используется для выполнения задач в медицине?
- Ответ: b) Робот-хирург
1.4 Какие роботы обычно используются в автопроме для сборки автомобилей?
- Ответ: c) Робот-сварщик
1.5 Какие роботы могут быть использованы для доставки почты и товаров в крупных городах?
- Ответ: c) Робот-почтальон
## Вопрос № 2
2.1 - Ответ: a. Термометр
2.2 - Ответ: b. Маршрутизатор
2.3 - Ответ: b. Радио
2.4 - Ответ: c. Радар
2.5 - Ответ: b. Магнитофон
## Вопрос 3.
3.1 Какое название передачи используется для передачи движения с помощью зубчатых колес?
- Ответ: c) Зубчатая передача
3.2 Какую передачу часто используют для передачи движения велосипедов?
- Ответ: b) Цепная передача
3.3 Какая передача обычно используется для переключения скоростей в автомобилях с механической коробкой передач?
- Ответ: c) Зубчатая передача
3.4 Какой тип передачи характеризуется использованием червячного винта?
- Ответ: d) Червячная передача
3.5 Какой вид передачи применяется для передачи движения между параллельными осями с помощью зубчатых колес?
- Ответ: c) Зубчатая передача
## Вопрос № 4.
4.1 Наташа хочет сшить две одинаковые подушки, и каждая подушка требует 1.5 метра ткани. Значит, общее количество ткани, необходимое для двух подушек, равно 2 * 1.5 м = 3 метра. Ткань стоит 250 рублей за метр, поэтому Наташа должна потратить: 3 м * 250 рублей/м = 750 рублей.
- Ответ: Наташе нужно потратить 750 рублей на ткань.
4.2 Макс хочет купить два яйца, и цена за одно яйцо составляет 5 рублей. Таким образом, Макс должен заплатить: 2 яйца * 5 рублей/яйцо = 10 рублей.
- Ответ: Макс должен заплатить 10 рублей за два яйца.
4.3 Аня хочет купить 12 карандашей, и каждый карандаш стоит 15 рублей. Сумма, которую Аня должна заплатить, равна: 12 карандашей * 15 рублей/карандаш = 180 рублей.
- Ответ: Аня должна заплатить 180 рублей за все карандаши.
4.4 Денис хочет купить два набора красок, и каждый набор стоит 500 рублей. Итак, Денис должен заплатить: 2 набора * 500 рублей/набор = 1000 рублей.
- Ответ: Денис должен заплатить 1000 рублей за оба набора красок.
4.5 Лена хочет купить книгу, которая стоит 350 рублей. У нее уже есть 200 рублей. Чтобы узнать, сколько ей еще нужно денег, вычитаем сумму, которая уже есть, из стоимости книги: 350 рублей - 200 рублей = 150 рублей.
- Ответ: Лене нужно еще 150 рублей, чтобы купить эту книгу.
## Вопрос № 5
Для решения этих задач, нам нужно найти периметр каждой фигуры, а затем перевести результаты в дециметры, так как 1 метр равен 10 дециметрам.
5.1. Периметр прямоугольника с данными сторонами: Периметр = 2 * (Длина + Ширина) = 2 * (560 мм + 320 мм) = 2 * 880 мм = 1760 мм Теперь переведем в дециметры: 1760 мм / 10 = 176 дм.
5.2. Периметр прямоугольной столешницы: Длина = 1.2 м * 10 дм/м = 12 дм Ширина = 80 см / 10 дм/см = 8 дм Периметр = 2 * (Длина + Ширина) = 2 * (12 дм + 8 дм) = 2 * 20 дм = 40 дм.
5.3. Периметр прямоугольного бассейна: Длина = 4 м * 10 дм/м = 40 дм Ширина = 2.5 м * 10 дм/м = 25 дм Периметр = 2 * (Длина + Ширина) = 2 * (40 дм + 25 дм) = 2 * 65 дм = 130 дм.
5.4. Периметр картины: Длина = 90 см / 10 дм/см = 9 дм Ширина = 60 см / 10 дм/см = 6 дм Периметр = 2 * (Длина + Ширина) = 2 * (9 дм + 6 дм) = 2 * 15 дм = 30 дм.
5.5. Периметр грядки: Длина = 5.6 м * 10 дм/м = 56 дм Ширина = 2.3 м * 10 дм/м = 23 дм Периметр = 2 * (Длина + Ширина) = 2 * (56 дм + 23 дм) = 2 * 79 дм = 158 дм.
Таким образом, периметры указанных фигур в дециметрах следующие: 5.1: 176 дм 5.2: 40 дм 5.3: 130 дм 5.4: 30 дм 5.5: 158 дм.
## Вопрос № 6
6.1 В механизме передачи движения от педалей к заднему колесу велосипеда используется передаточный рычаг. Этот рычаг позволяет увеличить момент силы, применяемой к педалям, для вращения заднего колеса.
6.2 Для управления дверью, прикрепленной к стене, используется рычаг первого рода. Рычаг первого рода позволяет легко открывать и закрывать дверь, применяя небольшую силу на конце рычага, чтобы создать момент силы, достаточный для вращения двери.
6.3 В рулевой колонке автомобиля используется рычаг второго рода, также известный как рулевая тяга. Этот тип рычага позволяет водителю поворачивать автомобиль, применяя силу к рулевой руке, что приводит к вращению передних колес и изменению направления движения.
6.4 В механизме передачи движения от мотора к ножам блендера используется рычаг третьего рода. Этот рычаг позволяет преобразовывать вращающееся движение от мотора во вращение ножей, обеспечивая нужное соотношение скорости и силы.
6.5 В механизме щипцов, используемых для захвата и поднятия предметов, также присутствует рычаг третьего рода. Этот тип рычага позволяет усилить приложенную силу, что помогает сжать щипцы и удерживать предметы.
## Вопрос № 7
7.1: 400 оборотов/сек
7.2: 12.5 оборотов/сек
## Вопрос № 8
8.1 Какая функция у современных мобильных телефонов позволяет им автоматически подстраивать яркость экрана в зависимости от окружающей освещенности?
- Ответ: b. Автоматическая яркость
8.2 Какая технология позволяет автомобилям самостоятельно удерживать полосу движения на дороге и адаптироваться к скорости других транспортных средств?
- Ответ: b. Автопилот
8.3 Какая функция в современных кофемашинах автоматически подстраивает степень помола кофейных зерен в зависимости от выбранного типа кофе?
- Ответ: d. Автоматический помол
8.4 Какая технология позволяет смарт-телевизорам автоматически регулировать качество изображения в зависимости от содержания и освещения в комнате?
- Ответ: c. Технология HDR
8.5 Какая функция в смарт-доме позволяет автоматически управлять освещением, открывать и закрывать жалюзи в зависимости от времени суток и погодных условий?
- Ответ: d. Умное управление освещением
## Вопрос № 9
9.1 Какую функцию могут выполнять роботы в космической индустрии?
- Ответ: a. Орбитальные ремонты и обслуживание
9.2 Какой тип роботов используется для исследования поверхности других планет?
- Ответ: a. Дроны
9.3 В каких операциях на МКС могут участвовать роботы?
- Ответ: b. Ремонт оборудования и d. Сборка космических кораблей
9.4 Для чего используются роботы-манипуляторы в космической индустрии?
- Ответ: a. Для выполнения сложных монтажных и ремонтных работ
9.5 Какие преимущества предоставляют роботы в космической исследовательской миссии?
- Ответ: a. Уменьшение риска для человека
## Вопрос № 10
10.1: Ответ: 22 оборота.
10.2: Ответ: 19 оборота.
10.3: Ответ: 13 оборота.
10.4: Ответ: 89 оборота.
10.5: Ответ: 50 оборота.
## Вопрос № 12
Давайте рассмотрим каждую задачу по порядку и определим передаточное соотношение между зубчатыми шестерёнками.
12.1
В данной задаче у нас есть две зубчатые шестерёнки с 16 и 24 зубьями. Для определения передаточного соотношения нужно разделить количество зубьев в большей шестерёнке на количество зубьев в меньшей:
Передаточное соотношение = (Количество зубьев в большей шестерёнке) / (Количество зубьев в меньшей шестерёнке)
Передаточное соотношение = 24 / 16 = 3 / 2 = 3:2
- Ответ: c. 3:2
12.2
В данной задаче у нас есть две зубчатые шестерёнки с 8 и 40 зубьями. Снова разделим количество зубьев в большей шестерёнке на количество зубьев в меньшей:
Передаточное соотношение = (Количество зубьев в большей шестерёнке) / (Количество зубьев в меньшей шестерёнке)
Передаточное соотношение = 40 / 8 = 5 / 1 = 5:1
- Ответ: b. 5:1
12.3
В этой задаче есть две шестерёнки с 12 и 20 зубьями. Опять же, найдём передаточное соотношение:
Передаточное соотношение = (Количество зубьев в большей шестерёнке) / (Количество зубьев в меньшей шестерёнке)
Передаточное соотношение = 20 / 12 = 5 / 3 = 5:3
- Ответ: c. 5:3
12.4
Здесь у нас две шестерёнки с 24 и 36 зубьями. Рассчитаем передаточное соотношение:
Передаточное соотношение = (Количество зубьев в большей шестерёнке) / (Количество зубьев в меньшей шестерёнке)
Передаточное соотношение = 36 / 24 = 3 / 2 = 3:2
- Ответ: a. 3:2
12.5
В данной задаче есть шестерёнки с 16 и 40 зубьями. Определим передаточное соотношение:
Передаточное соотношение = (Количество зубьев в большей шестерёнке) / (Количество зубьев в меньшей шестерёнке)
Передаточное соотношение = 40 / 16 = 5 / 2 = 5:2
- Ответ: b. 5:2
## Вопрос № 15
15.1
Робот-пылесос движется по комнате размером 4 метра на 4 метра. Его скорость - 0.5 м/с. Радиус робота - 20 см (0.2 м). Робот движется вдоль стен и очищает всю комнату.
Для нахождения времени, которое ему потребуется, мы можем рассмотреть одну сторону комнаты. Расстояние, которое робот должен пройти по стене, равно 4 метра, а его скорость составляет 0.5 м/с. Используем формулу времени:
Время=РасстояниеСкоростьВремя=СкоростьРасстояние
Время=4м0.5м/с=8секундВремя=0.5м/с4м=8секунд
Роботу потребуется 8 секунд на очистку одной стороны комнаты. Поскольку ему нужно очистить 4 стороны, полное время очистки будет равно 4×8сек=32сек4×8сек=32сек.
15.2
Робот-курьер должен доставить посылку на расстояние 2 километра (2000 м). Его скорость составляет 1 м/с.
Используем формулу времени:
Время=РасстояниеСкоростьВремя=СкоростьРасстояние
Время=2000м1м/с=2000секундВремя=1м/с2000м=2000секунд
Чтобы получить время в минутах, разделим на 60:
Время=2000сек60≈33.33минВремя=602000сек≈33.33мин
Роботу потребуется примерно 33.33 минуты для доставки посылки.
15.3
Робот-грузовик должен проехать 300 километров (300,000 м). Радиус колес грузовика составляет 40 см (0.4 м). Чтобы узнать, сколько оборотов совершит колесо, чтобы пройти это расстояние, мы можем использовать формулу:
Обороты=РасстояниеДлина_окружностиОбороты=Длина_окружностиРасстояние
Длина окружности колеса вычисляется как 2π×Радиус2π×Радиус:
Длина_окружности=2π×0.4м≈2.51мДлина_окружности=2π×0.4м≈2.51м
Теперь мы можем найти количество оборотов:
Обороты=300,000м2.51м/оборот≈119,522.91Обороты=2.51м/оборот300,000м≈119,522.91
Колесо грузовика совершит около 119,523 оборотов.
15.4
Робот-садовник перемешивает землю в саду радиусом 5 метров. Угловая скорость вращения его инструмента составляет 2 радиана в секунду. Чтобы перемешать всю землю в саду, робот должен сделать полный оборот вокруг центра сада.
Для нахождения времени, которое потребуется роботу, мы можем использовать следующую формулу:
Время=УголУгловая_скоростьВремя=Угловая_скоростьУгол
Полный угол (360 градусов) в радианах составляет 2π2π радиана. Теперь мы можем вычислить время:
Время=2πрад2рад/с=πсекундВремя=2рад/с2πрад=πсекунд
Роботу потребуется πсекундπсекунд или примерно 3.14 секунды, чтобы перемешать всю землю в саду.
15.5
Робот-подводник движется на глубину 100 метров. Его скорость - 0.2 м/с.
Используем формулу времени:
Время=ГлубинаСкоростьВремя=СкоростьГлубина
Время=100м0.2м/с=500секундВремя=0.2м/с100м=500секунд
Чтобы получить время в минутах, разделим на 60:
Время=500сек60=8.33минВремя=60500сек=8.33мин
Роботу потребуется примерно 8.33 минуты, чтобы достичь дна океана на глубине 100 метров.

@ -0,0 +1,170 @@
import {
defineDocumentType,
makeSource,
type ComputedFields,
} from 'contentlayer/source-files';
import { s } from 'hastscript';
import rehypeAutolinkHeadings, {
type Options as AutolinkOptions,
} from 'rehype-autolink-headings';
import rehypePrettyCode, {
type Options as PrettyCodeOptions,
} from 'rehype-pretty-code';
import rehypeSlug from 'rehype-slug';
import remarkGfm from 'remark-gfm';
import { blogConfig } from './config';
const computedFields: ComputedFields = {
slug: {
type: 'string',
description: 'The slug of the post, e.g. my-topic/my-post',
resolve: (doc) => doc._raw.flattenedPath.split('/').slice(1).join('/'),
},
};
export const Post = defineDocumentType(() => ({
name: 'Post',
filePathPattern: `posts/**/*.mdx`,
contentType: 'mdx',
fields: {
title: {
type: 'string',
description: 'The title of the post',
required: true,
},
date: {
type: 'date',
description: 'When the post was published',
required: true,
},
excerpt: {
type: 'string',
description: 'Short summary of the post',
required: true,
},
tags: {
type: 'list',
of: { type: 'string' },
description: 'A list of keywords that relate to the post',
required: true,
},
},
computedFields: {
url: {
type: 'string',
description: 'The URL of the post, e.g. /posts/my-post',
resolve: (post) => `/${post._raw.flattenedPath}`,
},
...computedFields,
},
}));
export const Page = defineDocumentType(() => ({
name: 'Page',
filePathPattern: `pages/**/*.mdx`,
contentType: 'mdx',
fields: {
title: {
type: 'string',
description: 'The title of the page',
required: true,
},
description: {
type: 'string',
description: 'The description of the page',
required: true,
},
},
computedFields: {
url: {
type: 'string',
description: 'The URL of the page, e.g. /about',
resolve: (post) =>
`/${post._raw.flattenedPath.split('/').slice(1).join('/')}`,
},
...computedFields,
},
}));
export default makeSource({
contentDirPath: './content',
documentTypes: [Post, Page],
mdx: {
remarkPlugins: [
/**
* Adds support for GitHub Flavored Markdown
*/
remarkGfm,
],
rehypePlugins: [
/**
* Adds ids to headings
*/
rehypeSlug,
[
/**
* Adds auto-linking button after h1, h2, h3 headings
*/
rehypeAutolinkHeadings,
{
behavior: 'append',
test: ['h1', 'h2', 'h3'],
content: s(
'svg',
{
xmlns: 'http://www.w3.org/2000/svg',
viewBox: '0 0 24 24',
width: '24',
height: '24',
fill: 'none',
stroke: 'currentColor',
'stroke-width': '2',
'stroke-linecap': 'round',
'stroke-linejoin': 'round',
'aria-label': 'Anchor link',
},
[
s('line', { x1: '4', y1: '9', x2: '20', y2: '9' }),
s('line', { x1: '4', y1: '15', x2: '20', y2: '15' }),
s('line', { x1: '10', y1: '3', x2: '8', y2: '21' }),
s('line', { x1: '16', y1: '3', x2: '14', y2: '21' }),
],
),
} satisfies Partial<AutolinkOptions>,
],
[
/**
* Enhances code blocks with syntax highlighting, line numbers,
* titles, and allows highlighting specific lines and words
*/
rehypePrettyCode,
{
theme: blogConfig.theme?.codeBlockTheme || {
light: 'github-light',
dark: 'github-dark',
},
onVisitLine(node) {
// Prevent lines from collapsing in `display: grid` mode, and
// allow empty lines to be copy/pasted
if (node.children.length === 0) {
node.children = [{ type: 'text', value: ' ' }];
}
},
onVisitHighlightedLine(node) {
node.properties.className.push('highlighted');
},
onVisitHighlightedWord(node) {
node.properties.className = ['word'];
},
tokensMap: {
fn: 'entity.name',
type: 'entity.name',
prop: 'entity.name',
const: 'variable.other.constant',
},
} satisfies Partial<PrettyCodeOptions>,
],
],
},
});

@ -0,0 +1,59 @@
import { describe, expect, it } from 'vitest';
import { formatDateTime } from './datetime';
describe('formatDateTime', () => {
it('formats the date to a string', () => {
expect(formatDateTime('2022-01-01').asString).toBe('January 1, 2022');
});
it('formats the date to relative time', () => {
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
expect(formatDateTime(yesterday.toISOString()).asRelativeTimeString).toBe(
'yesterday',
);
const threeWeeksAgo = new Date();
threeWeeksAgo.setDate(threeWeeksAgo.getDate() - 21);
expect(
formatDateTime(threeWeeksAgo.toISOString()).asRelativeTimeString,
).toBe('3 weeks ago');
});
it('format the date to an ISO string', () => {
let dateTime = formatDateTime('2022-01-01');
expect(dateTime.asISOString).toBe('2022-01-01T00:00:00.000Z');
expect(formatDateTime('2022-12-31').asISOString).toBe(
'2022-12-31T00:00:00.000Z',
);
});
it('should return whether date is fresh', () => {
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
expect(formatDateTime(yesterday.toISOString()).isFresh).toBe(true);
const fourDaysAgo = new Date();
fourDaysAgo.setDate(fourDaysAgo.getDate() - 4);
expect(formatDateTime(fourDaysAgo.toISOString()).isFresh).toBe(false);
const threeWeeksAgo = new Date();
threeWeeksAgo.setDate(threeWeeksAgo.getDate() - 21);
expect(formatDateTime(threeWeeksAgo.toISOString()).isFresh).toBe(false);
});
it('should handle invalid date strings', () => {
const invalidDateResult = {
asString: 'Invalid Date',
asISOString: 'Invalid Date',
asRelativeTimeString: 'Invalid Date',
isFresh: false,
};
expect(formatDateTime('2022-13-01')).toEqual(invalidDateResult);
expect(formatDateTime('foo')).toEqual(invalidDateResult);
expect(formatDateTime('')).toEqual(invalidDateResult);
});
});

@ -0,0 +1,107 @@
type DateTime = {
/** The date formatted as a string
* @example
* 'January 1, 2022'
* 'December 31, 2022'
*/
asString: string;
/** The date formatted as an ISO string
* @example
* '2022-01-01T00:00:00.000Z'
*/
asISOString: string;
/** The date formatted as a relative time string
* @example
* '2 days ago'
* '3 weeks ago'
*/
asRelativeTimeString: string;
/** A boolean indicating if the date is fresh, i.e. less than 4 days old */
isFresh: boolean;
};
const dateFormatter = new Intl.DateTimeFormat('en-US', {
month: 'long',
day: 'numeric',
year: 'numeric',
});
const relativeTimeFormatter = new Intl.RelativeTimeFormat('en-US', {
numeric: 'auto',
});
/**
* Formats a date string into a {@link DateTime} object.
* @example
* const dateTime = formatDateTime('2022-01-01');
* // dateTime = {
* // asString: 'January 1, 2022',
* // asISOString: '2022-01-01T00:00:00.000Z',
* // asRelativeTimeString: '2 days ago',
* // isFresh: true
* // }
*/
export function formatDateTime(dateString: string): DateTime {
const date = new Date(dateString);
if (isNaN(date.getTime())) {
return {
asString: 'Invalid Date',
asISOString: 'Invalid Date',
asRelativeTimeString: 'Invalid Date',
isFresh: false,
};
}
const { relativeTime, isFresh } = getRelativeTime(date);
return {
asString: dateFormatter.format(date),
asISOString: date.toISOString(),
asRelativeTimeString: relativeTime,
isFresh,
};
}
/**
* Formats a date into a relative time.
* @returns The relative time string and a boolean indicating if the date is fresh,
* i.e. less than 4 days old
* @example
* const { relativeTime, isFresh } = getRelativeTime(someDate);
* // relativeTime = '3 weeks ago'
* // isFresh = false
*/
function getRelativeTime(date: Date) {
const timeDiff = date.getTime() - new Date().getTime();
const days = Math.ceil(timeDiff / (1000 * 60 * 60 * 24));
if (days > -14) {
return {
relativeTime: relativeTimeFormatter.format(days, 'day'),
isFresh: days > -4,
};
}
const weeks = Math.floor(days / 7);
if (weeks > -8) {
return {
relativeTime: relativeTimeFormatter.format(weeks, 'week'),
isFresh: false,
};
}
const months = Math.floor(days / 30);
if (months > -12) {
return {
relativeTime: relativeTimeFormatter.format(months, 'month'),
isFresh: false,
};
}
const years = Math.floor(days / 365);
return {
relativeTime: relativeTimeFormatter.format(years, 'year'),
isFresh: false,
};
}

@ -0,0 +1,36 @@
import { type Post } from 'contentlayer/generated';
import { describe, expect, it } from 'vitest';
import { getTagsWithCount, searchPosts } from './search';
const posts = Array.from({ length: 10 }, (_, index) => ({
title: `Post ${index + 1}`,
tags: ['tag', `tag${index + 1}`],
excerpt: `Post ${index + 1} excerpt`,
date: '2022-01-01',
slug: `post-${index + 1}`,
url: `post-${index + 1}`,
body: {
raw: `Post ${index + 1} body`,
},
})) as Post[];
describe('searchPosts', () => {
it('returns the posts that best match the query', () => {
expect(searchPosts('Post 7', posts)[0].title).toBe('Post 7');
expect(searchPosts('tag4', posts)[0].title).toBe('Post 4');
});
it('returns no posts if the query is empty', () => {
expect(searchPosts('', posts)).toHaveLength(0);
});
});
describe('getTagsWithCount', () => {
it('returns the tags with their count', () => {
const tagsWithCount = getTagsWithCount(posts);
expect(tagsWithCount).toContainEqual(['tag', 10]);
expect(tagsWithCount).toContainEqual(['tag1', 1]);
expect(tagsWithCount).toContainEqual(['tag10', 1]);
});
});

@ -0,0 +1,67 @@
import { type Post } from 'contentlayer/generated';
/**
* Search for a query in a text.
* @returns A boolean indicating whether the query was found in the text.
*/
function searchHit(query: string, text: string) {
return text.toLowerCase().includes(query.toLowerCase());
}
/**
* Search for a query in a list of posts.
* @returns The posts that matched the query in descending order of relevance.
*/
export function searchPosts(query: string, posts: Array<Post>) {
const postsWithSearchHits = new Map<Post, number>();
posts.forEach((post) => {
if (!query) return;
const {
title,
excerpt,
tags,
body: { raw },
} = post;
let searchHits = 0;
if (tags.some((tag) => searchHit(query, tag))) {
searchHits += 10; // give tag hits heavy weight
}
if (searchHit(query, title)) {
searchHits += 10; // give title hits heavy weight
}
if (searchHit(query, excerpt)) {
searchHits += 5; // give excerpt hits lighter weight
}
if (searchHit(query, raw)) {
searchHits++; // give content hits lightest weight
}
if (searchHits > 0) {
postsWithSearchHits.set(post, searchHits);
}
});
return Array.from(postsWithSearchHits.entries())
.sort(([, a], [, b]) => b - a)
.map(([post]) => post);
}
/**
* Get all tags with their count
* @returns The tags with their count in descending order of count
*/
export function getTagsWithCount(posts: Array<Post>) {
const tagsWithCount = new Map<string, number>();
posts.forEach((post) => {
post.tags.forEach((tag) => {
tagsWithCount.set(tag, (tagsWithCount.get(tag) ?? 0) + 1);
});
});
return Array.from(tagsWithCount.entries()).sort(([, a], [, b]) => b - a);
}

@ -0,0 +1,7 @@
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';
/** Utility function to merge Tailwind classes with clsx and tailwind-merge */
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}

@ -0,0 +1,7 @@
const { blogConfig } = require('./config/index.js');
/** @type {import('next-sitemap').IConfig} */
module.exports = {
siteUrl: blogConfig.url,
generateRobotsTxt: true,
};

@ -0,0 +1,10 @@
const { withContentlayer } = require('next-contentlayer');
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
appDir: true,
},
};
module.exports = withContentlayer(nextConfig);

23542
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -0,0 +1,86 @@
{
"name": "blog",
"version": "1.0.0",
"scripts": {
"dev": "next dev",
"test": "npx vitest",
"build": "next build",
"postbuild": "next-sitemap",
"start": "next start",
"style:lint": "next lint",
"style:prettier": "prettier --check '**/*.{js,jsx,ts,tsx}'",
"style:all": "pnpm run style:lint && pnpm run style:prettier",
"format": "prettier --write '**/*.{js,jsx,ts,tsx}'",
"storybook": "storybook dev -p 6006",
"build-storybook": "npx contentlayer build && storybook build"
},
"dependencies": {
"@giscus/react": "^2.2.8",
"@vercel/analytics": "^1.0.0",
"@vercel/og": "^0.5.3",
"@vkontakte/icons": "^2.64.0",
"clsx": "^1.2.1",
"contentlayer": "^0.3.1",
"date-fns": "^2.29.3",
"eslint": "8.39.0",
"eslint-config-next": "13.3.1",
"grapheme-splitter": "^1.0.4",
"hastscript": "^7.2.0",
"lucide-react": "^0.176.0",
"next": "13.3.1",
"next-contentlayer": "^0.3.1",
"next-sitemap": "^4.0.7",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-tooltip": "5.11.1",
"react-typist-component": "^1.0.5",
"react-wrap-balancer": "^0.4.0",
"rehype-autolink-headings": "^6.1.1",
"rehype-pretty-code": "^0.9.5",
"rehype-slug": "^5.1.0",
"remark-gfm": "^3.0.1",
"shiki": "^0.14.1",
"tailwind-merge": "^1.12.0",
"unified": "^10.1.2",
"vitest": "^0.30.1",
"zustand": "^4.3.7"
},
"devDependencies": {
"@babel/core": "^7.21.4",
"@ianvs/prettier-plugin-sort-imports": "^3.7.2",
"@next/env": "^13.3.1",
"@storybook/addon-essentials": "^7.0.6",
"@storybook/addon-interactions": "^7.0.6",
"@storybook/addon-links": "^7.0.6",
"@storybook/addons": "^7.0.6",
"@storybook/api": "^7.0.6",
"@storybook/blocks": "^7.0.6",
"@storybook/components": "^7.0.6",
"@storybook/core-events": "^7.0.6",
"@storybook/nextjs": "^7.0.6",
"@storybook/react": "^7.0.6",
"@storybook/testing-library": "^0.1.0",
"@storybook/theming": "^7.0.6",
"@storybook/types": "^7.0.6",
"@tailwindcss/typography": "^0.5.9",
"@types/node": "18.16.0",
"@types/react": "18.0.38",
"@types/react-dom": "18.0.11",
"autoprefixer": "^10.4.14",
"css-loader": "^6.7.3",
"eslint-plugin-storybook": "^0.6.11",
"eslint-plugin-tailwindcss": "^3.11.0",
"postcss": "^8.4.23",
"postcss-loader": "^7.2.4",
"prettier": "2.8.8",
"prettier-plugin-tailwindcss": "^0.2.7",
"prop-types": "^15.8.1",
"storybook": "^7.0.6",
"storybook-tailwind-dark-mode": "^1.0.22",
"style-loader": "^3.3.2",
"tailwindcss": "^3.3.1",
"ts-node": "^10.9.1",
"typescript": "^5.0.4",
"webpack": "^5.80.0"
}
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

@ -0,0 +1,38 @@
/** @type {import("@ianvs/prettier-plugin-sort-imports").PrettierConfig} */
module.exports = {
plugins: [
'@ianvs/prettier-plugin-sort-imports',
'prettier-plugin-tailwindcss',
],
arrowParens: 'always',
bracketSpacing: true,
endOfLine: 'lf',
bracketSameLine: false,
jsxSingleQuote: false,
printWidth: 80,
proseWrap: 'preserve',
quoteProps: 'as-needed',
semi: true,
singleQuote: true,
tabWidth: 2,
trailingComma: 'all',
useTabs: false,
importOrder: [
'^(react/(.*)$)|^(react$)',
'^(next/(.*)$)|^(next$)',
'^(contentlayer/generated$)',
'<THIRD_PARTY_MODULES>',
'',
'^@/stores/(.*)$',
'^@/config$',
'^@/components/(.*)$',
'^@/lib/(.*)$',
'^[./]',
],
importOrderSeparation: false,
importOrderSortSpecifiers: true,
importOrderBuiltinModulesToTop: true,
importOrderParserPlugins: ['typescript', 'jsx'],
importOrderMergeDuplicateImports: true,
importOrderCombineTypeAndValueImports: true,
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 920 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 319 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 429 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 252 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save