From a72fc4e0f4aae020e9f0cec56d520ed9410a72b6 Mon Sep 17 00:00:00 2001 From: yossydev Date: Wed, 20 Mar 2024 00:41:07 +0900 Subject: [PATCH 01/29] feat(blog): building a blog environment using honox --- blog/.gitignore | 12 + blog/app/client.ts | 3 + blog/app/global.d.ts | 15 + blog/app/islands/counter.tsx | 11 + blog/app/routes/_renderer.tsx | 18 + blog/app/routes/index.tsx | 18 + blog/app/server.ts | 8 + blog/package.json | 20 + blog/tsconfig.json | 12 + blog/vite.config.ts | 15 + pnpm-lock.yaml | 25396 +++++++++++++++----------------- pnpm-workspace.yaml | 19 +- 12 files changed, 12145 insertions(+), 13402 deletions(-) create mode 100644 blog/.gitignore create mode 100644 blog/app/client.ts create mode 100644 blog/app/global.d.ts create mode 100644 blog/app/islands/counter.tsx create mode 100644 blog/app/routes/_renderer.tsx create mode 100644 blog/app/routes/index.tsx create mode 100644 blog/app/server.ts create mode 100644 blog/package.json create mode 100644 blog/tsconfig.json create mode 100644 blog/vite.config.ts diff --git a/blog/.gitignore b/blog/.gitignore new file mode 100644 index 000000000..bcab6efec --- /dev/null +++ b/blog/.gitignore @@ -0,0 +1,12 @@ +node_modules +dist +.wrangler +.dev.vars +.hono + +# Change them to your taste: +wrangler.toml +package-lock.json +yarn.lock +pnpm-lock.yaml +bun.lockb \ No newline at end of file diff --git a/blog/app/client.ts b/blog/app/client.ts new file mode 100644 index 000000000..16ecf9617 --- /dev/null +++ b/blog/app/client.ts @@ -0,0 +1,3 @@ +import { createClient } from 'honox/client' + +createClient() diff --git a/blog/app/global.d.ts b/blog/app/global.d.ts new file mode 100644 index 000000000..288f02baa --- /dev/null +++ b/blog/app/global.d.ts @@ -0,0 +1,15 @@ +import {} from 'hono' + +type Head = { + title?: string +} + +declare module 'hono' { + interface Env { + Variables: {} + Bindings: {} + } + interface ContextRenderer { + (content: string | Promise, head?: Head): Response | Promise + } +} diff --git a/blog/app/islands/counter.tsx b/blog/app/islands/counter.tsx new file mode 100644 index 000000000..0bb543841 --- /dev/null +++ b/blog/app/islands/counter.tsx @@ -0,0 +1,11 @@ +import { useState } from 'hono/jsx' + +export default function Counter() { + const [count, setCount] = useState(0) + return ( +
+

{count}

+ +
+ ) +} diff --git a/blog/app/routes/_renderer.tsx b/blog/app/routes/_renderer.tsx new file mode 100644 index 000000000..f72c28f16 --- /dev/null +++ b/blog/app/routes/_renderer.tsx @@ -0,0 +1,18 @@ +import { Style } from 'hono/css' +import { jsxRenderer } from 'hono/jsx-renderer' +import { Script } from 'honox/server' + +export default jsxRenderer(({ children, title }) => { + return ( + + + + + {title} + + + + + ) : ( + <> + + + + )} + {title ? {title} : ""} {children} - ) -}) + ); +}); diff --git a/blog/app/routes/index.tsx b/blog/app/routes/index.tsx index fc349558d..5d9dcda18 100644 --- a/blog/app/routes/index.tsx +++ b/blog/app/routes/index.tsx @@ -1,18 +1,13 @@ -import { css } from 'hono/css' -import { createRoute } from 'honox/factory' -import Counter from '../islands/counter' - -const className = css` - font-family: sans-serif; -` +import { createRoute } from "honox/factory"; +import Counter from "../islands/counter"; export default createRoute((c) => { - const name = c.req.query('name') ?? 'Hono' + const name = c.req.query("name") ?? "Hono"; return c.render( -
+

Hello, {name}!

, - { title: name } - ) -}) + { title: name }, + ); +}); diff --git a/blog/app/tailwind.css b/blog/app/tailwind.css new file mode 100644 index 000000000..6a7572500 --- /dev/null +++ b/blog/app/tailwind.css @@ -0,0 +1,76 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + :root { + --background: 0 0% 100%; + --foreground: 222.2 84% 4.9%; + + --card: 0 0% 100%; + --card-foreground: 222.2 84% 4.9%; + + --popover: 0 0% 100%; + --popover-foreground: 222.2 84% 4.9%; + + --primary: 222.2 47.4% 11.2%; + --primary-foreground: 210 40% 98%; + + --secondary: 210 40% 96.1%; + --secondary-foreground: 222.2 47.4% 11.2%; + + --muted: 210 40% 96.1%; + --muted-foreground: 215.4 16.3% 46.9%; + + --accent: 210 40% 96.1%; + --accent-foreground: 222.2 47.4% 11.2%; + + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 210 40% 98%; + + --border: 214.3 31.8% 91.4%; + --input: 214.3 31.8% 91.4%; + --ring: 222.2 84% 4.9%; + + --radius: 0.5rem; + } + + .dark { + --background: 222.2 84% 4.9%; + --foreground: 210 40% 98%; + + --card: 222.2 84% 4.9%; + --card-foreground: 210 40% 98%; + + --popover: 222.2 84% 4.9%; + --popover-foreground: 210 40% 98%; + + --primary: 210 40% 98%; + --primary-foreground: 222.2 47.4% 11.2%; + + --secondary: 217.2 32.6% 17.5%; + --secondary-foreground: 210 40% 98%; + + --muted: 217.2 32.6% 17.5%; + --muted-foreground: 215 20.2% 65.1%; + + --accent: 217.2 32.6% 17.5%; + --accent-foreground: 210 40% 98%; + + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 210 40% 98%; + + --border: 217.2 32.6% 17.5%; + --input: 217.2 32.6% 17.5%; + --ring: 212.7 26.8% 83.9%; + } +} + +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + } +} \ No newline at end of file diff --git a/blog/components.json b/blog/components.json new file mode 100644 index 000000000..4b700ce51 --- /dev/null +++ b/blog/components.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "default", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "tailwind.config.js", + "css": "app/tailwind.css", + "baseColor": "slate", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils" + } +} \ No newline at end of file diff --git a/blog/package.json b/blog/package.json index 644360b6e..e25331255 100644 --- a/blog/package.json +++ b/blog/package.json @@ -8,12 +8,26 @@ "deploy": "$npm_execpath run build && wrangler pages deploy ./dist" }, "dependencies": { + "@hono/react-renderer": "^0.1.1", + "@radix-ui/react-slot": "^1.0.2", + "class-variance-authority": "^0.7.0", + "clsx": "^2.1.0", "hono": "^4.1.2", - "honox": "^0.1.9" + "honox": "^0.1.9", + "lucide-react": "^0.358.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "tailwind-merge": "^2.2.0", + "tailwindcss-animate": "^1.0.7" }, "devDependencies": { "@cloudflare/workers-types": "^4.20240208.0", "@hono/vite-cloudflare-pages": "^0.2.4", + "@types/react": "^18.2.67", + "@types/react-dom": "^18.2.22", + "autoprefixer": "^10.4.16", + "postcss": "^8.4.33", + "tailwindcss": "^3.4.1", "vite": "^5.0.12", "wrangler": "^3.32.0" } diff --git a/blog/postcss.config.js b/blog/postcss.config.js new file mode 100644 index 000000000..2e7af2b7f --- /dev/null +++ b/blog/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/blog/tailwind.config.js b/blog/tailwind.config.js new file mode 100644 index 000000000..7cb7e37ab --- /dev/null +++ b/blog/tailwind.config.js @@ -0,0 +1,77 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + darkMode: ["class"], + content: [ + './pages/**/*.{ts,tsx}', + './components/**/*.{ts,tsx}', + './app/**/*.{ts,tsx}', + './src/**/*.{ts,tsx}', + ], + prefix: "", + theme: { + container: { + center: true, + padding: "2rem", + screens: { + "2xl": "1400px", + }, + }, + extend: { + colors: { + border: "hsl(var(--border))", + input: "hsl(var(--input))", + ring: "hsl(var(--ring))", + background: "hsl(var(--background))", + foreground: "hsl(var(--foreground))", + primary: { + DEFAULT: "hsl(var(--primary))", + foreground: "hsl(var(--primary-foreground))", + }, + secondary: { + DEFAULT: "hsl(var(--secondary))", + foreground: "hsl(var(--secondary-foreground))", + }, + destructive: { + DEFAULT: "hsl(var(--destructive))", + foreground: "hsl(var(--destructive-foreground))", + }, + muted: { + DEFAULT: "hsl(var(--muted))", + foreground: "hsl(var(--muted-foreground))", + }, + accent: { + DEFAULT: "hsl(var(--accent))", + foreground: "hsl(var(--accent-foreground))", + }, + popover: { + DEFAULT: "hsl(var(--popover))", + foreground: "hsl(var(--popover-foreground))", + }, + card: { + DEFAULT: "hsl(var(--card))", + foreground: "hsl(var(--card-foreground))", + }, + }, + borderRadius: { + lg: "var(--radius)", + md: "calc(var(--radius) - 2px)", + sm: "calc(var(--radius) - 4px)", + }, + keyframes: { + "accordion-down": { + from: { height: "0" }, + to: { height: "var(--radix-accordion-content-height)" }, + }, + "accordion-up": { + from: { height: "var(--radix-accordion-content-height)" }, + to: { height: "0" }, + }, + }, + animation: { + "accordion-down": "accordion-down 0.2s ease-out", + "accordion-up": "accordion-up 0.2s ease-out", + }, + }, + }, + plugins: [require("tailwindcss-animate")], +} \ No newline at end of file diff --git a/blog/tsconfig.json b/blog/tsconfig.json index 3aa213156..0778da121 100644 --- a/blog/tsconfig.json +++ b/blog/tsconfig.json @@ -5,8 +5,12 @@ "moduleResolution": "Bundler", "strict": true, "lib": ["ESNext", "DOM"], - "types": ["@cloudflare/workers-types"], + "types": ["@cloudflare/workers-types", "vite/client"], "jsx": "react-jsx", - "jsxImportSource": "hono/jsx" + "jsxImportSource": "react", + "baseUrl": ".", + "paths": { + "@/*": ["./app/*"] + } } } diff --git a/blog/vite.config.ts b/blog/vite.config.ts index 3c38e23ee..ac005ae43 100644 --- a/blog/vite.config.ts +++ b/blog/vite.config.ts @@ -1,15 +1,38 @@ +import path from "path"; import pages from "@hono/vite-cloudflare-pages"; import honox from "honox/vite"; -import client from "honox/vite/client"; import { defineConfig } from "vite"; +import client from "honox/vite/client"; export default defineConfig(({ mode }) => { + const common = { + resolve: { + alias: { + "@": path.resolve(__dirname, "./app"), + }, + }, + }; + if (mode === "client") { return { - plugins: [client()], + ...common, + plugins: [client({ jsxImportSource: "react" })], + build: { + rollupOptions: { + input: ["./app/client.ts", "/app/tailwind.css"], + output: { + entryFileNames: "static/client.js", + chunkFileNames: "static/assets/[name]-[hash].js", + assetFileNames: "static/assets/[name].[ext]", + }, + }, + }, + }; + } else { + return { + ...common, + ssr: { external: ["react", "react-dom"] }, + plugins: [honox(), pages()], }; } - return { - plugins: [honox(), pages()], - }; }); From dd4e7da1e4aa8740bbd52a34cf4d08fb591509f8 Mon Sep 17 00:00:00 2001 From: yossydev Date: Fri, 22 Mar 2024 23:00:01 +0900 Subject: [PATCH 03/29] feat: Display your blog on SSR. --- .../routes/blog/understanding-web-vitals.mdx | 58 +++++++++++++++++++ blog/app/routes/index.tsx | 50 ++++++++++++---- blog/package.json | 6 ++ blog/vite.config.ts | 32 +++++++--- 4 files changed, 128 insertions(+), 18 deletions(-) create mode 100644 blog/app/routes/blog/understanding-web-vitals.mdx diff --git a/blog/app/routes/blog/understanding-web-vitals.mdx b/blog/app/routes/blog/understanding-web-vitals.mdx new file mode 100644 index 000000000..b644e60e7 --- /dev/null +++ b/blog/app/routes/blog/understanding-web-vitals.mdx @@ -0,0 +1,58 @@ +--- +title: Understanding Core web vitals +description: How to enhance your website expirence by analyzing your core web vitals scores +snippet: Core Web Vitals are a set of standardized metrics from Google that help developers understand how users experience a web page. Tthey break down the user's real-world experience on a page. Core Web Vitals can improve your search results. They check website loading speed and help Google understand how well your website is performing and then identify areas where it can improve. Specifically, Core Web Vitals consider loading time, interactivity and visual stability metrics. These are included in Google’s algorithms to measure the health of your website. +image: /blogs/web-vitals-guide.png +publishedAt: "2023-09-28" +published: true +tags: + - Guides + - Core Web Vitals +authors: + - Beka +--- + +Core Web Vitals are a set of standardized metrics from Google that help developers understand how users experience a web page. They break down the user's real-world experience on a page. + +Core Web Vitals can improve your search results. They check website loading speed and help Google understand how well your website is performing and then identify areas where it can improve. Specifically, Core Web Vitals consider loading time, interactivity and visual stability metrics. These are included in Google’s algorithms to measure the health of your website. + +Core Web Vitals identify user experience issues by generating a metric for three primary areas of user experience, including: + +- Page loading performance + +- Ease of interaction + +- Visual stability of a page from a user’s perspective + +Let’s take a look 4 of the main metrics included in Core Web Vitals. + +### 1. Largest Contentful Paint (LCP) + +The amount of time to render the largest content element visible in the viewport, from when the user requests the URL. The largest element is typically an image or video, or perhaps a large block-level text element. This metric is important because it indicates how quickly a visitor sees that the URL is actually loading. + +**A good LCP time is considered to be 2.5 seconds or less.** + +### 2. First Input Delay (FID) + +The time from when a user first interacts with your page (when they clicked a link, tapped on a button, and so on) to the time when the browser responds to that interaction. This measurement is taken from whatever interactive element that the user first clicks. This is important on pages where the user needs to do something, because this is the delay before the page becomes interactive. Note that this might not be reported for each page since it'll need interaction to be reported. + +**A good FID score is 100 milliseconds or less.** + +### 3. Interaction to Next Paint (INP) + +A metric that assesses a page's overall responsiveness to user interactions by observing the time that it takes for the page to respond to all click, tap, and keyboard interactions that occur throughout the lifespan of a user's visit to a page. The final INP value is the longest interaction observed, ignoring outliers. + +**Lower INP times are better, but a specific target value for INP is not clearly defined yet, as it's an experimental metric.** + +### 4. Cumulative Layout Shift (CLS) + +CLS measures the sum total of all individual layout shift scores for every unexpected layout shift that occurs during the entire lifespan of the page. The score is zero to any positive number, where zero means no shifting and the larger the number, the more layout shift on the page. This is important because having pages elements shift while a user is trying to interact with it is a bad user experience. If you can't seem to find the reason for a high value, try interacting with the page to see how that affects the score. + +**A good CLS score is considered to be 0.1 or less.** + +## Loglib And Web Vitals + +Loglib enables the effortless collection and analysis of core web vitals, eliminating the need for any manual intervention. These vitals are gathered from authentic user devices and transmitted to loglib servers. Currently, we present five web vital metrics that are encompassed above. + +Loglib's speed insights furnish a comprehensive inventory of all Page Names and URLs accessed by users of your application. In this context, 'Page Names' denote the actual pages constructed, while 'URLs' signify the paths requested by visitors. However, loglib's capabilities extend beyond page URLs, as it also provides information pertaining to scores based on location (country and city), devices (mobile, desktop, tablet), as well as browser and operating system. + diff --git a/blog/app/routes/index.tsx b/blog/app/routes/index.tsx index 5d9dcda18..6ec955fde 100644 --- a/blog/app/routes/index.tsx +++ b/blog/app/routes/index.tsx @@ -1,13 +1,41 @@ -import { createRoute } from "honox/factory"; -import Counter from "../islands/counter"; +import { type FC } from "react"; -export default createRoute((c) => { - const name = c.req.query("name") ?? "Hono"; - return c.render( -
-

Hello, {name}!

- -
, - { title: name }, +export default function Top() { + return ( + <> + + ); -}); +} + +const Posts: FC = () => { + const blogs = import.meta.glob<{ + frontmatter: { title: string; date: string; published: boolean }; + }>("./blog/*.mdx", { eager: true }); + const entries = Object.entries(blogs).filter( + ([_, module]) => module.frontmatter.published, + ); + + return ( +
+ +
+ ); +}; diff --git a/blog/package.json b/blog/package.json index e25331255..808bc13b4 100644 --- a/blog/package.json +++ b/blog/package.json @@ -9,6 +9,8 @@ }, "dependencies": { "@hono/react-renderer": "^0.1.1", + "@hono/vite-ssg": "^0.1.0", + "@mdx-js/rollup": "^3.0.1", "@radix-ui/react-slot": "^1.0.2", "class-variance-authority": "^0.7.0", "clsx": "^2.1.0", @@ -17,10 +19,14 @@ "lucide-react": "^0.358.0", "react": "^18.2.0", "react-dom": "^18.2.0", + "rehype-highlight": "^7.0.0", + "remark-frontmatter": "^5.0.0", + "remark-mdx-frontmatter": "^4.0.0", "tailwind-merge": "^2.2.0", "tailwindcss-animate": "^1.0.7" }, "devDependencies": { + "@babel/plugin-transform-react-jsx": "^7.23.4", "@cloudflare/workers-types": "^4.20240208.0", "@hono/vite-cloudflare-pages": "^0.2.4", "@types/react": "^18.2.67", diff --git a/blog/vite.config.ts b/blog/vite.config.ts index ac005ae43..9a5d6e8fd 100644 --- a/blog/vite.config.ts +++ b/blog/vite.config.ts @@ -1,8 +1,15 @@ import path from "path"; import pages from "@hono/vite-cloudflare-pages"; import honox from "honox/vite"; -import { defineConfig } from "vite"; +import ssg from "@hono/vite-ssg"; +import mdx from "@mdx-js/rollup"; +import rehypeHighlight from "rehype-highlight"; +import remarkFrontmatter from "remark-frontmatter"; +import remarkMdxFrontmatter from "remark-mdx-frontmatter"; import client from "honox/vite/client"; +import { defineConfig } from "vite"; + +const entry = "./app/server.ts"; export default defineConfig(({ mode }) => { const common = { @@ -28,11 +35,22 @@ export default defineConfig(({ mode }) => { }, }, }; - } else { - return { - ...common, - ssr: { external: ["react", "react-dom"] }, - plugins: [honox(), pages()], - }; } + + return { + ...common, + build: { + emptyOutDir: false, + }, + ssr: { external: ["react", "react-dom"] }, + plugins: [ + honox(), + pages(), + mdx({ + jsxImportSource: "react", + remarkPlugins: [remarkFrontmatter, remarkMdxFrontmatter], + rehypePlugins: [rehypeHighlight], + }), + ], + }; }); From 5a38b08152d15a95bbd7ec9a49c5a31e8a687996 Mon Sep 17 00:00:00 2001 From: yossydev Date: Sat, 6 Apr 2024 18:28:08 +0900 Subject: [PATCH 04/29] Creation of top page UI --- blog/app/client.ts | 6 +- blog/app/components/ui/button.tsx | 91 +-- blog/app/constans/author.ts | 13 + blog/app/global.d.ts | 6 +- blog/app/index.css | 224 ++++++++ blog/app/islands/NavItems.tsx | 26 + blog/app/islands/PublicPage.tsx | 110 ++++ blog/app/routes/_renderer.tsx | 23 +- blog/app/routes/blog/_renderer.tsx | 11 + .../routes/blog/understanding-web-vitals.mdx | 9 +- blog/app/routes/index.tsx | 43 +- blog/app/tailwind.css | 76 --- blog/package.json | 11 +- blog/public/static/author/yossydev.jpg | Bin 0 -> 265983 bytes blog/public/static/web-vitals-guide.png | Bin 0 -> 22430 bytes blog/tailwind.config.js | 79 ++- blog/vite.config.ts | 66 ++- frontend/src/lib/utils.ts | 8 +- frontend/src/modules/common/public-page.tsx | 28 + frontend/src/modules/marketing/footer.tsx | 66 ++- frontend/src/modules/marketing/nav.tsx | 33 +- frontend/src/store/theme.ts | 2 +- pnpm-lock.yaml | 520 +++++++++++++++++- 23 files changed, 1191 insertions(+), 260 deletions(-) create mode 100644 blog/app/constans/author.ts create mode 100644 blog/app/index.css create mode 100644 blog/app/islands/NavItems.tsx create mode 100644 blog/app/islands/PublicPage.tsx create mode 100644 blog/app/routes/blog/_renderer.tsx delete mode 100644 blog/app/tailwind.css create mode 100644 blog/public/static/author/yossydev.jpg create mode 100644 blog/public/static/web-vitals-guide.png create mode 100644 frontend/src/modules/common/public-page.tsx diff --git a/blog/app/client.ts b/blog/app/client.ts index 8cb91e381..e0e95697d 100644 --- a/blog/app/client.ts +++ b/blog/app/client.ts @@ -1,12 +1,12 @@ -import { createClient } from "honox/client"; +import { createClient } from 'honox/client'; createClient({ hydrate: async (elem, root) => { - const { hydrateRoot } = await import("react-dom/client"); + const { hydrateRoot } = await import('react-dom/client'); hydrateRoot(root, elem); }, createElement: async (type: any, props: any) => { - const { createElement } = await import("react"); + const { createElement } = await import('react'); return createElement(type, props); }, }); diff --git a/blog/app/components/ui/button.tsx b/blog/app/components/ui/button.tsx index 0ba427735..10ec100d6 100644 --- a/blog/app/components/ui/button.tsx +++ b/blog/app/components/ui/button.tsx @@ -1,56 +1,77 @@ -import * as React from "react" -import { Slot } from "@radix-ui/react-slot" -import { cva, type VariantProps } from "class-variance-authority" +import { Slot } from '@radix-ui/react-slot'; +import { type VariantProps, cva } from 'class-variance-authority'; +import { Loader2 } from 'lucide-react'; +import * as React from 'react'; -import { cn } from "@/lib/utils" +import { cn } from '~/lib/utils'; const buttonVariants = cva( - "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", + 'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none [&:not(.absolute)]:active:translate-y-px disabled:opacity-50', { variants: { variant: { - default: "bg-primary text-primary-foreground hover:bg-primary/90", - destructive: - "bg-destructive text-destructive-foreground hover:bg-destructive/90", - outline: - "border border-input bg-background hover:bg-accent hover:text-accent-foreground", - secondary: - "bg-secondary text-secondary-foreground hover:bg-secondary/80", - ghost: "hover:bg-accent hover:text-accent-foreground", - link: "text-primary underline-offset-4 hover:underline", + default: 'bg-primary text-primary-foreground hover:bg-primary/80', + destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/80', + outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground', + secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80', + ghost: 'hover:bg-accent/50', + link: 'text-primary underline-offset-4 hover:underline', + // Add more variants here + cell: 'text-regular underline-offset-4 hover:underline !ring-offset-transparent !ring-transparent opacity-75 hover:opacity-100', + plain: 'text-primary bg-primary/5 border border-primary/30 hover:bg-primary/10 hover:border-primary/50', + glow: 'outline-glow-button bg-background !rounded-full relative active:bk-background', + gradient: + 'before:bg-primary before:rounded-md after:rounded-md z-0 bg-transparent relative text-primary-foreground gradient-button hover:before:bg-primary/80', }, size: { - default: "h-10 px-4 py-2", - sm: "h-9 rounded-md px-3", - lg: "h-11 rounded-md px-8", - icon: "h-10 w-10", + default: 'h-10 px-4 py-2', + sm: 'h-9 rounded-md px-3', + lg: 'h-11 rounded-md px-4', + icon: 'h-10 w-10', + xl: 'h-14 rounded-lg text-lg px-6', }, }, defaultVariants: { - variant: "default", - size: "default", + variant: 'default', + size: 'default', }, - } -) + }, +); -export interface ButtonProps - extends React.ButtonHTMLAttributes, - VariantProps { - asChild?: boolean +export interface ButtonProps extends React.ButtonHTMLAttributes, VariantProps { + loading?: boolean; + asChild?: boolean; } const Button = React.forwardRef( - ({ className, variant, size, asChild = false, ...props }, ref) => { - const Comp = asChild ? Slot : "button" + ({ className, variant, size, asChild = false, children, loading, disabled, ...props }, ref) => { + const Comp = asChild ? Slot : 'button'; + + if (asChild) { + return ( + + {children} + + ); + } + return ( - ) - } -) -Button.displayName = "Button" + > + {loading && ( +
+ +
+ )} + {children} +
+ ); + }, +); +Button.displayName = 'Button'; -export { Button, buttonVariants } +export { Button, buttonVariants }; diff --git a/blog/app/constans/author.ts b/blog/app/constans/author.ts new file mode 100644 index 000000000..d7738a7bf --- /dev/null +++ b/blog/app/constans/author.ts @@ -0,0 +1,13 @@ +type Author = { + name: string; + about: string; + icon: string; +}; + +export const authors: { [key: string]: Author } = { + yossydev: { + name: 'yossydev', + about: 'web developer', + icon: '/static/author/yossydev.jpg', + }, +} as const; diff --git a/blog/app/global.d.ts b/blog/app/global.d.ts index b4b70df77..ea527c07d 100644 --- a/blog/app/global.d.ts +++ b/blog/app/global.d.ts @@ -1,8 +1,8 @@ -import {} from "hono"; +import {} from 'hono'; -import "@hono/react-renderer"; +import '@hono/react-renderer'; -declare module "@hono/react-renderer" { +declare module '@hono/react-renderer' { interface Props { title?: string; } diff --git a/blog/app/index.css b/blog/app/index.css new file mode 100644 index 000000000..e6400f03b --- /dev/null +++ b/blog/app/index.css @@ -0,0 +1,224 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + :root { + --background: 0 0% 100%; + --foreground: 240 10% 10%; + --card: 0 0% 100%; + --card-foreground: 240 10% 10%; + --popover: 0 0% 100%; + --popover-foreground: 240 10% %; + --primary: 240 6% 10%; + --primary-foreground: 0 0% 98%; + --secondary: 240 5% 95%; + --secondary-foreground: 240 6% 20%; + --muted: 240 5% 77.2%; + --muted-foreground: 240 4% 40%; + --accent: 240 5% 95%; + --accent-foreground: 240 6% 10%; + --destructive: 0 85% 40%; + --destructive-foreground: 0 0% 98%; + --border: 240 6% 92%; + --input: 240 6% 92%; + --ring: 240 6% 10%; + --radius: 0.5rem; + --success: 120 100% 27%; + } + + .dark { + --background: 240 10% 9%; + --foreground: 0 0% 95%; + --card: 240 10% 14%; + --card-foreground: 0 0% 95%; + --popover: 240 10% 6%; + --popover-foreground: 0 0% 95%; + --primary: 0 0% 98%; + --primary-foreground: 240 5.9% 16%; + --secondary: 240 3.7% 15%; + --secondary-foreground: 0 0% 95%; + --muted: 240 3.7% 25%; + --muted-foreground: 240 5% 84.9%; + --accent: 240 3.7% 25%; + --accent-foreground: 0 0% 95%; + --destructive: 0 62.8% 50%; + --destructive-foreground: 0 0% 95%; + --border: 240 3.7% 20%; + --input: 240 3.7% 25%; + --ring: 240 4.9% 83.9%; + } + + .theme-rose.light { + --primary: 346.8 77.2% 49.8%; + --primary-foreground: 355.7 100% 97.3%; + --secondary: 240 4.8% 95.9%; + --secondary-foreground: 240 5.9% 10%; + --accent: 240 4.8% 95.9%; + --accent-foreground: 240 5.9% 10%; + } + + .theme-rose.dark { + --primary: 346.8 77.2% 49.8%; + --primary-foreground: 355.7 100% 97.3%; + --secondary: 240 3.7% 15%; + --secondary-foreground: 0 0% 98%; + --accent: 12 6.5% 15.1%; + --accent-foreground: 0 0% 98%; + } +} + +@layer base { + * { + @apply border-border; + } + + body { + @apply bg-background text-foreground h-svh; + } + + #root { + @apply flex flex-col h-full; + } +} + +@layer components { + .rich-gradient:after { + content: ''; + display: block; + position: absolute; + top: 0; + opacity: 1; + width: 100%; + height: 100%; + z-index: -3; + background-image: + linear-gradient(to bottom left, rgba(0, 0, 0, 0), rgba(255, 199, 147, 0.87)), + linear-gradient(to top left, rgba(0, 0, 0, 0), rgb(57, 160, 251)), + linear-gradient(to bottom right, rgba(0, 0, 0, 0), rgb(195, 120, 241)), + linear-gradient(to top right, rgba(0, 0, 0, 0), rgb(2, 155, 129)); + } + + .dark .rich-gradient:after { + opacity: .8; + background-image: + linear-gradient(to bottom left, rgba(0, 0, 0, 0), rgba(60, 0, 59, 1)), + linear-gradient(to top left, rgba(0, 0, 0, 0), rgba(15, 6, 86, 1)), + linear-gradient(to bottom right, rgba(0, 0, 0, 0), rgba(184, 144, 0, 1)), + linear-gradient(to top right, rgba(0, 0, 0, 0), rgba(11, 144, 122, 1)); + } + + .rich-gradient:before { + content: ''; + display: block; + position: absolute; + top: 0; + opacity: 1; + width: 100%; + height: 100%; + z-index: -2; + background-image: radial-gradient(circle 90vh at 40% 40%, hsla(var(--background)), rgba(255, 255, 255, 0.5)); + } + + .dark .rich-gradient:before { + opacity: .8; + background-image: radial-gradient(circle 90vh at 40% 40%, hsla(var(--background)), rgba(255, 255, 255, 0)); + } + + .rich-gradient.dark-gradient:after { + opacity: 1; + background: rgba(0, 0, 0, 1); + background-image: + linear-gradient(to bottom left, rgba(0, 0, 0, 0), rgba(255, 255, 255, 0.2)), + linear-gradient(to top left, rgba(0, 0, 0, 0), rgba(49, 49, 230, 0.3)), + linear-gradient(to bottom right, rgba(0, 0, 0, 0), rgba(156, 39, 228, 0.2)), + linear-gradient(to top right, rgba(0, 0, 0, 0), rgba(254, 185, 24, 0.3)); + } + + .dark .dark-gradient:after { + opacity: .2; + } + + .rich-gradient.dark-gradient:before { + background-image: radial-gradient(circle 120vh at 40% 120%, rgba(59, 17, 37, 0.6), rgba(255, 255, 255, 0)); + } +} + +.outline-glow-button:before { + content: ''; + display: block; + position: absolute; + top: -1px; + left: -1px; + opacity: .5; + width: calc(100% + 2px); + height: calc(100% + 2px); + z-index: -2; + border-radius: 100px; + background: linear-gradient(45deg, rgba(151, 53, 255, 1) -10%, rgba(251, 204, 38, 1) 60%, rgba(210, 35, 82, 1) 100%); +} + +.outline-glow-button:after { + content: ''; + display: block; + position: absolute; + top: -1px; + left: -1px; + filter: blur(6px); + opacity: .4; + width: calc(100% + 2px); + height: calc(100% + 2px); + z-index: -2; + border-radius: 100px; + background: linear-gradient(45deg, rgba(151, 53, 255, 1) -10%, rgba(251, 204, 38, 1) 60%, rgba(210, 35, 82, 1) 100%); +} + +.outline-glow-button:hover::before { + opacity: 1; +} + +.outline-glow-button:hover::after { + opacity: .8; +} + +.outline-glow-button:active::after { + position: absolute; + top: 0px; + left: 0px; + filter: none; + opacity: 1; + width: 100%; + height: 100%; + z-index: -1; + border-radius: 100px; + background: hsla(var(--background)); +} + +.gradient-button:after, +.gradient-button:before { + display: block; + left: 0; + top: 0; + position: absolute; + width: 100%; + height: 100%; +} + +.gradient-button:before { + z-index: -2; + filter: saturate(5); +} + +.gradient-button:after { + background: linear-gradient(to right, + rgb(100, 100, 100), + rgba(204, 204, 204, 0.8) 40%, + rgba(204, 204, 204, 0.8) 55%, + rgb(34, 34, 34)); + opacity: .3; + z-index: -1; +} + +.fill-grid { + block-size: 100%; +} diff --git a/blog/app/islands/NavItems.tsx b/blog/app/islands/NavItems.tsx new file mode 100644 index 000000000..b81f2fa7d --- /dev/null +++ b/blog/app/islands/NavItems.tsx @@ -0,0 +1,26 @@ +import { config } from 'config'; +import { buttonVariants } from '@/components/ui/button'; +import { cn } from '@/lib/utils'; +import { useTranslation } from 'frontend/node_modules/react-i18next'; + +const publicNavConfig = [ + { id: 'features', url: '/about', hash: 'features' }, + { id: 'pricing', url: '/about', hash: 'pricing' }, + { id: 'docs', url: `${config.backendUrl}/docs`, hash: '' }, +]; + +const RenderNavItems = () => { + const { t } = useTranslation(); + + return ( + <> + {publicNavConfig.map((item) => ( + + {t(item.id)} + + ))} + + ); +}; + +export default RenderNavItems; diff --git a/blog/app/islands/PublicPage.tsx b/blog/app/islands/PublicPage.tsx new file mode 100644 index 000000000..b4e08862d --- /dev/null +++ b/blog/app/islands/PublicPage.tsx @@ -0,0 +1,110 @@ +import { ReactNode, FC } from 'react'; +import PublicPage from 'frontend/src/modules/common/public-page'; +import RenderNavItems from './NavItems'; +import { config } from 'config'; +import { Github, Twitter } from 'lucide-react'; +import { useTranslation } from 'frontend/node_modules/react-i18next'; +import Logo from 'frontend/src/modules/common/logo'; + +type Props = { + children: ReactNode; +}; + +export const socials = [ + { title: 'Twitter', href: config.company.twitterUrl, icon: Twitter }, + { title: 'GitHub', href: config.company.githubUrl, icon: Github }, +]; + +const footerSections = [ + { + title: 'common:product', + links: [ + { title: 'common:about', href: '/about' }, + { title: 'common:sign_up', href: '/auth/sign-in' }, + ], + }, + { + title: 'common:documentation', + hideOnMobile: true, + links: [ + { title: 'common:api_docs', href: `${config.backendUrl}/docs` }, + { title: 'common:architecture', href: 'https://github.com/cellajs/cella/blob/main/info/ARCHITECTURE.md' }, + { title: 'common:roadmap', href: 'https://github.com/cellajs/cella/blob/main/info/ROADMAP.md' }, + ], + }, + { + title: 'common:connect', + links: [{ title: 'common:contact_us', href: '/contact' }, ...socials], + }, +]; + +const legalLinks = [ + { title: 'common:terms', href: '/terms' }, + { title: 'common:privacy', href: '/privacy' }, + { title: 'common:accessibility', href: '/accessibility' }, +]; + +type FooterLink = { + Link?: JSX.Element; +}; + +function FooterLinks({ Link }: FooterLink) { + const { t } = useTranslation(); + + return ( + + ); +} + +const AbountFooter = () => { + return ( + + + + ); +}; + +function PublicFooter() { + const { t } = useTranslation(); + + return legalLinks.map((link) => ( +
  • + + {t(link.title)} + +
  • + )); +} + +const MainLayout: FC = ({ children }) => { + return ( + } AboutLink={} LegalLinks={} FooterLink={}> + {children} + + ); +}; + +export default MainLayout; diff --git a/blog/app/routes/_renderer.tsx b/blog/app/routes/_renderer.tsx index d9df35eb8..763103a41 100644 --- a/blog/app/routes/_renderer.tsx +++ b/blog/app/routes/_renderer.tsx @@ -1,9 +1,10 @@ -import { reactRenderer } from "@hono/react-renderer"; -import { useRequestContext } from "@hono/react-renderer"; -import { FC, PropsWithChildren } from "react"; +import { reactRenderer } from '@hono/react-renderer'; +import { useRequestContext } from '@hono/react-renderer'; +import { FC, PropsWithChildren } from 'react'; +import MainLayout from '@/islands/PublicPage'; const HasIslands: FC = ({ children }) => { - const IMPORTING_ISLANDS_ID = "__importing_islands" as const; + const IMPORTING_ISLANDS_ID = '__importing_islands' as const; const c = useRequestContext(); return <>{c.get(IMPORTING_ISLANDS_ID) ? children : <>}; }; @@ -17,19 +18,21 @@ export default reactRenderer(({ children, title }) => { {import.meta.env.PROD ? ( <> - + - +