UI/GameCard.razor

A Blazor/Sandbox UI component for displaying a cloud package as a game card. Renders thumbnail, title, author, verified badge, stats or summary, and exposes a click callback and Loading state.

File Access
@namespace CardGames
@using Sandbox
@using Sandbox.UI
@using System
@using System.Linq
@using System.Collections.Generic
@inherits Panel

@*
	One game "table" - a cloud package shown as a card. Shared by the launcher (alongside the
	built-in games) and the full games browser, so the two stay in lockstep. Clicking it asks the host
	to mount + start the package; while that's in flight the parent sets Loading and we veil the art.
*@

<root class="card community" onclick=@(() => OnClick?.Invoke())>
	<div class="art">
		@if ( !string.IsNullOrEmpty( Package?.ThumbWide ) )
		{
			<img class="shot" src="@Package.ThumbWide" />
		}
		else
		{
			<div class="noart">♠</div>
		}
		@if ( Loading )
		{
			<div class="loading">Loading…</div>
		}
	</div>
	<div class="info">
		<div class="row">
			<span class="name">@Package?.Title</span>
			<span class="play">Play ›</span>
		</div>
		<div class="byline">
			<span class="tagline">@Author</span>
			@if ( Verified )
			{
				<span class="verified"><span class="ico">verified</span></span>
			}
		</div>
		<div class="stats">
			<span>@Stats</span>
		</div>
	</div>
</root>

@code
{
	/// <summary>The cloud package this card represents.</summary>
	[Parameter] public Package Package { get; set; }

	/// <summary>Invoked when the card is clicked - mount + start the package.</summary>
	[Parameter] public Action OnClick { get; set; }

	/// <summary>Veil the art while the package is being mounted/started.</summary>
	[Parameter] public bool Loading { get; set; }

	// Author label: the org's title, falling back to its ident.
	private string Author => !string.IsNullOrEmpty( Package?.Org?.Title )
		? Package.Org.Title
		: Package?.Org?.Ident ?? "Unknown";

	// First-party games - those published under the Facepunch org - carry a verified badge.
	private bool Verified => string.Equals( Package?.Org?.Ident, "facepunch", StringComparison.OrdinalIgnoreCase );

	// The muted line: rating, upvotes, players now - whichever the cloud has, joined with dots. Before any
	// of those stats exist, fall back to the package's own blurb (its summary), then a neutral label.
	private string Stats
	{
		get
		{
			var facts = StatsFor( Package ).ToList();
			if ( facts.Count > 0 ) return string.Join( " · ", facts );

			return !string.IsNullOrEmpty( Package?.Summary ) ? Package.Summary : "A card game";
		}
	}

	private static IEnumerable<string> StatsFor( Package p )
	{
		if ( p is null ) yield break;
		if ( p.Reviews.Total > 0 ) yield return $"{p.Reviews.Score * 100f:0}% rated";
		if ( p.VotesUp > 0 ) yield return $"{p.VotesUp} upvotes";
		if ( p.Usage.UsersNow > 0 ) yield return $"{p.Usage.UsersNow} playing";
	}

	protected override int BuildHash() => HashCode.Combine( Package?.FullIdent, Loading );
}