From 31a081c3794b7a4a5e2dd80f4ae44744ac555935 Mon Sep 17 00:00:00 2001 From: thomasabishop Date: Tue, 25 Nov 2025 17:11:28 +0000 Subject: [PATCH] feat: add coding stats and improve styles --- eslint.config.js | 45 +++++---- src/api/wakapi-api.js | 11 +++ src/components/EolasListing.jsx | 7 +- src/components/LanguagesChart.jsx | 29 ++++++ src/components/MetricBar.jsx | 37 ++++++++ src/components/ProjectsChart.jsx | 30 ++++++ src/components/Scorecard.jsx | 10 ++ src/containers/CodeStats.jsx | 109 ++++++++++++++++++++++ src/containers/PostListing.jsx | 6 +- src/index.css | 84 +++++++++-------- src/pages/home.jsx | 5 +- src/styles/_variables.css | 148 +++++++++++++++--------------- src/templates/BlogTemplate.jsx | 80 ++++++++-------- src/templates/MainTemplate.jsx | 126 +++++++++++-------------- src/utils/convertDate.js | 10 +- 15 files changed, 480 insertions(+), 257 deletions(-) create mode 100644 src/api/wakapi-api.js create mode 100644 src/components/LanguagesChart.jsx create mode 100644 src/components/MetricBar.jsx create mode 100644 src/components/ProjectsChart.jsx create mode 100644 src/components/Scorecard.jsx create mode 100644 src/containers/CodeStats.jsx diff --git a/eslint.config.js b/eslint.config.js index b628f77..dfca51e 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -2,27 +2,26 @@ import js from "@eslint/js" import globals from "globals" import reactHooks from "eslint-plugin-react-hooks" import reactRefresh from "eslint-plugin-react-refresh" -import tseslint from "typescript-eslint" -export default tseslint.config( - { ignores: ["dist"] }, - { - extends: [js.configs.recommended, ...tseslint.configs.recommended], - files: ["**/*.{ts,tsx}"], - languageOptions: { - ecmaVersion: 2020, - globals: globals.browser, - }, - plugins: { - "react-hooks": reactHooks, - "react-refresh": reactRefresh, - }, - rules: { - ...reactHooks.configs.recommended.rules, - "react-refresh/only-export-components": [ - "warn", - { allowConstantExport: true }, - ], - }, - } -) +// export default eslint.config( +// { ignores: ["dist"] }, +// { +// extends: [js.configs.recommended, ...tseslint.configs.recommended], +// files: ["**/*.{ts,tsx}"], +// languageOptions: { +// ecmaVersion: 2020, +// globals: globals.browser, +// }, +// plugins: { +// "react-hooks": reactHooks, +// "react-refresh": reactRefresh, +// }, +// rules: { +// ...reactHooks.configs.recommended.rules, +// "react-refresh/only-export-components": [ +// "warn", +// { allowConstantExport: true }, +// ], +// }, +// } +// ) diff --git a/src/api/wakapi-api.js b/src/api/wakapi-api.js new file mode 100644 index 0000000..dd921e8 --- /dev/null +++ b/src/api/wakapi-api.js @@ -0,0 +1,11 @@ +import axios from "axios" + +const wakapiApi = axios.create({ + baseURL: import.meta.env.VITE_WAKAPI_URL, + timeout: 10000, + headers: { + Authorization: `Basic ${import.meta.env.VITE_WAKAPI_TOKEN}`, + }, +}) + +export default wakapiApi diff --git a/src/components/EolasListing.jsx b/src/components/EolasListing.jsx index 5e9a045..3da6573 100644 --- a/src/components/EolasListing.jsx +++ b/src/components/EolasListing.jsx @@ -1,7 +1,6 @@ import { useQuery } from "@tanstack/react-query" import eolasApi from "@/api/eolas-api" import { convertDate } from "@/utils/convertDate" -import { Link } from "react-router" const EolasEntries = ({ entries }) => { return ( @@ -16,7 +15,7 @@ const EolasEntries = ({ entries }) => { {entry.title.replace(/_/g, " ")} @@ -38,11 +37,11 @@ const EolasListing = () => {
-

+

recent notes (external)

- {isLoading && Loading...} + {isLoading &&
Loading...
} {error ? (
diff --git a/src/components/LanguagesChart.jsx b/src/components/LanguagesChart.jsx new file mode 100644 index 0000000..b54d5f2 --- /dev/null +++ b/src/components/LanguagesChart.jsx @@ -0,0 +1,29 @@ +import MetricBar from "./MetricBar" + +const LanguagesChart = ({ chartData, error }) => { + return ( +
+
+ programming languages +
+ + {error ? ( +
Data could not be found!
+ ) : !chartData?.length ? ( +
No data for time period.
+ ) : ( + chartData.map((x) => ( + + )) + )} +
+ ) +} + +export default LanguagesChart diff --git a/src/components/MetricBar.jsx b/src/components/MetricBar.jsx new file mode 100644 index 0000000..1640d10 --- /dev/null +++ b/src/components/MetricBar.jsx @@ -0,0 +1,37 @@ +const MetricBar = ({ metric, hours, percentage, color }) => ( +
+
+ {metric} + + {hours}h ({percentage}%) + +
+
+
+
+
+) + +export default MetricBar diff --git a/src/components/ProjectsChart.jsx b/src/components/ProjectsChart.jsx new file mode 100644 index 0000000..44a8274 --- /dev/null +++ b/src/components/ProjectsChart.jsx @@ -0,0 +1,30 @@ +import MetricBar from "./MetricBar" + +const ProjectsChart = ({ chartData, error }) => { + return ( +
+
projects
+ + {error ? ( +
Data could not be found!
+ ) : !chartData?.length ? ( +
No data for time period.
+ ) : ( + chartData.map((x) => ( + + )) + )} +
+ Data excludes workplace repos. +
+
+ ) +} + +export default ProjectsChart diff --git a/src/components/Scorecard.jsx b/src/components/Scorecard.jsx new file mode 100644 index 0000000..fa2e1dd --- /dev/null +++ b/src/components/Scorecard.jsx @@ -0,0 +1,10 @@ +const Scorecard = ({ title, metric }) => { + return ( +
+
{title}
+
{metric}
+
+ ) +} + +export default Scorecard diff --git a/src/containers/CodeStats.jsx b/src/containers/CodeStats.jsx new file mode 100644 index 0000000..5bcbe82 --- /dev/null +++ b/src/containers/CodeStats.jsx @@ -0,0 +1,109 @@ +import { useQuery } from "@tanstack/react-query" +import wakapiApi from "../api/wakapi-api" +import { convertDateFriendly } from "../utils/convertDate" +import Scorecard from "../components/Scorecard" +import LanguagesChart from "../components/LanguagesChart" +import ProjectsChart from "../components/ProjectsChart" + +const convertSeconds = (secs) => { + return `${Math.floor(secs / 3600)}h ${Math.floor((secs % 3600) / 60)}m` +} + +const CodeStats = () => { + const { data, isLoading, error } = useQuery({ + queryKey: ["codestats"], + queryFn: () => + wakapiApi.get(`summary?interval=week`).then((res) => res.data), + }) + + const grandTotal = data?.projects.reduce((acc, curr) => acc + curr.total, 0) + const grandTotalFormatted = `${Math.floor(grandTotal / 3600)}h ${Math.floor((grandTotal % 3600) / 60)}m` + const os = data?.operating_systems + const osMetric = os ? ( +
+ {os[0]?.key}: {convertSeconds(os[0].total)}, {os[1]?.key}:{" "} + {convertSeconds(os[1].total)} +
+ ) : ( + "Error" + ) + + const personalProjects = + data && data?.projects.filter((project) => !project.key.includes("gp-")) + + const mainProject = personalProjects?.sort((a, b) => a.total > b.total)[0].key + + console.log(personalProjects) + + const languagesChartData = data?.languages + .map((lang) => ({ + metric: lang.key, + language: lang.key, + hours: (lang.total / 3600).toFixed(1), + percentage: ( + (lang.total / data.languages.reduce((sum, i) => sum + i.total, 0)) * + 100 + ).toFixed(1), + })) + .slice(0, 4) + + const projectsChartData = + personalProjects && + personalProjects.map((proj) => ({ + metric: proj.key, + project: proj.key, + hours: (proj.total / 3600).toFixed(1), + percentage: ( + (proj.total / personalProjects.reduce((sum, i) => sum + i.total, 0)) * + 100 + ).toFixed(1), + })) + + return ( +
+
+
+
+

+ code stats +

+
+ {convertDateFriendly(data?.from)} -{" "} + {convertDateFriendly(data?.to)} +
+
+ {/* Score-cards */} +
+ + + + + +
+ + +
+ Data sourced from my self-hosted{" "} + + Wakapi + {" "} + instance. +
+
+
+
+ ) +} + +export default CodeStats diff --git a/src/containers/PostListing.jsx b/src/containers/PostListing.jsx index 6328748..4696df8 100644 --- a/src/containers/PostListing.jsx +++ b/src/containers/PostListing.jsx @@ -8,7 +8,9 @@ const PostListing = ({ posts, title, showAllButton }) => {
-

{title}

+

+ {title} +

{posts.map((post) => (
  • @@ -19,7 +21,7 @@ const PostListing = ({ posts, title, showAllButton }) => { {post.title} diff --git a/src/index.css b/src/index.css index 54b7570..5e8da1e 100644 --- a/src/index.css +++ b/src/index.css @@ -4,82 +4,86 @@ @import "tw-animate-css"; * { - outline-color: color-mix(in srgb, var(--ring) 50%, transparent); + outline-color: color-mix(in srgb, var(--ring) 50%, transparent); } html { - font-family: var(--font-sansserif); + font-family: var(--font-sansserif); } body { - background-color: var(--background); - color: var(--foreground); + background-color: var(--background); + color: var(--foreground); } .condensed { - font-family: "IBM Plex Sans Condensed"; + font-family: "IBM Plex Sans Condensed"; } figcaption { - font-weight: 500; - font-family: "IBM Plex Sans Condensed"; + font-weight: 500; + font-family: "IBM Plex Sans Condensed"; } h1 { - color: var(--color-orange-light); + color: var(--color-orange-light); - font-family: "IBM Plex Sans Condensed"; + font-family: "IBM Plex Sans Condensed"; } h2 { - font-family: "IBM Plex Sans Condensed"; - color: var(--color-green-light); + font-family: "IBM Plex Sans Condensed"; + color: var(--color-green-light); } h3 { - font-family: "IBM Plex Sans Condensed"; + font-family: "IBM Plex Sans Condensed"; } + .monospaced-font { - font-family: "iA Writer Mono"; + font-family: "iA Writer Mono"; } + .scanlined { - position: relative; /* Add this */ + position: relative; + /* Add this */ } .scanlined::after { - content: ""; - position: absolute; /* Change from fixed */ - top: 0; - left: 0; - width: 100%; - height: 100%; - background-image: linear-gradient(rgba(0, 0, 0, 0.4) 1px, transparent 1px); - background-size: 2px 2px; - background-repeat: repeat; - pointer-events: none; - z-index: 9999; /* Might want to lower this too */ + content: ""; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-image: linear-gradient(rgba(0, 0, 0, 0.4) 1px, transparent 1px); + background-size: 2px 2px; + background-repeat: repeat; + pointer-events: none; + z-index: 9999; + /* Might want to lower this too */ } code { - font-family: var(--font-monospaced); + font-family: var(--font-monospaced); } p code { - color: var(--foreground); - background: #504945; - font-size: 14px; - padding: 0.2rem 0.3rem; - border-radius: var(--radius); - font-weight: 500; + color: var(--foreground); + background: #504945; + font-size: 14px; + padding: 0.2rem 0.3rem; + border-radius: var(--radius); + font-weight: 500; } .shiki { - padding: 1rem 1.2rem; - border-radius: 0; - overflow-x: auto; - margin: 1.5rem 0; - line-height: 1.3; - /* counter-reset: line; */ - font-family: var(--font-monospaced) !important; - font-size: 14px !important; + padding: 1rem 1.2rem; + border-radius: 0; + overflow-x: auto; + margin: 1.5rem 0; + line-height: 1.3; + /* counter-reset: line; */ + font-family: var(--font-monospaced) !important; + font-size: 14px !important; } diff --git a/src/pages/home.jsx b/src/pages/home.jsx index d175469..bd37e72 100644 --- a/src/pages/home.jsx +++ b/src/pages/home.jsx @@ -3,6 +3,7 @@ import PostListing from "@/containers/PostListing" import { usePosts } from "@/hooks/usePosts" import gruvboxComputer from "../images/gruvbox-computer.svg" import EolasListing from "@/components/EolasListing" +import CodeStats from "../containers/CodeStats" // import TodayILearned from "@/containers/TodayILearned" const HomePage = () => { @@ -42,7 +43,7 @@ const HomePage = () => {
    -

    +

    projects

      @@ -65,7 +66,7 @@ const HomePage = () => {
    - + ) diff --git a/src/styles/_variables.css b/src/styles/_variables.css index b2d79b8..4f349d2 100644 --- a/src/styles/_variables.css +++ b/src/styles/_variables.css @@ -1,79 +1,79 @@ :root { - --radius: 0.3rem; - --background: #282828; - --foreground: #ebdbb2; - --sidebar: #3c3836; - --color-red-light: #fb4934; - --color-orange-light: #fe8019; - --color-green-light: #b8bb26; - --color-aqua-muted: #689d6a; - --card: oklch(1 0 0); - --card-foreground: oklch(0.145 0 0); - --popover: oklch(1 0 0); - --popover-foreground: oklch(0.145 0 0); - --primary: #8ec07c; - --primary-muted: #689d6a; - --primary-foreground: oklch(0.985 0 0); - --secondary: oklch(0.97 0 0); - --secondary-foreground: oklch(0.205 0 0); - --muted: #bdae93; - --muted-foreground: #928374; - --accent: oklch(0.97 0 0); - --accent-foreground: oklch(0.205 0 0); - --destructive: oklch(0.577 0.245 27.325); - --border: oklch(0.922 0 0); - --input: oklch(0.922 0 0); - --ring: oklch(0.708 0 0); - --chart-1: oklch(0.646 0.222 41.116); - --chart-2: oklch(0.6 0.118 184.704); - --chart-3: oklch(0.398 0.07 227.392); - --chart-4: oklch(0.828 0.189 84.429); - --chart-5: oklch(0.769 0.188 70.08); - --sidebar-foreground: oklch(0.145 0 0); - --sidebar-primary: oklch(0.205 0 0); - --sidebar-primary-foreground: oklch(0.985 0 0); - --sidebar-accent: oklch(0.97 0 0); - --sidebar-accent-foreground: oklch(0.205 0 0); - --sidebar-border: oklch(0.922 0 0); - --sidebar-ring: oklch(0.708 0 0); - --font-monospaced: "IBM Plex Mono"; - --font-sansserif: "IBM Plex Sans", sans-serif; + --radius: 0.3rem; + --background: #282828; + --foreground: #ebdbb2; + --sidebar: #3c3836; + --color-red-light: #fb4934; + --color-orange-light: #fe8019; + --color-green-light: #b8bb26; + --color-aqua-muted: #689d6a; + --card: oklch(1 0 0); + --card-foreground: oklch(0.145 0 0); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.145 0 0); + --primary: #8ec07c; + --primary-muted: #689d6a; + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.97 0 0); + --secondary-foreground: oklch(0.205 0 0); + --muted: #bdae93; + --muted-foreground: #928374; + --accent: oklch(0.97 0 0); + --accent-foreground: oklch(0.205 0 0); + --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.922 0 0); + --input: oklch(0.922 0 0); + --ring: oklch(0.708 0 0); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --sidebar-foreground: oklch(0.145 0 0); + --sidebar-primary: oklch(0.205 0 0); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.97 0 0); + --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.922 0 0); + --sidebar-ring: oklch(0.708 0 0); + --font-monospaced: "IBM Plex Mono"; + --font-sansserif: "IBM Plex Sans", sans-serif; } @theme inline { - --radius-sm: calc(var(--radius) - 4px); - --radius-md: calc(var(--radius) - 2px); - --radius-lg: var(--radius); - --radius-xl: calc(var(--radius)); - --color-background: var(--background); - --color-foreground: var(--foreground); - --color-card: var(--card); - --color-card-foreground: var(--card-foreground); - --color-popover: var(--popover); - --color-popover-foreground: var(--popover-foreground); - --color-primary: var(--primary); - --color-primary-foreground: var(--primary-foreground); - --color-secondary: var(--secondary); - --color-secondary-foreground: var(--secondary-foreground); - --color-muted: var(--muted); - --color-muted-foreground: var(--muted-foreground); - --color-accent: var(--accent); - --color-accent-foreground: var(--accent-foreground); - --color-destructive: var(--destructive); - --color-border: var(--border); - --color-input: var(--input); - --color-ring: var(--ring); - --color-chart-1: var(--chart-1); - --color-chart-2: var(--chart-2); - --color-chart-3: var(--chart-3); - --color-chart-4: var(--chart-4); - --color-chart-5: var(--chart-5); - --color-sidebar: var(--sidebar); - --color-sidebar-foreground: var(--sidebar-foreground); - --color-sidebar-primary: var(--sidebar-primary); - --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); - --color-sidebar-accent: var(--sidebar-accent); - --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); - --color-sidebar-border: var(--sidebar-border); - --color-sidebar-ring: var(--sidebar-ring); + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius)); + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); } diff --git a/src/templates/BlogTemplate.jsx b/src/templates/BlogTemplate.jsx index b8cb3ad..4f2afb5 100644 --- a/src/templates/BlogTemplate.jsx +++ b/src/templates/BlogTemplate.jsx @@ -5,41 +5,41 @@ import { convertDate } from "@/utils/convertDate" import { usePosts } from "@/hooks/usePosts" const BlogTemplate = () => { - const { slug } = useParams() - const { posts } = usePosts() - const post = posts?.find((x) => x.slug === slug) + const { slug } = useParams() + const { posts } = usePosts() + const post = posts?.find((x) => x.slug === slug) - return ( - -
    - {!post ? ( -
    Loading...
    - ) : ( -
    -
    -

    - {post?.title} -

    -
    - -
    - {post?.tags?.map((tag, i) => ( - - {tag} - - ))} -
    -
    -
    + return ( + +
    + {!post ? ( +
    Loading...
    + ) : ( +
    +
    +

    + {post?.title} +

    +
    + +
    + {post?.tags?.map((tag, i) => ( + + {tag} + + ))} +
    +
    +
    -
    h2]:text-2xl [&>h2]:font-medium [&>h2]:my-4 [&>h2]:text-[#fabd2f]! [&>h3]:text-xl [&>h3]:font-medium [&>h3]:my-4 [&>h3]:text-[#fabd2f] [&>h4]:text-lg [&>h4]:font-medium [&>h4]:my-4 [&>h4]:text-[#fabd2f] @@ -65,13 +65,13 @@ const BlogTemplate = () => { [&>table>tbody>tr]:m-0 [&>table>tbody>tr]:border-t [&>table>tbody>tr]:p-0 [&>table>tbody>tr:even]:bg-muted [&>table>tbody>tr>td]:border [&>table>tbody>tr>td]:px-4 [&>table>tbody>tr>td]:py-2 [&>table>tbody>tr>td]:text-left [&>table>tbody>tr>td[align=center]]:text-center [&>table>tbody>tr>td[align=right]]:text-right " - dangerouslySetInnerHTML={{ __html: post?.html }} - /> -
    - )} -
    -
    - ) + dangerouslySetInnerHTML={{ __html: post?.html }} + /> +
    + )} +
    +
    + ) } export default BlogTemplate diff --git a/src/templates/MainTemplate.jsx b/src/templates/MainTemplate.jsx index 37b73d4..213c0d4 100644 --- a/src/templates/MainTemplate.jsx +++ b/src/templates/MainTemplate.jsx @@ -3,82 +3,66 @@ import gruvboxComputer from "../images/gruvbox-computer.svg" import { Link } from "react-router" const Header = () => { - return ( -
    - -
    - ) + return ( +
    + +
    + ) } const Footer = () => { - return ( - - ) + return ( + + ) } const MainTemplate = ({ children }) => { - return ( -
    -
    -
    -
    {children}
    -
    -
    -
    - ) + return ( +
    +
    +
    +
    {children}
    +
    +
    + ) } export default MainTemplate diff --git a/src/utils/convertDate.js b/src/utils/convertDate.js index 5917668..fc054dd 100644 --- a/src/utils/convertDate.js +++ b/src/utils/convertDate.js @@ -39,4 +39,12 @@ const convertDate = (isoStamp) => { return `${year}-${month}-${day}` } -export { convertDate } +const convertDateFriendly = (isoStamp) => { + const unixSeconds = new Date(isoStamp) + const day = unixSeconds.getDate() + const month = months[unixSeconds.getMonth()] + const year = unixSeconds.getFullYear() + return `${day} ${month} ${year}` +} + +export { convertDate, convertDateFriendly }