Files
Keith Solomon 58a0b0c808 Add website fields to project meta box
Adds a 'This project is a website' toggle to the Repository meta box.
When checked, site fields (URL, platform, optional screenshot) replace the
git-repo fields; when unchecked, repo fields are shown and site fields
are cleared. Existing posts default to repo mode.

- admin/metabox.php: checkbox, site URL/platform/screenshot inputs with
  media-library picker, JS toggle, save handler that switches modes
- includes/helper-functions.php: projects_portfolio_is_website / get
  _site_url / get_site_platform / get_site_screenshot_url helpers
- templates/single-projects.php: website-mode rendering (Visit Site
  button, platform row, screenshot)
- tests/test-metabox.php: website-mode persistence + mode-switch tests
- tests/wp-stubs.php: sanitize_text_field, absint stubs
2026-08-22 15:16:51 -05:00

282 lines
13 KiB
PHP

<?php
// If this file is called directly, abort.
if ( ! defined( 'WPINC' ) ) {
die;
}
/**
* Add Repository meta box (provider-aware).
*
* @since 1.0.0
* @return void
*/
function projects_portfolio_add_meta_boxes() {
add_meta_box(
'projects_portfolio_repo',
esc_html__( 'Repository', 'projects-wp' ),
'projects_portfolio_render_meta_box',
'projects',
'side'
);
}
add_action( 'add_meta_boxes', 'projects_portfolio_add_meta_boxes' );
/**
* Render the Repository meta box.
*
* @param WP_Post $post
*
* @since 1.0.0
* @return void
*/
function projects_portfolio_render_meta_box( $post ) {
wp_nonce_field( 'projects_portfolio_save_meta_box', 'projects_portfolio_meta_box_nonce' );
$is_website = (bool) get_post_meta( $post->ID, '_projects_portfolio_is_website', true );
$provider = get_post_meta( $post->ID, '_projects_portfolio_provider', true );
if ( '' === $provider ) {
$provider = 'github';
}
$repo_url = get_post_meta( $post->ID, '_projects_portfolio_repo_url', true );
if ( '' === $repo_url ) {
$repo_url = get_post_meta( $post->ID, '_projects_portfolio_github_url', true );
}
$gitea_base_url = get_post_meta( $post->ID, '_projects_portfolio_gitea_base_url', true );
$site_url = get_post_meta( $post->ID, '_projects_portfolio_site_url', true );
$site_platform = get_post_meta( $post->ID, '_projects_portfolio_site_platform', true );
$site_screenshot = (int) get_post_meta( $post->ID, '_projects_portfolio_site_screenshot_id', true );
?>
<p>
<label>
<input type="checkbox" id="projects_portfolio_is_website" name="projects_portfolio_is_website" value="1" <?php checked( $is_website, true ); ?> />
<?php esc_html_e( 'This project is a website', 'projects-wp' ); ?>
</label>
</p>
<div class="projects-portfolio-repo-fields" <?php echo $is_website ? 'style="display:none;"' : ''; ?>>
<p>
<label for="projects_portfolio_provider"><?php esc_html_e( 'Provider', 'projects-wp' ); ?></label>
<select id="projects_portfolio_provider" name="projects_portfolio_provider" style="width: 100%;">
<option value="github" <?php selected( $provider, 'github' ); ?>><?php esc_html_e( 'GitHub', 'projects-wp' ); ?></option>
<option value="gitea" <?php selected( $provider, 'gitea' ); ?>><?php esc_html_e( 'Gitea', 'projects-wp' ); ?></option>
</select>
</p>
<p>
<label for="projects_portfolio_repo_url"><?php esc_html_e( 'Repository URL', 'projects-wp' ); ?></label>
<input type="url" id="projects_portfolio_repo_url" name="projects_portfolio_repo_url" value="<?php echo esc_attr( $repo_url ); ?>" style="width: 100%;" placeholder="https://github.com/owner/repo" />
</p>
<p class="projects-portfolio-gitea-only" <?php echo 'gitea' !== $provider ? 'style="display:none;"' : ''; ?>>
<label for="projects_portfolio_gitea_base_url"><?php esc_html_e( 'Gitea Base URL (optional override)', 'projects-wp' ); ?></label>
<input type="url" id="projects_portfolio_gitea_base_url" name="projects_portfolio_gitea_base_url" value="<?php echo esc_attr( $gitea_base_url ); ?>" style="width: 100%;" placeholder="https://codeberg.org" />
<span class="description"><?php esc_html_e( 'Leave blank to use the default from Settings.', 'projects-wp' ); ?></span>
</p>
</div>
<div class="projects-portfolio-website-fields"<?php echo $is_website ? '' : ' style="display:none;"'; ?>>
<p>
<label for="projects_portfolio_site_url"><?php esc_html_e( 'Site URL', 'projects-wp' ); ?></label>
<input type="url" id="projects_portfolio_site_url" name="projects_portfolio_site_url" value="<?php echo esc_attr( $site_url ); ?>" style="width: 100%;" placeholder="https://example.com" />
</p>
<p>
<label for="projects_portfolio_site_platform"><?php esc_html_e( 'Platform', 'projects-wp' ); ?></label>
<textarea id="projects_portfolio_site_platform" name="projects_portfolio_site_platform" rows="3" style="width: 100%;" placeholder="<?php esc_attr_e( 'e.g. WordPress, custom Next.js, etc.', 'projects-wp' ); ?>"><?php echo esc_textarea( $site_platform ); ?></textarea>
</p>
<p>
<label for="projects_portfolio_site_screenshot_id"><?php esc_html_e( 'Screenshot (optional)', 'projects-wp' ); ?></label>
<span class="projects-portfolio-screenshot-preview" style="<?php echo $site_screenshot ? '' : 'display:none;'; ?> margin-right: 8px;">
<?php if ( $site_screenshot ) : ?>
<?php echo wp_get_attachment_image( $site_screenshot, 'thumbnail' ); // phpcs:ignore ?>
<?php endif; ?>
</span>
<input type="hidden" id="projects_portfolio_site_screenshot_id" name="projects_portfolio_site_screenshot_id" value="<?php echo esc_attr( $site_screenshot ); ?>" />
<button type="button" class="button projects-portfolio-screenshot-select"><?php esc_html_e( 'Select / Upload', 'projects-wp' ); ?></button>
<button type="button" class="button projects-portfolio-screenshot-clear" <?php echo $site_screenshot ? '' : 'style="display:none;"'; ?>><?php esc_html_e( 'Remove', 'projects-wp' ); ?></button>
</p>
</div>
<script>
(function(){
var sel = document.getElementById('projects_portfolio_provider');
if (sel) {
var gitea = document.querySelector('.projects-portfolio-gitea-only');
function toggleGitea(){
if (!gitea) return;
gitea.style.display = sel.value === 'gitea' ? '' : 'none';
}
sel.addEventListener('change', toggleGitea);
toggleGitea();
}
var toggle = document.getElementById('projects_portfolio_is_website');
if (toggle) {
var repoBox = document.querySelector('.projects-portfolio-repo-fields');
var siteBox = document.querySelector('.projects-portfolio-website-fields');
function toggleMode(){
var on = !!toggle.checked;
if (repoBox) repoBox.style.display = on ? 'none' : '';
if (siteBox) siteBox.style.display = on ? '' : 'none';
}
toggle.addEventListener('change', toggleMode);
toggleMode();
}
var frame;
var selectBtn = document.querySelector('.projects-portfolio-screenshot-select');
var clearBtn = document.querySelector('.projects-portfolio-screenshot-clear');
var inputEl = document.getElementById('projects_portfolio_site_screenshot_id');
var previewEl = document.querySelector('.projects-portfolio-screenshot-preview');
function openFrame(){
if (typeof wp === 'undefined' || !wp.media) { return; }
frame = wp.media({
title: '<?php echo esc_js( __( 'Select Screenshot', 'projects-wp' ) ); ?>',
button: { text: '<?php echo esc_js( __( 'Use this image', 'projects-wp' ) ); ?>' },
library: { type: 'image' },
multiple: false
});
frame.on('select', function(){
var attachment = frame.state().get('selection').first().toJSON();
inputEl.value = attachment.id;
previewEl.innerHTML = '<img src="' + attachment.sizes && attachment.sizes.thumbnail ? attachment.sizes.thumbnail.url : attachment.url + '" alt="" />';
previewEl.style.display = '';
clearBtn.style.display = '';
});
frame.open();
}
if (selectBtn) { selectBtn.addEventListener('click', openFrame); }
if (clearBtn) {
clearBtn.addEventListener('click', function(){
inputEl.value = '';
previewEl.innerHTML = '';
previewEl.style.display = 'none';
clearBtn.style.display = 'none';
});
}
})();
</script>
<?php
}
/**
* Enqueue media picker assets for the project meta box.
*
* @since 1.4.0
* @return void
*/
function projects_portfolio_enqueue_media_for_meta_box( $hook ) {
if ( ! in_array( $hook, [ 'post.php', 'post-new.php' ], true ) ) {
return;
}
$screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
if ( ! $screen || 'projects' !== $screen->post_type ) {
return;
}
wp_enqueue_media();
}
add_action( 'admin_enqueue_scripts', 'projects_portfolio_enqueue_media_for_meta_box' );
/**
* Save the Repository meta box.
*
* Performs a lazy migration from legacy `_projects_portfolio_github_url`.
*
* @param int $post_id
*
* @since 1.0.0
* @return void
*/
function projects_portfolio_save_meta_box( $post_id ) {
if ( ! isset( $_POST['projects_portfolio_meta_box_nonce'] ) || ! wp_verify_nonce( $_POST['projects_portfolio_meta_box_nonce'], 'projects_portfolio_save_meta_box' ) ) {
return;
}
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
return;
}
$existing_provider = get_post_meta( $post_id, '_projects_portfolio_provider', true );
$existing_repo_url = get_post_meta( $post_id, '_projects_portfolio_repo_url', true );
$legacy_github = get_post_meta( $post_id, '_projects_portfolio_github_url', true );
// Lazy migration: legacy URL present, no explicit choice.
if ( '' === $existing_provider && '' === $existing_repo_url && '' !== $legacy_github ) {
update_post_meta( $post_id, '_projects_portfolio_provider', 'github' );
update_post_meta( $post_id, '_projects_portfolio_repo_url', $legacy_github );
}
// "This project is a website" toggle — drives which set of fields is kept.
$is_website = isset( $_POST['projects_portfolio_is_website'] ) && '1' === $_POST['projects_portfolio_is_website'];
if ( $is_website ) {
update_post_meta( $post_id, '_projects_portfolio_is_website', '1' );
} else {
delete_post_meta( $post_id, '_projects_portfolio_is_website' );
}
if ( isset( $_POST['projects_portfolio_provider'] ) ) {
$provider = ( 'gitea' === $_POST['projects_portfolio_provider'] ) ? 'gitea' : 'github';
update_post_meta( $post_id, '_projects_portfolio_provider', $provider );
}
if ( isset( $_POST['projects_portfolio_repo_url'] ) ) {
$repo_url = esc_url_raw( wp_unslash( $_POST['projects_portfolio_repo_url'] ) );
update_post_meta( $post_id, '_projects_portfolio_repo_url', $repo_url );
// Maintain the legacy key for backward-compat reads (only when this is a GitHub project).
$provider_now = get_post_meta( $post_id, '_projects_portfolio_provider', true );
if ( 'github' === $provider_now ) {
if ( '' === $repo_url ) {
// Clearing the field on a GitHub project should clear the legacy key too.
delete_post_meta( $post_id, '_projects_portfolio_github_url' );
} else {
update_post_meta( $post_id, '_projects_portfolio_github_url', $repo_url );
}
} else {
// Provider is gitea (or any non-github): ensure no stale GitHub URL can
// resurrect via the legacy fallback chain.
delete_post_meta( $post_id, '_projects_portfolio_github_url' );
}
}
if ( isset( $_POST['projects_portfolio_gitea_base_url'] ) ) {
update_post_meta( $post_id, '_projects_portfolio_gitea_base_url', esc_url_raw( wp_unslash( $_POST['projects_portfolio_gitea_base_url'] ) ) );
}
// Website fields.
if ( isset( $_POST['projects_portfolio_site_url'] ) ) {
$site_url = esc_url_raw( wp_unslash( $_POST['projects_portfolio_site_url'] ) );
if ( '' === $site_url ) {
delete_post_meta( $post_id, '_projects_portfolio_site_url' );
} else {
update_post_meta( $post_id, '_projects_portfolio_site_url', $site_url );
}
}
if ( isset( $_POST['projects_portfolio_site_platform'] ) ) {
$platform = sanitize_text_field( wp_unslash( $_POST['projects_portfolio_site_platform'] ) );
if ( '' === $platform ) {
delete_post_meta( $post_id, '_projects_portfolio_site_platform' );
} else {
update_post_meta( $post_id, '_projects_portfolio_site_platform', $platform );
}
}
if ( isset( $_POST['projects_portfolio_site_screenshot_id'] ) ) {
$screenshot_id = absint( wp_unslash( $_POST['projects_portfolio_site_screenshot_id'] ) );
if ( $screenshot_id <= 0 ) {
delete_post_meta( $post_id, '_projects_portfolio_site_screenshot_id' );
} else {
update_post_meta( $post_id, '_projects_portfolio_site_screenshot_id', $screenshot_id );
}
}
// When modes switch, clear the opposite mode's fields so the project is cleanly one or the other.
if ( $is_website ) {
delete_post_meta( $post_id, '_projects_portfolio_provider' );
delete_post_meta( $post_id, '_projects_portfolio_repo_url' );
delete_post_meta( $post_id, '_projects_portfolio_gitea_base_url' );
delete_post_meta( $post_id, '_projects_portfolio_github_url' );
} else {
delete_post_meta( $post_id, '_projects_portfolio_site_url' );
delete_post_meta( $post_id, '_projects_portfolio_site_platform' );
delete_post_meta( $post_id, '_projects_portfolio_site_screenshot_id' );
}
}
add_action( 'save_post', 'projects_portfolio_save_meta_box' );