Added social stuff to more areas

This commit is contained in:
sarah committed 2026-09-07 19:11:45 +02:00
1 parent 56d70e8c0c
commit bafb53e4d5
7 files changed
+540 -181

No files matched your search

+28 -6
View File
@@ -256,12 +256,12 @@ $PageSection = "Garden";
>
<h2 class="PanelHeader PanelHeaderInset">Player</h2>
<p>
Username:
<strong id="UsernameDisplay">
Loading...
</strong>
</p>
<div
id="PlayerIdentity"
class="GardenPlayerIdentity"
>
Loading...
</div>
<form
id="UsernameForm"
@@ -283,6 +283,28 @@ $PageSection = "Garden";
<p id="UsernameMessage"></p>
</form>
<h3>Comments</h3>
<p
id="PlayerCommentMessage"
class="ProfileMessage"
aria-live="polite"
></p>
<div
id="PlayerCommentList"
class="GardenerCommentList"
></div>
<button
id="LoadMorePlayerCommentsButton"
class="ActionButton GardenerLoadMoreButton"
type="button"
hidden
>
Load more comments
</button>
</section>
</main>
+5 -2
View File
@@ -336,7 +336,8 @@ $PageSection = "Guide";
The <a href="/Pages/Social.html">Social</a> 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.
</p>
<p>
@@ -350,7 +351,9 @@ $PageSection = "Guide";
<p>
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.
</p>
+11 -134
View File
@@ -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(
+15 -3
View File
@@ -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();
+1 -1
View File
@@ -263,7 +263,7 @@ function CreateGardenerSearchResult(
CreateGardenerAvatar(
Gardener.ProfilePicture,
Gardener.Username,
"GardenerAvatarSearch"
"GardenerAvatarLarge"
);
const Details =
+468 -31
View File
@@ -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
)
);
}
);
+12 -4
View File
@@ -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;
}