Add the /donate page

Just doing some house-cleaning on easy pages that need converted before DTI Classic can retire!
This commit is contained in:
Emi Matchu 2022-09-25 08:05:38 -07:00
parent 2b486ea218
commit 07e2c0f7b1
9 changed files with 567 additions and 305 deletions

105
pages/donate.tsx Normal file
View file

@ -0,0 +1,105 @@
import DonationsPage from "../src/app/DonationsPage";
// @ts-ignore: doesn't understand module.exports
import connectToDb from "../src/server/db";
type Props = { campaigns: Campaign[] };
type Campaign = {
id: string;
name: string;
donationFeatures: DonationFeature[];
};
type DonationFeature = {
id: string;
donorName: string | null;
outfit: { id: string; name: string; updatedAt: string } | null;
};
export default function DonationsPageWrapper({ campaigns }: Props) {
return <DonationsPage campaigns={campaigns} />;
}
/**
* getStaticProps loads the donation info and organizes it into a convenient
* structure for the page props.
*
* This happens at build time! This data basically hasn't changed since 2017,
* so we'd rather have the performance benefits and reliability of just
* building it up-front than making this an SSR thing. (It also makes it harder
* for someone to add a surprise prank message years later when we're not
* paying attention, by editing the outfit titleit would still probably make
* it onto the site eventually, but not until the next build, which should be
* discouraging. But nobody's ever tried to prank this page before, so, shrug!)
*
* I also just went with a direct DB query, to avoid putting any of this in our
* GraphQL schema when it's really super-duper *just* for this page and *just*
* going to be requested in super-duper *one* way.
*/
export async function getStaticProps() {
const db = await connectToDb();
const [rows]: [QueryRow[]] = await db.query({
sql: `
SELECT
donation_features.id,
donations.donor_name,
campaigns.id, campaigns.name,
outfits.id, outfits.name, outfits.updated_at
FROM donation_features
INNER JOIN donations ON donations.id = donation_features.donation_id
INNER JOIN campaigns ON campaigns.id = donations.campaign_id
LEFT JOIN outfits ON outfits.id = donation_features.outfit_id
ORDER BY campaigns.created_at DESC, donations.created_at ASC;
`,
nestTables: true,
});
// Reorganize the query rows into campaign objects with lists of donation
// features.
const campaigns: Campaign[] = [];
for (const row of rows) {
// Find the campaign for this feature in our campaigns list, or add it if
// it's not present yet.
let campaign = campaigns.find((c) => c.id === String(row.campaigns.id));
if (campaign == null) {
campaign = {
id: String(row.campaigns.id),
name: row.campaigns.name,
donationFeatures: [],
};
campaigns.push(campaign);
}
// Reformat the outfit and donation feature into safer and more
// serializable forms.
const outfit =
row.outfits.id != null
? {
id: String(row.outfits.id),
name: row.outfits.name,
updatedAt: row.outfits.updated_at.toISOString(),
}
: null;
const donationFeature: DonationFeature = {
id: String(row.donation_features.id),
donorName: row.donations.donor_name,
outfit,
};
// Add this donation feature to the campaign.
campaign.donationFeatures.push(donationFeature);
}
return {
props: { campaigns },
};
}
type QueryRow = {
donation_features: { id: number };
donations: { donor_name: string | null };
campaigns: { id: string; name: string };
outfits:
| { id: number; name: string; updated_at: Date }
| { id: null; name: null; updated_at: null };
};

View file

@ -1,8 +1,11 @@
USE openneo_impress;
-- Public data tables: read
GRANT SELECT ON campaigns TO impress2020;
GRANT SELECT ON colors TO impress2020;
GRANT SELECT ON color_translations TO impress2020;
GRANT SELECT ON donation_features TO impress2020;
GRANT SELECT ON donations TO impress2020;
GRANT SELECT ON items TO impress2020;
GRANT SELECT ON item_translations TO impress2020;
GRANT SELECT ON modeling_logs TO impress2020;

127
src/app/DonationsPage.js Normal file
View file

@ -0,0 +1,127 @@
import React from "react";
import Head from "next/head";
import { Heading1, Heading2 } from "./util";
import TextContent from "./components/TextContent";
import FastlyLogoImg from "./images/fastly-logo.svg";
import { Box, Wrap, WrapItem } from "@chakra-ui/react";
import Image from "next/image";
import { OutfitCard } from "./UserOutfitsPage";
function DonationsPage({ campaigns }) {
return (
<>
<Head>
<title>Donations | Dress to Impress</title>
</Head>
<TextContent>
<Heading1 marginBottom="4">Our donors</Heading1>
<p>
Dress to Impress has been around a long timefrom back when Matchu was
in middle school! Without a real source of income, we used to depend a
lot on community donations to keep the site running.
</p>
<p>
Since then, life has changed a lot, and we're able to comfortably fund
Dress to Impress out-of-pocket. But we're still very grateful to the
donors who got us through those tougher years! Here's a showcase of
their outfits 💖
</p>
<p>
{/* Thanking Fastly somewhere in a sponsors page on our site is a
* condition of the program, so here we are! But also it's a great
* deal and I mean what I say! */}
We're also grateful to <FastlyLogoLink />, who offer us some CDN
hosting services under their non-profit &amp; open-source partner
program! They help us load all the big images around the site much
faster! Thank you!
</p>
</TextContent>
{campaigns.map((campaign) => (
<CampaignSection key={campaign.id} campaign={campaign} />
))}
</>
);
}
function CampaignSection({ campaign }) {
const { donationFeatures, name } = campaign;
const featuresWithNames = donationFeatures.filter(
(f) => f.donorName?.length > 0
);
const allDonorNames = new Set(featuresWithNames.map((f) => f.donorName));
const donorNamesWithOutfits = new Set(
featuresWithNames.filter((f) => f.outfit != null).map((f) => f.donorName)
);
const donorNamesWithoutOutfits = [...allDonorNames]
.filter((n) => !donorNamesWithOutfits.has(n))
.sort((a, b) => a.localeCompare(b));
return (
<Box marginBottom="8">
<Heading2 marginBottom="4">{name} donors</Heading2>
<Wrap spacing="4" justify="space-around">
{donationFeatures
.filter((f) => f.outfit != null)
.map((donationFeature) => (
<WrapItem key={donationFeature.outfit.id}>
<DonationOutfitCard
outfit={donationFeature.outfit}
donorName={donationFeature.donorName || "Anonymous"}
/>
</WrapItem>
))}
</Wrap>
{donorNamesWithoutOutfits.length > 0 && (
<Box
textAlign="center"
fontStyle="italic"
maxWidth="800px"
marginX="auto"
marginTop="8"
>
<strong>And a few more:</strong> {donorNamesWithoutOutfits.join(", ")}
. <strong>Thank you!</strong>
</Box>
)}
</Box>
);
}
function DonationOutfitCard({ outfit, donorName }) {
return (
<OutfitCard
outfit={outfit}
caption={
<Box>
<Box fontSize="sm">Thank you, {donorName}!</Box>
<Box fontSize="xs">{outfit.name}</Box>
</Box>
}
alt={`Outfit thumbnail: ${outfit.name}`}
/>
);
}
function FastlyLogoLink() {
return (
<a href="https://www.fastly.com/">
<Box
display="inline-block"
verticalAlign="middle"
filter="saturate(90%)"
marginBottom="-2px"
>
<Image
src={FastlyLogoImg}
width={40}
height={16}
alt="Fastly"
title="Fastly"
/>
</Box>
</a>
);
}
export default DonationsPage;

View file

@ -31,6 +31,9 @@ function GlobalFooter() {
<Link href="/privacy" passHref>
<ChakraLink>Privacy Policy (Sep 2022)</ChakraLink>
</Link>
<Link href="/donate" passHref>
<ChakraLink>Donors</ChakraLink>
</Link>
<ChakraLink href={classicDTIUrl}>Classic DTI</ChakraLink>
</HStack>
<Box as="p" opacity="0.75">

View file

@ -1,10 +1,10 @@
import React from "react";
import { css } from "@emotion/react";
import { VStack } from "@chakra-ui/react";
import { Heading1, Heading2, Heading3 } from "./util";
import { useAuthModeFeatureFlag } from "./components/useCurrentUser";
import Head from "next/head";
import TextContent from "./components/TextContent";
function PrivacyPolicyPage() {
const [authMode] = useAuthModeFeatureFlag();
@ -15,204 +15,190 @@ function PrivacyPolicyPage() {
<title>Privacy Policy | Dress to Impress</title>
</Head>
<Heading1 marginBottom="4">Our privacy policy</Heading1>
<VStack
spacing="4"
alignItems="flex-start"
css={css`
max-width: 800px;
p {
margin-bottom: 1em;
}
a {
text-decoration: underline;
}
h2,
h3 {
margin-bottom: 0.5em;
}
`}
>
<section>
<p>
Hi, friends! Dress to Impress collects certain personal data. Here's
how we use it!
</p>
<p>
First off, we'll <em>never</em> sell your private data, ever. It'll
only be available to you and our small trusted staffand we'll only
use it to serve you directly, debug site issues, and help you share
your creations with others.
</p>
</section>
{authMode === "auth0" && (
<TextContent maxWidth="800px">
<VStack spacing="4" alignItems="flex-start">
<section>
<Heading2>Account management</Heading2>
<p>
While our <a href="https://impress.openneo.net/">classic app</a>{" "}
uses its own authentication, the app you're using now uses a
service called <a href="https://auth0.com/">Auth0</a> to manage
account creation and login.
Hi, friends! Dress to Impress collects certain personal data.
Here's how we use it!
</p>
<p>
We made this decision because authentication is difficult to write
and maintain securely. We felt that Auth0 was the smoothest and
most secure experience we could offer, especially as a small team
of volunteers{" "}
<span role="img" aria-label="Sweat smile emoji">
😅
</span>
</p>
<p>
<a href="https://auth0.com/legal/ss-tos">
Auth0's terms of service
</a>{" "}
commit to treating your user data as confidential information, not
to be shared with anyone else, and only to be used as part of
Dress to Impress. (The details are in Sections 6 and 7!)
</p>
<p>
When signing up, Auth0 will ask for a username, password, and
email address. They store your password as a <em>hash</em> (which,
colloquially, is like a one-way encryption), rather than as the
plain password itself.
</p>
<p>
Some user accounts were created before we moved to Auth0. For
those users, we imported their accounts from our custom database
into Auth0. This included username, password hash, and email
address.
First off, we'll <em>never</em> sell your private data, ever.
It'll only be available to you and our small trusted staffand
we'll only use it to serve you directly, debug site issues, and
help you share your creations with others.
</p>
</section>
)}
<section>
<Heading2>Analytics and logging</Heading2>
<p>
To understand how people use our site, we use a service called{" "}
<a href="https://plausible.io/">Plausible</a>. Every time you visit
a page, we send them a{" "}
<a href="https://plausible.io/data-policy">
small packet of information
</a>
.
</p>
<p>
Plausible is a privacy-focused service. It doesn't store your IP
address in a retrievable way, or add cookies to your browser, or
track you across multiple websites or over time.{" "}
<a href="https://plausible.io/data-policy">
Here's their data policy.
</a>
</p>
<p>
We also use a service called <a href="https://sentry.io/">Sentry</a>{" "}
to track errors. When you encounter an error on our site, we send a
copy of it to our Sentry account, to help us debug it later. This
might sometimes include personal data, but Sentry will only share it
with us.{" "}
<a href="https://sentry.io/legal/dpa/2.0.0/">
Here's their data policy.
</a>
</p>
<p>
We also use <a href="https://www.linode.com/">Linode</a> and{" "}
<a href="https://www.fastly.com/">Fastly</a> for web hosting. Linode
stores our database, and handles most web traffic dealing with
personal data. Personal data also travels through Fastly's servers
temporarily, but they only store aggregate usage logs for us, not
any personally-identifying data.
</p>
</section>
<section>
<Heading2>Creations and contributions</Heading2>
<p>
People use Dress to Impress to create, share, and communicate! Some
of these things are public, some are private, and some are
configurable.
</p>
<Heading3>Outfits</Heading3>
<p>
Outfits are the central creation on Dress to Impress: combining a
pet with items to make something that looks nice!
</p>
<p>
Users can log in and save outfits to their account. They can also
share outfits by URL without logging in.
</p>
<p>
When you save an outfit to your account, it's somewhat private, but
somewhat public.
</p>
<p>
It's private in the sense that there is no central place where
another user can look up your list of outfits.
</p>
<p>
But it's public in the sense that anyone with the URL can see
itand, because the URLs are based on a simple incrementing global
outfit ID, it's easy to look up all the outfits on the site.
</p>
<p>
We might change this in the future, to make the URLs hard to guess
and <em>genuinely</em> private. Until then, we advise users to not
to include sensitive data in the outfits they save to their account.
</p>
<Heading3>Item lists</Heading3>
<p>
Logged-in users can track the Neopets customization items they own
and want, by saving item lists to their account.
</p>
<p>
These lists are private by default, but can be configured to either
be "public" or "trading" as well.
</p>
<p>
The "public" status means that anyone who knows your Dress to
Impress username, or item list URL, can see this list.
</p>
<p>
The "trading" status includes the same visibility as "public", and
additionally we'll advertise that you own/want this item on its
public list of trades.
</p>
<Heading3>Modeling contributions</Heading3>
<p>
When a logged-in user enters their Neopets's name on the site, we
look up that pet's public data on Neopets.com.
</p>
<p>
Sometimes, this will download new public outfit data that we've
never seen before. For example, you might show us a Draik (a species
of Neopet) wearing a new item, and we don't have data for a Draik
wearing that item yet.
</p>
<p>
When that happens, we'll extract that specific piece of data from
your pet's outfit, and save it to our database, for other users to
mix and match into their own outfits. This process is called
"modeling".
</p>
<p>
When you model new data for us, it's separated from your pet. Users
can't discover what pet modeled a certain piece of data, or what
else that pet was wearing.
</p>
<p>
But, if you're logged in when modeling, we'll publicly credit your
account for the new "contribution". This will appear in a number of
places, including a list of the most recent contributions, and it
will add points to your account that contribute to a public high
score list. This will publicly display your username.
</p>
<p>
Right now, modeling contributions from logged-in users are always
public. This is a limitation of our system, and we might change it
in the future! For now, if you would like to have your public
contributions removed from the site, please use the contact link at
the bottom of the page.
</p>
</section>
</VStack>
{authMode === "auth0" && (
<section>
<Heading2>Account management</Heading2>
<p>
While our <a href="https://impress.openneo.net/">classic app</a>{" "}
uses its own authentication, the app you're using now uses a
service called <a href="https://auth0.com/">Auth0</a> to manage
account creation and login.
</p>
<p>
We made this decision because authentication is difficult to
write and maintain securely. We felt that Auth0 was the
smoothest and most secure experience we could offer, especially
as a small team of volunteers{" "}
<span role="img" aria-label="Sweat smile emoji">
😅
</span>
</p>
<p>
<a href="https://auth0.com/legal/ss-tos">
Auth0's terms of service
</a>{" "}
commit to treating your user data as confidential information,
not to be shared with anyone else, and only to be used as part
of Dress to Impress. (The details are in Sections 6 and 7!)
</p>
<p>
When signing up, Auth0 will ask for a username, password, and
email address. They store your password as a <em>hash</em>{" "}
(which, colloquially, is like a one-way encryption), rather than
as the plain password itself.
</p>
<p>
Some user accounts were created before we moved to Auth0. For
those users, we imported their accounts from our custom database
into Auth0. This included username, password hash, and email
address.
</p>
</section>
)}
<section>
<Heading2>Analytics and logging</Heading2>
<p>
To understand how people use our site, we use a service called{" "}
<a href="https://plausible.io/">Plausible</a>. Every time you
visit a page, we send them a{" "}
<a href="https://plausible.io/data-policy">
small packet of information
</a>
.
</p>
<p>
Plausible is a privacy-focused service. It doesn't store your IP
address in a retrievable way, or add cookies to your browser, or
track you across multiple websites or over time.{" "}
<a href="https://plausible.io/data-policy">
Here's their data policy.
</a>
</p>
<p>
We also use a service called{" "}
<a href="https://sentry.io/">Sentry</a> to track errors. When you
encounter an error on our site, we send a copy of it to our Sentry
account, to help us debug it later. This might sometimes include
personal data, but Sentry will only share it with us.{" "}
<a href="https://sentry.io/legal/dpa/2.0.0/">
Here's their data policy.
</a>
</p>
<p>
We also use <a href="https://www.linode.com/">Linode</a> and{" "}
<a href="https://www.fastly.com/">Fastly</a> for web hosting.
Linode stores our database, and handles most web traffic dealing
with personal data. Personal data also travels through Fastly's
servers temporarily, but they only store aggregate usage logs for
us, not any personally-identifying data.
</p>
</section>
<section>
<Heading2>Creations and contributions</Heading2>
<p>
People use Dress to Impress to create, share, and communicate!
Some of these things are public, some are private, and some are
configurable.
</p>
<Heading3>Outfits</Heading3>
<p>
Outfits are the central creation on Dress to Impress: combining a
pet with items to make something that looks nice!
</p>
<p>
Users can log in and save outfits to their account. They can also
share outfits by URL without logging in.
</p>
<p>
When you save an outfit to your account, it's somewhat private,
but somewhat public.
</p>
<p>
It's private in the sense that there is no central place where
another user can look up your list of outfits.
</p>
<p>
But it's public in the sense that anyone with the URL can see
itand, because the URLs are based on a simple incrementing global
outfit ID, it's easy to look up all the outfits on the site.
</p>
<p>
We might change this in the future, to make the URLs hard to guess
and <em>genuinely</em> private. Until then, we advise users to not
to include sensitive data in the outfits they save to their
account.
</p>
<Heading3>Item lists</Heading3>
<p>
Logged-in users can track the Neopets customization items they own
and want, by saving item lists to their account.
</p>
<p>
These lists are private by default, but can be configured to
either be "public" or "trading" as well.
</p>
<p>
The "public" status means that anyone who knows your Dress to
Impress username, or item list URL, can see this list.
</p>
<p>
The "trading" status includes the same visibility as "public", and
additionally we'll advertise that you own/want this item on its
public list of trades.
</p>
<Heading3>Modeling contributions</Heading3>
<p>
When a logged-in user enters their Neopets's name on the site, we
look up that pet's public data on Neopets.com.
</p>
<p>
Sometimes, this will download new public outfit data that we've
never seen before. For example, you might show us a Draik (a
species of Neopet) wearing a new item, and we don't have data for
a Draik wearing that item yet.
</p>
<p>
When that happens, we'll extract that specific piece of data from
your pet's outfit, and save it to our database, for other users to
mix and match into their own outfits. This process is called
"modeling".
</p>
<p>
When you model new data for us, it's separated from your pet.
Users can't discover what pet modeled a certain piece of data, or
what else that pet was wearing.
</p>
<p>
But, if you're logged in when modeling, we'll publicly credit your
account for the new "contribution". This will appear in a number
of places, including a list of the most recent contributions, and
it will add points to your account that contribute to a public
high score list. This will publicly display your username.
</p>
<p>
Right now, modeling contributions from logged-in users are always
public. This is a limitation of our system, and we might change it
in the future! For now, if you would like to have your public
contributions removed from the site, please use the contact link
at the bottom of the page.
</p>
</section>
</VStack>
</TextContent>
</>
);
}

View file

@ -1,8 +1,8 @@
import { css } from "@emotion/react";
import { VStack } from "@chakra-ui/react";
import Head from "next/head";
import { Heading1, Heading2 } from "./util";
import TextContent from "./components/TextContent";
function TermsOfUsePage() {
return (
@ -11,114 +11,100 @@ function TermsOfUsePage() {
<title>Terms of Use | Dress to Impres</title>
</Head>
<Heading1 marginBottom="4">Our terms of use</Heading1>
<VStack
spacing="4"
alignItems="flex-start"
css={css`
max-width: 800px;
p {
margin-bottom: 1em;
}
a {
text-decoration: underline;
}
h2,
h3 {
margin-bottom: 0.5em;
}
`}
>
<section>
<p>
Hi, friends! Here's some information about how Dress to Impress is
meant to be used. The rules here aren't very formal, but we hope
they're clear, and we take them very seriously. Thank you for taking
the time to read!
</p>
</section>
<section>
<Heading2>Who can use this service</Heading2>
<p>
<strong>No crypto or NFT projects.</strong> Dress to Impress must
not be used as part of a cryptocurrency-related or NFT-related
project, commercial or otherwise. If you use our code, service, or
data to generate NFTs or other products distributed on the
blockchain or similar technologies, the expected remediation is to
cease and desist all distribution of works derived from this
service, in addition to offering appropriate compensation.
</p>
<p>
<strong>Some users might get banned.</strong> We sometimes refuse
service to users we feel are detrimental to our community, at our
sole discretion. This includes users who post content that doesn't
adhere to our terms, which you can see below.
</p>
</section>
<section>
<Heading2>What you can post on this service</Heading2>
<p>
<strong>Keep it Neoboard-safe.</strong> Neopets.com allows links to
Dress to Impress, so everything needs to be safe for Neopians of all
ages! Please keep all content "PG" and appropriate for young
community members, just like you do on Neopets.com. (That said, the
rules on the Neoboards haven't always been morally right, such as
when LGBTQIA+ discussion was banned. We'll always diverge from those
rules when it's ethically appropriate!)
</p>
<p>
<strong>Don't sell things for real money here.</strong> We don't
have the capacity to validate who is and isn't a legitimate seller,
so we err on the side of safety and ban <em>all</em> sales. If
you're selling something, please do it in a community where trust
and reputation can be managed more appropriately, and please make
sure it's in line with Neopets's terms.
</p>
</section>
<section>
<Heading2>How you can use our data</Heading2>
<p>
<strong>Be thoughtful using Neopets's data.</strong> While Dress to
Impress has a license to distribute Neopets data and images, we
aren't authorized to extend all of the same permissions to you.
Please think carefully about how you use Neopets's art and data you
find on this site, and make sure you're complying with their
licensing agreements and fair use laws, especially for derived works
like outfits. But personal use, and usage that stays on our site,
are always okay!
</p>
<p>
<strong>Be thoughtful using user-generated data.</strong> Some data
posted to Dress to Impress is generated by our users, like their
outfits and item lists. When you post those to Dress to Impress, you
grant us a license to redistribute them with attribution as part of
the site's functionality, respecting your privacy settings when
applicable. But each user still owns their own creations, so only
they can grant you permission to use or share it yourself.
</p>
<p>
<strong>Please reach out before using our APIs!</strong> If you'd
like to use our data to build something new, please contact us! We'd
like to help if we can. But please don't use our APIs without
talking to us first: it can cause performance issues for us, and
reliability issues for you. But we have a few folks who use Dress to
Impress for things like Discord bots, and we'd like to support you
and your community too!
</p>
</section>
<section>
<Heading2>Warranty and liability</Heading2>
<p>
<strong>Our data won't always be correct.</strong> While we do our
best to keep the customization on our site in sync with Neopets.com,
sometimes our data is out-of-date, and sometimes an item looks
different on our site than on Neopets.com. We're glad to be a
resource for users buying Neocash items, but as an unofficial
service we simply can't make guarantees, and we encourage you to
check other sources before making a purchase.
</p>
</section>
</VStack>
<TextContent maxWidth="800px">
<VStack spacing="4" alignItems="flex-start">
<section>
<p>
Hi, friends! Here's some information about how Dress to Impress is
meant to be used. The rules here aren't very formal, but we hope
they're clear, and we take them very seriously. Thank you for
taking the time to read!
</p>
</section>
<section>
<Heading2>Who can use this service</Heading2>
<p>
<strong>No crypto or NFT projects.</strong> Dress to Impress must
not be used as part of a cryptocurrency-related or NFT-related
project, commercial or otherwise. If you use our code, service, or
data to generate NFTs or other products distributed on the
blockchain or similar technologies, the expected remediation is to
cease and desist all distribution of works derived from this
service, in addition to offering appropriate compensation.
</p>
<p>
<strong>Some users might get banned.</strong> We sometimes refuse
service to users we feel are detrimental to our community, at our
sole discretion. This includes users who post content that doesn't
adhere to our terms, which you can see below.
</p>
</section>
<section>
<Heading2>What you can post on this service</Heading2>
<p>
<strong>Keep it Neoboard-safe.</strong> Neopets.com allows links
to Dress to Impress, so everything needs to be safe for Neopians
of all ages! Please keep all content "PG" and appropriate for
young community members, just like you do on Neopets.com. (That
said, the rules on the Neoboards haven't always been morally
right, such as when LGBTQIA+ discussion was banned. We'll always
diverge from those rules when it's ethically appropriate!)
</p>
<p>
<strong>Don't sell things for real money here.</strong> We don't
have the capacity to validate who is and isn't a legitimate
seller, so we err on the side of safety and ban <em>all</em>{" "}
sales. If you're selling something, please do it in a community
where trust and reputation can be managed more appropriately, and
please make sure it's in line with Neopets's terms.
</p>
</section>
<section>
<Heading2>How you can use our data</Heading2>
<p>
<strong>Be thoughtful using Neopets's data.</strong> While Dress
to Impress has a license to distribute Neopets data and images, we
aren't authorized to extend all of the same permissions to you.
Please think carefully about how you use Neopets's art and data
you find on this site, and make sure you're complying with their
licensing agreements and fair use laws, especially for derived
works like outfits. But personal use, and usage that stays on our
site, are always okay!
</p>
<p>
<strong>Be thoughtful using user-generated data.</strong> Some
data posted to Dress to Impress is generated by our users, like
their outfits and item lists. When you post those to Dress to
Impress, you grant us a license to redistribute them with
attribution as part of the site's functionality, respecting your
privacy settings when applicable. But each user still owns their
own creations, so only they can grant you permission to use or
share it yourself.
</p>
<p>
<strong>Please reach out before using our APIs!</strong> If you'd
like to use our data to build something new, please contact us!
We'd like to help if we can. But please don't use our APIs without
talking to us first: it can cause performance issues for us, and
reliability issues for you. But we have a few folks who use Dress
to Impress for things like Discord bots, and we'd like to support
you and your community too!
</p>
</section>
<section>
<Heading2>Warranty and liability</Heading2>
<p>
<strong>Our data won't always be correct.</strong> While we do our
best to keep the customization on our site in sync with
Neopets.com, sometimes our data is out-of-date, and sometimes an
item looks different on our site than on Neopets.com. We're glad
to be a resource for users buying Neocash items, but as an
unofficial service we simply can't make guarantees, and we
encourage you to check other sources before making a purchase.
</p>
</section>
</VStack>
</TextContent>
</>
);
}

View file

@ -139,14 +139,14 @@ function UserOutfitsPageContent() {
);
}
function OutfitCard({ outfit }) {
export function OutfitCard({ outfit, caption = null, alt = null }) {
const image = (
<ClassNames>
{({ css }) => (
<OutfitThumbnail
outfitId={outfit.id}
updatedAt={outfit.updatedAt}
alt={buildOutfitAltText(outfit)}
alt={alt ?? buildOutfitAltText(outfit)}
// Firefox shows alt text as a fallback for images it can't show yet.
// Show our alt text clearly if the image failed to load... but hide
// it if it's still loading. It's normal for these to take a second
@ -184,7 +184,7 @@ function OutfitCard({ outfit }) {
outline: "none",
}}
>
<OutfitCardLayout image={image} caption={outfit.name} />
<OutfitCardLayout image={image} caption={caption ?? outfit.name} />
</Box>
</Link>
);

View file

@ -0,0 +1,31 @@
import React from "react";
import { Box } from "@chakra-ui/react";
import { css } from "@emotion/react";
/**
* TextContent is a wrapper for just, like, normal chill text-based content!
* Its children will receive some simple CSS for tags like <p>, <a>, etc.
*/
function TextContent({ children, ...props }) {
return (
<Box
{...props}
css={css`
p {
margin-bottom: 1em;
}
a {
text-decoration: underline;
}
h2,
h3 {
margin-bottom: 0.5em;
}
`}
>
{children}
</Box>
);
}
export default TextContent;

View file

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Created with Inkscape (http://www.inkscape.org/) by Marsupilami -->
<svg
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
id="svg278"
version="1.1"
width="1024"
height="411"
viewBox="-18.0552558 -18.0552558 1590.2355116 637.9523716">
<defs
id="defs275" />
<path
style="fill:#ff282d;fill-opacity:1"
id="polygon204"
d="m 871.9707,0 v 363.35742 c 0,71.341 17.60489,103.9004 94.3379,103.9004 18.173,0 43.145,-4.67685 61.998,-8.71485 l -12.1269,-72.81836 c -12.759,2.692 -23.9153,2.36741 -31.9883,2.56641 -33.557,0.825 -30.6582,-10.20375 -30.6582,-41.84375 V 209.67188 h 63.8731 V 149.06055 H 953.5332 V 0 Z m 267.25,0 -81.5625,0.0117 0.012,61.75391 v 405.49414 h 121.8008 v -61.95703 h -40.25 z M 139.89062,0.002 c -84.128,0 -98.48632,28.62866 -98.48632,94.59765 v 54.46289 L 0,155.87114 v 53.80078 H 41.4043 V 405.30274 H 0.00195 v 61.96484 l 163.94141,-0.0176 V 405.30469 H 122.96875 V 209.67188 h 61.54102 v -60.61133 h -61.54102 v -47.13086 c 0,-28.959 7.49381,-31.91602 37.13281,-31.91602 8.072,0 14.40774,0.35982 27.17774,2.38282 L 198.45508,6.08203 c -18.856,-3.397 -40.39445,-6.08008 -58.56446,-6.08008 z m 171.1504,85.85546 v 20.71485 h 9.94921 v 28.16601 h 0.56641 c -78.009,14.361 -137.12695,82.67004 -137.12695,164.83204 0,92.595 75.0632,167.6582 167.6582,167.6582 31.602,0 61.15477,-8.75508 86.38477,-23.95508 l 14.69336,23.98633 h 86.15234 v -81.53516 h -19.49414 l -0.0547,-253.81055 h -81.56055 v 23.80274 c -16.799,-10.078 -35.51107,-17.28452 -55.45507,-20.97852 h 0.46093 v -28.16601 h 9.95118 V 85.85746 Z m 391.59179,45.52735 c -76.571,0 -122.55078,26.96884 -122.55078,95.71484 0,72.629 56.74414,97.03886 128.24414,109.25586 37.951,7.696 49.5625,15.4097 49.5625,31.4707 0,11.573 -6.55011,33.33985 -47.03711,33.33985 -14.549,0 -42.41709,0.51486 -68.99609,-4.61914 l 0.0352,-10.82227 H 580.127 v 60.91016 c 37.934,9.626 87.64872,20.625 138.88672,20.625 76.57,0 112.94727,-36.51722 112.94727,-107.19922 0,-74.531 -60.36566,-88.60952 -120.47266,-102.10352 -41.121,-8.991 -46.38867,-17.33743 -46.38867,-32.77343 0,-10.911 4.62894,-31.03907 42.58594,-31.03907 13.292,0 37.7957,0.0635 62.4707,4.56446 v 10.85156 h 61.74805 v -60.5 c -37.954,-9.631 -77.39649,-17.67578 -129.27149,-17.67578 z m 683.26949,17.67578 v 60.55469 h 40.1211 l -59.2246,145.70117 -59.2226,-145.70117 h 40.2051 v -60.55274 h -168.3106 v 60.55274 h 40.2109 l 104.5371,257.23242 c -12.101,37.004 -50.7927,58.25781 -85.0937,58.25781 -7.404,0 -21.5161,-1.3268 -32.2891,-3.3418 l -7.3769,74.02735 c 16.789,4.034 40.9995,6.05078 59.1855,6.05078 73.966,0 122.5326,-67.1082 152.1016,-138.4082 L 1513.916,209.61332 h 40.209 V 149.06055 Z M 356.97266,213.22852 c 42.935,2.295 77.52081,35.97276 81.25781,78.50976 v 2.80274 h -9.79102 v 9.77148 h 9.79297 v 2.67578 c -3.717,42.557 -38.31076,76.25578 -81.25976,78.55078 v -9.58789 h -9.77149 v 9.5625 c -43.775,-2.551 -78.79022,-37.72236 -81.07422,-81.56835 h 9.71289 v -9.77149 h -9.66211 c 2.596,-43.542 37.46644,-78.37701 81.02344,-80.91601 v 9.60937 h 9.77149 z m 31.71679,43.66211 -31.52343,27.46875 c -1.591,-0.57 -3.29232,-0.89649 -5.07032,-0.89649 -8.506,0 -15.39257,7.08922 -15.39258,15.82422 0,8.745 6.88658,15.83203 15.39258,15.83203 8.502,0 15.4043,-7.08703 15.4043,-15.83203 0,-1.659 -0.25189,-3.25676 -0.71289,-4.75976 l 28.10742,-31.42774 z M 1522.8594,405.63672 c -17.14,0 -30.9404,13.62458 -30.9414,30.76758 0,17.138 13.8004,30.75976 30.9414,30.75976 17.14,0 31.0234,-13.62176 31.0234,-30.75976 0,-17.143 -13.8824,-30.76758 -31.0234,-30.76758 z m 0,5.1875 c 14.239,0 25.8379,11.33508 25.8379,25.58008 0,14.237 -11.5989,25.92383 -25.8379,25.92383 -14.236,0 -25.7568,-11.68783 -25.7578,-25.92383 0,-14.244 11.5218,-25.58008 25.7578,-25.58008 z m -11.7754,10.45508 v 30.23828 h 6.9414 v -9.14062 h 4.3047 l 6.2422,9.14062 h 8.5254 l -7.5567,-10.37305 c 3.864,-1.14 6.3262,-4.48443 6.3262,-9.39843 0,-6.686 -4.6598,-10.4668 -12.1328,-10.4668 z m 6.9414,6.06641 h 5.709 c 2.995,0 5.1035,1.23734 5.1035,4.40234 0,3.334 -2.1074,4.5625 -5.2754,4.5625 h -5.5371 z"
clip-path="none"
mask="none" />
</svg>
<!-- version: 20171223, original size: 1554.125 601.84186, border: 3% -->

After

Width:  |  Height:  |  Size: 4.2 KiB