Merge pull request #765 from arifszn/v4

Release v4
This commit is contained in:
Ariful Alam 2025-07-06 18:22:09 +06:00 committed by GitHub
commit 8f413562e9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 1041 additions and 1425 deletions

View File

@ -43,7 +43,7 @@
**Features:**
✓ [Easy to Setup](#-installation--setup)
✓ [33 Themes](#themes)
✓ [37 Themes](#themes)
✓ [Google Analytics](#google-analytics)
✓ [Hotjar](#hotjar)
✓ [SEO](#seo)
@ -202,11 +202,7 @@ const CONFIG = {
],
},
},
seo: {
title: 'Portfolio of Ariful Alam',
description: '',
imageURL: '',
},
seo: { title: 'Portfolio of Ariful Alam', description: '', imageURL: '' },
social: {
linkedin: 'ariful-alam',
x: 'arif_szn',
@ -317,10 +313,7 @@ const CONFIG = {
id: '', // GA3 tracking id/GA4 tag id UA-XXXXXXXXX-X | G-XXXXXXXXXX
},
// Track visitor interaction and behavior. https://www.hotjar.com
hotjar: {
id: '',
snippetVersion: 6,
},
hotjar: { id: '', snippetVersion: 6 },
themeConfig: {
defaultTheme: 'nord',
@ -369,19 +362,11 @@ const CONFIG = {
'dim',
'nord',
'sunset',
'caramellatte',
'abyss',
'silk',
'procyon',
],
// Custom theme, applied to `procyon` theme
customTheme: {
primary: '#fc055b',
secondary: '#219aaf',
accent: '#e8d03a',
neutral: '#2A2730',
'base-100': '#E3E3ED',
'--rounded-box': '3rem',
'--rounded-btn': '3rem',
},
},
// Optional Footer. Supports plain text or HTML.
@ -399,7 +384,7 @@ export default CONFIG;
### Themes
There are 33 themes available that can be selected from the dropdown.
There are 37 themes available that can be selected from the dropdown.
The default theme can be specified.
@ -418,34 +403,23 @@ const CONFIG = {
<img src="https://github.com/arifszn/gitprofile/assets/45073703/91a2d9e6-67e5-47b4-9752-1881ac0f907f" alt="Theme Dropdown" width="50%">
</p>
You can create your own custom theme by modifying these values. Theme `procyon` will have the custom styles.
You can create your own custom theme by modifying the CSS variables in `src/assets/index.css`. Theme `procyon` is defined as a custom theme.
```ts
// gitprofile.config.ts
const CONFIG = {
/**
* Defines the custom theme colors and styles for the application.
* The theme includes the following properties:
* - `primary`: The primary color used throughout the application.
* - `secondary`: The secondary color used for accents and highlights.
* - `accent`: The accent color used for special elements.
* - `neutral`: The neutral color used for backgrounds and text.
* - `base-100`: The base background color.
* - `--rounded-box`: The border radius for boxes and containers.
* - `--rounded-btn`: The border radius for buttons.
*/
themeConfig: {
customTheme: {
primary: '#fc055b',
secondary: '#219aaf',
accent: '#e8d03a',
neutral: '#2A2730',
'base-100': '#E3E3ED',
'--rounded-box': '3rem',
'--rounded-btn': '3rem',
},
},
};
```css
/* src/assets/index.css */
@plugin "daisyui/theme" {
name: 'procyon';
color-scheme: light;
--color-base-100: #e3e3ed;
--color-base-200: #d1d1db;
--color-base-300: #bfbfc9;
--color-base-content: #2a2730;
--color-primary: #fc055b;
--color-primary-content: #ffffff;
--color-secondary: #219aaf;
--color-secondary-content: #ffffff;
}
```
### Google Analytics
@ -456,9 +430,7 @@ const CONFIG = {
// gitprofile.config.ts
const CONFIG = {
// ...
googleAnalytics: {
id: 'G-XXXXXXXXX',
},
googleAnalytics: { id: 'G-XXXXXXXXX' },
};
```
@ -472,10 +444,7 @@ Besides tracking visitors, it will track `click events` on projects and blog pos
// gitprofile.config.ts
const CONFIG = {
// ...
hotjar: {
id: '',
snippetVersion: 6,
},
hotjar: { id: '', snippetVersion: 6 },
};
```
@ -487,11 +456,7 @@ You can customize the meta tags for SEO in `seo`.
// gitprofile.config.ts
const CONFIG = {
// ...
seo: {
title: 'Portfolio of Ariful Alam',
description: '',
imageURL: '',
},
seo: { title: 'Portfolio of Ariful Alam', description: '', imageURL: '' },
};
```
@ -730,11 +695,7 @@ If you have [medium](https://medium.com) or [dev](https://dev.to) account, you c
// gitprofile.config.ts
const CONFIG = {
// ...
blog: {
source: 'dev',
username: 'arifszn',
limit: 5,
},
blog: { source: 'dev', username: 'arifszn', limit: 5 },
};
```

View File

@ -51,11 +51,7 @@ const CONFIG = {
],
},
},
seo: {
title: 'Portfolio of Ariful Alam',
description: '',
imageURL: '',
},
seo: { title: 'Portfolio of Ariful Alam', description: '', imageURL: '' },
social: {
linkedin: 'ariful-alam',
x: 'arif_szn',
@ -166,10 +162,7 @@ const CONFIG = {
id: '', // GA3 tracking id/GA4 tag id UA-XXXXXXXXX-X | G-XXXXXXXXXX
},
// Track visitor interaction and behavior. https://www.hotjar.com
hotjar: {
id: '',
snippetVersion: 6,
},
hotjar: { id: '', snippetVersion: 6 },
themeConfig: {
defaultTheme: 'lofi',
@ -218,19 +211,11 @@ const CONFIG = {
'dim',
'nord',
'sunset',
'caramellatte',
'abyss',
'silk',
'procyon',
],
// Custom theme, applied to `procyon` theme
customTheme: {
primary: '#fc055b',
secondary: '#219aaf',
accent: '#e8d03a',
neutral: '#2A2730',
'base-100': '#E3E3ED',
'--rounded-box': '3rem',
'--rounded-btn': '3rem',
},
},
// Optional Footer. Supports plain text or HTML.

42
global.d.ts vendored
View File

@ -284,43 +284,6 @@ interface Blog {
limit?: number;
}
interface CustomTheme {
/**
* Primary color
*/
primary?: string;
/**
* Secondary color
*/
secondary?: string;
/**
* Accent color
*/
accent?: string;
/**
* Neutral color
*/
neutral?: string;
/**
* Base color of page
*/
'base-100'?: string;
/**
* Border radius of rounded-box
*/
'--rounded-box'?: string;
/**
* Border radius of rounded-btn
*/
'--rounded-btn'?: string;
}
interface ThemeConfig {
/**
* Default theme
@ -346,11 +309,6 @@ interface ThemeConfig {
* Available themes
*/
themes?: Array<string>;
/**
* Custom theme
*/
customTheme?: CustomTheme;
}
interface Config {

View File

@ -23,6 +23,7 @@
<meta name="twitter:title" content="<%- metaTitle %>" />
<meta name="twitter:description" content="<%- metaDescription %>" />
<meta name="twitter:image" content="<%- metaImageURL %>" />
<%- googleAnalyticsScript %>
</head>
<body>
<div id="root"></div>

1567
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
{
"name": "@arifszn/gitprofile",
"description": "Create an automatic portfolio based on GitHub profile",
"version": "3.1.0",
"version": "4.0.0",
"type": "module",
"license": "MIT",
"author": "arifszn",
@ -22,31 +22,30 @@
"preview": "vite preview"
},
"dependencies": {
"react": "^18.3.1",
"react-dom": "^18.3.1"
"react": "^19.1.0",
"react-dom": "^19.1.0"
},
"devDependencies": {
"@arifszn/blog-js": "^2.0.6",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@tailwindcss/postcss": "^4.1.11",
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
"@typescript-eslint/eslint-plugin": "^7.8.0",
"@typescript-eslint/parser": "^7.3.1",
"@vitejs/plugin-react": "^4.4.1",
"autoprefixer": "^10.4.20",
"axios": "^1.10.0",
"daisyui": "^4.11.1",
"daisyui": "^5.0.43",
"date-fns": "^4.1.0",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-config-prettier": "^10.1.5",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-react-hooks": "^5.1.0",
"eslint-plugin-react-refresh": "^0.4.20",
"postcss": "^8.4.38",
"prettier": "^3.3.2",
"react-helmet-async": "^2.0.5",
"prettier": "^3.6.2",
"react-hotjar": "^6.3.1",
"react-icons": "^5.4.0",
"tailwindcss": "^3.4.3",
"tailwindcss": "^4.1.11",
"typescript": "^5.8.3",
"vail": "^1.0.3",
"vite": "^7.0.2",

View File

@ -1,6 +1,5 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
'@tailwindcss/postcss': {},
},
};

View File

@ -1,6 +1,55 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@import 'tailwindcss';
@plugin "daisyui" {
themes:
'light', 'dark', 'cupcake', 'bumblebee', 'emerald', 'corporate',
'synthwave', 'retro', 'cyberpunk', 'valentine', 'halloween', 'garden',
'forest', 'aqua', 'lofi', 'pastel', 'fantasy', 'wireframe', 'black',
'luxury', 'dracula', 'cmyk', 'autumn', 'business', 'acid', 'lemonade',
'night', 'coffee', 'winter', 'dim', 'nord', 'sunset', 'caramellatte',
'abyss', 'silk', 'procyon';
}
@plugin "daisyui/theme" {
name: 'procyon';
color-scheme: light;
--color-base-100: #e3e3ed;
--color-base-200: #d1d1db;
--color-base-300: #bfbfc9;
--color-base-content: #2a2730;
--color-primary: #fc055b;
--color-primary-content: #ffffff;
--color-secondary: #219aaf;
--color-secondary-content: #ffffff;
--color-accent: #e8d03a;
--color-accent-content: #2a2730;
--color-neutral: #2a2730;
--color-neutral-content: #ffffff;
--color-info: #219aaf;
--color-info-content: #ffffff;
--color-success: #16a34a;
--color-success-content: #ffffff;
--color-warning: #e8d03a;
--color-warning-content: #2a2730;
--color-error: #dc2626;
--color-error-content: #ffffff;
/* border radius */
--radius-selector: 3rem;
--radius-field: 3rem;
--radius-box: 3rem;
/* base sizes */
--size-selector: 0.25rem;
--size-field: 0.25rem;
/* border size */
--border: 1px;
/* effects */
--depth: 1;
--noise: 0;
}
* {
scrollbar-width: thin;
@ -40,10 +89,6 @@ code {
source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace;
}
.text-base-content-important {
color: hsla(var(--bc) / var(--tw-text-opacity)) !important;
}
svg {
vertical-align: unset;
}

View File

@ -23,9 +23,9 @@ const AvatarCard: React.FC<AvatarCardProps> = ({
loading,
avatarRing,
resumeFileUrl,
}): JSX.Element => {
}): React.JSX.Element => {
return (
<div className="card shadow-lg compact bg-base-100">
<div className="card shadow-lg card-sm bg-base-100">
<div className="grid place-items-center py-8">
{loading || !profile ? (
<div className="avatar opacity-90">
@ -42,7 +42,7 @@ const AvatarCard: React.FC<AvatarCardProps> = ({
<div
className={`mb-8 rounded-full w-32 h-32 ${
avatarRing
? 'ring ring-primary ring-offset-base-100 ring-offset-2'
? 'ring-3 ring-primary ring-offset-base-100 ring-offset-2'
: ''
}`}
>
@ -70,7 +70,7 @@ const AvatarCard: React.FC<AvatarCardProps> = ({
</span>
)}
</h5>
<div className="mt-3 text-base-content text-opacity-60 font-mono">
<div className="mt-3 text-base-content font-mono">
{loading || !profile
? skeleton({ widthCls: 'w-48', heightCls: 'h-5' })
: profile.bio}

View File

@ -1,6 +1,6 @@
import { useEffect, useState } from 'react';
import LazyImage from '../lazy-image';
import { AiOutlineContainer } from 'react-icons/ai';
import { PiNewspaper } from 'react-icons/pi';
import { getDevPost, getMediumPost } from '@arifszn/blog-js';
import { formatDistance } from 'date-fns';
import { SanitizedBlog } from '../../interfaces/sanitized-config';
@ -38,7 +38,7 @@ const BlogCard = ({
const array = [];
for (let index = 0; index < blog.limit; index++) {
array.push(
<div className="card shadow-lg compact bg-base-100" key={index}>
<div className="card shadow-md card-sm bg-base-100" key={index}>
<div className="p-8 h-full w-full">
<div className="flex items-center flex-col md:flex-row">
<div className="avatar mb-5 md:mb-0">
@ -95,7 +95,7 @@ const BlogCard = ({
return articles && articles.length ? (
articles.slice(0, blog.limit).map((article, index) => (
<a
className="card shadow-lg compact bg-base-100 cursor-pointer"
className="card shadow-md card-sm bg-base-100 cursor-pointer"
key={index}
href={article.link}
onClick={(e) => {
@ -140,7 +140,7 @@ const BlogCard = ({
addSuffix: true,
})}
</p>
<p className="mt-3 text-base-content text-opacity-60 text-sm">
<p className="mt-3 text-base-content text-sm">
{article.description}
</p>
<div className="mt-4 flex items-center flex-wrap justify-center md:justify-start">
@ -162,7 +162,7 @@ const BlogCard = ({
))
) : (
<div className="text-center mb-6">
<AiOutlineContainer className="mx-auto h-12 w-12 opacity-30" />
<PiNewspaper className="mx-auto h-12 w-12 opacity-30" />
<p className="mt-1 text-sm opacity-50 text-base-content">
No recent post
</p>
@ -172,37 +172,41 @@ const BlogCard = ({
return (
<div className="col-span-1 lg:col-span-2">
<div className="grid grid-cols-2 gap-6">
<div className="col-span-2">
<div
className={`card compact bg-base-100 ${
loading || (articles && articles.length)
? 'shadow bg-opacity-40'
: 'shadow-lg'
}`}
>
<div className="card-body">
<div className="mx-3 mb-2">
<h5 className="card-title">
<div className="card bg-base-200 shadow-xl border border-base-300">
<div className="card-body p-8">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 mb-8">
<div className="flex items-center space-x-3">
{loading ? (
skeleton({ widthCls: 'w-28', heightCls: 'h-8' })
skeleton({
widthCls: 'w-12',
heightCls: 'h-12',
className: 'rounded-xl',
})
) : (
<span className="text-base-content opacity-70">
My Articles
</span>
)}
</h5>
<div className="flex items-center justify-center w-12 h-12 bg-primary/10 rounded-xl">
<PiNewspaper className="text-2xl" />
</div>
)}
<div className="min-w-0 flex-1">
<h3 className="text-base sm:text-lg font-bold text-base-content truncate">
{loading
? skeleton({ widthCls: 'w-28', heightCls: 'h-8' })
: 'My Articles'}
</h3>
<div className="text-base-content/60 text-xs sm:text-sm mt-1 truncate">
{loading
? skeleton({ widthCls: 'w-32', heightCls: 'h-4' })
: 'Recent posts'}
</div>
</div>
</div>
</div>
<div className="col-span-2">
<div className="grid grid-cols-1 gap-6">
{loading || !articles ? renderSkeleton() : renderArticles()}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
);
};

View File

@ -59,7 +59,7 @@ const CertificationCard = ({
};
return (
<div className="card shadow-lg compact bg-base-100">
<div className="card shadow-lg card-sm bg-base-100">
<div className="card-body">
<div className="mx-3">
<h5 className="card-title">
@ -72,7 +72,7 @@ const CertificationCard = ({
)}
</h5>
</div>
<div className="text-base-content text-opacity-60">
<div className="text-base-content">
<ol className="relative border-l border-base-300 border-opacity-30 my-2 mx-4">
{loading ? (
renderSkeleton()

View File

@ -66,12 +66,12 @@ const ListItem: React.FC<{
}> = ({ icon, title, value, link, skeleton = false }) => {
return (
<div className="flex justify-start py-2 px-1 items-center">
<div className="flex-grow font-medium gap-2 flex items-center my-1">
<div className="grow font-medium gap-2 flex items-center my-1">
{icon} {title}
</div>
<div
className={`${
skeleton ? 'flex-grow' : ''
skeleton ? 'grow' : ''
} text-sm font-normal text-right mr-2 ml-3 ${link ? 'truncate' : ''}`}
style={{
wordBreak: 'break-word',
@ -124,12 +124,12 @@ const OrganizationItem: React.FC<{
return (
<div className="flex justify-start py-2 px-1 items-center">
<div className="flex-grow font-medium gap-2 flex items-center my-1">
<div className="grow font-medium gap-2 flex items-center my-1">
{icon} {title}
</div>
<div
className={`${
skeleton ? 'flex-grow' : ''
skeleton ? 'grow' : ''
} text-sm font-normal text-right mr-2 ml-3 space-x-2 ${link ? 'truncate' : ''}`}
style={{
wordBreak: 'break-word',
@ -169,9 +169,9 @@ const DetailsCard = ({ profile, loading, social, github }: Props) => {
};
return (
<div className="card shadow-lg compact bg-base-100">
<div className="card shadow-lg card-sm bg-base-100">
<div className="card-body">
<div className="text-base-content text-opacity-60">
<div className="text-base-content">
{loading || !profile ? (
renderSkeleton()
) : (

View File

@ -53,7 +53,7 @@ const EducationCard = ({
};
return (
<div className="card shadow-lg compact bg-base-100">
<div className="card shadow-lg card-sm bg-base-100">
<div className="card-body">
<div className="mx-3">
<h5 className="card-title">
@ -64,7 +64,7 @@ const EducationCard = ({
)}
</h5>
</div>
<div className="text-base-content text-opacity-60">
<div className="text-base-content">
<ol className="relative border-l border-base-300 border-opacity-30 my-2 mx-4">
{loading ? (
renderSkeleton()

View File

@ -16,9 +16,7 @@ const ErrorPage: React.FC<CustomError> = (props) => {
{`${props.status}`}
</h1>
<p className="text-lg pb-2 text-base-content">{props.title}</p>
<div className="text-base-content text-opacity-60">
{props.subTitle}
</div>
<div className="text-base-content">{props.subTitle}</div>
</div>
</div>
</div>

View File

@ -58,7 +58,7 @@ const ExperienceCard = ({
return array;
};
return (
<div className="card shadow-lg compact bg-base-100">
<div className="card shadow-lg card-sm bg-base-100">
<div className="card-body">
<div className="mx-3">
<h5 className="card-title">
@ -69,7 +69,7 @@ const ExperienceCard = ({
)}
</h5>
</div>
<div className="text-base-content text-opacity-60">
<div className="text-base-content">
<ol className="relative border-l border-base-300 border-opacity-30 my-2 mx-4">
{loading ? (
renderSkeleton()

View File

@ -1,5 +1,6 @@
import { Fragment } from 'react';
import LazyImage from '../lazy-image';
import { MdOpenInNew } from 'react-icons/md';
import { ga, skeleton } from '../../utils';
import { SanitizedExternalProject } from '../../interfaces/sanitized-config';
@ -18,7 +19,7 @@ const ExternalProjectCard = ({
const array = [];
for (let index = 0; index < externalProjects.length; index++) {
array.push(
<div className="card shadow-lg compact bg-base-100" key={index}>
<div className="card shadow-md card-sm bg-base-100" key={index}>
<div className="p-8 h-full w-full">
<div className="flex items-center flex-col">
<div className="w-full">
@ -69,7 +70,7 @@ const ExternalProjectCard = ({
const renderExternalProjects = () => {
return externalProjects.map((item, index) => (
<a
className="card shadow-lg compact bg-base-100 cursor-pointer"
className="card shadow-md card-sm bg-base-100 cursor-pointer"
key={index}
href={item.link}
onClick={(e) => {
@ -111,7 +112,7 @@ const ExternalProjectCard = ({
</div>
</div>
)}
<p className="mt-2 text-base-content text-opacity-60 text-sm text-justify">
<p className="mt-2 text-base-content text-sm text-justify">
{item.description}
</p>
</div>
@ -126,31 +127,41 @@ const ExternalProjectCard = ({
return (
<Fragment>
<div className="col-span-1 lg:col-span-2">
<div className="grid grid-cols-2 gap-6">
<div className="col-span-2">
<div className="card compact bg-base-100 shadow bg-opacity-40">
<div className="card-body">
<div className="mx-3 flex items-center justify-between mb-2">
<h5 className="card-title">
<div className="card bg-base-200 shadow-xl border border-base-300">
<div className="card-body p-8">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 mb-8">
<div className="flex items-center space-x-3">
{loading ? (
skeleton({ widthCls: 'w-40', heightCls: 'h-8' })
skeleton({
widthCls: 'w-12',
heightCls: 'h-12',
className: 'rounded-xl',
})
) : (
<span className="text-base-content opacity-70">
{header}
</span>
)}
</h5>
<div className="flex items-center justify-center w-12 h-12 bg-primary/10 rounded-xl">
<MdOpenInNew className="text-2xl" />
</div>
)}
<div className="min-w-0 flex-1">
<h3 className="text-base sm:text-lg font-bold text-base-content truncate">
{loading
? skeleton({ widthCls: 'w-40', heightCls: 'h-8' })
: header}
</h3>
<div className="text-base-content/60 text-xs sm:text-sm mt-1 truncate">
{loading
? skeleton({ widthCls: 'w-32', heightCls: 'h-4' })
: `Showcasing ${externalProjects.length} external projects`}
</div>
</div>
</div>
</div>
<div className="col-span-2">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{loading ? renderSkeleton() : renderExternalProjects()}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</Fragment>
);
};

View File

@ -1,5 +1,5 @@
import { Fragment } from 'react';
import { AiOutlineFork, AiOutlineStar } from 'react-icons/ai';
import { AiOutlineFork, AiOutlineStar, AiOutlineGithub } from 'react-icons/ai';
import { MdInsertLink } from 'react-icons/md';
import { ga, getLanguageColor, skeleton } from '../../utils';
import { GithubProject } from '../../interfaces/github-project';
@ -9,14 +9,12 @@ const GithubProjectCard = ({
githubProjects,
loading,
limit,
username,
googleAnalyticsId,
}: {
header: string;
githubProjects: GithubProject[];
loading: boolean;
limit: number;
username: string;
googleAnalyticsId?: string;
}) => {
if (!loading && githubProjects.length === 0) {
@ -27,7 +25,7 @@ const GithubProjectCard = ({
const array = [];
for (let index = 0; index < limit; index++) {
array.push(
<div className="card shadow-lg compact bg-base-100" key={index}>
<div className="card shadow-md card-sm bg-base-100" key={index}>
<div className="flex justify-between flex-col p-8 h-full w-full">
<div>
<div className="flex items-center">
@ -51,7 +49,7 @@ const GithubProjectCard = ({
</div>
</div>
<div className="flex justify-between">
<div className="flex flex-grow">
<div className="flex grow">
<span className="mr-3 flex items-center">
{skeleton({ widthCls: 'w-12', heightCls: 'h-4' })}
</span>
@ -76,7 +74,7 @@ const GithubProjectCard = ({
const renderProjects = () => {
return githubProjects.map((item, index) => (
<a
className="card shadow-lg compact bg-base-100 cursor-pointer"
className="card shadow-md card-sm bg-base-100 cursor-pointer"
href={item.html_url}
key={index}
onClick={(e) => {
@ -84,9 +82,7 @@ const GithubProjectCard = ({
try {
if (googleAnalyticsId) {
ga.event('Click project', {
project: item.name,
});
ga.event('Click project', { project: item.name });
}
} catch (error) {
console.error(error);
@ -103,12 +99,12 @@ const GithubProjectCard = ({
<span>{item.name}</span>
</div>
</div>
<p className="mb-5 mt-1 text-base-content text-opacity-60 text-sm">
<p className="mb-5 mt-1 text-base-content text-sm">
{item.description}
</p>
</div>
<div className="flex justify-between text-sm text-base-content text-opacity-60 truncate">
<div className="flex flex-grow">
<div className="flex justify-between text-sm text-base-content truncate">
<div className="flex grow">
<span className="mr-3 flex items-center">
<AiOutlineStar className="mr-0.5" />
<span>{item.stargazers_count}</span>
@ -136,43 +132,44 @@ const GithubProjectCard = ({
return (
<Fragment>
<div className="col-span-1 lg:col-span-2">
<div className="grid grid-cols-2 gap-6">
<div className="col-span-2">
<div className="card compact bg-base-100 shadow bg-opacity-40">
<div className="card-body">
<div className="mx-3 flex items-center justify-between mb-2">
<h5 className="card-title">
<div className="card bg-base-200 shadow-xl border border-base-300">
<div className="card-body p-8">
{/* Enhanced Header Section */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 mb-8">
<div className="flex items-center space-x-3">
{loading ? (
skeleton({ widthCls: 'w-40', heightCls: 'h-8' })
skeleton({
widthCls: 'w-12',
heightCls: 'h-12',
className: 'rounded-xl',
})
) : (
<span className="text-base-content opacity-70">
{header}
</span>
)}
</h5>
{loading ? (
skeleton({ widthCls: 'w-10', heightCls: 'h-5' })
) : (
<a
href={`https://github.com/${username}?tab=repositories`}
target="_blank"
rel="noreferrer"
className="text-base-content opacity-50 hover:underline"
>
See All
</a>
)}
<div className="flex items-center justify-center w-12 h-12 bg-primary/10 rounded-xl">
<AiOutlineGithub className="text-2xl" />
</div>
<div className="col-span-2">
)}
<div className="min-w-0 flex-1">
<h3 className="text-base sm:text-lg font-bold text-base-content truncate">
{loading
? skeleton({ widthCls: 'w-48', heightCls: 'h-8' })
: header}
</h3>
<div className="text-base-content/60 text-xs sm:text-sm mt-1 truncate">
{loading
? skeleton({ widthCls: 'w-32', heightCls: 'h-4' })
: `Showcasing ${githubProjects.length} featured repositories`}
</div>
</div>
</div>
</div>
{/* Projects Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{loading ? renderSkeleton() : renderProjects()}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</Fragment>
);
};

View File

@ -8,12 +8,10 @@ import {
INVALID_GITHUB_USERNAME_ERROR,
setTooManyRequestError,
} from '../constants/errors';
import { HelmetProvider } from 'react-helmet-async';
import '../assets/index.css';
import { getInitialTheme, getSanitizedConfig, setupHotjar } from '../utils';
import { SanitizedConfig } from '../interfaces/sanitized-config';
import ErrorPage from './error-page';
import HeadTagEditor from './head-tag-editor';
import { DEFAULT_THEMES } from '../constants/default-themes';
import ThemeChanger from './theme-changer';
import { BG_COLOR } from '../constants';
@ -180,7 +178,6 @@ const GitProfile = ({ config }: { config: Config }) => {
};
return (
<HelmetProvider>
<div className="fade-in h-screen">
{error ? (
<ErrorPage
@ -190,10 +187,6 @@ const GitProfile = ({ config }: { config: Config }) => {
/>
) : (
<>
<HeadTagEditor
googleAnalyticsId={sanitizedConfig.googleAnalytics.id}
appliedTheme={theme}
/>
<div className={`p-4 lg:p-10 min-h-full ${BG_COLOR}`}>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 rounded-box">
<div className="col-span-1">
@ -252,7 +245,6 @@ const GitProfile = ({ config }: { config: Config }) => {
limit={sanitizedConfig.projects.github.automatic.limit}
githubProjects={githubProjects}
loading={loading}
username={sanitizedConfig.github.username}
googleAnalyticsId={sanitizedConfig.googleAnalytics.id}
/>
)}
@ -262,8 +254,7 @@ const GitProfile = ({ config }: { config: Config }) => {
publications={sanitizedConfig.publications}
/>
)}
{sanitizedConfig.projects.external.projects.length !==
0 && (
{sanitizedConfig.projects.external.projects.length !== 0 && (
<ExternalProjectCard
loading={loading}
header={sanitizedConfig.projects.external.header}
@ -288,7 +279,7 @@ const GitProfile = ({ config }: { config: Config }) => {
<footer
className={`p-4 footer ${BG_COLOR} text-base-content footer-center`}
>
<div className="card compact bg-base-100 shadow">
<div className="card card-sm bg-base-100 shadow-sm">
<Footer content={sanitizedConfig.footer} loading={loading} />
</div>
</footer>
@ -296,7 +287,6 @@ const GitProfile = ({ config }: { config: Config }) => {
</>
)}
</div>
</HelmetProvider>
);
};

View File

@ -1,47 +0,0 @@
import { Helmet } from 'react-helmet-async';
import { isDarkishTheme } from '../../utils';
type HeadTagEditorProps = {
googleAnalyticsId?: string;
appliedTheme: string;
};
/**
* Renders the head tag editor component.
*
* @param {HeadTagEditorProps} googleAnalyticsId - The Google Analytics ID.
* @param {HeadTagEditorProps} appliedTheme - The applied theme.
* @return {React.ReactElement} The head tag editor component.
*/
const HeadTagEditor: React.FC<HeadTagEditorProps> = ({
googleAnalyticsId,
appliedTheme,
}) => {
return (
<Helmet>
<meta
name="theme-color"
content={isDarkishTheme(appliedTheme) ? '#000000' : '#ffffff'}
/>
{googleAnalyticsId && (
<>
<script
async
src={`https://www.googletagmanager.com/gtag/js?id=${googleAnalyticsId}`}
></script>
<script>
{`window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', '${googleAnalyticsId}');
`}
</script>
</>
)}
</Helmet>
);
};
export default HeadTagEditor;

View File

@ -1,4 +1,5 @@
import { Fragment } from 'react';
import { AiOutlineBook } from 'react-icons/ai';
import { SanitizedPublication } from '../../interfaces/sanitized-config';
import { skeleton } from '../../utils';
@ -13,7 +14,7 @@ const PublicationCard = ({
const array = [];
for (let index = 0; index < publications.length; index++) {
array.push(
<div className="card shadow-lg compact bg-base-100" key={index}>
<div className="card shadow-md card-sm bg-base-100" key={index}>
<div className="p-8 h-full w-full">
<div className="flex items-center flex-col">
<div className="w-full">
@ -76,7 +77,7 @@ const PublicationCard = ({
const renderPublications = () => {
return publications.map((item, index) => (
<a
className="card shadow-lg compact bg-base-100 cursor-pointer"
className="card shadow-md card-sm bg-base-100 cursor-pointer"
key={index}
href={item.link}
target="_blank"
@ -104,7 +105,7 @@ const PublicationCard = ({
</p>
)}
{item.description && (
<p className="mt-2 text-base-content text-opacity-60 text-sm text-justify">
<p className="mt-2 text-base-content text-sm text-justify">
{item.description}
</p>
)}
@ -120,31 +121,41 @@ const PublicationCard = ({
return (
<Fragment>
<div className="col-span-1 lg:col-span-2">
<div className="grid grid-cols-2 gap-6">
<div className="col-span-2">
<div className="card compact bg-base-100 shadow bg-opacity-40">
<div className="card-body">
<div className="mx-3 flex items-center justify-between mb-2">
<h5 className="card-title">
<div className="card bg-base-200 shadow-xl border border-base-300">
<div className="card-body p-8">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 mb-8">
<div className="flex items-center space-x-3">
{loading ? (
skeleton({ widthCls: 'w-40', heightCls: 'h-8' })
skeleton({
widthCls: 'w-12',
heightCls: 'h-12',
className: 'rounded-xl',
})
) : (
<span className="text-base-content opacity-70">
Publications
</span>
)}
</h5>
<div className="flex items-center justify-center w-12 h-12 bg-primary/10 rounded-xl">
<AiOutlineBook className="text-2xl" />
</div>
)}
<div className="min-w-0 flex-1">
<h3 className="text-base sm:text-lg font-bold text-base-content truncate">
{loading
? skeleton({ widthCls: 'w-40', heightCls: 'h-8' })
: 'Publications'}
</h3>
<div className="text-base-content/60 text-xs sm:text-sm mt-1 truncate">
{loading
? skeleton({ widthCls: 'w-32', heightCls: 'h-4' })
: `Showcasing ${publications.length} publications`}
</div>
</div>
</div>
</div>
<div className="col-span-2">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{loading ? renderSkeleton() : renderPublications()}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</Fragment>
);
};

View File

@ -21,7 +21,7 @@ const SkillCard = ({
};
return (
<div className="card shadow-lg compact bg-base-100">
<div className="card shadow-lg card-sm bg-base-100">
<div className="card-body">
<div className="mx-3">
<h5 className="card-title">
@ -33,14 +33,11 @@ const SkillCard = ({
</h5>
</div>
<div className="p-3 flow-root">
<div className="-m-1 flex flex-wrap justify-center">
<div className="-m-1 flex flex-wrap justify-center gap-2">
{loading
? renderSkeleton()
: skills.map((skill, index) => (
<div
key={index}
className="m-1 text-xs inline-flex items-center font-bold leading-sm px-3 py-1 badge-primary bg-opacity-90 rounded-full"
>
<div key={index} className="badge badge-primary badge-sm">
{skill}
</div>
))}

View File

@ -1,4 +1,4 @@
import { AiOutlineControl } from 'react-icons/ai';
import { RiDice4Line } from 'react-icons/ri';
import { SanitizedThemeConfig } from '../../interfaces/sanitized-config';
import { LOCAL_STORAGE_KEY_NAME } from '../../constants';
import { skeleton } from '../../utils';
@ -40,7 +40,7 @@ const ThemeChanger = ({
};
return (
<div className="card overflow-visible shadow-lg compact bg-base-100">
<div className="card overflow-visible shadow-lg card-sm bg-base-100">
<div className="flex-row items-center space-x-4 flex pl-6 pr-2 py-4">
<div className="flex-1">
<h5 className="card-title">
@ -54,7 +54,7 @@ const ThemeChanger = ({
<span className="text-base-content opacity-70">Theme</span>
)}
</h5>
<span className="text-base-content text-opacity-40 capitalize text-sm">
<span className="text-base-content/50 capitalize text-sm">
{loading
? skeleton({ widthCls: 'w-16', heightCls: 'h-5' })
: theme === themeConfig.defaultTheme
@ -65,7 +65,7 @@ const ThemeChanger = ({
<div className="flex-0">
{loading ? (
skeleton({
widthCls: 'w-14 md:w-28',
widthCls: 'w-12',
heightCls: 'h-10',
className: 'mr-6',
})
@ -73,23 +73,15 @@ const ThemeChanger = ({
<div title="Change Theme" className="dropdown dropdown-end">
<div
tabIndex={0}
className="btn btn-ghost m-1 normal-case opacity-50 text-base-content"
className="btn btn-ghost m-1 normal-case opacity-50 text-base-content flex items-center whitespace-nowrap"
>
<AiOutlineControl className="inline-block w-5 h-5 stroke-current md:mr-2" />
<span className="hidden md:inline">Change Theme</span>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 1792 1792"
className="inline-block w-4 h-4 ml-1 fill-current"
>
<path d="M1395 736q0 13-10 23l-466 466q-10 10-23 10t-23-10l-466-466q-10-10-10-23t10-23l50-50q10-10 23-10t23 10l393 393 393-393q10-10 23-10t23 10l50 50q10 10 10 23z" />
</svg>
<RiDice4Line className="inline-block w-5 h-5 stroke-current" />
</div>
<div
tabIndex={0}
className="mt-16 overflow-y-auto shadow-2xl top-px dropdown-content max-h-96 w-52 rounded-lg bg-base-200 text-base-content z-10"
className="mt-16 overflow-y-auto shadow-2xl top-px dropdown-content max-h-96 min-w-max rounded-lg bg-base-200 text-base-content z-10"
>
<ul className="p-4 menu compact">
<ul className="p-4 menu menu-sm">
{[
themeConfig.defaultTheme,
...themeConfig.themes.filter(

View File

@ -1,9 +0,0 @@
export const DEFAULT_CUSTOM_THEME = {
primary: '#fc055b',
secondary: '#219aaf',
accent: '#e8d03a',
neutral: '#2A2730',
'base-100': '#E3E3ED',
'--rounded-box': '3rem',
'--rounded-btn': '3rem',
};

View File

@ -31,5 +31,8 @@ export const DEFAULT_THEMES = [
'dim',
'nord',
'sunset',
'caramellatte',
'abyss',
'silk',
'procyon',
];

View File

@ -116,23 +116,12 @@ export interface SanitizedBlog {
limit: number;
}
export interface SanitizedCustomTheme {
primary: string;
secondary: string;
accent: string;
neutral: string;
'base-100': string;
'--rounded-box': string;
'--rounded-btn': string;
}
export interface SanitizedThemeConfig {
defaultTheme: string;
disableSwitch: boolean;
respectPrefersColorScheme: boolean;
displayAvatarRing: boolean;
themes: Array<string>;
customTheme: SanitizedCustomTheme;
}
export interface SanitizedConfig {

View File

@ -1,6 +1,7 @@
import React from 'react';
import { hotjar } from 'react-hotjar';
import { LOCAL_STORAGE_KEY_NAME } from '../constants';
import { DEFAULT_CUSTOM_THEME } from '../constants/default-custom-theme';
import { DEFAULT_THEMES } from '../constants/default-themes';
import colors from '../data/colors.json';
import {
@ -124,29 +125,6 @@ export const getSanitizedConfig = (
config?.themeConfig?.respectPrefersColorScheme || false,
displayAvatarRing: config?.themeConfig?.displayAvatarRing ?? true,
themes: config?.themeConfig?.themes || DEFAULT_THEMES,
customTheme: {
primary:
config?.themeConfig?.customTheme?.primary ||
DEFAULT_CUSTOM_THEME.primary,
secondary:
config?.themeConfig?.customTheme?.secondary ||
DEFAULT_CUSTOM_THEME.secondary,
accent:
config?.themeConfig?.customTheme?.accent ||
DEFAULT_CUSTOM_THEME.accent,
neutral:
config?.themeConfig?.customTheme?.neutral ||
DEFAULT_CUSTOM_THEME.neutral,
'base-100':
config?.themeConfig?.customTheme?.['base-100'] ||
DEFAULT_CUSTOM_THEME['base-100'],
'--rounded-box':
config?.themeConfig?.customTheme?.['--rounded-box'] ||
DEFAULT_CUSTOM_THEME['--rounded-box'],
'--rounded-btn':
config?.themeConfig?.customTheme?.['--rounded-btn'] ||
DEFAULT_CUSTOM_THEME['--rounded-btn'],
},
},
footer: config?.footer,
enablePWA: config?.enablePWA ?? true,
@ -194,7 +172,7 @@ export const skeleton = ({
style?: React.CSSProperties;
shape?: string;
className?: string | null;
}): JSX.Element => {
}): React.JSX.Element => {
const classNames = ['bg-base-300', 'animate-pulse', shape];
if (className) {
classNames.push(className);

View File

@ -1,17 +0,0 @@
import CONFIG from './gitprofile.config';
/** @type {import('tailwindcss').Config} */
export default {
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
theme: {
extend: {},
},
plugins: [require('daisyui')],
daisyui: {
logs: false,
themes: [
...CONFIG.themeConfig.themes,
{ procyon: CONFIG.themeConfig.customTheme },
],
},
};

View File

@ -15,6 +15,16 @@ export default defineConfig({
metaTitle: CONFIG.seo.title,
metaDescription: CONFIG.seo.description,
metaImageURL: CONFIG.seo.imageURL,
googleAnalyticsScript: CONFIG.googleAnalytics.id
? `<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=${CONFIG.googleAnalytics.id}"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '${CONFIG.googleAnalytics.id}');
</script>`
: '',
},
},
}),