Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions .babelrc

This file was deleted.

26 changes: 0 additions & 26 deletions jest.config.js

This file was deleted.

1 change: 0 additions & 1 deletion next-env.d.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
/// <reference types="next/navigation-types/compat/navigation" />
/// <reference path="./.next/types/routes.d.ts" />

// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
18 changes: 13 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
"build": "next build",
"start": "next start",
"lint": "eslint src",
"test": "jest",
"test": "vitest",
"analyze": "ANALYZE=true yarn build"
},
"dependencies": {
Expand All @@ -60,29 +60,37 @@
"react-markdown": "^10.1.0",
"react-share": "^5.2.2",
"react-twc": "^1.4.2",
"rehype-raw": "^7.0.0",
"rehype-stringify": "^10.0.1",
"remark-parse": "^11.0.0",
"remark-rehype": "^11.1.2",
"tailwind-merge": "^3.3.0",
"tailwind-variants": "^1.0.0",
"tailwindcss": "^4.1.7",
"unified": "^11.0.5",
"uuid": "^11.1.0"
},
"devDependencies": {
"@eslint/js": "^9.27.0",
"@next/bundle-analyzer": "^15.3.2",
"@types/jest": "^29.5.14",
"@testing-library/react": "^16.3.0",
"@types/node": "22.15.18",
"@types/prismjs": "^1.26.5",
"@types/react": "19.1.4",
"@types/react-dom": "19.1.5",
"@types/uuid": "^10.0.0",
"@vitejs/plugin-react": "^5.1.0",
"@vitest/coverage-v8": "4.0.5",
"eslint": "^9.27.0",
"eslint-config-prettier": "^10.1.5",
"eslint-plugin-prettier": "^5.4.0",
"eslint-plugin-react": "^7.37.5",
"globals": "^16.1.0",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"jsdom": "^27.1.0",
"prettier": "^3.5.3",
"typescript": "^5.8.3",
"typescript-eslint": "^8.32.1"
"typescript-eslint": "^8.32.1",
"vite-tsconfig-paths": "^5.1.4",
"vitest": "^4.0.5"
}
}
2 changes: 1 addition & 1 deletion public/sw.js

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions public/workbox-1bb06f5e.js

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion public/workbox-588899ac.js

This file was deleted.

22 changes: 22 additions & 0 deletions src/components/canvas/actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,28 @@ export function CanvasActions() {
<Icon name="sun-moon" />
</Clickable.Button>
</Tooltip>

<Tooltip position="left" content="Import README" tone="brand">
<Clickable.Button
aria-label="Import README"
size="icon"
variant="icon"
tone="brand"
onClick={events.canvas.loadImportFile}
>
<Icon name="upload-cloud" />
</Clickable.Button>
</Tooltip>

<input
id="readme-file-import"
type="file"
accept=".md,text/markdown"
style={{
display: 'none',
}}
onChange={events.canvas.import}
/>
</div>
</div>
</>
Expand Down
4 changes: 3 additions & 1 deletion src/config/general/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ const general = {
},

stats: {
imageBaseUrl: 'https://github-readme-stats.vercel.app/api',
statsBaseUrl: 'https://github-readme-stats.vercel.app/api?username=',
languagesBaseUrl:
'https://github-readme-stats.vercel.app/api/top-langs?username=',
streakBaseUrl: 'https://streak-stats.demolab.com',
trophiBaseUrl: 'https://github-profile-trophy.vercel.app',
activityGraphBaseUrl:
Expand Down
21 changes: 20 additions & 1 deletion src/contexts/canvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import { events } from '@events';
import { Sections, CanvasSection, Events, PanelsEnum } from 'types';

import { deepChangeObjectProperty } from 'utils';
import { deepChangeObjectProperty, parseImportedReadme } from 'utils';
import { useExtensions, usePersistedState } from 'hooks';

type HandleAddSectionArgs = CustomEvent<Sections>;
Expand All @@ -24,6 +24,8 @@
children: React.ReactNode;
};

type ImportReadme = CustomEvent<React.ChangeEvent<HTMLInputElement>>;

const CanvasContext = createContext<CanvasContextData>({} as CanvasContextData);

const CanvasProvider = ({ children }: CanvasProviderProps) => {
Expand Down Expand Up @@ -53,6 +55,19 @@
setSections(state => [...state, newSection]);
};

const loadReadmeFile = async () => {
document.getElementById('readme-file-import')?.click();
};

const importReadme = async (event: ImportReadme) => {
const file = event?.detail?.target?.files?.[0];
if (!file) return; // TODO: toast error event
const text = await file.text();
const sections = await parseImportedReadme(text);
handleClearCanvas();
setSections(sections);
};

const handleEditSection = (event: HandleEditSectionArgs) => {
const { id = currentSection?.id, path, value } = event.detail;

Expand All @@ -75,7 +90,7 @@

const isEditingCurrentSection = currentSection?.id === id;

isEditingCurrentSection && setCurrentSection(result);

Check warning on line 93 in src/contexts/canvas.tsx

View workflow job for this annotation

GitHub Actions / ci

Expected an assignment or function call and instead saw an expression
};

const handleRemoveSection = (event: CustomEvent<string>) => {
Expand Down Expand Up @@ -180,6 +195,8 @@
events.on(Events.CANVAS_CLEAR_SECTIONS, handleClearCanvas);
events.on(Events.CANVAS_MOVE_SECTION_UP, moveSectionUp);
events.on(Events.CANVAS_MOVE_SECTION_DOWN, moveSectionDown);
events.on(Events.CANVAS_IMPORT_README, importReadme);
events.on(Events.CANVAS_IMPORT_README_FILE, loadReadmeFile);

return () => {
events.off(Events.CANVAS_EDIT_SECTION, handleEditSection);
Expand All @@ -190,6 +207,8 @@
events.off(Events.CANVAS_CLEAR_SECTIONS, handleClearCanvas);
events.off(Events.CANVAS_MOVE_SECTION_UP, moveSectionUp);
events.off(Events.CANVAS_MOVE_SECTION_DOWN, moveSectionDown);
events.off(Events.CANVAS_IMPORT_README, importReadme);
events.off(Events.CANVAS_IMPORT_README_FILE, loadReadmeFile);
};
}, [sections, currentSection]);

Expand Down
8 changes: 8 additions & 0 deletions src/events/handles/canvas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@ class CanvasHandleEvents extends BaseEventHandle {
this.emit(Events.CANVAS_REMOVE_SECTION, sectionId);
};

loadImportFile = () => {
this.emit(Events.CANVAS_IMPORT_README_FILE);
};

import = (event: React.ChangeEvent<HTMLInputElement>) => {
this.emit(Events.CANVAS_IMPORT_README, event);
};

clear = () => {
this.emit(Events.CANVAS_CLEAR_SECTIONS);
};
Expand Down
33 changes: 33 additions & 0 deletions src/features/activities/importer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { CanvasSection, Sections } from 'types';
import { defaultActivitiesSectionConfig } from './default-config';
import { v4 as uuid } from 'uuid';
import type { Element } from 'hast';
import { deepCopy } from 'utils/deepCopy';

const activitiesImporter = (activityDiv: Element): CanvasSection | null => {
const defaultConfig = deepCopy(defaultActivitiesSectionConfig);

// NOTE: currently only Medium posts supported
if (activityDiv.children.length === 0) return null;

const posts = activityDiv.children.filter(
child => child.type === 'element' && child.tagName === 'a'
) as Element[];

const href = posts[0].properties.href as string;
const usernamePart = href.split('@')[1];
const username = usernamePart.split('/')[0];

defaultConfig.props.content.username = username;
defaultConfig.props.content.limit = posts.length;
defaultConfig.props.styles.align =
activityDiv.properties['align'] || 'center';

return {
id: uuid(),
type: Sections.ACTIVITIES,
...defaultConfig,
};
};

export { activitiesImporter };
3 changes: 2 additions & 1 deletion src/features/activities/parser.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Sections } from 'types';
import { getActivitiesUrl } from 'utils';
import { ActivityUrlType } from 'utils/getActivitiesUrl';

Expand Down Expand Up @@ -35,7 +36,7 @@ const _handleMediumPosts = (
const count = (rest.limit as number) || 3;
const username = rest.username as string;

let result = `<div align="${align}" style="width: 100%">`;
let result = `<div data-importer="${Sections.ACTIVITIES}" align="${align}" style="width: 100%">`;

for (let i = 0; i < count; i++) {
const url = `${baseUrl}/@${username}/${i}`;
Expand Down
80 changes: 80 additions & 0 deletions src/features/border/importer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { CanvasSection, Sections } from 'types';
import { defaultImageSectionConfig } from './default-config';
import { v4 as uuid } from 'uuid';
import type { Element } from 'hast';
import { deepCopy } from 'utils/deepCopy';

const _updateBorderConfig = (params: URLSearchParams, config: any): any => {
config['type'] = params.get('type') || config['type'];
config['height'] = params.get('height')
? parseInt(params.get('height') as string, 10)
: config['height'];
config['section'] = params.get('section') || config['section'];
config['reversal'] = params.get('reversal') === 'true' || false;
config['color'] = {
type: params.get('color') || config['color']['type'],
theme: params.get('theme') || config['color']['theme'],
};
config['text'] = params.get('text') || config['text'];
config['fontSize'] = params.get('fontSize')
? parseInt(params.get('fontSize') as string, 10)
: config['fontSize'];
config['fontColor'] = params.get('fontColor') || config['fontColor'];
config['fontAlign'] = params.get('fontAlign')
? parseInt(params.get('fontAlign') as string, 10)
: config['fontAlign'];
config['fontAlignY'] = params.get('fontAlignY')
? parseInt(params.get('fontAlignY') as string, 10)
: config['fontAlignY'];
config['rotate'] = params.get('rotate')
? parseInt(params.get('rotate') as string, 10)
: config['rotate'];
config['stroke'] = params.get('stroke') || config['stroke'];
config['strokeWidth'] = params.get('strokeWidth')
? parseInt(params.get('strokeWidth') as string, 10)
: config['strokeWidth'];
config['animation'] = params.get('animation') || config['animation'];
config['desc'] = params.get('desc') || config['desc'];
config['descSize'] = params.get('descSize')
? parseInt(params.get('descSize') as string, 10)
: config['descSize'];
config['descAlign'] = params.get('descAlign')
? parseInt(params.get('descAlign') as string, 10)
: config['descAlign'];
config['descAlignY'] = params.get('descAlignY')
? parseInt(params.get('descAlignY') as string, 10)
: config['descAlignY'];
config['textBg'] = params.get('textBg') === 'true' || false;

return config;
};

const borderImporter = (borderElement: Element): CanvasSection | null => {
const defaultConfig = deepCopy(defaultImageSectionConfig);

if (borderElement.children.length === 0) return null;

const image = borderElement.children.find(
child => child.type === 'element' && child.tagName === 'img'
) as Element;

if (!image || !image.properties || !image.properties.src) return null;

const src = image.properties.src as string;
const url = new URL(src);
const params = url.searchParams;

defaultImageSectionConfig.props.content.borders['capsule-render'] =
_updateBorderConfig(
params,
defaultConfig.props.content.borders['capsule-render']
);

return {
id: uuid(),
type: Sections.BORDER,
...defaultConfig,
};
};

export { borderImporter };
4 changes: 2 additions & 2 deletions src/features/border/parser.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Params } from 'types';
import { Params, Sections } from 'types';
import { getBorderUrl } from 'utils/getBorderUrl';

type Borders = Parameters<typeof getBorderUrl>[0];
Expand All @@ -21,7 +21,7 @@ const borderSectionParser = ({ content }: BorderSectionParserArgs) => {
const url = getBorderUrl(provider, borders[provider]);

return `
<div>
<div data-importer="${Sections.BORDER}">
<img style="100%" src="${url}" />
</div>
`;
Expand Down
33 changes: 33 additions & 0 deletions src/features/image/importer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { CanvasSection, Sections } from 'types';
import { defaultImageSectionConfig } from './default-config';
import { v4 as uuid } from 'uuid';
import type { Element } from 'hast';
import { deepCopy } from 'utils/deepCopy';

const imageImporter = (imageElement: Element): CanvasSection | null => {
const defaultConfig = deepCopy(defaultImageSectionConfig);

defaultConfig.props.styles.align = imageElement.properties?.align || 'center';

if (imageElement.tagName === 'img') {
defaultConfig.props.styles.float = imageElement.properties?.align || 'none';
defaultConfig.props.styles.height = imageElement.properties?.height || 200;
defaultConfig.props.content.url = imageElement.properties?.src || '';
} else if (imageElement.tagName === 'div') {
const image = imageElement.children.find(child => {
return (child as Element).tagName === 'img';
}) as Element;
defaultConfig.props.styles.height = image.properties?.height || 200;
defaultConfig.props.content.url = image.properties?.src || '';
} else {
return null;
}

return {
id: uuid(),
type: Sections.IMAGE,
...defaultConfig,
};
};

export { imageImporter };
6 changes: 4 additions & 2 deletions src/features/image/parser.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { Sections } from 'types';

type Content = {
url: string;
};
Expand All @@ -22,8 +24,8 @@ const imageSectionParser = ({ content, styles }: ImageSectionParserArgs) => {
const floatStyle = `align="${float}" `;

return `
${!hasFloat ? `<div align="${align}">` : ''}
<img ${hasFloat ? floatStyle : ''}height="${height}" src="${url}" />
${!hasFloat ? `<div data-importer="${Sections.IMAGE}" align="${align}">` : ''}
<img data-importer="${Sections.IMAGE}" ${hasFloat ? floatStyle : ''}height="${height}" src="${url}" />
${!hasFloat ? '</div>' : ''}
`;
};
Expand Down
Loading
Loading