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.
@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 );
}