Add Git repository embed block plugin

Introduces a WordPress block plugin to embed GitHub repositories with a customizable card UI. Includes block registration, AJAX data fetching, server-side rendering, and styles for alignment and responsive display.
This commit is contained in:
feibisi 2025-07-31 14:43:41 +08:00
parent e764912546
commit 729238cb5e
3 changed files with 776 additions and 0 deletions

252
block.js Normal file
View file

@ -0,0 +1,252 @@
(function() {
const { registerBlockType } = wp.blocks;
const { createElement: el, useState, useEffect } = wp.element;
const {
InspectorControls,
BlockControls,
BlockAlignmentToolbar,
useBlockProps
} = wp.blockEditor;
const {
PanelBody,
TextControl,
ToggleControl,
SelectControl,
Button,
Spinner,
Notice
} = wp.components;
const { __ } = wp.i18n;
registerBlockType('git-embed-feicode/repository', {
title: __('Git Repository', 'git-embed-feicode'),
description: __('Embed a Git repository with information and stats', 'git-embed-feicode'),
icon: 'admin-links',
category: 'embed',
supports: {
align: ['left', 'center', 'right', 'wide', 'full']
},
attributes: {
platform: {
type: 'string',
default: 'github'
},
owner: {
type: 'string',
default: ''
},
repo: {
type: 'string',
default: ''
},
showDescription: {
type: 'boolean',
default: true
},
showStats: {
type: 'boolean',
default: true
},
showDownload: {
type: 'boolean',
default: true
},
alignment: {
type: 'string',
default: 'none'
}
},
edit: function(props) {
const { attributes, setAttributes } = props;
const { platform, owner, repo, showDescription, showStats, showDownload, alignment } = attributes;
const [repoData, setRepoData] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const blockProps = useBlockProps({
className: alignment !== 'none' ? `align${alignment}` : ''
});
const fetchRepoData = () => {
if (!owner || !repo) {
setError('Please enter repository owner and name');
return;
}
setLoading(true);
setError('');
const formData = new FormData();
formData.append('action', 'git_embed_fetch');
formData.append('platform', platform);
formData.append('owner', owner);
formData.append('repo', repo);
formData.append('nonce', gitEmbedAjax.nonce);
fetch(gitEmbedAjax.url, {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
setLoading(false);
if (data.success) {
setRepoData(data.data);
setError('');
} else {
setError(data.data || 'Failed to fetch repository');
setRepoData(null);
}
})
.catch(err => {
setLoading(false);
setError('Network error occurred');
setRepoData(null);
});
};
useEffect(() => {
if (owner && repo) {
fetchRepoData();
}
}, [owner, repo, platform]);
const renderPreview = () => {
if (loading) {
return el('div', { className: 'git-embed-loading' },
el(Spinner),
el('p', null, 'Fetching repository data...')
);
}
if (error) {
return el(Notice, {
status: 'error',
isDismissible: false
}, error);
}
if (!repoData) {
return el('div', { className: 'git-embed-placeholder' },
el('div', { className: 'git-embed-placeholder-content' },
el('span', { className: 'git-embed-placeholder-icon' }, '📦'),
el('h3', null, 'Git Repository Embed'),
el('p', null, 'Configure your repository details in the sidebar')
)
);
}
return el('div', { className: 'git-embed-card' },
el('div', { className: 'git-embed-header' },
el('h3', { className: 'git-embed-title' },
el('a', {
href: repoData.html_url,
target: '_blank',
rel: 'noopener'
}, repoData.full_name)
),
repoData.language && el('span', { className: 'git-embed-language' }, repoData.language)
),
showDescription && repoData.description &&
el('p', { className: 'git-embed-description' }, repoData.description),
showStats && el('div', { className: 'git-embed-stats' },
el('span', { className: 'git-embed-stat' },
el('span', { className: 'git-embed-icon' }, '⭐'),
repoData.stargazers_count.toLocaleString()
),
el('span', { className: 'git-embed-stat' },
el('span', { className: 'git-embed-icon' }, '🍴'),
repoData.forks_count.toLocaleString()
),
el('span', { className: 'git-embed-stat' },
el('span', { className: 'git-embed-icon' }, '📝'),
repoData.open_issues_count.toLocaleString()
)
),
showDownload && el('div', { className: 'git-embed-actions' },
el('a', {
href: repoData.html_url,
className: 'git-embed-button git-embed-button-primary',
target: '_blank',
rel: 'noopener'
}, 'View Repository'),
el('a', {
href: repoData.clone_url,
className: 'git-embed-button git-embed-button-secondary'
}, 'Clone')
)
);
};
return el('div', blockProps,
el(BlockControls, null,
el(BlockAlignmentToolbar, {
value: alignment,
onChange: (value) => setAttributes({ alignment: value })
})
),
el(InspectorControls, null,
el(PanelBody, {
title: __('Repository Settings', 'git-embed-feicode'),
initialOpen: true
},
el(SelectControl, {
label: __('Platform', 'git-embed-feicode'),
value: platform,
options: [
{ label: 'GitHub', value: 'github' }
],
onChange: (value) => setAttributes({ platform: value })
}),
el(TextControl, {
label: __('Repository Owner', 'git-embed-feicode'),
value: owner,
onChange: (value) => setAttributes({ owner: value }),
placeholder: 'e.g. facebook'
}),
el(TextControl, {
label: __('Repository Name', 'git-embed-feicode'),
value: repo,
onChange: (value) => setAttributes({ repo: value }),
placeholder: 'e.g. react'
}),
el(Button, {
isPrimary: true,
onClick: fetchRepoData,
disabled: loading || !owner || !repo
}, loading ? 'Fetching...' : 'Fetch Repository')
),
el(PanelBody, {
title: __('Display Options', 'git-embed-feicode'),
initialOpen: false
},
el(ToggleControl, {
label: __('Show Description', 'git-embed-feicode'),
checked: showDescription,
onChange: (value) => setAttributes({ showDescription: value })
}),
el(ToggleControl, {
label: __('Show Statistics', 'git-embed-feicode'),
checked: showStats,
onChange: (value) => setAttributes({ showStats: value })
}),
el(ToggleControl, {
label: __('Show Download Links', 'git-embed-feicode'),
checked: showDownload,
onChange: (value) => setAttributes({ showDownload: value })
})
)
),
el('div', { className: 'wp-block-git-embed-feicode-repository' },
renderPreview()
)
);
},
save: function() {
return null;
}
});
})();

249
git-embed-feicode.php Normal file
View file

@ -0,0 +1,249 @@
<?php
declare(strict_types=1);
/**
* Plugin Name: Git Embed for feiCode
* Description: Embed Git repositories from GitHub with beautiful cards
* Version: 1.0.0
* Author: feiCode
* Text Domain: git-embed-feicode
* Domain Path: /languages
*/
if (!defined('ABSPATH')) {
exit;
}
class GitEmbedFeiCode {
private const PLUGIN_VERSION = '1.0.0';
private const BLOCK_NAME = 'git-embed-feicode/repository';
public function __construct() {
add_action('init', [$this, 'init']);
add_action('wp_ajax_git_embed_fetch', [$this, 'ajax_fetch_repo']);
add_action('wp_ajax_nopriv_git_embed_fetch', [$this, 'ajax_fetch_repo']);
}
public function init(): void {
$this->register_block();
$this->load_textdomain();
}
private function register_block(): void {
register_block_type(self::BLOCK_NAME, [
'editor_script' => 'git-embed-feicode-editor',
'editor_style' => 'git-embed-feicode-style',
'style' => 'git-embed-feicode-style',
'render_callback' => [$this, 'render_block'],
'attributes' => [
'platform' => [
'type' => 'string',
'default' => 'github'
],
'owner' => [
'type' => 'string',
'default' => ''
],
'repo' => [
'type' => 'string',
'default' => ''
],
'showDescription' => [
'type' => 'boolean',
'default' => true
],
'showStats' => [
'type' => 'boolean',
'default' => true
],
'showDownload' => [
'type' => 'boolean',
'default' => true
],
'alignment' => [
'type' => 'string',
'default' => 'none'
]
]
]);
wp_register_script(
'git-embed-feicode-editor',
plugin_dir_url(__FILE__) . 'block.js',
['wp-blocks', 'wp-element', 'wp-editor', 'wp-components', 'wp-i18n'],
self::PLUGIN_VERSION
);
wp_register_style(
'git-embed-feicode-style',
plugin_dir_url(__FILE__) . 'style.css',
[],
self::PLUGIN_VERSION
);
wp_localize_script('git-embed-feicode-editor', 'gitEmbedAjax', [
'url' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('git_embed_nonce')
]);
}
public function render_block(array $attributes): string {
$owner = sanitize_text_field($attributes['owner'] ?? '');
$repo = sanitize_text_field($attributes['repo'] ?? '');
$platform = sanitize_text_field($attributes['platform'] ?? 'github');
if (empty($owner) || empty($repo)) {
return '<div class="git-embed-error">Repository information required</div>';
}
$repo_data = $this->fetch_repository_data($platform, $owner, $repo);
if (!$repo_data) {
return '<div class="git-embed-error">Failed to fetch repository data</div>';
}
return $this->render_repository_card($repo_data, $attributes);
}
private function fetch_repository_data(string $platform, string $owner, string $repo): ?array {
if ($platform !== 'github') {
return null;
}
$cache_key = "git_embed_{$platform}_{$owner}_{$repo}";
$cached = get_transient($cache_key);
if ($cached !== false) {
return $cached;
}
$url = "https://api.github.com/repos/{$owner}/{$repo}";
$response = wp_remote_get($url, [
'timeout' => 10,
'headers' => [
'User-Agent' => 'Git-Embed-FeiCode/1.0'
]
]);
if (is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200) {
return null;
}
$data = json_decode(wp_remote_retrieve_body($response), true);
if (!$data) {
return null;
}
$repo_data = [
'name' => $data['name'],
'full_name' => $data['full_name'],
'description' => $data['description'],
'html_url' => $data['html_url'],
'language' => $data['language'],
'stargazers_count' => $data['stargazers_count'],
'forks_count' => $data['forks_count'],
'open_issues_count' => $data['open_issues_count'],
'clone_url' => $data['clone_url'],
'archive_url' => $data['archive_url']
];
set_transient($cache_key, $repo_data, HOUR_IN_SECONDS);
return $repo_data;
}
private function render_repository_card(array $repo_data, array $attributes): string {
$show_description = $attributes['showDescription'] ?? true;
$show_stats = $attributes['showStats'] ?? true;
$show_download = $attributes['showDownload'] ?? true;
$alignment = $attributes['alignment'] ?? 'none';
$align_class = $alignment !== 'none' ? " align{$alignment}" : '';
ob_start();
?>
<div class="wp-block-git-embed-feicode-repository<?php echo esc_attr($align_class); ?>">
<div class="git-embed-card">
<div class="git-embed-header">
<h3 class="git-embed-title">
<a href="<?php echo esc_url($repo_data['html_url']); ?>" target="_blank" rel="noopener">
<?php echo esc_html($repo_data['full_name']); ?>
</a>
</h3>
<?php if ($repo_data['language']): ?>
<span class="git-embed-language"><?php echo esc_html($repo_data['language']); ?></span>
<?php endif; ?>
</div>
<?php if ($show_description && $repo_data['description']): ?>
<p class="git-embed-description"><?php echo esc_html($repo_data['description']); ?></p>
<?php endif; ?>
<?php if ($show_stats): ?>
<div class="git-embed-stats">
<span class="git-embed-stat">
<span class="git-embed-icon"></span>
<?php echo number_format_i18n($repo_data['stargazers_count']); ?>
</span>
<span class="git-embed-stat">
<span class="git-embed-icon">🍴</span>
<?php echo number_format_i18n($repo_data['forks_count']); ?>
</span>
<span class="git-embed-stat">
<span class="git-embed-icon">📝</span>
<?php echo number_format_i18n($repo_data['open_issues_count']); ?>
</span>
</div>
<?php endif; ?>
<?php if ($show_download): ?>
<div class="git-embed-actions">
<a href="<?php echo esc_url($repo_data['html_url']); ?>"
class="git-embed-button git-embed-button-primary"
target="_blank" rel="noopener">
View Repository
</a>
<a href="<?php echo esc_url($repo_data['clone_url']); ?>"
class="git-embed-button git-embed-button-secondary">
Clone
</a>
</div>
<?php endif; ?>
</div>
</div>
<?php
return ob_get_clean();
}
public function ajax_fetch_repo(): void {
check_ajax_referer('git_embed_nonce', 'nonce');
$platform = sanitize_text_field($_POST['platform'] ?? '');
$owner = sanitize_text_field($_POST['owner'] ?? '');
$repo = sanitize_text_field($_POST['repo'] ?? '');
if (empty($owner) || empty($repo)) {
wp_send_json_error('Repository information required');
}
$repo_data = $this->fetch_repository_data($platform, $owner, $repo);
if (!$repo_data) {
wp_send_json_error('Failed to fetch repository data');
}
wp_send_json_success($repo_data);
}
private function load_textdomain(): void {
load_plugin_textdomain(
'git-embed-feicode',
false,
dirname(plugin_basename(__FILE__)) . '/languages'
);
}
}
new GitEmbedFeiCode();

275
style.css Normal file
View file

@ -0,0 +1,275 @@
.wp-block-git-embed-feicode-repository {
margin: 1.5em 0;
max-width: 100%;
}
.wp-block-git-embed-feicode-repository.alignleft {
float: left;
margin-right: 1.5em;
max-width: 360px;
}
.wp-block-git-embed-feicode-repository.alignright {
float: right;
margin-left: 1.5em;
max-width: 360px;
}
.wp-block-git-embed-feicode-repository.aligncenter {
margin-left: auto;
margin-right: auto;
max-width: 600px;
}
.wp-block-git-embed-feicode-repository.alignwide {
max-width: 800px;
}
.wp-block-git-embed-feicode-repository.alignfull {
max-width: none;
}
.git-embed-card {
background: #ffffff;
border: 1px solid #d1d5da;
border-radius: 8px;
padding: 20px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
transition: box-shadow 0.15s ease-in-out;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}
.git-embed-card:hover {
box-shadow: 0 3px 12px rgba(0, 0, 0, 0.15);
}
.git-embed-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
flex-wrap: wrap;
gap: 8px;
}
.git-embed-title {
margin: 0;
font-size: 18px;
font-weight: 600;
line-height: 1.3;
flex: 1;
min-width: 0;
}
.git-embed-title a {
color: #0969da;
text-decoration: none;
word-break: break-word;
}
.git-embed-title a:hover {
text-decoration: underline;
}
.git-embed-language {
background: #f6f8fa;
border: 1px solid #d1d5da;
border-radius: 12px;
padding: 4px 8px;
font-size: 12px;
font-weight: 500;
color: #656d76;
white-space: nowrap;
}
.git-embed-description {
color: #656d76;
font-size: 14px;
line-height: 1.5;
margin: 0 0 16px 0;
}
.git-embed-stats {
display: flex;
gap: 16px;
margin-bottom: 16px;
flex-wrap: wrap;
}
.git-embed-stat {
display: flex;
align-items: center;
gap: 4px;
font-size: 12px;
color: #656d76;
font-weight: 500;
}
.git-embed-icon {
font-size: 14px;
}
.git-embed-actions {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.git-embed-button {
display: inline-flex;
align-items: center;
padding: 6px 12px;
font-size: 12px;
font-weight: 500;
line-height: 1.5;
border-radius: 6px;
text-decoration: none;
cursor: pointer;
border: 1px solid;
transition: all 0.15s ease-in-out;
}
.git-embed-button-primary {
color: #ffffff;
background-color: #2da44e;
border-color: #2da44e;
}
.git-embed-button-primary:hover {
background-color: #2c974b;
border-color: #2c974b;
color: #ffffff;
text-decoration: none;
}
.git-embed-button-secondary {
color: #24292f;
background-color: #f6f8fa;
border-color: #d1d5da;
}
.git-embed-button-secondary:hover {
background-color: #f3f4f6;
border-color: #c7ccd1;
color: #24292f;
text-decoration: none;
}
.git-embed-placeholder {
border: 2px dashed #c3c4c7;
border-radius: 8px;
padding: 40px 20px;
text-align: center;
background: #f9f9f9;
}
.git-embed-placeholder-content {
max-width: 300px;
margin: 0 auto;
}
.git-embed-placeholder-icon {
font-size: 48px;
display: block;
margin-bottom: 16px;
}
.git-embed-placeholder h3 {
margin: 0 0 8px 0;
color: #1e1e1e;
font-size: 18px;
}
.git-embed-placeholder p {
margin: 0;
color: #757575;
font-size: 14px;
line-height: 1.4;
}
.git-embed-loading {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
padding: 40px 20px;
text-align: center;
border: 1px solid #ddd;
border-radius: 8px;
background: #f9f9f9;
}
.git-embed-loading p {
margin: 16px 0 0 0;
color: #757575;
font-size: 14px;
}
.git-embed-error {
background: #f8d7da;
color: #721c24;
padding: 12px 16px;
border-radius: 6px;
border: 1px solid #f1aeb5;
font-size: 14px;
margin: 16px 0;
}
@media (max-width: 768px) {
.git-embed-card {
padding: 16px;
}
.git-embed-header {
flex-direction: column;
align-items: flex-start;
}
.git-embed-stats {
gap: 12px;
}
.git-embed-actions {
width: 100%;
}
.git-embed-button {
flex: 1;
justify-content: center;
min-width: 0;
}
.wp-block-git-embed-feicode-repository.alignleft,
.wp-block-git-embed-feicode-repository.alignright {
float: none;
margin-left: auto;
margin-right: auto;
max-width: 100%;
}
}
@media (max-width: 480px) {
.git-embed-title {
font-size: 16px;
}
.git-embed-actions {
flex-direction: column;
}
.git-embed-stats {
justify-content: space-between;
}
}
.wp-block[data-type="git-embed-feicode/repository"] {
position: relative;
}
.wp-block[data-type="git-embed-feicode/repository"]:not(.is-selected) .git-embed-card {
pointer-events: none;
}
.wp-block[data-type="git-embed-feicode/repository"].is-selected .git-embed-card {
box-shadow: 0 0 0 2px #007cba;
}