feat: add avatar/bio/links/names/pronouns to user page
This commit is contained in:
parent
412d720abc
commit
862a64840e
16 changed files with 650 additions and 90 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -5,3 +5,4 @@ node_modules/
|
||||||
config.ini
|
config.ini
|
||||||
*.DotSettings.user
|
*.DotSettings.user
|
||||||
proxy-config.json
|
proxy-config.json
|
||||||
|
.DS_Store
|
||||||
|
|
|
@ -2,6 +2,37 @@
|
||||||
<profile version="1.0">
|
<profile version="1.0">
|
||||||
<option name="myName" value="Project Default" />
|
<option name="myName" value="Project Default" />
|
||||||
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
|
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="HttpUrlsUsage" enabled="true" level="WEAK WARNING" enabled_by_default="true">
|
||||||
|
<option name="ignoredUrls">
|
||||||
|
<list>
|
||||||
|
<option value="http://" />
|
||||||
|
<option value="http://0.0.0.0" />
|
||||||
|
<option value="http://127.0.0.1" />
|
||||||
|
<option value="http://activemq.apache.org/schema/" />
|
||||||
|
<option value="http://cxf.apache.org/schemas/" />
|
||||||
|
<option value="http://java.sun.com/" />
|
||||||
|
<option value="http://javafx.com/fxml" />
|
||||||
|
<option value="http://javafx.com/javafx/" />
|
||||||
|
<option value="http://json-schema.org/draft" />
|
||||||
|
<option value="http://localhost" />
|
||||||
|
<option value="http://maven.apache.org/POM/" />
|
||||||
|
<option value="http://maven.apache.org/xsd/" />
|
||||||
|
<option value="http://primefaces.org/ui" />
|
||||||
|
<option value="http://schema.cloudfoundry.org/spring/" />
|
||||||
|
<option value="http://schemas.xmlsoap.org/" />
|
||||||
|
<option value="http://tiles.apache.org/" />
|
||||||
|
<option value="http://www.ibm.com/webservices/xsd" />
|
||||||
|
<option value="http://www.jboss.com/xml/ns/" />
|
||||||
|
<option value="http://www.jboss.org/j2ee/schema/" />
|
||||||
|
<option value="http://www.springframework.org/schema/" />
|
||||||
|
<option value="http://www.springframework.org/security/tags" />
|
||||||
|
<option value="http://www.springframework.org/tags" />
|
||||||
|
<option value="http://www.thymeleaf.org" />
|
||||||
|
<option value="http://www.w3.org/" />
|
||||||
|
<option value="http://xmlns.jcp.org/" />
|
||||||
|
</list>
|
||||||
|
</option>
|
||||||
|
</inspection_tool>
|
||||||
<inspection_tool class="RequiredAttributes" enabled="true" level="WARNING" enabled_by_default="true">
|
<inspection_tool class="RequiredAttributes" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
<option name="myAdditionalRequiredHtmlAttributes" value="column" />
|
<option name="myAdditionalRequiredHtmlAttributes" value="column" />
|
||||||
</inspection_tool>
|
</inspection_tool>
|
||||||
|
|
|
@ -50,7 +50,8 @@ public class MembersController(
|
||||||
("bio", ValidationUtils.ValidateBio(req.Bio)),
|
("bio", ValidationUtils.ValidateBio(req.Bio)),
|
||||||
("avatar", ValidationUtils.ValidateAvatar(req.Avatar)),
|
("avatar", ValidationUtils.ValidateAvatar(req.Avatar)),
|
||||||
..ValidationUtils.ValidateFields(req.Fields, CurrentUser!.CustomPreferences),
|
..ValidationUtils.ValidateFields(req.Fields, CurrentUser!.CustomPreferences),
|
||||||
..ValidationUtils.ValidateFieldEntries(req.Names?.ToArray(), CurrentUser!.CustomPreferences, "names")
|
..ValidationUtils.ValidateFieldEntries(req.Names?.ToArray(), CurrentUser!.CustomPreferences, "names"),
|
||||||
|
..ValidationUtils.ValidatePronouns(req.Pronouns?.ToArray(), CurrentUser!.CustomPreferences)
|
||||||
]);
|
]);
|
||||||
|
|
||||||
var member = new Member
|
var member = new Member
|
||||||
|
|
|
@ -62,9 +62,28 @@ public class UsersController(
|
||||||
|
|
||||||
if (req.HasProperty(nameof(req.Links)))
|
if (req.HasProperty(nameof(req.Links)))
|
||||||
{
|
{
|
||||||
|
// TODO: validate link length
|
||||||
user.Links = req.Links ?? [];
|
user.Links = req.Links ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (req.Names != null)
|
||||||
|
{
|
||||||
|
errors.AddRange(ValidationUtils.ValidateFieldEntries(req.Names, CurrentUser!.CustomPreferences, "names"));
|
||||||
|
user.Names = req.Names.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.Pronouns != null)
|
||||||
|
{
|
||||||
|
errors.AddRange(ValidationUtils.ValidatePronouns(req.Pronouns, CurrentUser!.CustomPreferences));
|
||||||
|
user.Pronouns = req.Pronouns.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.Fields != null)
|
||||||
|
{
|
||||||
|
errors.AddRange(ValidationUtils.ValidateFields(req.Fields.ToList(), CurrentUser!.CustomPreferences));
|
||||||
|
user.Fields = req.Fields.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
if (req.HasProperty(nameof(req.Avatar)))
|
if (req.HasProperty(nameof(req.Avatar)))
|
||||||
errors.Add(("avatar", ValidationUtils.ValidateAvatar(req.Avatar)));
|
errors.Add(("avatar", ValidationUtils.ValidateAvatar(req.Avatar)));
|
||||||
|
|
||||||
|
@ -157,6 +176,9 @@ public class UsersController(
|
||||||
public string? Bio { get; init; }
|
public string? Bio { get; init; }
|
||||||
public string? Avatar { get; init; }
|
public string? Avatar { get; init; }
|
||||||
public string[]? Links { get; init; }
|
public string[]? Links { get; init; }
|
||||||
|
public FieldEntry[]? Names { get; init; }
|
||||||
|
public Pronoun[]? Pronouns { get; init; }
|
||||||
|
public Field[]? Fields { get; init; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -156,7 +156,7 @@ public static class ValidationUtils
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
errors = errors.Concat(ValidateFieldEntries(field.Entries, customPreferences, $"fields.{index}")).ToList();
|
errors = errors.Concat(ValidateFieldEntries(field.Entries, customPreferences, $"fields.{index}.entries")).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
return errors;
|
return errors;
|
||||||
|
@ -169,7 +169,7 @@ public static class ValidationUtils
|
||||||
var errors = new List<(string, ValidationError?)>();
|
var errors = new List<(string, ValidationError?)>();
|
||||||
|
|
||||||
if (entries.Length > Limits.FieldEntriesLimit)
|
if (entries.Length > Limits.FieldEntriesLimit)
|
||||||
errors.Add(($"{errorPrefix}.entries",
|
errors.Add((errorPrefix,
|
||||||
ValidationError.LengthError("Field has too many entries", 0, Limits.FieldEntriesLimit,
|
ValidationError.LengthError("Field has too many entries", 0, Limits.FieldEntriesLimit,
|
||||||
entries.Length)));
|
entries.Length)));
|
||||||
|
|
||||||
|
@ -181,12 +181,12 @@ public static class ValidationUtils
|
||||||
switch (entry.Value.Length)
|
switch (entry.Value.Length)
|
||||||
{
|
{
|
||||||
case > Limits.FieldEntryTextLimit:
|
case > Limits.FieldEntryTextLimit:
|
||||||
errors.Add(($"{errorPrefix}.entries.{entryIdx}.value",
|
errors.Add(($"{errorPrefix}.{entryIdx}.value",
|
||||||
ValidationError.LengthError("Field value is too long", 1, Limits.FieldEntryTextLimit,
|
ValidationError.LengthError("Field value is too long", 1, Limits.FieldEntryTextLimit,
|
||||||
entry.Value.Length)));
|
entry.Value.Length)));
|
||||||
break;
|
break;
|
||||||
case < 1:
|
case < 1:
|
||||||
errors.Add(($"{errorPrefix}.entries.{entryIdx}.value",
|
errors.Add(($"{errorPrefix}.{entryIdx}.value",
|
||||||
ValidationError.LengthError("Field value is too short", 1, Limits.FieldEntryTextLimit,
|
ValidationError.LengthError("Field value is too short", 1, Limits.FieldEntryTextLimit,
|
||||||
entry.Value.Length)));
|
entry.Value.Length)));
|
||||||
break;
|
break;
|
||||||
|
@ -195,7 +195,64 @@ public static class ValidationUtils
|
||||||
var customPreferenceIds = customPreferences?.Keys.Select(id => id.ToString()) ?? [];
|
var customPreferenceIds = customPreferences?.Keys.Select(id => id.ToString()) ?? [];
|
||||||
|
|
||||||
if (!DefaultStatusOptions.Contains(entry.Status) && !customPreferenceIds.Contains(entry.Status))
|
if (!DefaultStatusOptions.Contains(entry.Status) && !customPreferenceIds.Contains(entry.Status))
|
||||||
errors.Add(($"{errorPrefix}.entries.{entryIdx}.status",
|
errors.Add(($"{errorPrefix}.{entryIdx}.status",
|
||||||
|
ValidationError.GenericValidationError("Invalid status", entry.Status)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IEnumerable<(string, ValidationError?)> ValidatePronouns(Pronoun[]? entries,
|
||||||
|
IReadOnlyDictionary<Snowflake, User.CustomPreference> customPreferences, string errorPrefix = "pronouns")
|
||||||
|
{
|
||||||
|
if (entries == null || entries.Length == 0) return [];
|
||||||
|
var errors = new List<(string, ValidationError?)>();
|
||||||
|
|
||||||
|
if (entries.Length > Limits.FieldEntriesLimit)
|
||||||
|
errors.Add((errorPrefix,
|
||||||
|
ValidationError.LengthError("Too many pronouns", 0, Limits.FieldEntriesLimit,
|
||||||
|
entries.Length)));
|
||||||
|
|
||||||
|
// Same as above, no overwhelming this function with a ridiculous amount of entries
|
||||||
|
if (entries.Length > Limits.FieldEntriesLimit + 50) return errors;
|
||||||
|
|
||||||
|
foreach (var (entry, entryIdx) in entries.Select((entry, entryIdx) => (entry, entryIdx)))
|
||||||
|
{
|
||||||
|
switch (entry.Value.Length)
|
||||||
|
{
|
||||||
|
case > Limits.FieldEntryTextLimit:
|
||||||
|
errors.Add(($"{errorPrefix}.{entryIdx}.value",
|
||||||
|
ValidationError.LengthError("Pronoun value is too long", 1, Limits.FieldEntryTextLimit,
|
||||||
|
entry.Value.Length)));
|
||||||
|
break;
|
||||||
|
case < 1:
|
||||||
|
errors.Add(($"{errorPrefix}.{entryIdx}.value",
|
||||||
|
ValidationError.LengthError("Pronoun value is too short", 1, Limits.FieldEntryTextLimit,
|
||||||
|
entry.Value.Length)));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.DisplayText != null)
|
||||||
|
{
|
||||||
|
switch (entry.DisplayText.Length)
|
||||||
|
{
|
||||||
|
case > Limits.FieldEntryTextLimit:
|
||||||
|
errors.Add(($"{errorPrefix}.{entryIdx}.value",
|
||||||
|
ValidationError.LengthError("Pronoun display text is too long", 1, Limits.FieldEntryTextLimit,
|
||||||
|
entry.Value.Length)));
|
||||||
|
break;
|
||||||
|
case < 1:
|
||||||
|
errors.Add(($"{errorPrefix}.{entryIdx}.value",
|
||||||
|
ValidationError.LengthError("Pronoun display text is too short", 1, Limits.FieldEntryTextLimit,
|
||||||
|
entry.Value.Length)));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var customPreferenceIds = customPreferences?.Keys.Select(id => id.ToString()) ?? [];
|
||||||
|
|
||||||
|
if (!DefaultStatusOptions.Contains(entry.Status) && !customPreferenceIds.Contains(entry.Status))
|
||||||
|
errors.Add(($"{errorPrefix}.{entryIdx}.status",
|
||||||
ValidationError.GenericValidationError("Invalid status", entry.Status)));
|
ValidationError.GenericValidationError("Invalid status", entry.Status)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
16
Foxnouns.Frontend/app/components/KeyedIcon.tsx
Normal file
16
Foxnouns.Frontend/app/components/KeyedIcon.tsx
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import * as icons from "react-bootstrap-icons";
|
||||||
|
import { IconProps as BaseIconProps } from "react-bootstrap-icons";
|
||||||
|
import { pascalCase } from "change-case";
|
||||||
|
|
||||||
|
const startsWithNumberRegex = /^\d/;
|
||||||
|
|
||||||
|
export default function Icon({ iconName, ...props }: BaseIconProps & { iconName: string }) {
|
||||||
|
let icon = pascalCase(iconName);
|
||||||
|
if (startsWithNumberRegex.test(icon)) {
|
||||||
|
icon = `Icon${icon}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line import/namespace
|
||||||
|
const BootstrapIcon = icons[icon as keyof typeof icons];
|
||||||
|
return <BootstrapIcon {...props} />;
|
||||||
|
}
|
27
Foxnouns.Frontend/app/components/ProfileLink.tsx
Normal file
27
Foxnouns.Frontend/app/components/ProfileLink.tsx
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
import { Globe } from "react-bootstrap-icons";
|
||||||
|
|
||||||
|
export default function ProfileLink({ link }: { link: string }) {
|
||||||
|
const isLink = link.startsWith("http://") || link.startsWith("https://");
|
||||||
|
|
||||||
|
let displayLink = link;
|
||||||
|
if (link.startsWith("http://")) displayLink = link.substring("http://".length);
|
||||||
|
else if (link.startsWith("https://")) displayLink = link.substring("https://".length);
|
||||||
|
if (displayLink.endsWith("/")) displayLink = displayLink.substring(0, displayLink.length - 1);
|
||||||
|
|
||||||
|
if (isLink) {
|
||||||
|
return (
|
||||||
|
<a href={link} className="text-decoration-none" rel="me nofollow noreferrer" target="_blank">
|
||||||
|
<li className="py-2 py-lg-0">
|
||||||
|
<Globe className="text-body" aria-hidden={true} />{" "}
|
||||||
|
<span className="text-decoration-underline">{displayLink}</span>
|
||||||
|
</li>
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li className="py-2 py-lg-0">
|
||||||
|
<Globe aria-hidden={true} /> {displayLink}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
32
Foxnouns.Frontend/app/components/PronounLink.tsx
Normal file
32
Foxnouns.Frontend/app/components/PronounLink.tsx
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
import { Pronoun } from "~/lib/api/user";
|
||||||
|
import { Link } from "@remix-run/react";
|
||||||
|
|
||||||
|
export default function PronounLink({ pronoun }: { pronoun: Pronoun }) {
|
||||||
|
let displayText: string;
|
||||||
|
if (pronoun.display_text) displayText = pronoun.display_text;
|
||||||
|
else {
|
||||||
|
const split = pronoun.value.split("/");
|
||||||
|
if (split.length === 5) displayText = split.splice(0, 2).join("/");
|
||||||
|
else displayText = pronoun.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
let link: string;
|
||||||
|
const linkBase = pronoun.value
|
||||||
|
.split("/")
|
||||||
|
.map((value) => encodeURIComponent(value))
|
||||||
|
.join("/");
|
||||||
|
|
||||||
|
if (pronoun.display_text) {
|
||||||
|
link = `${linkBase},${encodeURIComponent(pronoun.display_text)}`;
|
||||||
|
} else {
|
||||||
|
link = linkBase;
|
||||||
|
}
|
||||||
|
|
||||||
|
return pronoun.value.split("/").length === 5 ? (
|
||||||
|
<Link className="text-reset" to={`/pronouns/${link}`}>
|
||||||
|
{displayText}
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
<>{displayText}</>
|
||||||
|
);
|
||||||
|
}
|
34
Foxnouns.Frontend/app/components/StatusIcon.tsx
Normal file
34
Foxnouns.Frontend/app/components/StatusIcon.tsx
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
import { CustomPreference, defaultPreferences } from "~/lib/api/user";
|
||||||
|
import { OverlayTrigger, Tooltip } from "react-bootstrap";
|
||||||
|
import Icon from "~/components/KeyedIcon";
|
||||||
|
|
||||||
|
export default function StatusIcon({
|
||||||
|
preferences,
|
||||||
|
status,
|
||||||
|
}: {
|
||||||
|
preferences: Record<string, CustomPreference>;
|
||||||
|
status: string;
|
||||||
|
}) {
|
||||||
|
const mergedPrefs = Object.assign({}, defaultPreferences, preferences);
|
||||||
|
const currentPref = status in mergedPrefs ? mergedPrefs[status] : defaultPreferences.missing;
|
||||||
|
|
||||||
|
const id = crypto.randomUUID();
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<OverlayTrigger
|
||||||
|
key={id}
|
||||||
|
placement="top"
|
||||||
|
overlay={
|
||||||
|
<Tooltip id={id} aria-hidden={true}>
|
||||||
|
{currentPref.tooltip}
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span className="d-inline-block">
|
||||||
|
<Icon iconName={currentPref.icon} aria-hidden={true} style={{ pointerEvents: "none" }} />
|
||||||
|
</span>
|
||||||
|
</OverlayTrigger>
|
||||||
|
<span className="visually-hidden">{currentPref.tooltip}:</span>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
36
Foxnouns.Frontend/app/components/StatusLine.tsx
Normal file
36
Foxnouns.Frontend/app/components/StatusLine.tsx
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
import {
|
||||||
|
CustomPreference,
|
||||||
|
defaultPreferences,
|
||||||
|
FieldEntry,
|
||||||
|
PreferenceSize,
|
||||||
|
Pronoun,
|
||||||
|
} from "~/lib/api/user";
|
||||||
|
import classNames from "classnames";
|
||||||
|
import { ReactNode } from "react";
|
||||||
|
import StatusIcon from "~/components/StatusIcon";
|
||||||
|
|
||||||
|
export default function StatusLine({
|
||||||
|
entry,
|
||||||
|
preferences,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
entry: FieldEntry | Pronoun;
|
||||||
|
preferences: Record<string, CustomPreference>;
|
||||||
|
children: ReactNode;
|
||||||
|
}) {
|
||||||
|
const mergedPrefs = Object.assign({}, defaultPreferences, preferences);
|
||||||
|
const currentPref =
|
||||||
|
entry.status in mergedPrefs ? mergedPrefs[entry.status] : defaultPreferences.missing;
|
||||||
|
|
||||||
|
const classes = classNames({
|
||||||
|
"text-muted": currentPref.muted,
|
||||||
|
"fw-bold fs-5": currentPref.size == PreferenceSize.Large,
|
||||||
|
"fs-6": currentPref.size == PreferenceSize.Small,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className={classes}>
|
||||||
|
<StatusIcon preferences={preferences} status={entry.status} /> {children}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
|
@ -12,6 +12,7 @@ export type User = PartialUser & {
|
||||||
names: FieldEntry[];
|
names: FieldEntry[];
|
||||||
pronouns: Pronoun[];
|
pronouns: Pronoun[];
|
||||||
fields: Field[];
|
fields: Field[];
|
||||||
|
custom_preferences: Record<string, CustomPreference>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type UserWithMembers = User & { members: PartialMember[] };
|
export type UserWithMembers = User & { members: PartialMember[] };
|
||||||
|
@ -47,3 +48,62 @@ export type Field = {
|
||||||
name: string;
|
name: string;
|
||||||
entries: FieldEntry[];
|
entries: FieldEntry[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type CustomPreference = {
|
||||||
|
icon: string;
|
||||||
|
tooltip: string;
|
||||||
|
muted: boolean;
|
||||||
|
favourite: boolean;
|
||||||
|
size: PreferenceSize;
|
||||||
|
};
|
||||||
|
|
||||||
|
export enum PreferenceSize {
|
||||||
|
Large = "LARGE",
|
||||||
|
Normal = "NORMAL",
|
||||||
|
Small = "SMALL",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const defaultPreferences = Object.freeze({
|
||||||
|
favourite: {
|
||||||
|
icon: "heart-fill",
|
||||||
|
tooltip: "Favourite",
|
||||||
|
size: PreferenceSize.Large,
|
||||||
|
muted: false,
|
||||||
|
favourite: true,
|
||||||
|
},
|
||||||
|
okay: {
|
||||||
|
icon: "hand-thumbs-up",
|
||||||
|
tooltip: "Okay",
|
||||||
|
size: PreferenceSize.Normal,
|
||||||
|
muted: false,
|
||||||
|
favourite: false,
|
||||||
|
},
|
||||||
|
jokingly: {
|
||||||
|
icon: "emoji-laughing",
|
||||||
|
tooltip: "Jokingly",
|
||||||
|
size: PreferenceSize.Normal,
|
||||||
|
muted: false,
|
||||||
|
favourite: false,
|
||||||
|
},
|
||||||
|
friends_only: {
|
||||||
|
icon: "people",
|
||||||
|
tooltip: "Friends only",
|
||||||
|
size: PreferenceSize.Normal,
|
||||||
|
muted: false,
|
||||||
|
favourite: false,
|
||||||
|
},
|
||||||
|
avoid: {
|
||||||
|
icon: "hand-thumbs-down",
|
||||||
|
tooltip: "Avoid",
|
||||||
|
size: PreferenceSize.Small,
|
||||||
|
muted: true,
|
||||||
|
favourite: false,
|
||||||
|
},
|
||||||
|
missing: {
|
||||||
|
icon: "question-lg",
|
||||||
|
tooltip: "Unknown (missing)",
|
||||||
|
size: PreferenceSize.Normal,
|
||||||
|
muted: false,
|
||||||
|
favourite: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
22
Foxnouns.Frontend/app/lib/markdown.ts
Normal file
22
Foxnouns.Frontend/app/lib/markdown.ts
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import MarkdownIt from "markdown-it";
|
||||||
|
import sanitize from "sanitize-html";
|
||||||
|
|
||||||
|
const md = new MarkdownIt({
|
||||||
|
html: false,
|
||||||
|
breaks: true,
|
||||||
|
linkify: true,
|
||||||
|
}).disable(["heading", "lheading", "link", "table", "blockquote"]);
|
||||||
|
|
||||||
|
const unsafeMd = new MarkdownIt({
|
||||||
|
html: false,
|
||||||
|
breaks: true,
|
||||||
|
linkify: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
export function renderMarkdown(src: string | null) {
|
||||||
|
return src ? sanitize(md.render(src)) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function renderUnsafeMarkdown(src: string) {
|
||||||
|
return sanitize(unsafeMd.render(src));
|
||||||
|
}
|
|
@ -1,7 +1,14 @@
|
||||||
import { json, LoaderFunctionArgs, MetaFunction } from "@remix-run/node";
|
import { json, LoaderFunctionArgs, MetaFunction } from "@remix-run/node";
|
||||||
import { redirect, useLoaderData } from "@remix-run/react";
|
import { Link, redirect, useLoaderData, useRouteLoaderData } from "@remix-run/react";
|
||||||
import { UserWithMembers } from "~/lib/api/user";
|
import { UserWithMembers } from "~/lib/api/user";
|
||||||
import serverRequest from "~/lib/request.server";
|
import serverRequest from "~/lib/request.server";
|
||||||
|
import { loader as rootLoader } from "~/root";
|
||||||
|
import { Alert } from "react-bootstrap";
|
||||||
|
import { Trans, useTranslation } from "react-i18next";
|
||||||
|
import { renderMarkdown } from "~/lib/markdown";
|
||||||
|
import ProfileLink from "~/components/ProfileLink";
|
||||||
|
import StatusLine from "~/components/StatusLine";
|
||||||
|
import PronounLink from "~/components/PronounLink";
|
||||||
|
|
||||||
export const meta: MetaFunction<typeof loader> = ({ data }) => {
|
export const meta: MetaFunction<typeof loader> = ({ data }) => {
|
||||||
const { user } = data!;
|
const { user } = data!;
|
||||||
|
@ -25,11 +32,87 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function UserPage() {
|
export default function UserPage() {
|
||||||
|
const { t } = useTranslation();
|
||||||
const { user } = useLoaderData<typeof loader>();
|
const { user } = useLoaderData<typeof loader>();
|
||||||
|
const { meUser } = useRouteLoaderData<typeof rootLoader>("root") || { meUser: undefined };
|
||||||
|
|
||||||
|
const bio = renderMarkdown(user.bio);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
hello! this is the user page for @{user.username}. their ID is {user.id}
|
{meUser && meUser.id === user.id && (
|
||||||
|
<Alert variant="secondary">
|
||||||
|
<Trans t={t} i18nKey="user.own-profile-alert">
|
||||||
|
You are currently viewing your <strong>public</strong> profile.
|
||||||
|
<br />
|
||||||
|
<Link to={`/@${user.username}/edit`}>Edit your profile</Link>
|
||||||
|
</Trans>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
<div className="grid row-gap-3">
|
||||||
|
<div className="row">
|
||||||
|
<div className="col-md-4 text-center">
|
||||||
|
<img
|
||||||
|
src={user.avatar_url || "https://pronouns.cc/default/512.webp"}
|
||||||
|
alt={t("user.avatar-alt", { username: user.username })}
|
||||||
|
width={200}
|
||||||
|
height={200}
|
||||||
|
className="rounded-circle img-fluid"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="col-md">
|
||||||
|
{user.display_name ? (
|
||||||
|
<>
|
||||||
|
<h2>{user.display_name}</h2>
|
||||||
|
<p className="fs-5 text-body-secondary">@{user.username}</p>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<h2>@{user.username}</h2>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{bio && (
|
||||||
|
<>
|
||||||
|
<hr />
|
||||||
|
<p dangerouslySetInnerHTML={{ __html: bio }}></p>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{user.links.length > 0 && (
|
||||||
|
<div className="col-md d-flex align-items-center">
|
||||||
|
<ul className="list-unstyled">
|
||||||
|
{user.links.map((l, i) => (
|
||||||
|
<ProfileLink link={l} key={i} />
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="row row-cols-1 row-cols-sm-2 row-cols-md-3">
|
||||||
|
{user.names.length > 0 && (
|
||||||
|
<div className="col-md">
|
||||||
|
<h3>{t("user.heading.names")}</h3>
|
||||||
|
<ul className="list-unstyled fs-5">
|
||||||
|
{user.names.map((n, i) => (
|
||||||
|
<StatusLine entry={n} preferences={user.custom_preferences} key={i}>
|
||||||
|
{n.value}
|
||||||
|
</StatusLine>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{user.pronouns.length > 0 && (
|
||||||
|
<div className="col-md">
|
||||||
|
<h3>{t("user.heading.pronouns")}</h3>
|
||||||
|
{user.pronouns.map((p, i) => (
|
||||||
|
<StatusLine entry={p} preferences={user.custom_preferences} key={i}>
|
||||||
|
<PronounLink pronoun={p} />
|
||||||
|
</StatusLine>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
"@remix-run/react": "^2.11.2",
|
"@remix-run/react": "^2.11.2",
|
||||||
"@remix-run/serve": "^2.11.2",
|
"@remix-run/serve": "^2.11.2",
|
||||||
"bootstrap": "^5.3.3",
|
"bootstrap": "^5.3.3",
|
||||||
|
"change-case": "^5.4.4",
|
||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
"compression": "^1.7.4",
|
"compression": "^1.7.4",
|
||||||
"cookie": "^0.6.0",
|
"cookie": "^0.6.0",
|
||||||
|
@ -29,13 +30,15 @@
|
||||||
"i18next-fs-backend": "^2.3.2",
|
"i18next-fs-backend": "^2.3.2",
|
||||||
"i18next-http-backend": "^2.6.1",
|
"i18next-http-backend": "^2.6.1",
|
||||||
"isbot": "^4.1.0",
|
"isbot": "^4.1.0",
|
||||||
|
"markdown-it": "^14.1.0",
|
||||||
"morgan": "^1.10.0",
|
"morgan": "^1.10.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-bootstrap": "^2.10.4",
|
"react-bootstrap": "^2.10.4",
|
||||||
"react-bootstrap-icons": "^1.11.4",
|
"react-bootstrap-icons": "^1.11.4",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-i18next": "^15.0.1",
|
"react-i18next": "^15.0.1",
|
||||||
"remix-i18next": "^6.3.0"
|
"remix-i18next": "^6.3.0",
|
||||||
|
"sanitize-html": "^2.13.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@fontsource/firago": "^5.0.11",
|
"@fontsource/firago": "^5.0.11",
|
||||||
|
@ -43,9 +46,11 @@
|
||||||
"@types/compression": "^1.7.5",
|
"@types/compression": "^1.7.5",
|
||||||
"@types/cookie": "^0.6.0",
|
"@types/cookie": "^0.6.0",
|
||||||
"@types/express": "^4.17.21",
|
"@types/express": "^4.17.21",
|
||||||
|
"@types/markdown-it": "^14.1.2",
|
||||||
"@types/morgan": "^1.9.9",
|
"@types/morgan": "^1.9.9",
|
||||||
"@types/react": "^18.2.20",
|
"@types/react": "^18.2.20",
|
||||||
"@types/react-dom": "^18.2.7",
|
"@types/react-dom": "^18.2.7",
|
||||||
|
"@types/sanitize-html": "^2.13.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.7.4",
|
"@typescript-eslint/eslint-plugin": "^6.7.4",
|
||||||
"@typescript-eslint/parser": "^6.7.4",
|
"@typescript-eslint/parser": "^6.7.4",
|
||||||
"eslint": "^8.38.0",
|
"eslint": "^8.38.0",
|
||||||
|
|
|
@ -31,6 +31,14 @@
|
||||||
"theme-dark": "Dark",
|
"theme-dark": "Dark",
|
||||||
"theme-light": "Light"
|
"theme-light": "Light"
|
||||||
},
|
},
|
||||||
|
"user": {
|
||||||
|
"own-profile-alert": "You are currently viewing your <1>public</1> profile.<3></3><4>Edit your profile</4>",
|
||||||
|
"avatar-alt": "Avatar for @{{username}}",
|
||||||
|
"heading": {
|
||||||
|
"names": "Names",
|
||||||
|
"pronouns": "Pronouns"
|
||||||
|
}
|
||||||
|
},
|
||||||
"log-in": {
|
"log-in": {
|
||||||
"callback": {
|
"callback": {
|
||||||
"title": {
|
"title": {
|
||||||
|
|
|
@ -1336,6 +1336,19 @@
|
||||||
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
|
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
|
||||||
integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==
|
integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==
|
||||||
|
|
||||||
|
"@types/linkify-it@^5":
|
||||||
|
version "5.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/linkify-it/-/linkify-it-5.0.0.tgz#21413001973106cda1c3a9b91eedd4ccd5469d76"
|
||||||
|
integrity sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==
|
||||||
|
|
||||||
|
"@types/markdown-it@^14.1.2":
|
||||||
|
version "14.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/markdown-it/-/markdown-it-14.1.2.tgz#57f2532a0800067d9b934f3521429a2e8bfb4c61"
|
||||||
|
integrity sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==
|
||||||
|
dependencies:
|
||||||
|
"@types/linkify-it" "^5"
|
||||||
|
"@types/mdurl" "^2"
|
||||||
|
|
||||||
"@types/mdast@^3.0.0":
|
"@types/mdast@^3.0.0":
|
||||||
version "3.0.15"
|
version "3.0.15"
|
||||||
resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-3.0.15.tgz#49c524a263f30ffa28b71ae282f813ed000ab9f5"
|
resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-3.0.15.tgz#49c524a263f30ffa28b71ae282f813ed000ab9f5"
|
||||||
|
@ -1343,6 +1356,11 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/unist" "^2"
|
"@types/unist" "^2"
|
||||||
|
|
||||||
|
"@types/mdurl@^2":
|
||||||
|
version "2.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/mdurl/-/mdurl-2.0.0.tgz#d43878b5b20222682163ae6f897b20447233bdfd"
|
||||||
|
integrity sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==
|
||||||
|
|
||||||
"@types/mdx@^2.0.0", "@types/mdx@^2.0.5":
|
"@types/mdx@^2.0.0", "@types/mdx@^2.0.5":
|
||||||
version "2.0.13"
|
version "2.0.13"
|
||||||
resolved "https://registry.yarnpkg.com/@types/mdx/-/mdx-2.0.13.tgz#68f6877043d377092890ff5b298152b0a21671bd"
|
resolved "https://registry.yarnpkg.com/@types/mdx/-/mdx-2.0.13.tgz#68f6877043d377092890ff5b298152b0a21671bd"
|
||||||
|
@ -1414,6 +1432,13 @@
|
||||||
"@types/prop-types" "*"
|
"@types/prop-types" "*"
|
||||||
csstype "^3.0.2"
|
csstype "^3.0.2"
|
||||||
|
|
||||||
|
"@types/sanitize-html@^2.13.0":
|
||||||
|
version "2.13.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/sanitize-html/-/sanitize-html-2.13.0.tgz#ac3620e867b7c68deab79c72bd117e2049cdd98e"
|
||||||
|
integrity sha512-X31WxbvW9TjIhZZNyNBZ/p5ax4ti7qsNDBDEnH4zAgmEh35YnFD1UiS6z9Cd34kKm0LslFW0KPmTQzu/oGtsqQ==
|
||||||
|
dependencies:
|
||||||
|
htmlparser2 "^8.0.0"
|
||||||
|
|
||||||
"@types/semver@^7.5.0":
|
"@types/semver@^7.5.0":
|
||||||
version "7.5.8"
|
version "7.5.8"
|
||||||
resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.8.tgz#8268a8c57a3e4abd25c165ecd36237db7948a55e"
|
resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.8.tgz#8268a8c57a3e4abd25c165ecd36237db7948a55e"
|
||||||
|
@ -2075,6 +2100,11 @@ chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.2:
|
||||||
ansi-styles "^4.1.0"
|
ansi-styles "^4.1.0"
|
||||||
supports-color "^7.1.0"
|
supports-color "^7.1.0"
|
||||||
|
|
||||||
|
change-case@^5.4.4:
|
||||||
|
version "5.4.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/change-case/-/change-case-5.4.4.tgz#0d52b507d8fb8f204343432381d1a6d7bff97a02"
|
||||||
|
integrity sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==
|
||||||
|
|
||||||
character-entities-html4@^2.0.0:
|
character-entities-html4@^2.0.0:
|
||||||
version "2.1.0"
|
version "2.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/character-entities-html4/-/character-entities-html4-2.1.0.tgz#1f1adb940c971a4b22ba39ddca6b618dc6e56b2b"
|
resolved "https://registry.yarnpkg.com/character-entities-html4/-/character-entities-html4-2.1.0.tgz#1f1adb940c971a4b22ba39ddca6b618dc6e56b2b"
|
||||||
|
@ -3760,6 +3790,16 @@ html-parse-stringify@^3.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
void-elements "3.1.0"
|
void-elements "3.1.0"
|
||||||
|
|
||||||
|
htmlparser2@^8.0.0:
|
||||||
|
version "8.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-8.0.2.tgz#f002151705b383e62433b5cf466f5b716edaec21"
|
||||||
|
integrity sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==
|
||||||
|
dependencies:
|
||||||
|
domelementtype "^2.3.0"
|
||||||
|
domhandler "^5.0.3"
|
||||||
|
domutils "^3.0.1"
|
||||||
|
entities "^4.4.0"
|
||||||
|
|
||||||
htmlparser2@^9.1.0:
|
htmlparser2@^9.1.0:
|
||||||
version "9.1.0"
|
version "9.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-9.1.0.tgz#cdb498d8a75a51f739b61d3f718136c369bc8c23"
|
resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-9.1.0.tgz#cdb498d8a75a51f739b61d3f718136c369bc8c23"
|
||||||
|
@ -4120,6 +4160,11 @@ is-plain-obj@^4.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-4.1.0.tgz#d65025edec3657ce032fd7db63c97883eaed71f0"
|
resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-4.1.0.tgz#d65025edec3657ce032fd7db63c97883eaed71f0"
|
||||||
integrity sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==
|
integrity sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==
|
||||||
|
|
||||||
|
is-plain-object@^5.0.0:
|
||||||
|
version "5.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-5.0.0.tgz#4427f50ab3429e9025ea7d52e9043a9ef4159344"
|
||||||
|
integrity sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==
|
||||||
|
|
||||||
is-reference@^3.0.0:
|
is-reference@^3.0.0:
|
||||||
version "3.0.2"
|
version "3.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/is-reference/-/is-reference-3.0.2.tgz#154747a01f45cd962404ee89d43837af2cba247c"
|
resolved "https://registry.yarnpkg.com/is-reference/-/is-reference-3.0.2.tgz#154747a01f45cd962404ee89d43837af2cba247c"
|
||||||
|
@ -4370,6 +4415,13 @@ lilconfig@^3.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-3.1.2.tgz#e4a7c3cb549e3a606c8dcc32e5ae1005e62c05cb"
|
resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-3.1.2.tgz#e4a7c3cb549e3a606c8dcc32e5ae1005e62c05cb"
|
||||||
integrity sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==
|
integrity sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==
|
||||||
|
|
||||||
|
linkify-it@^5.0.0:
|
||||||
|
version "5.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-5.0.0.tgz#9ef238bfa6dc70bd8e7f9572b52d369af569b421"
|
||||||
|
integrity sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==
|
||||||
|
dependencies:
|
||||||
|
uc.micro "^2.0.0"
|
||||||
|
|
||||||
loader-utils@^3.2.0:
|
loader-utils@^3.2.0:
|
||||||
version "3.3.1"
|
version "3.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-3.3.1.tgz#735b9a19fd63648ca7adbd31c2327dfe281304e5"
|
resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-3.3.1.tgz#735b9a19fd63648ca7adbd31c2327dfe281304e5"
|
||||||
|
@ -4452,6 +4504,18 @@ markdown-extensions@^1.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/markdown-extensions/-/markdown-extensions-1.1.1.tgz#fea03b539faeaee9b4ef02a3769b455b189f7fc3"
|
resolved "https://registry.yarnpkg.com/markdown-extensions/-/markdown-extensions-1.1.1.tgz#fea03b539faeaee9b4ef02a3769b455b189f7fc3"
|
||||||
integrity sha512-WWC0ZuMzCyDHYCasEGs4IPvLyTGftYwh6wIEOULOF0HXcqZlhwRzrK0w2VUlxWA98xnvb/jszw4ZSkJ6ADpM6Q==
|
integrity sha512-WWC0ZuMzCyDHYCasEGs4IPvLyTGftYwh6wIEOULOF0HXcqZlhwRzrK0w2VUlxWA98xnvb/jszw4ZSkJ6ADpM6Q==
|
||||||
|
|
||||||
|
markdown-it@^14.1.0:
|
||||||
|
version "14.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-14.1.0.tgz#3c3c5992883c633db4714ccb4d7b5935d98b7d45"
|
||||||
|
integrity sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==
|
||||||
|
dependencies:
|
||||||
|
argparse "^2.0.1"
|
||||||
|
entities "^4.4.0"
|
||||||
|
linkify-it "^5.0.0"
|
||||||
|
mdurl "^2.0.0"
|
||||||
|
punycode.js "^2.3.1"
|
||||||
|
uc.micro "^2.1.0"
|
||||||
|
|
||||||
matcher-collection@^2.0.0:
|
matcher-collection@^2.0.0:
|
||||||
version "2.0.1"
|
version "2.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/matcher-collection/-/matcher-collection-2.0.1.tgz#90be1a4cf58d6f2949864f65bb3b0f3e41303b29"
|
resolved "https://registry.yarnpkg.com/matcher-collection/-/matcher-collection-2.0.1.tgz#90be1a4cf58d6f2949864f65bb3b0f3e41303b29"
|
||||||
|
@ -4590,6 +4654,11 @@ mdast-util-to-string@^3.0.0, mdast-util-to-string@^3.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/mdast" "^3.0.0"
|
"@types/mdast" "^3.0.0"
|
||||||
|
|
||||||
|
mdurl@^2.0.0:
|
||||||
|
version "2.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-2.0.0.tgz#80676ec0433025dd3e17ee983d0fe8de5a2237e0"
|
||||||
|
integrity sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==
|
||||||
|
|
||||||
media-query-parser@^2.0.2:
|
media-query-parser@^2.0.2:
|
||||||
version "2.0.2"
|
version "2.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/media-query-parser/-/media-query-parser-2.0.2.tgz#ff79e56cee92615a304a1c2fa4f2bd056c0a1d29"
|
resolved "https://registry.yarnpkg.com/media-query-parser/-/media-query-parser-2.0.2.tgz#ff79e56cee92615a304a1c2fa4f2bd056c0a1d29"
|
||||||
|
@ -5380,6 +5449,11 @@ parse-ms@^2.1.0:
|
||||||
resolved "https://registry.yarnpkg.com/parse-ms/-/parse-ms-2.1.0.tgz#348565a753d4391fa524029956b172cb7753097d"
|
resolved "https://registry.yarnpkg.com/parse-ms/-/parse-ms-2.1.0.tgz#348565a753d4391fa524029956b172cb7753097d"
|
||||||
integrity sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA==
|
integrity sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA==
|
||||||
|
|
||||||
|
parse-srcset@^1.0.2:
|
||||||
|
version "1.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/parse-srcset/-/parse-srcset-1.0.2.tgz#f2bd221f6cc970a938d88556abc589caaaa2bde1"
|
||||||
|
integrity sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==
|
||||||
|
|
||||||
parse5-htmlparser2-tree-adapter@^7.0.0:
|
parse5-htmlparser2-tree-adapter@^7.0.0:
|
||||||
version "7.0.0"
|
version "7.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.0.0.tgz#23c2cc233bcf09bb7beba8b8a69d46b08c62c2f1"
|
resolved "https://registry.yarnpkg.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.0.0.tgz#23c2cc233bcf09bb7beba8b8a69d46b08c62c2f1"
|
||||||
|
@ -5473,7 +5547,7 @@ periscopic@^3.0.0:
|
||||||
estree-walker "^3.0.0"
|
estree-walker "^3.0.0"
|
||||||
is-reference "^3.0.0"
|
is-reference "^3.0.0"
|
||||||
|
|
||||||
picocolors@^1.0.0, picocolors@^1.0.1:
|
picocolors@^1.0.0, picocolors@^1.0.1, picocolors@^1.1.0:
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.0.tgz#5358b76a78cde483ba5cef6a9dc9671440b27d59"
|
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.0.tgz#5358b76a78cde483ba5cef6a9dc9671440b27d59"
|
||||||
integrity sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==
|
integrity sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==
|
||||||
|
@ -5570,6 +5644,15 @@ postcss-value-parser@^4.1.0:
|
||||||
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514"
|
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514"
|
||||||
integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==
|
integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==
|
||||||
|
|
||||||
|
postcss@^8.3.11:
|
||||||
|
version "8.4.47"
|
||||||
|
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.47.tgz#5bf6c9a010f3e724c503bf03ef7947dcb0fea365"
|
||||||
|
integrity sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==
|
||||||
|
dependencies:
|
||||||
|
nanoid "^3.3.7"
|
||||||
|
picocolors "^1.1.0"
|
||||||
|
source-map-js "^1.2.1"
|
||||||
|
|
||||||
postcss@^8.4.19, postcss@^8.4.43:
|
postcss@^8.4.19, postcss@^8.4.43:
|
||||||
version "8.4.45"
|
version "8.4.45"
|
||||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.45.tgz#538d13d89a16ef71edbf75d895284ae06b79e603"
|
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.45.tgz#538d13d89a16ef71edbf75d895284ae06b79e603"
|
||||||
|
@ -5684,6 +5767,11 @@ pumpify@^1.3.3:
|
||||||
inherits "^2.0.3"
|
inherits "^2.0.3"
|
||||||
pump "^2.0.0"
|
pump "^2.0.0"
|
||||||
|
|
||||||
|
punycode.js@^2.3.1:
|
||||||
|
version "2.3.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/punycode.js/-/punycode.js-2.3.1.tgz#6b53e56ad75588234e79f4affa90972c7dd8cdb7"
|
||||||
|
integrity sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==
|
||||||
|
|
||||||
punycode@^2.1.0:
|
punycode@^2.1.0:
|
||||||
version "2.3.1"
|
version "2.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5"
|
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5"
|
||||||
|
@ -6097,6 +6185,18 @@ safe-regex-test@^1.0.3:
|
||||||
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
|
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
|
||||||
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
|
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
|
||||||
|
|
||||||
|
sanitize-html@^2.13.0:
|
||||||
|
version "2.13.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-2.13.0.tgz#71aedcdb777897985a4ea1877bf4f895a1170dae"
|
||||||
|
integrity sha512-Xff91Z+4Mz5QiNSLdLWwjgBDm5b1RU6xBT0+12rapjiaR7SwfRdjw8f+6Rir2MXKLrDicRFHdb51hGOAxmsUIA==
|
||||||
|
dependencies:
|
||||||
|
deepmerge "^4.2.2"
|
||||||
|
escape-string-regexp "^4.0.0"
|
||||||
|
htmlparser2 "^8.0.0"
|
||||||
|
is-plain-object "^5.0.0"
|
||||||
|
parse-srcset "^1.0.2"
|
||||||
|
postcss "^8.3.11"
|
||||||
|
|
||||||
sass@1.77.6:
|
sass@1.77.6:
|
||||||
version "1.77.6"
|
version "1.77.6"
|
||||||
resolved "https://registry.yarnpkg.com/sass/-/sass-1.77.6.tgz#898845c1348078c2e6d1b64f9ee06b3f8bd489e4"
|
resolved "https://registry.yarnpkg.com/sass/-/sass-1.77.6.tgz#898845c1348078c2e6d1b64f9ee06b3f8bd489e4"
|
||||||
|
@ -6233,6 +6333,11 @@ sort-keys@^5.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.0.tgz#16b809c162517b5b8c3e7dcd315a2a5c2612b2af"
|
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.0.tgz#16b809c162517b5b8c3e7dcd315a2a5c2612b2af"
|
||||||
integrity sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==
|
integrity sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==
|
||||||
|
|
||||||
|
source-map-js@^1.2.1:
|
||||||
|
version "1.2.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46"
|
||||||
|
integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==
|
||||||
|
|
||||||
source-map-support@^0.5.21:
|
source-map-support@^0.5.21:
|
||||||
version "0.5.21"
|
version "0.5.21"
|
||||||
resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f"
|
resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f"
|
||||||
|
@ -6339,8 +6444,16 @@ string-hash@^1.1.1:
|
||||||
resolved "https://registry.yarnpkg.com/string-hash/-/string-hash-1.1.3.tgz#e8aafc0ac1855b4666929ed7dd1275df5d6c811b"
|
resolved "https://registry.yarnpkg.com/string-hash/-/string-hash-1.1.3.tgz#e8aafc0ac1855b4666929ed7dd1275df5d6c811b"
|
||||||
integrity sha512-kJUvRUFK49aub+a7T1nNE66EJbZBMnBgoC1UbCZ5n6bsZKBRga4KgBRTMn/pFkeCZSYtNeSyMxPDM0AXWELk2A==
|
integrity sha512-kJUvRUFK49aub+a7T1nNE66EJbZBMnBgoC1UbCZ5n6bsZKBRga4KgBRTMn/pFkeCZSYtNeSyMxPDM0AXWELk2A==
|
||||||
|
|
||||||
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0:
|
"string-width-cjs@npm:string-width@^4.2.0":
|
||||||
name string-width-cjs
|
version "4.2.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
||||||
|
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||||
|
dependencies:
|
||||||
|
emoji-regex "^8.0.0"
|
||||||
|
is-fullwidth-code-point "^3.0.0"
|
||||||
|
strip-ansi "^6.0.1"
|
||||||
|
|
||||||
|
string-width@^4.1.0:
|
||||||
version "4.2.3"
|
version "4.2.3"
|
||||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
||||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||||
|
@ -6442,7 +6555,14 @@ stringify-entities@^4.0.0:
|
||||||
character-entities-html4 "^2.0.0"
|
character-entities-html4 "^2.0.0"
|
||||||
character-entities-legacy "^3.0.0"
|
character-entities-legacy "^3.0.0"
|
||||||
|
|
||||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
|
||||||
|
version "6.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
||||||
|
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||||
|
dependencies:
|
||||||
|
ansi-regex "^5.0.1"
|
||||||
|
|
||||||
|
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||||
version "6.0.1"
|
version "6.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
||||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||||
|
@ -6724,6 +6844,11 @@ typescript@^5.1.6:
|
||||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.5.4.tgz#d9852d6c82bad2d2eda4fd74a5762a8f5909e9ba"
|
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.5.4.tgz#d9852d6c82bad2d2eda4fd74a5762a8f5909e9ba"
|
||||||
integrity sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==
|
integrity sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==
|
||||||
|
|
||||||
|
uc.micro@^2.0.0, uc.micro@^2.1.0:
|
||||||
|
version "2.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-2.1.0.tgz#f8d3f7d0ec4c3dea35a7e3c8efa4cb8b45c9e7ee"
|
||||||
|
integrity sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==
|
||||||
|
|
||||||
ufo@^1.5.3:
|
ufo@^1.5.3:
|
||||||
version "1.5.4"
|
version "1.5.4"
|
||||||
resolved "https://registry.yarnpkg.com/ufo/-/ufo-1.5.4.tgz#16d6949674ca0c9e0fbbae1fa20a71d7b1ded754"
|
resolved "https://registry.yarnpkg.com/ufo/-/ufo-1.5.4.tgz#16d6949674ca0c9e0fbbae1fa20a71d7b1ded754"
|
||||||
|
|
Loading…
Reference in a new issue