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
10 changes: 9 additions & 1 deletion src/hooks/useReadmeState.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,14 @@ export function useReadmeState() {
});
}, [sectionState, selectedTechs, selectedBadges, scheduleSave]);

const updateMultipleFields = useCallback((fieldsObj) => {
setFormData(prev => {
const next = { ...prev, ...fieldsObj };
scheduleSave(next, sectionState, selectedTechs, selectedBadges);
return next;
});
}, [sectionState, selectedTechs, selectedBadges, scheduleSave]);

const toggleSection = useCallback((id, checked) => {
setSectionState(prev => {
const next = { ...prev, [id]: checked };
Expand Down Expand Up @@ -145,7 +153,7 @@ export function useReadmeState() {
}, []);

return {
formData, updateField,
formData, updateField, updateMultipleFields,
sectionState, toggleSection,
selectedTechs, toggleTech,
selectedBadges, toggleBadge,
Expand Down
58 changes: 57 additions & 1 deletion src/pages/ReadmeMaker/EditorPanel.jsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { isValidUrl } from '../../hooks/useUrlValidation';
import { useRef } from 'react';
import { useRef, useState } from 'react';
import { TECHS, BADGES } from '../../utils/constants';
import { convertStructure } from '../../utils/structureUtils';
import { getWordCount } from '../../utils/markdownUtils';
import { parseGitHubUrl, fetchRepoData } from '../../utils/githubApi';

function WordCount({ text }) {
const count = getWordCount(text);
Expand Down Expand Up @@ -34,10 +35,34 @@ export default function EditorPanel({
selectedTechs, toggleTech,
selectedBadges, toggleBadge,
screenshots, addScreenshots, removeScreenshot,
onGitHubImport,
}) {
const fileInputRef = useRef(null);
const dropZoneRef = useRef(null);

const [ghUrl, setGhUrl] = useState('');
const [ghLoading, setGhLoading] = useState(false);
const [ghStatus, setGhStatus] = useState({ type: '', message: '' });

async function handleGitHubFetch() {
setGhStatus({ type: '', message: '' });
const parsed = parseGitHubUrl(ghUrl);
if (!parsed) {
setGhStatus({ type: 'error', message: 'Invalid GitHub URL. Use format: https://github.com/owner/repo' });
return;
}
setGhLoading(true);
try {
const data = await fetchRepoData(parsed.owner, parsed.repo);
onGitHubImport(data);
setGhStatus({ type: 'success', message: `✓ Imported "${data.name}" successfully!` });
} catch (err) {
setGhStatus({ type: 'error', message: err.message });
} finally {
setGhLoading(false);
}
}

const structPreview = formData.rawStructure
? convertStructure(formData.rawStructure, formData.projName || 'project')
: 'Paste structure above to preview...';
Expand All @@ -61,6 +86,37 @@ export default function EditorPanel({
<div className="editor">
<div className="editor-inner" id="editorInner">

<div className="github-import-bar">
<div className="github-import-header">
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z"/>
</svg>
<span>Import from GitHub</span>
</div>
<div className="github-import-input-row">
<input
type="url"
className="github-import-input"
placeholder="https://github.com/owner/repo"
value={ghUrl}
onChange={e => setGhUrl(e.target.value)}
onKeyDown={e => e.key === 'Enter' && handleGitHubFetch()}
/>
<button
className="github-import-btn"
onClick={handleGitHubFetch}
disabled={ghLoading || !ghUrl.trim()}
>
{ghLoading ? 'Fetching...' : 'Fetch'}
</button>
</div>
{ghStatus.message && (
<div className={`github-import-status ${ghStatus.type}`}>
{ghStatus.message}
</div>
)}
</div>

<EditorSection num={1} title="Project Title & Badges" hidden={!sectionState.title}>
<div className="two-col">
<div>
Expand Down
27 changes: 26 additions & 1 deletion src/pages/ReadmeMaker/ReadmeMaker.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export default function ReadmeMaker() {
const toast = useToast();

const {
formData, updateField,
formData, updateField, updateMultipleFields,
sectionState, toggleSection,
selectedTechs, toggleTech,
selectedBadges, toggleBadge,
Expand All @@ -25,6 +25,30 @@ export default function ReadmeMaker() {

const [activeTemplate, setActiveTemplate] = useState(null);

function handleGitHubImport(data) {
const fieldsToUpdate = {};
if (data.name) {
fieldsToUpdate.projName = data.name;
fieldsToUpdate.repoSlug = data.name;
}
if (data.description) {
fieldsToUpdate.tagline = data.description;
fieldsToUpdate.description = data.description;
}
if (data.owner) {
fieldsToUpdate.ghUser = data.owner;
fieldsToUpdate.authorGh = data.owner;
}
if (data.license) {
fieldsToUpdate.license = data.license;
}
if (data.homepage) {
fieldsToUpdate.demoUrl = data.homepage;
}
updateMultipleFields(fieldsToUpdate);
toast('✓ GitHub repository data imported!');
}

const currentMd = useMemo(() =>
generateMarkdown({ formData, sectionState, selectedTechs, selectedBadges, screenshots }),
[formData, sectionState, selectedTechs, selectedBadges, screenshots]
Expand Down Expand Up @@ -105,6 +129,7 @@ export default function ReadmeMaker() {
screenshots={screenshots}
addScreenshots={addScreenshots}
removeScreenshot={removeScreenshot}
onGitHubImport={handleGitHubImport}
/>
<PreviewPanel
currentMd={currentMd}
Expand Down
82 changes: 82 additions & 0 deletions src/styles/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -3401,3 +3401,85 @@ body.light-mode .gh-preview {
.faq-question { font-size: 14px; padding: 16px; }
.faq-answer { padding: 0 16px 16px; padding-top: 12px; }
}

/* ── GitHub Import Bar ── */
.github-import-bar {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 16px;
margin-bottom: 24px;
padding: 20px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
}

.github-import-header {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
font-weight: 600;
margin-bottom: 12px;
color: var(--text);
font-family: 'Space Grotesk', sans-serif;
}

.github-import-header svg {
color: var(--accent);
}

.github-import-input-row {
display: flex;
gap: 12px;
}

.github-import-input {
flex: 1;
}

.github-import-btn {
padding: 10px 20px;
border-radius: 10px;
font-size: 13px;
font-family: "Space Grotesk", sans-serif;
font-weight: 600;
cursor: pointer;
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
background: linear-gradient(135deg, var(--accent), var(--accent2));
border: none;
color: #fff;
box-shadow: 0 4px 15px rgba(56, 189, 248, 0.3);
}

.github-import-btn:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(56, 189, 248, 0.5);
}

.github-import-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
box-shadow: none;
}

.github-import-status {
margin-top: 12px;
padding: 10px 14px;
border-radius: 8px;
font-size: 13px;
font-family: "Space Grotesk", sans-serif;
font-weight: 500;
}

.github-import-status.success {
background: rgba(16, 185, 129, 0.1);
border: 1px solid rgba(16, 185, 129, 0.2);
color: var(--green);
}

.github-import-status.error {
background: rgba(244, 63, 94, 0.1);
border: 1px solid rgba(244, 63, 94, 0.2);
color: var(--red);
}

74 changes: 74 additions & 0 deletions src/utils/githubApi.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/**
* GitHub API utility for fetching repository metadata.
* Uses the public GitHub REST API (no auth required, 60 req/hr rate limit).
*/

/**
* Parse a GitHub URL and extract owner and repo name.
* Supports formats:
* - https://github.com/owner/repo
* - https://github.com/owner/repo.git
* - github.com/owner/repo
* - http://github.com/owner/repo
*/
export function parseGitHubUrl(url) {
if (!url || typeof url !== 'string') return null;

const trimmed = url.trim();

// Add protocol if missing
let normalized = trimmed;
if (!normalized.startsWith('http://') && !normalized.startsWith('https://')) {
normalized = 'https://' + normalized;
}

try {
const parsed = new URL(normalized);
if (!parsed.hostname.includes('github.com')) return null;

// Remove leading slash and .git suffix
const pathname = parsed.pathname.replace(/^\//, '').replace(/\.git$/, '');
const parts = pathname.split('/').filter(Boolean);

if (parts.length < 2) return null;

return { owner: parts[0], repo: parts[1] };
} catch {
return null;
}
}

/**
* Fetch repository data from the GitHub public API.
* Returns a clean object with relevant fields or throws an error.
*/
export async function fetchRepoData(owner, repo) {
const response = await fetch(`https://api.github.com/repos/${owner}/${repo}`, {
headers: { 'Accept': 'application/vnd.github.v3+json' },
});

if (response.status === 404) {
throw new Error('Repository not found. Please check the URL.');
}

if (response.status === 403) {
throw new Error('API rate limit exceeded. Please try again later.');
}

if (!response.ok) {
throw new Error(`GitHub API error (${response.status}). Please try again.`);
}

const data = await response.json();

return {
name: data.name || '',
description: data.description || '',
owner: data.owner?.login || '',
license: data.license?.spdx_id || '',
homepage: data.homepage || '',
language: data.language || '',
topics: data.topics || [],
html_url: data.html_url || '',
};
}
Loading