diff --git a/Pages/Garden.php b/Pages/Garden.php index 49b23c5..5a9562a 100644 --- a/Pages/Garden.php +++ b/Pages/Garden.php @@ -256,12 +256,12 @@ $PageSection = "Garden"; >

Player

-

- Username: - - Loading... - -

+
+ Loading... +

+ +

Comments

+ +

+ +
+ + diff --git a/Pages/Guide.php b/Pages/Guide.php index 851fd35..9201966 100644 --- a/Pages/Guide.php +++ b/Pages/Guide.php @@ -336,7 +336,8 @@ $PageSection = "Guide"; The Social page lets you search for other gardeners by username and open their public Gardener page. Gardener profile pictures use a plant - and growth stage the player has discovered. + and growth stage the player has discovered. Gardeners who + have not chosen one yet use a mature Red Rose by default.

@@ -350,7 +351,9 @@ $PageSection = "Guide";

Comments are simple messages left on a Gardener page. A comment can be deleted by its author or by the owner of the - Gardener page. Dew donations transfer spendable Dew between + Gardener page. Your own received comments are also shown in + the Player panel on your Garden, even while public comments + are disabled. Dew donations transfer spendable Dew between accounts, but donated Dew does not increase the recipient's Lifetime Dew.

diff --git a/Scripts/Garden/Gardener.js b/Scripts/Garden/Gardener.js index 4942faa..7e81d65 100644 --- a/Scripts/Garden/Gardener.js +++ b/Scripts/Garden/Gardener.js @@ -1084,29 +1084,12 @@ async function LoadGardenerComments( } try { - const Response = await fetch( - ApiUrl + "/Comments.php", - { - method: "POST", - headers: { - "Content-Type": - "application/json" - }, - body: JSON.stringify({ - Action: "List", - SaveKey: GetSaveKey(), - Username: - GardenerData.Username, - Offset: - GardenerCommentOffset, - Limit: - GardenerCommentPageSize - }) - } - ); - const Result = - await Response.json(); + await GetGardenerComments( + GardenerData.Username, + GardenerCommentOffset, + GardenerCommentPageSize + ); if (!Result.Success) { throw new Error( @@ -1186,102 +1169,10 @@ async function LoadGardenerComments( function CreateGardenerComment( Comment ) { - const Article = - document.createElement( - "article" - ); - - Article.className = - "PlantTile GardenerComment"; - - Article.dataset.commentId = - String(Comment.Id); - - const Header = - document.createElement( - "div" - ); - - Header.className = - "GardenerCommentHeader"; - - const Identity = - CreateGardenerIdentity( - Comment.Username, - Comment.Colour, - Comment.ProfilePicture - ); - - const Time = - document.createElement( - "time" - ); - - Time.className = - "GardenerCommentTime"; - - const CreatedAt = Number( - Comment.CreatedAt + return CreateGardenerCommentCard( + Comment, + DeleteGardenerComment ); - - if (Number.isFinite(CreatedAt)) { - const DateValue = - new Date(CreatedAt); - - Time.dateTime = - DateValue.toISOString(); - - Time.textContent = - DateValue.toLocaleString(); - } - - Header.append( - Identity, - Time - ); - - const Text = - document.createElement( - "p" - ); - - Text.className = - "GardenerCommentText"; - - Text.textContent = - Comment.Text ?? ""; - - Article.append( - Header, - Text - ); - - if (Comment.CanDelete === true) { - const DeleteButton = - document.createElement( - "button" - ); - - DeleteButton.type = "button"; - DeleteButton.className = - "ActionButton GardenerCommentDelete"; - DeleteButton.textContent = - "Delete"; - - DeleteButton.addEventListener( - "click", - () => DeleteGardenerComment( - Number(Comment.Id), - DeleteButton - ) - ); - - Article.appendChild( - DeleteButton - ); - } - - return Article; } @@ -1402,24 +1293,10 @@ async function DeleteGardenerComment( ); try { - const Response = await fetch( - ApiUrl + "/Comments.php", - { - method: "POST", - headers: { - "Content-Type": - "application/json" - }, - body: JSON.stringify({ - Action: "Delete", - SaveKey: GetSaveKey(), - CommentId: CommentId - }) - } - ); - const Result = - await Response.json(); + await DeleteGardenerCommentRequest( + CommentId + ); if (!Result.Success) { throw new Error( diff --git a/Scripts/Garden/Profile.js b/Scripts/Garden/Profile.js index 4615f5b..5b7969b 100644 --- a/Scripts/Garden/Profile.js +++ b/Scripts/Garden/Profile.js @@ -1194,6 +1194,10 @@ function InitialiseProfilePictureChooser() { SavedProfilePicture.GrowthStage ); } else { + const DefaultPlantKey = + DefaultGardenerProfilePicture + .PlantKey; + const FirstEntry = GetProfilePicturePlantEntries() .sort( @@ -1203,11 +1207,19 @@ function InitialiseProfilePictureChooser() { )[0] ?? null; ProfilePictureSelectedPlantKey = - FirstEntry?.PlantKey ?? - null; + IsProfilePicturePlantAvailable( + DefaultPlantKey + ) + ? DefaultPlantKey + : FirstEntry?.PlantKey ?? + null; ProfilePictureSelectedGrowthStage = - null; + ProfilePictureSelectedPlantKey === + DefaultPlantKey + ? DefaultGardenerProfilePicture + .GrowthStage + : null; } RenderProfilePictureChooser(); diff --git a/Scripts/Garden/Social.js b/Scripts/Garden/Social.js index 28a8149..5ac1422 100644 --- a/Scripts/Garden/Social.js +++ b/Scripts/Garden/Social.js @@ -263,7 +263,7 @@ function CreateGardenerSearchResult( CreateGardenerAvatar( Gardener.ProfilePicture, Gardener.Username, - "GardenerAvatarSearch" + "GardenerAvatarLarge" ); const Details = diff --git a/Scripts/Garden/Users.js b/Scripts/Garden/Users.js index 69a31d8..de11249 100644 --- a/Scripts/Garden/Users.js +++ b/Scripts/Garden/Users.js @@ -1,5 +1,14 @@ const DefaultColourPickerValue = "#000000"; +const DefaultGardenerProfilePicture = Object.freeze({ + PlantKey: "RedRose", + GrowthStage: 3 +}); + +let PlayerCommentUsername = null; +let PlayerCommentOffset = 0; +const PlayerCommentPageSize = 20; + function RenderGardenOwnerHeading( Username @@ -194,20 +203,19 @@ function ApplyPlayerColour( function GetGardenerProfilePictureSource( ProfilePicture ) { - if ( - ProfilePicture === null || - typeof ProfilePicture !== "object" || - Array.isArray(ProfilePicture) || - typeof ProfilePicture.PlantKey !== - "string" || - !Number.isInteger( + const Candidate = + ProfilePicture !== null && + typeof ProfilePicture === "object" && + !Array.isArray(ProfilePicture) && + typeof ProfilePicture.PlantKey === + "string" && + Number.isInteger( Number( ProfilePicture.GrowthStage ) ) - ) { - return null; - } + ? ProfilePicture + : DefaultGardenerProfilePicture; if ( typeof Plants === "undefined" || @@ -217,11 +225,24 @@ function GetGardenerProfilePictureSource( return null; } - const Plant = - Plants[ - ProfilePicture.PlantKey + let Plant = + Plants[Candidate.PlantKey]; + + let GrowthStage = Number( + Candidate.GrowthStage + ); + + if (Plant === undefined) { + Plant = Plants[ + DefaultGardenerProfilePicture + .PlantKey ]; + GrowthStage = + DefaultGardenerProfilePicture + .GrowthStage; + } + if (Plant === undefined) { return null; } @@ -231,10 +252,6 @@ function GetGardenerProfilePictureSource( Plant ); - const GrowthStage = Number( - ProfilePicture.GrowthStage - ); - const RequestedSource = Images[GrowthStage - 1]; @@ -260,6 +277,15 @@ function GetGardenerProfilePictureSource( } } + if ( + Candidate !== + DefaultGardenerProfilePicture + ) { + return GetGardenerProfilePictureSource( + DefaultGardenerProfilePicture + ); + } + return null; } @@ -404,6 +430,163 @@ function CreateGardenerIdentity( } +function CreateGardenerCommentCard( + Comment, + DeleteHandler = null +) { + const Article = + document.createElement( + "article" + ); + + Article.className = + "PlantTile GardenerComment"; + + Article.dataset.commentId = + String(Comment.Id); + + const Header = + document.createElement( + "div" + ); + + Header.className = + "GardenerCommentHeader"; + + const Identity = + CreateGardenerIdentity( + Comment.Username, + Comment.Colour, + Comment.ProfilePicture + ); + + const Time = + document.createElement( + "time" + ); + + Time.className = + "GardenerCommentTime"; + + const CreatedAt = Number( + Comment.CreatedAt + ); + + if (Number.isFinite(CreatedAt)) { + const DateValue = + new Date(CreatedAt); + + Time.dateTime = + DateValue.toISOString(); + + Time.textContent = + DateValue.toLocaleString(); + } + + Header.append( + Identity, + Time + ); + + const Text = + document.createElement( + "p" + ); + + Text.className = + "GardenerCommentText"; + + Text.textContent = + Comment.Text ?? ""; + + Article.append( + Header, + Text + ); + + if ( + Comment.CanDelete === true && + typeof DeleteHandler === + "function" + ) { + const DeleteButton = + document.createElement( + "button" + ); + + DeleteButton.type = "button"; + DeleteButton.className = + "ActionButton GardenerCommentDelete"; + DeleteButton.textContent = + "Delete"; + + DeleteButton.addEventListener( + "click", + () => DeleteHandler( + Number(Comment.Id), + DeleteButton + ) + ); + + Article.appendChild( + DeleteButton + ); + } + + return Article; +} + + +async function GetGardenerComments( + Username, + Offset = 0, + Limit = 20 +) { + const Response = await fetch( + ApiUrl + "/Comments.php", + { + method: "POST", + headers: { + "Content-Type": + "application/json" + }, + body: JSON.stringify({ + Action: "List", + SaveKey: GetSaveKey(), + Username: Username, + Offset: Offset, + Limit: Limit + }) + } + ); + + return await Response.json(); +} + + +async function DeleteGardenerCommentRequest( + CommentId +) { + const Response = await fetch( + ApiUrl + "/Comments.php", + { + method: "POST", + headers: { + "Content-Type": + "application/json" + }, + body: JSON.stringify({ + Action: "Delete", + SaveKey: GetSaveKey(), + CommentId: CommentId + }) + } + ); + + return await Response.json(); +} + + async function GetProfile() { const Response = await fetch( ApiUrl + "/Profile.php", @@ -501,9 +684,9 @@ async function RenderUser() { const Profile = await GetProfile(); - const UsernameDisplay = + const PlayerIdentity = document.getElementById( - "UsernameDisplay" + "PlayerIdentity" ); const UsernameForm = @@ -515,13 +698,29 @@ async function RenderUser() { Profile.Success && Profile.Username !== null ) { - UsernameDisplay.textContent = - Profile.Username; + if (PlayerIdentity !== null) { + const Identity = + CreateGardenerIdentity( + Profile.Username, + Profile.Colour, + Profile.ProfilePicture, + false + ); - ApplyPlayerColour( - UsernameDisplay, - Profile.Colour - ); + Identity.classList.add( + "GardenPlayerIdentityContent" + ); + + Identity.querySelector( + ".GardenerAvatar" + )?.classList.add( + "GardenerAvatarLarge" + ); + + PlayerIdentity.replaceChildren( + Identity + ); + } UsernameForm.hidden = true; @@ -529,22 +728,250 @@ async function RenderUser() { Profile.Username ); + await LoadPlayerComments( + Profile.Username, + true + ); + return; } - UsernameDisplay.textContent = - "Unnamed"; + if (PlayerIdentity !== null) { + const Identity = + CreateGardenerIdentity( + "Unnamed", + null, + null, + false + ); - ApplyPlayerColour( - UsernameDisplay, - null - ); + Identity.classList.add( + "GardenPlayerIdentityContent" + ); + + Identity.querySelector( + ".GardenerAvatar" + )?.classList.add( + "GardenerAvatarLarge" + ); + + PlayerIdentity.replaceChildren( + Identity + ); + } UsernameForm.hidden = false; RenderGardenOwnerHeading( null ); + + PlayerCommentUsername = null; + PlayerCommentOffset = 0; + + const CommentList = + document.getElementById( + "PlayerCommentList" + ); + + const CommentMessage = + document.getElementById( + "PlayerCommentMessage" + ); + + const LoadMoreButton = + document.getElementById( + "LoadMorePlayerCommentsButton" + ); + + CommentList?.replaceChildren(); + + if (CommentMessage !== null) { + CommentMessage.textContent = + "Set a username to receive comments."; + } + + if (LoadMoreButton !== null) { + LoadMoreButton.hidden = true; + } +} + + +async function LoadPlayerComments( + Username, + Reset +) { + const List = + document.getElementById( + "PlayerCommentList" + ); + + const Message = + document.getElementById( + "PlayerCommentMessage" + ); + + const LoadMoreButton = + document.getElementById( + "LoadMorePlayerCommentsButton" + ); + + if (List === null) { + return; + } + + if (Reset) { + PlayerCommentUsername = Username; + PlayerCommentOffset = 0; + List.replaceChildren(); + } + + if ( + typeof PlayerCommentUsername !== + "string" || + PlayerCommentUsername.length === 0 + ) { + if (Message !== null) { + Message.textContent = + "Set a username to receive comments."; + } + + if (LoadMoreButton !== null) { + LoadMoreButton.hidden = true; + } + + return; + } + + if (LoadMoreButton !== null) { + LoadMoreButton.disabled = true; + } + + try { + const Result = + await GetGardenerComments( + PlayerCommentUsername, + PlayerCommentOffset, + PlayerCommentPageSize + ); + + if (!Result.Success) { + throw new Error( + Result.Error ?? + "Couldn't load comments." + ); + } + + const Comments = + Array.isArray(Result.Comments) + ? Result.Comments + : []; + + for (const Comment of Comments) { + List.appendChild( + CreateGardenerCommentCard( + Comment, + DeletePlayerComment + ) + ); + } + + PlayerCommentOffset += + Comments.length; + + if ( + Reset && + Comments.length === 0 + ) { + const Empty = + document.createElement( + "p" + ); + + Empty.className = + "GardenerCommentsEmpty"; + + Empty.textContent = + "No comments yet."; + + List.appendChild( + Empty + ); + } + + if (Message !== null) { + Message.textContent = ""; + } + + if (LoadMoreButton !== null) { + LoadMoreButton.hidden = + Result.HasMore !== true; + } + } catch (Error) { + console.error( + "Couldn't load player comments:", + Error + ); + + if (Message !== null) { + Message.textContent = + "Couldn't load comments."; + } + } finally { + if (LoadMoreButton !== null) { + LoadMoreButton.disabled = false; + } + } +} + + +async function DeletePlayerComment( + CommentId, + Button +) { + Button.disabled = true; + + const Message = + document.getElementById( + "PlayerCommentMessage" + ); + + try { + const Result = + await DeleteGardenerCommentRequest( + CommentId + ); + + if (!Result.Success) { + throw new Error( + Result.Error ?? + "Couldn't delete comment." + ); + } + + await LoadPlayerComments( + PlayerCommentUsername, + true + ); + + if (Message !== null) { + Message.textContent = + "Comment deleted."; + } + } catch (Error) { + console.error( + "Couldn't delete player comment:", + Error + ); + + if (Message !== null) { + Message.textContent = + Error.message ?? + "Couldn't delete comment."; + } + + Button.disabled = false; + } } @@ -596,5 +1023,15 @@ document.addEventListener( SubmitUsername ); } + + document.getElementById( + "LoadMorePlayerCommentsButton" + )?.addEventListener( + "click", + () => LoadPlayerComments( + PlayerCommentUsername, + false + ) + ); } ); diff --git a/Styles/Style.css b/Styles/Style.css index 0d7fa31..14eaa06 100644 --- a/Styles/Style.css +++ b/Styles/Style.css @@ -1025,7 +1025,7 @@ input, button, textarea, select { padding: 0.75rem; } -.ProfileSection h3:not(.PanelHeader) { +.GardenUser h3:not(.PanelHeader), .ProfileSection h3:not(.PanelHeader) { margin-top: 1.5rem; } @@ -1643,10 +1643,18 @@ input, button, textarea, select { white-space: nowrap; } -.GardenerIdentity:hover .GardenerIdentityName { +a.GardenerIdentity:hover .GardenerIdentityName { text-decoration: underline; } +.GardenPlayerIdentity { + min-height: 4.25rem; +} + +.GardenPlayerIdentityContent { + font-size: 1.35rem; +} + .GardenerSearchResults { display: grid; gap: 0.75rem; @@ -1671,7 +1679,7 @@ input, button, textarea, select { box-shadow: 2px 2px 0 var(--Crust); } -.GardenerAvatarSearch { +.GardenerAvatarLarge { width: 4.25rem; height: 4.25rem; flex-basis: 4.25rem; @@ -1813,7 +1821,7 @@ input, button, textarea, select { overflow-wrap: anywhere; } -.InteractivePanel .GardenerCommentDelete { +.GardenerCommentDelete { width: auto; margin-top: 0.75rem; }