diff --git a/.env.local.encrypted b/.env.local.encrypted index e049434..8d18f72 100644 --- a/.env.local.encrypted +++ b/.env.local.encrypted @@ -1 +1 @@ -e3db010b2ba38157507757a646313d99-640e1634a1f60c6350f00f10d340ec04b8beb40d8e6a22a09860359d10117e46ea6b536040d1b45f0eb2c2029a72ce3dc9179be447146f8b1b5ee1cc3e5ca0bcb75eafd814f3d768fed262afd7408104f76b7bec08c514a0c5da74001a497da831f7221be5c9b08981bbe0083036c8f408fa93d135f8e706164578a5c9cc82d4474f8577a35b78e176e8e34d8ab027b3525a4e78f71b160e7697a3937ddfe91458acafbe8761c0fa2c2e8198007a24e7fc0ab42fb66f12a308e357d31cd9c5ba285bb08dbaf1d65927054b053f14413f77f40e51f232cafc4343feba85175e3f +69773f4ab32c8c448b11bdc54da2c4fd-2e3474c171b89de539c0c8e0054a9ca3da02669bc7434f394b258b30d3c1606e42042b913ace46cc060b8523dea43bace50168c188d8706efeb80d53b3a71eaea7e10a79b00a83906716b0d31e9b3e81b0fc1c8debf43496343ced9c434cc927e10849fde0143106c7f2aa4b91eb619a616903a460f9061c8a673f7ce45b67af5ee3c721a0f62942cabfcd51452725ae553cc0f46cae4208437cdcb661357fda4cc9760096fd6b19d30b30aa45cd35eb4b9ac8520643d6f565ae87e8fa091558 diff --git a/.env.production.encrypted b/.env.production.encrypted index 2dce211..a896e55 100644 --- a/.env.production.encrypted +++ b/.env.production.encrypted @@ -1 +1 @@ -92c4a7b0062523ddfbb3d9b5422887c8-9e2335b9db88981c443d0d1e1951d17accf92f4f66c195f6c77282a4c008edd20238a1bd26f22f1ef56f025e9c5a785bc14f02bbda865b0d313b3cd87d6ac46650ae1291eed16c691d8a2c5fe64f5c337a531c14eaee0a14d59de341448c05f97b806df66893757f6967fdc2fe2f0fa8911b22add5fa13464292483d0d15fe8916412dae0d52e3ddb854f7415ceac7687562e8ee3747da3b4cf4f469aca1f62a00bc409593036bbc8cee287102d9928f725395da13e74964038c04e95fe89c7b783203eb39268a95b00ca73218800c8a061c0e9f2e296eda48cb8c47f8fdb89e2a6e514a6b27ec9e3c718efcb88485b9 +ecbba554e0136e4a9e454a4a8c9832e0-83e61ff988e7ead98f69747ce662f13b74f4ae1308cf723160bd747b3d318c5c9a0630fa96c23726162b624621abf9868b2e4dcbcd5af8c172d76deada94479ed49ea04b02ee87c8f779df09f7e1591dfaaf9b15c349aad8b1f80fafb125ffc39d7468eac5dcec69e79e8ab686da6db44b84b960eb40da147d95897bb25942e639ea9e32752c83bb21d161293d0c34fe52168c703024de24f6ffb0cbcab3cc930e823bb6e02fa25cd19d156e5c0d5b299dec4c861ef74f800699158deb756abe diff --git a/next.config.mjs b/next.config.mjs index 35c1b3f..ddde9e7 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -10,6 +10,8 @@ const nextConfig = { { hostname: 'lh3.googleusercontent.com' }, { hostname: '*.optimisticoder.com' }, { hostname: 'api.dicebear.com' }, + { hostname: 'raw.githubusercontent.com' }, + { hostname: 'github.com' }, ], }, }; diff --git a/scripts/prebuild.ts b/scripts/prebuild.ts index 9adc496..197ac82 100644 --- a/scripts/prebuild.ts +++ b/scripts/prebuild.ts @@ -1,196 +1,9 @@ -import { execSync } from 'child_process'; import 'dotenv/config'; import fs from 'fs-extra'; -import path from 'path'; -import matter from 'gray-matter'; -import type { MetadataRoute } from 'next'; -import type { CategoryIndexEntry, StoriesIndexEntry } from '@/types/common'; -import readingTime from 'reading-time'; -import { APP_ENV } from '@/config/common'; -import { simpleGit } from 'simple-git'; - -const storiesDir = '_stories'; -const storiesIndexerPath = path.join( - process.cwd(), - storiesDir, - 'index-stories.json', -); -const storiesMetaPath = path.join(process.cwd(), storiesDir, 'index-meta.json'); - -const generatedSitemapFilePath = path.join( - process.cwd(), - 'src', - 'generated-sitemap.json', -); - -async function linkAssets() { - if (!fs.pathExistsSync(`./public/assets`)) { - try { - fs.symlinkSync(path.join(process.cwd(), storiesDir), './public/assets'); - console.info('Successfuly link stories folder.'); - } catch (error) { - console.error((error as Error).message); - } - } else { - console.info('No link necessary to the stories folder.'); - } -} - -async function prepareStoriesFolder() { - const STORIES_KEY = process.env.STORIES_KEY; - const STORIES_REPO = process.env.STORIES_REPO; - const gitUrl = `https://oauth2:${STORIES_KEY}@github.com/${STORIES_REPO}.git`; - if ( - !fs.pathExistsSync(`./${storiesDir}`) || - fs.readdirSync(`./${storiesDir}`).length === 0 - ) { - try { - console.info("Stories folder doesn't exist, cloning..."); - execSync(`git clone ${gitUrl} ${storiesDir}`); - if (APP_ENV !== 'production') { - execSync(` - cd ${storiesDir} && - git checkout test`); - simpleGit(storiesDir).checkout('test'); - console.info( - 'Environment is not for production, checked in to test branch instead.', - ); - } - console.info('Successfuly cloning stories folder.'); - } catch (error) { - console.error('Failed to clone the repository.'); - } - return; - } - try { - console.info('Stories folder already exist, syncing...'); - execSync(` - cd ${storiesDir} && - git fetch origin && - git reset --hard origin/main && - `); - if (APP_ENV !== 'production') { - execSync(` - cd ${storiesDir} && - git checkout test`); - simpleGit(storiesDir).checkout('test'); - console.info( - 'Environment is not for production, checked in to test branch instead.', - ); - } - simpleGit(storiesDir).pull(); - console.info('Successfuly synced stories folder.'); - } catch (error) { - console.error('Failed to sync the repository.'); - } - return; -} - -async function getFiles(dir: string, depth: number = 0): Promise { - const subdirs = await fs.readdir(dir); - const files = await Promise.all( - subdirs.map(async (subdir) => { - const res = path.resolve(dir, subdir); - const stat = await fs.stat(res); - - // Skip .git, node_modules, and all first-level files - if ( - subdir === '.git' || - subdir === 'node_modules' || - (depth === 0 && stat.isFile()) - ) { - return []; - } - - // Recursively get files from directories, filter out non-page.md files - if (stat.isDirectory()) { - return getFiles(res, depth + 1); - } else if (path.basename(res) === 'page.md') { - return [res]; - } else { - return []; - } - }), - ); - - return files.reduce((a, f) => a.concat(f), []); -} - -async function indexFiles() { - const files = await getFiles(storiesDir); - const storiesIndex: StoriesIndexEntry[] = []; - const categoriesIndex: CategoryIndexEntry = []; - - for (const file of files) { - if (path.basename(file) === 'page.md') { - const content = await fs.readFile(file, 'utf8'); - const { data } = matter(content); - const relativePath = path.relative(storiesDir, file); - const [category, slug] = relativePath.split(path.sep); - - storiesIndex.push({ - title: data.title, - slug: slug, - category: category, - subtitle: data.subtitle, - keywords: data.keywords, - excerpt: data.excerpt, - cover: data.cover, - date: data.date, - readTime: readingTime(content).text, - }); - if (!categoriesIndex.includes(category)) { - categoriesIndex.push(category); - } - } - } - // Sort the index by date - storiesIndex.sort( - (a, b) => new Date(b.date).getTime() - new Date(a.date).getTime(), - ); - - fs.writeFileSync( - storiesMetaPath, - JSON.stringify( - { - totalStories: storiesIndex.length, - categories: categoriesIndex, - }, - null, - 2, - ), - ); - fs.writeFileSync(storiesIndexerPath, JSON.stringify(storiesIndex, null, 2)); - console.info('Indexing complete. Check index file for results.'); -} - -async function generateSitemapXml() { - const sitemapEntries: MetadataRoute.Sitemap = []; - const storiesIndexes: StoriesIndexEntry[] = JSON.parse( - fs.readFileSync(storiesIndexerPath).toString(), - ); - storiesIndexes.forEach((item) => { - sitemapEntries.push({ - url: `https://optimisticoder.com/stories/${item.slug}`, - lastModified: item.date, - changeFrequency: 'weekly', - priority: 0.5, - }); - }); - fs.writeFileSync( - generatedSitemapFilePath, - JSON.stringify(sitemapEntries, null, 2), - ); - console.info('Sitemap successfully generated.'); -} async function main() { const APP_ENV = process.env.NEXT_PUBLIC_APP_ENV ?? 'local'; try { - await prepareStoriesFolder(); - await indexFiles(); - await linkAssets(); - await generateSitemapXml(); if (APP_ENV === 'local') { return; } diff --git a/src/app/components.tsx b/src/app/components.tsx index edd48f9..a61d798 100644 --- a/src/app/components.tsx +++ b/src/app/components.tsx @@ -163,10 +163,14 @@ function MostHighlightedStory({ story }: { story?: StoriesIndexEntry }) { className="px-8 md:px-32 py-16" >
- + { try { - const { content: markdown, data } = matter( - readFileSync( - path.join(process.cwd(), './src', 'markdowns', 'disclaimer.md'), - 'utf8', - ), - ); + const res = (await sfetch(`${STORIES_URL}/docs/disclaimer.md`, { + next: { + revalidate: 604800, + }, + })) as PlainTextResponse; + if (!res.ok) { + throw new Error((await res.json()).message); + } + + const { content: markdown, data } = matter(await res.text()); return { content: markdown.toString(), title: data.title, subtitle: `Updated ${new Date(data.date).toLocaleDateString('en-US', { year: 'numeric', - month: 'short', + month: 'long', day: 'numeric', })}.`, }; } catch (error) { - notFound(); + return notFound(); } }); diff --git a/src/app/page.tsx b/src/app/page.tsx index c4bd505..3cb1eea 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -4,19 +4,26 @@ import { LatestStories, MostHighlightedStory, } from '@/app/components'; -import { STORIES_DIR } from '@/config/common'; +import { STORIES_URL } from '@/config/common'; +import { sfetch } from '@/helpers/common'; import type { StoriesIndexEntry } from '@/types/common'; -import { readFileSync } from 'fs'; +import type { CommonResponse } from '@/types/responses'; import jsonata from 'jsonata'; -import path from 'path'; async function listFiveLatestStories() { try { - const indexStories: StoriesIndexEntry[] = JSON.parse( - readFileSync( - path.join(process.cwd(), STORIES_DIR, 'index-stories.json'), - ).toString(), - ); + const res = (await sfetch(`${STORIES_URL}/stories/stories.json`, { + next: { + revalidate: 86400, + tags: ['stories'], + }, + })) as CommonResponse; + if (!res.ok) { + throw new Error((await res.json()).message); + } + const indexStories = (await res.json()).map((story) => { + return { ...story, cover: `${STORIES_URL}/stories/${story.cover}` }; + }); const expression = jsonata(`$[]`); const result: StoriesIndexEntry[] = await expression.evaluate(indexStories); return result.slice(0, 5); diff --git a/src/app/privacy-policy/page.tsx b/src/app/privacy-policy/page.tsx index a45f2c0..62e9acf 100644 --- a/src/app/privacy-policy/page.tsx +++ b/src/app/privacy-policy/page.tsx @@ -1,10 +1,11 @@ export const revalidate = 604800; import { Breadcrumbs, MarkdownUI, StoryTitle } from '@/components/common'; -import { readFileSync } from 'fs'; +import { STORIES_URL } from '@/config/common'; +import { sfetch } from '@/helpers/common'; +import type { PlainTextResponse } from '@/types/responses'; import matter from 'gray-matter'; import type { Metadata, ResolvingMetadata } from 'next'; import { notFound } from 'next/navigation'; -import path from 'path'; import { cache } from 'react'; type Props = { @@ -13,23 +14,27 @@ type Props = { const getPrivacyPolicy = cache(async () => { try { - const { content: markdown, data } = matter( - readFileSync( - path.join(process.cwd(), './src', 'markdowns', 'privacy-policy.md'), - 'utf8', - ), - ); + const res = (await sfetch(`${STORIES_URL}/docs/privacy-policy.md`, { + next: { + revalidate: 604800, + }, + })) as PlainTextResponse; + if (!res.ok) { + throw new Error((await res.json()).message); + } + + const { content: markdown, data } = matter(await res.text()); return { content: markdown.toString(), title: data.title, subtitle: `Updated ${new Date(data.date).toLocaleDateString('en-US', { year: 'numeric', - month: 'short', + month: 'long', day: 'numeric', })}.`, }; } catch (error) { - notFound(); + return notFound(); } }); diff --git a/src/app/sitemap.ts b/src/app/sitemap.ts index 626d4e1..4b3df12 100644 --- a/src/app/sitemap.ts +++ b/src/app/sitemap.ts @@ -1,6 +1,28 @@ +import { STORIES_URL } from '@/config/common'; +import { sfetch } from '@/helpers/common'; +import type { CommonResponse } from '@/types/responses'; import { type MetadataRoute } from 'next'; -import generatedSitemap from '@/generated-sitemap.json'; -export default function sitemap(): MetadataRoute.Sitemap { +import { cache } from 'react'; + +const getSitemap = cache(async () => { + try { + const res = (await sfetch(`${STORIES_URL}/stories/sitemap.json`, { + next: { + revalidate: 86400, + }, + })) as CommonResponse; + if (!res.ok) { + throw new Error((await res.json()).message); + } + return await res.json(); + } catch (error) { + return []; + } +}); + +export default async function sitemap(): Promise { + const remoteSitemap = await getSitemap(); + return [ { url: 'https://optimisticoder.com', @@ -14,7 +36,7 @@ export default function sitemap(): MetadataRoute.Sitemap { changeFrequency: 'weekly', priority: 0.8, }, - ...(generatedSitemap as MetadataRoute.Sitemap), + ...remoteSitemap, { url: 'https://optimisticoder.com/privacy-policy', lastModified: new Date(), diff --git a/src/app/stories/[slug]/page.tsx b/src/app/stories/[slug]/page.tsx index 550fa25..c9db23b 100644 --- a/src/app/stories/[slug]/page.tsx +++ b/src/app/stories/[slug]/page.tsx @@ -2,47 +2,63 @@ export const revalidate = 86400; import { ShareButtons, WriterDisplay } from '@/app/stories/[slug]/components'; import { Breadcrumbs, MarkdownUI, StoryTitle } from '@/components/common'; import { StoryCard } from '@/components/common/story-card'; -import { APP_URL, STORIES_DIR } from '@/config/common'; +import { APP_URL, STORIES_URL } from '@/config/common'; +import { sfetch } from '@/helpers/common'; import type { StoriesIndexEntry } from '@/types/common'; -import { readFileSync } from 'fs'; +import type { CommonResponse, PlainTextResponse } from '@/types/responses'; import matter from 'gray-matter'; import jsonata from 'jsonata'; import type { Metadata } from 'next'; import Link from 'next/link'; import { notFound } from 'next/navigation'; -import path from 'path'; import { cache } from 'react'; type Props = { params: { slug: string }; }; +const listStories = cache(async (): Promise => { + try { + const res = (await sfetch(`${STORIES_URL}/stories/stories.json`, { + next: { + revalidate: 86400, + tags: ['stories'], + }, + })) as CommonResponse; + if (!res.ok) { + throw new Error((await res.json()).message); + } + return (await res.json()).map((story) => { + return { ...story, cover: `${STORIES_URL}/stories/${story.cover}` }; + }); + } catch (error) { + return []; + } +}); + const getStoryBySlug = cache(async (slug: string) => { try { - const indexStories: StoriesIndexEntry[] = JSON.parse( - readFileSync( - path.join(process.cwd(), STORIES_DIR, 'index-stories.json'), - ).toString(), - ); + const indexStories = await listStories(); const expression = jsonata(`$[slug='${slug}']`); const result: StoriesIndexEntry = await expression.evaluate(indexStories); - const { content: markdown } = matter( - readFileSync( - path.join( - process.cwd(), - STORIES_DIR, - result.category, - result.slug, - 'page.md', - ), - 'utf8', - ), - ); + const res = (await sfetch( + `${STORIES_URL}/stories/${result.category}/${result.slug}/page.md`, + { + next: { + revalidate: 86400, + }, + }, + )) as PlainTextResponse; + if (!res.ok) { + throw new Error((await res.json()).message); + } + const { content: markdown } = matter(await res.text()); + return { content: markdown .toString() - .replace(/!\[([^\]]*)\]\((?!\/assets\/)([^)]+)\)/g, (_, p1, p2) => { - return `![${p1}](/assets/${result.category}/${result.slug}/${p2})`; + .replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (_, p1, p2) => { + return `![${p1}](${STORIES_URL}/stories/${result.category}/${result.slug}/${p2})`; }), ...result, }; @@ -53,11 +69,7 @@ const getStoryBySlug = cache(async (slug: string) => { async function listRandomStories() { try { - const indexStories: StoriesIndexEntry[] = JSON.parse( - readFileSync( - path.join(process.cwd(), STORIES_DIR, 'index-stories.json'), - ).toString(), - ); + const indexStories: StoriesIndexEntry[] = await listStories(); const expression = jsonata(`$shuffle($[])`); const result: StoriesIndexEntry[] = await expression.evaluate(indexStories); return result.slice(0, 4); diff --git a/src/app/stories/sections.tsx b/src/app/stories/sections.tsx index 04d31e4..dc7652c 100644 --- a/src/app/stories/sections.tsx +++ b/src/app/stories/sections.tsx @@ -1,24 +1,29 @@ import { FilteringHandler } from '@/app/stories/components'; import { StoryCard } from '@/components/common/story-card'; -import { STORIES_DIR } from '@/config/common'; +import { STORIES_URL } from '@/config/common'; +import { sfetch } from '@/helpers/common'; import type { StoriesIndexEntry, StoriesMeta } from '@/types/common'; +import type { CommonResponse } from '@/types/responses'; import Stars from '@public/static/svg/stars.svg'; -import { readFileSync } from 'fs'; import jsonata from 'jsonata'; import Image from 'next/image'; import Link from 'next/link'; import { redirect } from 'next/navigation'; -import path from 'path'; import { cache } from 'react'; import { FiChevronLeft, FiChevronRight } from 'react-icons/fi'; const checkMeta = cache(async function checkMeta() { try { - const indexMeta: StoriesMeta = JSON.parse( - readFileSync( - path.join(process.cwd(), STORIES_DIR, 'index-meta.json'), - ).toString(), - ); + const res = (await sfetch(`${STORIES_URL}/stories/meta.json`, { + next: { + revalidate: 86400, + tags: ['meta'], + }, + })) as CommonResponse; + if (!res.ok) { + throw new Error((await res.json()).message); + } + const indexMeta = await res.json(); const expression = jsonata(`$[]`); const result: StoriesMeta = await expression.evaluate(indexMeta); return result; @@ -36,11 +41,18 @@ async function listStories( value?: string, ) { try { - const indexStories: StoriesIndexEntry[] = JSON.parse( - readFileSync( - path.join(process.cwd(), STORIES_DIR, 'index-stories.json'), - ).toString(), - ); + const res = (await sfetch(`${STORIES_URL}/stories/stories.json`, { + next: { + revalidate: 86400, + tags: ['stories'], + }, + })) as CommonResponse; + if (!res.ok) { + throw new Error((await res.json()).message); + } + const indexStories = (await res.json()).map((story) => { + return { ...story, cover: `${STORIES_URL}/stories/${story.cover}` }; + }); const meta = await checkMeta(); let query = `$[]`; switch (key) { diff --git a/src/components/common/story-card.tsx b/src/components/common/story-card.tsx index 0e00018..05da741 100644 --- a/src/components/common/story-card.tsx +++ b/src/components/common/story-card.tsx @@ -14,16 +14,19 @@ function StoryCard({ story, className }: StoryCardProps) { href={`/stories/${story.slug}`} title={`Read ${story.title}`} data-item="story-card" - className={`border-[#909090] border bg-light dark:bg-[#1f201f] dark:border-[#5d5d5d] w-full rounded-lg overflow-hidden group flex justify-between flex-col ${className}`} + className={`border-[#909090] border group bg-light dark:bg-[#1f201f] dark:border-[#5d5d5d] w-full rounded-lg overflow-hidden group flex justify-between flex-col ${className}`} > - +
+ +
+

diff --git a/src/config/common/main.ts b/src/config/common/main.ts index f511108..810c4c6 100644 --- a/src/config/common/main.ts +++ b/src/config/common/main.ts @@ -2,6 +2,7 @@ type Environments = 'local' | 'production'; const APP_URL = process.env.NEXT_PUBLIC_APP_URL as string; const APP_ENV = (process.env.NEXT_PUBLIC_APP_ENV as Environments) ?? 'local'; -const STORIES_DIR = './_stories'; -export { APP_ENV, APP_URL, STORIES_DIR }; +const STORIES_URL = process.env.NEXT_PUBLIC_STORIES_URL; + +export { APP_ENV, APP_URL, STORIES_URL }; diff --git a/src/helpers/common.ts b/src/helpers/common.ts index d645e12..aa1fee5 100644 --- a/src/helpers/common.ts +++ b/src/helpers/common.ts @@ -1,3 +1,26 @@ +async function sfetch( + input: RequestInfo | URL, + init?: (RequestInit & { timeout?: number }) | undefined, +) { + const timeout = init?.timeout; + delete init?.timeout; + const additionalHeaders: RequestInit['headers'] = { + 'Accept-Language': 'en', + }; + const controller = new AbortController(); + setTimeout(() => controller.abort(), timeout); + init = { + ...init, + headers: { + ...additionalHeaders, + ...init?.headers, + }, + signal: timeout && timeout > 0 ? controller.signal : null, + }; + const fetcher = await fetch(input, init); + return fetcher; +} + function capitalize(string: string) { return string.charAt(0).toUpperCase() + string.slice(1); } @@ -40,4 +63,10 @@ function isMobileBrowser() { ); } -export { capitalize, formatDate, generateFallbackImage, isMobileBrowser }; +export { + capitalize, + formatDate, + generateFallbackImage, + isMobileBrowser, + sfetch, +}; diff --git a/src/markdowns/disclaimer.md b/src/markdowns/disclaimer.md deleted file mode 100644 index 628aa39..0000000 --- a/src/markdowns/disclaimer.md +++ /dev/null @@ -1,50 +0,0 @@ ---- -title: Disclaimer -date: 2024-08-21T11:21:46.751Z ---- - -# Site Description - -Welcome to [optimisticoder.com](http://optimisticoder.com/)! - -Optimisticoder is a website managed by me, Azvya, a software developer with experience in full stack, back end, front end, and cloud. This site serves as a blogging and portfolio platform to share knowledge, experiences, and projects that I have worked on. - -# Site Purpose - -The main purposes of this site are to: - -- Share informative articles and content about technology developments, coding tips, and tutorials. -- Present a personal portfolio that includes my work experience and completed projects. -- Interact with the tech community through valuable and inspiring content. - -# Content - -All content presented on Optimisticoder is my own work, unless otherwise stated. I strive to provide accurate and up-to-date information, but I do not guarantee that all the information provided is complete, correct, or always current. - -# Third Party Connection - -The information presented on this site is indeed linked to 3rd party applications such as: - -## LinkedIn Integration - -This website integrates with LinkedIn to fetch my personal data, such as name, photo, and work history. This data is used to enrich the portfolio content and provide a more comprehensive view of my professional experience. - -## Google Adsense and Publisher - -Optimisticoder uses Google Adsense to display relevant ads to visitors. Additionally, some articles may be published through Google Publisher to appear in Google Feeds/News and MSN. - -# Side Projects - -This site also features several additional applications (side projects) created by me. Each of these applications may have its own separate policies and terms of use, independent of the main Optimisticoder website. Users are encouraged to review the respective policies for each application before use. - -# Responsibility - -I am not responsible for any losses or damages arising from the use of the information on this site or its associated applications. Any actions taken based on the information on this site are entirely at your own risk. - -# Content Changes - -I reserve the right to change, add, or delete content on this site at any time without prior notice. - -If you have any questions or need further clarification about this disclaimer, please contact me through the contact page on this site. - -Thank you for visiting Optimisticoder! \ No newline at end of file diff --git a/src/markdowns/privacy-policy.md b/src/markdowns/privacy-policy.md deleted file mode 100644 index cf63efd..0000000 --- a/src/markdowns/privacy-policy.md +++ /dev/null @@ -1,61 +0,0 @@ ---- -title: Privacy Policy -date: 2024-08-21T11:21:46.751Z ---- - -Welcome to [optimisticoder.com](http://optimisticoder.com/)! We value your privacy and are committed to protecting the personal information you provide to us. This Privacy Policy explains how we collect, use, and protect your personal information. - -## Information Collection - -We collect various types of information to provide and improve our services to you, including: - -1. **Personal Identification Information:** Name, email address, and other contact information that you voluntarily provide through the contact form on our site. -2. **Usage Information:** Data about how you interact with our site, including IP address, device type, browser, pages visited, and only managed by cloudflare. -3. **Information from LinkedIn:** Personal data retrieved from LinkedIn, such as name, photo, and work history, used to enrich portfolio content. Only the site owner's (Azvya Erstevan) data will be retrieved. - -## Information Usage - -We use the information we collect for various purposes, including: - -- Providing, operating, and maintaining our website. -- Improving, personalizing, and expanding our services. -- Managing accounts and providing customer service. -- Sending notifications, updates, and other information related to our website. -- Displaying relevant advertisements through Google Adsense. -- Analyzing site usage for development and service improvement. - -## Information Sharing - -We will not sell, trade, or rent your personal information to third parties without your consent, except: - -- When required by law or to comply with legal processes. -- To protect our rights, property, or safety, or those of other users. -- With third-party service providers who assist us in operating our website and conducting our business, provided they agree to keep this information confidential. - -## Information Security - -We take reasonable steps to protect your personal information from unauthorized access, alteration, disclosure, or destruction. However, no method of transmission over the internet or electronic storage is 100% secure, so we cannot guarantee absolute security. - -## Cookies and Tracking Technologies - -We use cookies and other tracking technologies to enhance your experience on our site. Cookies are small files stored on your device that help us analyze site usage and provide customized features. You can set your browser to refuse cookies, but this may affect the functionality of our site. - -## Your Privacy Rights - -You have the right to: - -- Access, update, or delete your personal information that we hold, which we obtain through the contact form you voluntarily provide. - -## Privacy Policy for Additional Applications - -This site also features several additional applications (side projects) that may have different privacy policies. Users are encouraged to review the privacy policies of each application before use. - -## Changes to This Privacy Policy - -We may update this Privacy Policy from time to time. Any changes will be posted on this page, and we will notify you via email or a notice on our site before the changes take effect. You are advised to review this Privacy Policy periodically to stay informed about how we protect your information. - -## Contact Us - -If you have any questions or require further information about this Privacy Policy, please contact us through the contact page on this site. - -Thank you for trusting your personal information to Optimisticoder! \ No newline at end of file diff --git a/src/types/responses.d.ts b/src/types/responses.d.ts new file mode 100644 index 0000000..ff6e1b5 --- /dev/null +++ b/src/types/responses.d.ts @@ -0,0 +1,85 @@ +import type { IntRange, MergeObject } from './utils'; + +type ErrorResponse = { + ok: false; + status: Exclude, 422>; + json(): Promise<{ message: string }>; +} & Omit; + +type OffsetResourceResponse = + | ({ + ok: true; + status: IntRange<100, 299>; + json(): Promise<{ total: number; retrieved: number; data: T[] }>; + } & Omit) + | RedirectResponse + | ErrorResponse; + +type CursorResourceResponse = + | ({ + ok: true; + status: IntRange<100, 299>; + json(): Promise<{ + total: number; + retrieved: number; + next: string | null; + prev: string | null; + data: T[]; + }>; + } & Omit) + | RedirectResponse + | ErrorResponse; + +type CommonResponse = + | ({ + ok: true; + status: IntRange<100, 299>; + json: T extends never + ? never + : () => Promise< + T extends { message: string } + ? MergeObject + : T + >; + } & Omit) + | RedirectResponse + | ErrorResponse; + +type RedirectResponse = { + ok: false; + status: Exclude, 303>; + json: never; +} & Omit; + +type SeeOtherResponse = { + ok: false; + status: 303; + json: T extends never + ? never + : () => Promise< + T extends { message: string } + ? MergeObject + : T + >; +} & Omit; + +type ValidationResponse = { + ok: false; + status: 422; + json(): Promise<{ + message: string; + errors: { + [key in T[number]]?: string[]; + }; + }>; +} & Omit; + +type PlainTextResponse = + | ({ + ok: true; + status: IntRange<100, 299>; + json: never; + text(): Promise; + } & Omit) + | RedirectResponse + | ErrorResponse;