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; USE openneo_impress;
-- Public data tables: read -- Public data tables: read
GRANT SELECT ON campaigns TO impress2020;
GRANT SELECT ON colors TO impress2020; GRANT SELECT ON colors TO impress2020;
GRANT SELECT ON color_translations 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 items TO impress2020;
GRANT SELECT ON item_translations TO impress2020; GRANT SELECT ON item_translations TO impress2020;
GRANT SELECT ON modeling_logs 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> <Link href="/privacy" passHref>
<ChakraLink>Privacy Policy (Sep 2022)</ChakraLink> <ChakraLink>Privacy Policy (Sep 2022)</ChakraLink>
</Link> </Link>
<Link href="/donate" passHref>
<ChakraLink>Donors</ChakraLink>
</Link>
<ChakraLink href={classicDTIUrl}>Classic DTI</ChakraLink> <ChakraLink href={classicDTIUrl}>Classic DTI</ChakraLink>
</HStack> </HStack>
<Box as="p" opacity="0.75"> <Box as="p" opacity="0.75">

View file

@ -1,10 +1,10 @@
import React from "react"; import React from "react";
import { css } from "@emotion/react";
import { VStack } from "@chakra-ui/react"; import { VStack } from "@chakra-ui/react";
import { Heading1, Heading2, Heading3 } from "./util"; import { Heading1, Heading2, Heading3 } from "./util";
import { useAuthModeFeatureFlag } from "./components/useCurrentUser"; import { useAuthModeFeatureFlag } from "./components/useCurrentUser";
import Head from "next/head"; import Head from "next/head";
import TextContent from "./components/TextContent";
function PrivacyPolicyPage() { function PrivacyPolicyPage() {
const [authMode] = useAuthModeFeatureFlag(); const [authMode] = useAuthModeFeatureFlag();
@ -15,204 +15,190 @@ function PrivacyPolicyPage() {
<title>Privacy Policy | Dress to Impress</title> <title>Privacy Policy | Dress to Impress</title>
</Head> </Head>
<Heading1 marginBottom="4">Our privacy policy</Heading1> <Heading1 marginBottom="4">Our privacy policy</Heading1>
<VStack <TextContent maxWidth="800px">
spacing="4" <VStack spacing="4" alignItems="flex-start">
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" && (
<section> <section>
<Heading2>Account management</Heading2>
<p> <p>
While our <a href="https://impress.openneo.net/">classic app</a>{" "} Hi, friends! Dress to Impress collects certain personal data.
uses its own authentication, the app you're using now uses a Here's how we use it!
service called <a href="https://auth0.com/">Auth0</a> to manage
account creation and login.
</p> </p>
<p> <p>
We made this decision because authentication is difficult to write First off, we'll <em>never</em> sell your private data, ever.
and maintain securely. We felt that Auth0 was the smoothest and It'll only be available to you and our small trusted staffand
most secure experience we could offer, especially as a small team we'll only use it to serve you directly, debug site issues, and
of volunteers{" "} help you share your creations with others.
<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> </p>
</section> </section>
)} {authMode === "auth0" && (
<section> <section>
<Heading2>Analytics and logging</Heading2> <Heading2>Account management</Heading2>
<p> <p>
To understand how people use our site, we use a service called{" "} While our <a href="https://impress.openneo.net/">classic app</a>{" "}
<a href="https://plausible.io/">Plausible</a>. Every time you visit uses its own authentication, the app you're using now uses a
a page, we send them a{" "} service called <a href="https://auth0.com/">Auth0</a> to manage
<a href="https://plausible.io/data-policy"> account creation and login.
small packet of information </p>
</a> <p>
. We made this decision because authentication is difficult to
</p> write and maintain securely. We felt that Auth0 was the
<p> smoothest and most secure experience we could offer, especially
Plausible is a privacy-focused service. It doesn't store your IP as a small team of volunteers{" "}
address in a retrievable way, or add cookies to your browser, or <span role="img" aria-label="Sweat smile emoji">
track you across multiple websites or over time.{" "} 😅
<a href="https://plausible.io/data-policy"> </span>
Here's their data policy. </p>
</a> <p>
</p> <a href="https://auth0.com/legal/ss-tos">
<p> Auth0's terms of service
We also use a service called <a href="https://sentry.io/">Sentry</a>{" "} </a>{" "}
to track errors. When you encounter an error on our site, we send a commit to treating your user data as confidential information,
copy of it to our Sentry account, to help us debug it later. This not to be shared with anyone else, and only to be used as part
might sometimes include personal data, but Sentry will only share it of Dress to Impress. (The details are in Sections 6 and 7!)
with us.{" "} </p>
<a href="https://sentry.io/legal/dpa/2.0.0/"> <p>
Here's their data policy. When signing up, Auth0 will ask for a username, password, and
</a> email address. They store your password as a <em>hash</em>{" "}
</p> (which, colloquially, is like a one-way encryption), rather than
<p> as the plain password itself.
We also use <a href="https://www.linode.com/">Linode</a> and{" "} </p>
<a href="https://www.fastly.com/">Fastly</a> for web hosting. Linode <p>
stores our database, and handles most web traffic dealing with Some user accounts were created before we moved to Auth0. For
personal data. Personal data also travels through Fastly's servers those users, we imported their accounts from our custom database
temporarily, but they only store aggregate usage logs for us, not into Auth0. This included username, password hash, and email
any personally-identifying data. address.
</p> </p>
</section> </section>
<section> )}
<Heading2>Creations and contributions</Heading2> <section>
<p> <Heading2>Analytics and logging</Heading2>
People use Dress to Impress to create, share, and communicate! Some <p>
of these things are public, some are private, and some are To understand how people use our site, we use a service called{" "}
configurable. <a href="https://plausible.io/">Plausible</a>. Every time you
</p> visit a page, we send them a{" "}
<Heading3>Outfits</Heading3> <a href="https://plausible.io/data-policy">
<p> small packet of information
Outfits are the central creation on Dress to Impress: combining a </a>
pet with items to make something that looks nice! .
</p> </p>
<p> <p>
Users can log in and save outfits to their account. They can also Plausible is a privacy-focused service. It doesn't store your IP
share outfits by URL without logging in. address in a retrievable way, or add cookies to your browser, or
</p> track you across multiple websites or over time.{" "}
<p> <a href="https://plausible.io/data-policy">
When you save an outfit to your account, it's somewhat private, but Here's their data policy.
somewhat public. </a>
</p> </p>
<p> <p>
It's private in the sense that there is no central place where We also use a service called{" "}
another user can look up your list of outfits. <a href="https://sentry.io/">Sentry</a> to track errors. When you
</p> encounter an error on our site, we send a copy of it to our Sentry
<p> account, to help us debug it later. This might sometimes include
But it's public in the sense that anyone with the URL can see personal data, but Sentry will only share it with us.{" "}
itand, because the URLs are based on a simple incrementing global <a href="https://sentry.io/legal/dpa/2.0.0/">
outfit ID, it's easy to look up all the outfits on the site. Here's their data policy.
</p> </a>
<p> </p>
We might change this in the future, to make the URLs hard to guess <p>
and <em>genuinely</em> private. Until then, we advise users to not We also use <a href="https://www.linode.com/">Linode</a> and{" "}
to include sensitive data in the outfits they save to their account. <a href="https://www.fastly.com/">Fastly</a> for web hosting.
</p> Linode stores our database, and handles most web traffic dealing
<Heading3>Item lists</Heading3> with personal data. Personal data also travels through Fastly's
<p> servers temporarily, but they only store aggregate usage logs for
Logged-in users can track the Neopets customization items they own us, not any personally-identifying data.
and want, by saving item lists to their account. </p>
</p> </section>
<p> <section>
These lists are private by default, but can be configured to either <Heading2>Creations and contributions</Heading2>
be "public" or "trading" as well. <p>
</p> People use Dress to Impress to create, share, and communicate!
<p> Some of these things are public, some are private, and some are
The "public" status means that anyone who knows your Dress to configurable.
Impress username, or item list URL, can see this list. </p>
</p> <Heading3>Outfits</Heading3>
<p> <p>
The "trading" status includes the same visibility as "public", and Outfits are the central creation on Dress to Impress: combining a
additionally we'll advertise that you own/want this item on its pet with items to make something that looks nice!
public list of trades. </p>
</p> <p>
<Heading3>Modeling contributions</Heading3> Users can log in and save outfits to their account. They can also
<p> share outfits by URL without logging in.
When a logged-in user enters their Neopets's name on the site, we </p>
look up that pet's public data on Neopets.com. <p>
</p> When you save an outfit to your account, it's somewhat private,
<p> but somewhat public.
Sometimes, this will download new public outfit data that we've </p>
never seen before. For example, you might show us a Draik (a species <p>
of Neopet) wearing a new item, and we don't have data for a Draik It's private in the sense that there is no central place where
wearing that item yet. another user can look up your list of outfits.
</p> </p>
<p> <p>
When that happens, we'll extract that specific piece of data from But it's public in the sense that anyone with the URL can see
your pet's outfit, and save it to our database, for other users to itand, because the URLs are based on a simple incrementing global
mix and match into their own outfits. This process is called outfit ID, it's easy to look up all the outfits on the site.
"modeling". </p>
</p> <p>
<p> We might change this in the future, to make the URLs hard to guess
When you model new data for us, it's separated from your pet. Users and <em>genuinely</em> private. Until then, we advise users to not
can't discover what pet modeled a certain piece of data, or what to include sensitive data in the outfits they save to their
else that pet was wearing. account.
</p> </p>
<p> <Heading3>Item lists</Heading3>
But, if you're logged in when modeling, we'll publicly credit your <p>
account for the new "contribution". This will appear in a number of Logged-in users can track the Neopets customization items they own
places, including a list of the most recent contributions, and it and want, by saving item lists to their account.
will add points to your account that contribute to a public high </p>
score list. This will publicly display your username. <p>
</p> These lists are private by default, but can be configured to
<p> either be "public" or "trading" as well.
Right now, modeling contributions from logged-in users are always </p>
public. This is a limitation of our system, and we might change it <p>
in the future! For now, if you would like to have your public The "public" status means that anyone who knows your Dress to
contributions removed from the site, please use the contact link at Impress username, or item list URL, can see this list.
the bottom of the page. </p>
</p> <p>
</section> The "trading" status includes the same visibility as "public", and
</VStack> 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 { VStack } from "@chakra-ui/react";
import Head from "next/head"; import Head from "next/head";
import { Heading1, Heading2 } from "./util"; import { Heading1, Heading2 } from "./util";
import TextContent from "./components/TextContent";
function TermsOfUsePage() { function TermsOfUsePage() {
return ( return (
@ -11,114 +11,100 @@ function TermsOfUsePage() {
<title>Terms of Use | Dress to Impres</title> <title>Terms of Use | Dress to Impres</title>
</Head> </Head>
<Heading1 marginBottom="4">Our terms of use</Heading1> <Heading1 marginBottom="4">Our terms of use</Heading1>
<VStack <TextContent maxWidth="800px">
spacing="4" <VStack spacing="4" alignItems="flex-start">
alignItems="flex-start" <section>
css={css` <p>
max-width: 800px; 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
p { they're clear, and we take them very seriously. Thank you for
margin-bottom: 1em; taking the time to read!
} </p>
a { </section>
text-decoration: underline; <section>
} <Heading2>Who can use this service</Heading2>
h2, <p>
h3 { <strong>No crypto or NFT projects.</strong> Dress to Impress must
margin-bottom: 0.5em; 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
<section> cease and desist all distribution of works derived from this
<p> service, in addition to offering appropriate compensation.
Hi, friends! Here's some information about how Dress to Impress is </p>
meant to be used. The rules here aren't very formal, but we hope <p>
they're clear, and we take them very seriously. Thank you for taking <strong>Some users might get banned.</strong> We sometimes refuse
the time to read! service to users we feel are detrimental to our community, at our
</p> sole discretion. This includes users who post content that doesn't
</section> adhere to our terms, which you can see below.
<section> </p>
<Heading2>Who can use this service</Heading2> </section>
<p> <section>
<strong>No crypto or NFT projects.</strong> Dress to Impress must <Heading2>What you can post on this service</Heading2>
not be used as part of a cryptocurrency-related or NFT-related <p>
project, commercial or otherwise. If you use our code, service, or <strong>Keep it Neoboard-safe.</strong> Neopets.com allows links
data to generate NFTs or other products distributed on the to Dress to Impress, so everything needs to be safe for Neopians
blockchain or similar technologies, the expected remediation is to of all ages! Please keep all content "PG" and appropriate for
cease and desist all distribution of works derived from this young community members, just like you do on Neopets.com. (That
service, in addition to offering appropriate compensation. said, the rules on the Neoboards haven't always been morally
</p> right, such as when LGBTQIA+ discussion was banned. We'll always
<p> diverge from those rules when it's ethically appropriate!)
<strong>Some users might get banned.</strong> We sometimes refuse </p>
service to users we feel are detrimental to our community, at our <p>
sole discretion. This includes users who post content that doesn't <strong>Don't sell things for real money here.</strong> We don't
adhere to our terms, which you can see below. have the capacity to validate who is and isn't a legitimate
</p> seller, so we err on the side of safety and ban <em>all</em>{" "}
</section> sales. If you're selling something, please do it in a community
<section> where trust and reputation can be managed more appropriately, and
<Heading2>What you can post on this service</Heading2> please make sure it's in line with Neopets's terms.
<p> </p>
<strong>Keep it Neoboard-safe.</strong> Neopets.com allows links to </section>
Dress to Impress, so everything needs to be safe for Neopians of all <section>
ages! Please keep all content "PG" and appropriate for young <Heading2>How you can use our data</Heading2>
community members, just like you do on Neopets.com. (That said, the <p>
rules on the Neoboards haven't always been morally right, such as <strong>Be thoughtful using Neopets's data.</strong> While Dress
when LGBTQIA+ discussion was banned. We'll always diverge from those to Impress has a license to distribute Neopets data and images, we
rules when it's ethically appropriate!) aren't authorized to extend all of the same permissions to you.
</p> Please think carefully about how you use Neopets's art and data
<p> you find on this site, and make sure you're complying with their
<strong>Don't sell things for real money here.</strong> We don't licensing agreements and fair use laws, especially for derived
have the capacity to validate who is and isn't a legitimate seller, works like outfits. But personal use, and usage that stays on our
so we err on the side of safety and ban <em>all</em> sales. If site, are always okay!
you're selling something, please do it in a community where trust </p>
and reputation can be managed more appropriately, and please make <p>
sure it's in line with Neopets's terms. <strong>Be thoughtful using user-generated data.</strong> Some
</p> data posted to Dress to Impress is generated by our users, like
</section> their outfits and item lists. When you post those to Dress to
<section> Impress, you grant us a license to redistribute them with
<Heading2>How you can use our data</Heading2> attribution as part of the site's functionality, respecting your
<p> privacy settings when applicable. But each user still owns their
<strong>Be thoughtful using Neopets's data.</strong> While Dress to own creations, so only they can grant you permission to use or
Impress has a license to distribute Neopets data and images, we share it yourself.
aren't authorized to extend all of the same permissions to you. </p>
Please think carefully about how you use Neopets's art and data you <p>
find on this site, and make sure you're complying with their <strong>Please reach out before using our APIs!</strong> If you'd
licensing agreements and fair use laws, especially for derived works like to use our data to build something new, please contact us!
like outfits. But personal use, and usage that stays on our site, We'd like to help if we can. But please don't use our APIs without
are always okay! talking to us first: it can cause performance issues for us, and
</p> reliability issues for you. But we have a few folks who use Dress
<p> to Impress for things like Discord bots, and we'd like to support
<strong>Be thoughtful using user-generated data.</strong> Some data you and your community too!
posted to Dress to Impress is generated by our users, like their </p>
outfits and item lists. When you post those to Dress to Impress, you </section>
grant us a license to redistribute them with attribution as part of <section>
the site's functionality, respecting your privacy settings when <Heading2>Warranty and liability</Heading2>
applicable. But each user still owns their own creations, so only <p>
they can grant you permission to use or share it yourself. <strong>Our data won't always be correct.</strong> While we do our
</p> best to keep the customization on our site in sync with
<p> Neopets.com, sometimes our data is out-of-date, and sometimes an
<strong>Please reach out before using our APIs!</strong> If you'd item looks different on our site than on Neopets.com. We're glad
like to use our data to build something new, please contact us! We'd to be a resource for users buying Neocash items, but as an
like to help if we can. But please don't use our APIs without unofficial service we simply can't make guarantees, and we
talking to us first: it can cause performance issues for us, and encourage you to check other sources before making a purchase.
reliability issues for you. But we have a few folks who use Dress to </p>
Impress for things like Discord bots, and we'd like to support you </section>
and your community too! </VStack>
</p> </TextContent>
</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>
</> </>
); );
} }

View file

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