diff --git a/admin/metabox.php b/admin/metabox.php index e3a7b6c..0217a9a 100644 --- a/admin/metabox.php +++ b/admin/metabox.php @@ -33,6 +33,7 @@ add_action( 'add_meta_boxes', 'projects_portfolio_add_meta_boxes' ); 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'; @@ -42,39 +43,139 @@ function projects_portfolio_render_meta_box( $post ) { $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 ); ?>

- - -

-

- - -

-

> - - - +

+ +
> +

+ + +

+

+ + +

+

> + + + +

+
+ + + post_type ) { + return; + } + wp_enqueue_media(); +} +add_action( 'admin_enqueue_scripts', 'projects_portfolio_enqueue_media_for_meta_box' ); + /** * Save the Repository meta box. * @@ -103,6 +204,14 @@ function projects_portfolio_save_meta_box( $post_id ) { 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 ); @@ -130,5 +239,43 @@ function projects_portfolio_save_meta_box( $post_id ) { 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' ); diff --git a/includes/helper-functions.php b/includes/helper-functions.php index b51e0fe..f4a73c0 100644 --- a/includes/helper-functions.php +++ b/includes/helper-functions.php @@ -104,6 +104,56 @@ function projects_portfolio_get_repo_browse_url( int $post_id ): string { return projects_portfolio_get_provider( $post_id )->get_repo_browse_url(); } +/** + * Whether this project is configured as a website rather than a git repo. + * + * @since 1.4.0 + * @param int $post_id + * @return bool + */ +function projects_portfolio_is_website( int $post_id ): bool { + return (bool) get_post_meta( $post_id, '_projects_portfolio_is_website', true ); +} + +/** + * Public URL for the project's website (when configured as a website). + * + * @since 1.4.0 + * @param int $post_id + * @return string + */ +function projects_portfolio_get_site_url( int $post_id ): string { + return (string) get_post_meta( $post_id, '_projects_portfolio_site_url', true ); +} + +/** + * Platform label for the project's website (when configured as a website). + * + * @since 1.4.0 + * @param int $post_id + * @return string + */ +function projects_portfolio_get_site_platform( int $post_id ): string { + return (string) get_post_meta( $post_id, '_projects_portfolio_site_platform', true ); +} + +/** + * Screenshot attachment URL for the project's website. + * + * @since 1.4.0 + * @param int $post_id + * @param string $size + * @return string + */ +function projects_portfolio_get_site_screenshot_url( int $post_id, string $size = 'large' ): string { + $attachment_id = (int) get_post_meta( $post_id, '_projects_portfolio_site_screenshot_id', true ); + if ( $attachment_id <= 0 ) { + return ''; + } + $src = wp_get_attachment_image_src( $attachment_id, $size ); + return $src[0] ?? ''; +} + /** * GitHub API call for owner data * diff --git a/templates/single-projects.php b/templates/single-projects.php index 1a0f5e7..d48e62c 100644 --- a/templates/single-projects.php +++ b/templates/single-projects.php @@ -13,23 +13,31 @@ $settings = projects_portfolio_settings(); $project_id = get_the_ID(); $download_count = (int) get_post_meta( $project_id, '_projects_portfolio_download_count', true ); -$repo_url = get_post_meta( $project_id, '_projects_portfolio_repo_url', true ); -if ( '' === $repo_url ) { - $repo_url = get_post_meta( $project_id, '_projects_portfolio_github_url', true ); +$is_website = projects_portfolio_is_website( $project_id ); + +if ( $is_website ) { + $site_url = projects_portfolio_get_site_url( $project_id ); + $site_platform = projects_portfolio_get_site_platform( $project_id ); + $site_shot_url = projects_portfolio_get_site_screenshot_url( $project_id, 'large' ); +} else { + $repo_url = get_post_meta( $project_id, '_projects_portfolio_repo_url', true ); + if ( '' === $repo_url ) { + $repo_url = get_post_meta( $project_id, '_projects_portfolio_github_url', true ); + } + + $repo_data = projects_portfolio_get_repo_data( $project_id ); + $version = projects_portfolio_get_version( $project_id ); + $owner = projects_portfolio_get_owner( $project_id ); + + $owner_avatar = $repo_data['owner']['avatar_url'] ?? ''; + $owner_name = $repo_data['owner']['login'] ?? ''; + $owner_url = $repo_data['owner']['html_url'] ?? ''; + $last_updated = $repo_data['updated_at'] ?? ''; + $language = $repo_data['language'] ?? ''; + $license = $repo_data['license']['name'] ?? 'None'; + $browse_url = $repo_url ? projects_portfolio_get_repo_browse_url( $project_id ) : ''; } -$repo_data = projects_portfolio_get_repo_data( $project_id ); -$version = projects_portfolio_get_version( $project_id ); -$owner = projects_portfolio_get_owner( $project_id ); - -$owner_avatar = $repo_data['owner']['avatar_url'] ?? ''; -$owner_name = $repo_data['owner']['login'] ?? ''; -$owner_url = $repo_data['owner']['html_url'] ?? ''; -$last_updated = $repo_data['updated_at'] ?? ''; -$language = $repo_data['language'] ?? ''; -$license = $repo_data['license']['name'] ?? 'None'; -$browse_url = $repo_url ? projects_portfolio_get_repo_browse_url( $project_id ) : ''; - if ( $last_updated ) { $last_updated = date_i18n( get_option( 'date_format' ), strtotime( $last_updated ) ); } @@ -49,6 +57,13 @@ if ( $last_updated ) {
+ +
+ + <?php echo esc_attr( get_the_title() ); ?> + +
+

" class="button project-download-button"> - + + + + + + +

+ + @@ -84,7 +107,15 @@ if ( $last_updated ) { ?> - + + + + + + + + + @@ -141,12 +172,13 @@ if ( $last_updated ) {

- +
diff --git a/tests/test-metabox.php b/tests/test-metabox.php index eb88532..5485d2a 100644 --- a/tests/test-metabox.php +++ b/tests/test-metabox.php @@ -142,4 +142,115 @@ class Metabox_Test extends \PHPUnit\Framework\TestCase { $this->assertArrayNotHasKey( '_projects_portfolio_github_url', $existing ); $this->assertContains( '_projects_portfolio_github_url', $deleted ); } + + public function test_website_mode_persists_website_fields_and_clears_repo_fields(): void { + $existing = [ + '_projects_portfolio_provider' => 'github', + '_projects_portfolio_repo_url' => 'https://github.com/owner/repo', + '_projects_portfolio_github_url' => 'https://github.com/owner/repo', + ]; + $deleted = []; + \Brain\Monkey\Functions\stubs( [ + 'get_post_meta' => function ( $post_id, $key, $single = false ) use ( &$existing ) { + return $existing[ $key ] ?? ''; + }, + 'update_post_meta' => function ( $post_id, $key, $value ) use ( &$existing ) { + $existing[ $key ] = $value; + return true; + }, + 'delete_post_meta' => function ( $post_id, $key ) use ( &$existing, &$deleted ) { + unset( $existing[ $key ] ); + $deleted[] = $key; + return true; + }, + 'wp_verify_nonce' => function () { return true; }, + 'wp_nonce_field' => function () { /* noop */ }, + ] ); + + $_POST['projects_portfolio_meta_box_nonce'] = 'nonce'; + $_POST['projects_portfolio_is_website'] = '1'; + $_POST['projects_portfolio_site_url'] = 'https://example.com'; + $_POST['projects_portfolio_site_platform'] = 'WordPress'; + $_POST['projects_portfolio_site_screenshot_id'] = '42'; + + projects_portfolio_save_meta_box( 11 ); + + $this->assertSame( '1', $existing['_projects_portfolio_is_website'] ); + $this->assertSame( 'https://example.com', $existing['_projects_portfolio_site_url'] ); + $this->assertSame( 'WordPress', $existing['_projects_portfolio_site_platform'] ); + $this->assertSame( 42, $existing['_projects_portfolio_site_screenshot_id'] ); + + // Repo-mode fields should be cleared when switching to website mode. + $this->assertArrayNotHasKey( '_projects_portfolio_provider', $existing ); + $this->assertArrayNotHasKey( '_projects_portfolio_repo_url', $existing ); + $this->assertArrayNotHasKey( '_projects_portfolio_github_url', $existing ); + $this->assertContains( '_projects_portfolio_provider', $deleted ); + $this->assertContains( '_projects_portfolio_repo_url', $deleted ); + $this->assertContains( '_projects_portfolio_github_url', $deleted ); + } + + public function test_unchecked_website_checkbox_clears_website_fields(): void { + $existing = [ + '_projects_portfolio_is_website' => '1', + '_projects_portfolio_site_url' => 'https://example.com', + '_projects_portfolio_site_platform' => 'WordPress', + '_projects_portfolio_site_screenshot_id' => 42, + ]; + $deleted = []; + \Brain\Monkey\Functions\stubs( [ + 'get_post_meta' => function ( $post_id, $key, $single = false ) use ( &$existing ) { + return $existing[ $key ] ?? ''; + }, + 'update_post_meta' => function ( $post_id, $key, $value ) use ( &$existing ) { + $existing[ $key ] = $value; + return true; + }, + 'delete_post_meta' => function ( $post_id, $key ) use ( &$existing, &$deleted ) { + unset( $existing[ $key ] ); + $deleted[] = $key; + return true; + }, + 'wp_verify_nonce' => function () { return true; }, + 'wp_nonce_field' => function () { /* noop */ }, + ] ); + + $_POST['projects_portfolio_meta_box_nonce'] = 'nonce'; + // is_website is absent — repo mode is implied. + + projects_portfolio_save_meta_box( 13 ); + + $this->assertArrayNotHasKey( '_projects_portfolio_is_website', $existing ); + $this->assertArrayNotHasKey( '_projects_portfolio_site_url', $existing ); + $this->assertArrayNotHasKey( '_projects_portfolio_site_platform', $existing ); + $this->assertArrayNotHasKey( '_projects_portfolio_site_screenshot_id', $existing ); + $this->assertContains( '_projects_portfolio_is_website', $deleted ); + } + + public function test_existing_repo_post_without_checkbox_keeps_repo_fields(): void { + $existing = [ + '_projects_portfolio_provider' => 'github', + '_projects_portfolio_repo_url' => 'https://github.com/owner/repo', + ]; + \Brain\Monkey\Functions\stubs( [ + 'get_post_meta' => function ( $post_id, $key, $single = false ) use ( &$existing ) { + return $existing[ $key ] ?? ''; + }, + 'update_post_meta' => function ( $post_id, $key, $value ) use ( &$existing ) { + $existing[ $key ] = $value; + return true; + }, + 'delete_post_meta' => function () { return true; }, + 'wp_verify_nonce' => function () { return true; }, + 'wp_nonce_field' => function () { /* noop */ }, + ] ); + + $_POST['projects_portfolio_meta_box_nonce'] = 'nonce'; + // is_website not present in $_POST — should be treated as unchecked. + + projects_portfolio_save_meta_box( 17 ); + + $this->assertSame( 'github', $existing['_projects_portfolio_provider'] ); + $this->assertSame( 'https://github.com/owner/repo', $existing['_projects_portfolio_repo_url'] ); + $this->assertArrayNotHasKey( '_projects_portfolio_is_website', $existing ); + } } diff --git a/tests/wp-stubs.php b/tests/wp-stubs.php index 6bc1544..33b738f 100644 --- a/tests/wp-stubs.php +++ b/tests/wp-stubs.php @@ -80,6 +80,12 @@ if ( ! function_exists( 'wp_unslash' ) ) { if ( ! function_exists( 'esc_url_raw' ) ) { function esc_url_raw( $url ) { return $url; } } +if ( ! function_exists( 'sanitize_text_field' ) ) { + function sanitize_text_field( $str ) { return is_string( $str ) ? trim( $str ) : ''; } +} +if ( ! function_exists( 'absint' ) ) { + function absint( $maybeint ) { return abs( (int) $maybeint ); } +} // Minimal WP_Error stub if Brain\Monkey doesn't supply one. if ( ! class_exists( 'WP_Error' ) ) {