kopia lustrzana https://github.com/cloudflare/wildebeest
rodzic
40d59fbccd
commit
3d0e394a14
|
@ -0,0 +1,6 @@
|
|||
import { adjustLocalHostDomain } from './adjustLocalHostDomain'
|
||||
|
||||
export function getDomain(url: URL | string) {
|
||||
const domain = new URL(url).hostname
|
||||
return adjustLocalHostDomain(domain)
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
import { component$, Slot, useSignal } from '@builder.io/qwik'
|
||||
|
||||
type Props = {
|
||||
title: string
|
||||
}
|
||||
|
||||
export const Accordion = component$<Props>(({ title }) => {
|
||||
const headerId = useSignal(
|
||||
`accordion-${title.replace(/\s/g, '_')}-${`${Math.round(Math.random() * 99999)}`.padStart(5, '0')}`
|
||||
).value
|
||||
|
||||
const expanded = useSignal(false)
|
||||
|
||||
return (
|
||||
<div class="bg-wildebeest-600 border border-wildebeest-700 rounded overflow-hidden">
|
||||
<header id={headerId} class=" bg-wildebeest-700 text-wildebeest-vibrant-400">
|
||||
<button
|
||||
class="py-4 px-5 text-start w-full flex items-center"
|
||||
onClick$={() => (expanded.value = !expanded.value)}
|
||||
>
|
||||
<i class={`fa-solid fa-chevron-${expanded.value ? 'down' : 'right'} mr-3 text-xl`}></i>
|
||||
<span class="font-semibold">{title}</span>
|
||||
</button>
|
||||
</header>
|
||||
{expanded.value && (
|
||||
<section aria-labelledby={headerId}>
|
||||
<Slot />
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})
|
|
@ -1,16 +1,16 @@
|
|||
import { component$ } from '@builder.io/qwik'
|
||||
import { Link } from '@builder.io/qwik-city'
|
||||
import { type MastodonStatus } from '~/types'
|
||||
import { type Account } from '~/types'
|
||||
import { getDisplayNameElement } from '~/utils/getDisplayNameElement'
|
||||
import { useAccountUrl } from '~/utils/useAccountUrl'
|
||||
import { Avatar, type AvatarDetails } from '../avatar'
|
||||
|
||||
export const StatusAccountCard = component$<{
|
||||
status: MastodonStatus
|
||||
export const AccountCard = component$<{
|
||||
account: Account
|
||||
subText: 'username' | 'acct'
|
||||
secondaryAvatar?: AvatarDetails | null
|
||||
}>(({ status, subText, secondaryAvatar }) => {
|
||||
const accountUrl = useAccountUrl(status.account)
|
||||
}>(({ account, subText, secondaryAvatar }) => {
|
||||
const accountUrl = useAccountUrl(account)
|
||||
|
||||
return (
|
||||
<Link
|
||||
|
@ -18,13 +18,13 @@ export const StatusAccountCard = component$<{
|
|||
class="inline-grid grid-cols-[repeat(2,_max-content)] grid-rows-[1fr,1fr] items-center no-underline"
|
||||
>
|
||||
<div class="row-span-2">
|
||||
<Avatar primary={status.account} secondary={secondaryAvatar ?? null} />
|
||||
<Avatar primary={account} secondary={secondaryAvatar ?? null} />
|
||||
</div>
|
||||
<div data-testid="account-display-name" class="ml-2 col-start-2 row-start-1">
|
||||
{getDisplayNameElement(status.account)}
|
||||
{getDisplayNameElement(account)}
|
||||
</div>
|
||||
<div class="ml-2 text-wildebeest-400 col-start-2 row-start-2">
|
||||
@{subText === 'username' ? status.account.username : status.account.acct}
|
||||
@{subText === 'username' ? account.username : account.acct}
|
||||
</div>
|
||||
</Link>
|
||||
)
|
|
@ -14,6 +14,30 @@
|
|||
}
|
||||
}
|
||||
|
||||
:global(:is(b, h1, h2, h3, h4, h5, h6)) {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
:global(h1) {
|
||||
font-size: 1.5em;
|
||||
margin-top: 0;
|
||||
margin-bottom: 1em;
|
||||
line-height: 1.33;
|
||||
}
|
||||
|
||||
:global(h2) {
|
||||
font-size: 1.25em;
|
||||
margin-top: 1.6em;
|
||||
margin-bottom: 0.6em;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
:global(:is(h3, h4, h5, h6)) {
|
||||
margin-top: 1.5em;
|
||||
margin-bottom: 0.5em;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
:global(p) {
|
||||
margin-bottom: theme('spacing.4');
|
||||
overflow-wrap: break-word;
|
||||
|
|
|
@ -5,7 +5,7 @@ import type { Account, MastodonStatus } from '~/types'
|
|||
import { MediaGallery } from '../MediaGallery.tsx'
|
||||
import { useAccountUrl } from '~/utils/useAccountUrl'
|
||||
import { getDisplayNameElement } from '~/utils/getDisplayNameElement'
|
||||
import { StatusAccountCard } from '../StatusAccountCard/StatusAccountCard'
|
||||
import { AccountCard } from '../AccountCard/AccountCard'
|
||||
import { HtmlContent } from '../HtmlContent/HtmlContent'
|
||||
import { StatusInfoTray } from '../StatusInfoTray/StatusInfoTray'
|
||||
|
||||
|
@ -33,7 +33,7 @@ export default component$((props: Props) => {
|
|||
<article class="p-4 border-t border-wildebeest-700 break-words">
|
||||
<RebloggerLink account={reblogger}></RebloggerLink>
|
||||
<div class="flex justify-between mb-3">
|
||||
<StatusAccountCard status={status} subText={props.accountSubText} secondaryAvatar={reblogger} />
|
||||
<AccountCard account={status.account} subText={props.accountSubText} secondaryAvatar={reblogger} />
|
||||
<Link class="no-underline" href={statusUrl}>
|
||||
<div class="text-wildebeest-500 flex items-baseline">
|
||||
<i style={{ height: '0.75rem', width: '0.75rem' }} class="fa fa-xs fa-globe w-3 h-3" />
|
||||
|
|
|
@ -0,0 +1,142 @@
|
|||
import { component$ } from '@builder.io/qwik'
|
||||
import { DocumentHead, loader$ } from '@builder.io/qwik-city'
|
||||
import { getDomain } from 'wildebeest/backend/src/utils/getDomain'
|
||||
import { Accordion } from '~/components/Accordion/Accordion'
|
||||
import { AccountCard } from '~/components/AccountCard/AccountCard'
|
||||
import { HtmlContent } from '~/components/HtmlContent/HtmlContent'
|
||||
import { george } from '~/dummyData/accounts'
|
||||
import { Account } from '~/types'
|
||||
import { getDocumentHead } from '~/utils/getDocumentHead'
|
||||
import { instanceLoader } from '../layout'
|
||||
|
||||
type AboutInfo = {
|
||||
image: string
|
||||
domain: string
|
||||
contact: {
|
||||
account: Account
|
||||
email: string
|
||||
}
|
||||
rules: { id: string; text: string }[]
|
||||
extended_description: {
|
||||
updated_at: string
|
||||
content: string
|
||||
}
|
||||
}
|
||||
|
||||
export const aboutInfoLoader = loader$<Promise<AboutInfo>>(async ({ resolveValue, request, redirect }) => {
|
||||
// TODO: properly implement loader and remove redirect
|
||||
throw redirect(302, '/')
|
||||
|
||||
const instance = await resolveValue(instanceLoader)
|
||||
return {
|
||||
image: instance.thumbnail,
|
||||
domain: getDomain(request.url),
|
||||
contact: {
|
||||
account: george,
|
||||
email: 'test@test.com',
|
||||
},
|
||||
rules: [
|
||||
{
|
||||
id: '1',
|
||||
text: 'Sexually explicit or violent media must be marked as sensitive when posting',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
text: 'No racism, sexism, homophobia, transphobia, xenophobia, or casteism',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
text: 'No incitement of violence or promotion of violent ideologies',
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
text: 'No harassment, dogpiling or doxxing of other users',
|
||||
},
|
||||
{
|
||||
id: '7',
|
||||
text: 'Do not share intentionally false or misleading information',
|
||||
},
|
||||
],
|
||||
extended_description: {
|
||||
updated_at: '2023-01-19T14:55:44Z',
|
||||
content:
|
||||
'<p>Please mind that the <a href="mailto:staff@mastodon.social">staff@mastodon.social</a> e-mail is for inquiries related to the operation of the mastodon.social server specifically. If your account is on another server, <strong>we will not be able to assist you</strong>. For inquiries not related specifically to the operation of this server, such as press inquiries about Mastodon gGmbH, please contact <a href="mailto:press@joinmastodon.org">press@joinmastodon.org</a>. Additional addresses:</p>\n\n<ul>\n<li>Legal, GDPR, DMCA: <a href="mailto:legal@mastodon.social">legal@mastodon.social</a></li>\n<li>Appeals: <a href="mailto:moderation@mastodon.social">moderation@mastodon.social</a></li>\n</ul>\n\n<h2>Funding</h2>\n\n<p>This server is crowdfunded by <a href="https://patreon.com/mastodon">Patreon donations</a>. For a list of sponsors, see <a href="https://joinmastodon.org/sponsors">joinmastodon.org</a>.</p>\n\n<h2>Reporting and moderation</h2>\n\n<p>When reporting accounts, please make sure to include at least a few posts that show rule-breaking behaviour, when applicable. If there is any additional context that might help make a decision, please also include it in the comment. This is especially important when the content is in a language nobody on the moderation team speaks.</p>\n\n<p>We usually handle reports within 24 hours. Please mind that you are not notified when a report you have made has led to a punitive action, and that not all punitive actions are externally visible. For first time offenses, we may opt to delete offending content, escalating to harsher measures on repeat offenses.</p>\n\n<p>We have a team of paid moderators. If you would like to become a moderator, get in touch with us through the e-mail address above.</p>\n\n<h2>Impressum</h2>\n\n<p>Mastodon gGmbH<br>\nMühlenstraße 8a<br>\n14167 Berlin<br>\nGermany</p>\n\n<p>E-Mail-Adresse: hello@joinmastodon.org</p>\n\n<p>Vertretungsberechtigt: Eugen Rochko (Geschäftsführer)</p>\n\n<p>Umsatzsteuer Identifikationsnummer (USt-ID): DE344258260</p>\n\n<p>Handelsregister<br>\nGeführt bei: Amtsgericht Charlottenburg<br>\nNummer: HRB 230086 B</p>\n',
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
export default component$(() => {
|
||||
const aboutInfo = aboutInfoLoader().value
|
||||
|
||||
return (
|
||||
<>
|
||||
<div class="bg-wildebeest-900 sticky top-[3.9rem] xl:top-0 xl:pt-2.5 z-10">
|
||||
<div class="flex flex-col items-center bg-wildebeest-600 xl:rounded-t overflow-hidden p-5">
|
||||
<img class="rounded w-full aspect-[1.9] mb-5" src={aboutInfo.image} alt="" />
|
||||
<h2 data-testid="domain-text" class="my-4 text-2xl font-semibold">
|
||||
{aboutInfo.domain}
|
||||
</h2>
|
||||
<p data-testid="social-text" class="mb-6 text-wildebeest-500">
|
||||
<span>
|
||||
Decentralised social media powered by{' '}
|
||||
<a href="https://joinmastodon.org" class="no-underline text-wildebeest-200 font-semibold" target="_blank">
|
||||
Mastodon
|
||||
</a>
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<div class="rounded bg-wildebeest-700 flex flex-col md:flex-row p-2 w-full my-5" data-testid="contact">
|
||||
<div class="flex-1 p-4">
|
||||
<span class="block uppercase text-wildebeest-500 font-semibold mb-5">Administered by:</span>
|
||||
<AccountCard account={aboutInfo.contact.account} subText="username" />
|
||||
</div>
|
||||
<div class="flex-1 p-4 pt-6 md:pt-4 md:pl-6 border-wildebeest-500 border-solid border-t md:border-t-0 md:border-l">
|
||||
<span class="block uppercase text-wildebeest-500 font-semibold mb-5">Contact:</span>
|
||||
<span>{aboutInfo.contact.email}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col w-full my-5">
|
||||
<div class="my-1">
|
||||
<Accordion title="About">
|
||||
<div class="p-6">
|
||||
<HtmlContent html={aboutInfo.extended_description.content} />
|
||||
</div>
|
||||
</Accordion>
|
||||
</div>
|
||||
<div class="my-1">
|
||||
<Accordion title="Server rules">
|
||||
<ol class="list-none flex flex-col gap-1 my-5 px-6">
|
||||
{aboutInfo.rules.map(({ id, text }) => (
|
||||
<li key={id} class="flex items-center border-wildebeest-700 border-b last-of-type:border-b-0 py-2">
|
||||
<span class="bg-wildebeest-vibrant-400 text-wildebeest-900 mr-4 my-1 p-4 rounded-full w-5 h-5 grid place-content-center">
|
||||
{id}
|
||||
</span>
|
||||
<span>{text}</span>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</Accordion>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
})
|
||||
|
||||
export const head: DocumentHead = ({ resolveValue, head }) => {
|
||||
const instance = resolveValue(instanceLoader)
|
||||
|
||||
return getDocumentHead(
|
||||
{
|
||||
title: `About - ${instance.title}`,
|
||||
description: `About page for the ${instance.title} Mastodon instance`,
|
||||
og: {
|
||||
type: 'website',
|
||||
image: instance.thumbnail,
|
||||
},
|
||||
},
|
||||
head
|
||||
)
|
||||
}
|
|
@ -1,10 +1,7 @@
|
|||
import { useLocation } from '@builder.io/qwik-city'
|
||||
import { adjustLocalHostDomain } from 'wildebeest/backend/src/utils/adjustLocalHostDomain'
|
||||
import { getDomain } from 'wildebeest/backend/src/utils/getDomain'
|
||||
|
||||
export const useDomain = () => {
|
||||
const location = useLocation()
|
||||
const url = new URL(location.href)
|
||||
const domain = url.hostname
|
||||
const adjustedDomain = adjustLocalHostDomain(domain)
|
||||
return adjustedDomain
|
||||
return getDomain(location.url)
|
||||
}
|
||||
|
|
|
@ -38,8 +38,8 @@
|
|||
"pages": "NO_D1_WARNING=true wrangler pages",
|
||||
"database:migrate": "yarn d1 migrations apply DATABASE",
|
||||
"database:create-mock": "rm -f .wrangler/state/d1/DATABASE.sqlite3 && CI=true yarn database:migrate --local && node ./frontend/mock-db/run.mjs",
|
||||
"dev": "export COMMIT_HASH=$(git rev-parse HEAD) && yarn build && yarn database:migrate --local && yarn pages dev frontend/dist --d1 DATABASE --persist --compatibility-date=2022-12-20 --binding 'INSTANCE_DESCR=My Wildebeest Instance' --live-reload",
|
||||
"ci-dev-test-ui": "yarn build && yarn database:create-mock && yarn pages dev frontend/dist --d1 DATABASE --persist --port 8788 --binding 'INSTANCE_DESCR=My Wildebeest Instance' --compatibility-date=2022-12-20",
|
||||
"dev": "export COMMIT_HASH=$(git rev-parse HEAD) && yarn build && yarn database:migrate --local && yarn pages dev frontend/dist --d1 DATABASE --persist --compatibility-date=2022-12-20 --binding 'INSTANCE_TITLE=Test Wildebeest' 'INSTANCE_DESCR=My Wildebeest Instance' --live-reload",
|
||||
"ci-dev-test-ui": "yarn build && yarn database:create-mock && yarn pages dev frontend/dist --d1 DATABASE --persist --port 8788 --binding 'INSTANCE_TITLE=Test Wildebeest' 'INSTANCE_DESCR=My Wildebeest Instance' --compatibility-date=2022-12-20",
|
||||
"deploy:init": "yarn pages project create wildebeest && yarn d1 create wildebeest",
|
||||
"deploy": "yarn build && yarn database:migrate && yarn pages publish frontend/dist --project-name=wildebeest"
|
||||
},
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
import { test, expect } from '@playwright/test'
|
||||
|
||||
test.skip('Navigation to about page', async () => {
|
||||
// TODO: Implement after a navigation has been implemented
|
||||
})
|
||||
|
||||
// To update and unskip when we enable the about page
|
||||
test.skip('View of the about page', async ({ page }) => {
|
||||
await page.goto('http://127.0.0.1:8788/about')
|
||||
|
||||
await expect(page.getByTestId('domain-text')).toHaveText('0.0.0.0')
|
||||
await expect(page.getByTestId('social-text')).toHaveText('Decentralised social media powered by Mastodon')
|
||||
|
||||
await expect(page.getByTestId('contact').getByText('Administered by:George 👍@george')).toBeVisible()
|
||||
await expect(page.getByTestId('contact').getByText('contact:test@test.com')).toBeVisible()
|
||||
|
||||
await expect(page.getByRole('region').filter({ hasText: 'Please mind that the staff' })).not.toBeVisible()
|
||||
await page.getByRole('button', { name: 'About' }).click()
|
||||
await expect(page.getByRole('region').filter({ hasText: 'Please mind that the staff' })).toBeVisible()
|
||||
await page.getByRole('button', { name: 'About' }).click()
|
||||
await expect(page.getByRole('region').filter({ hasText: 'Please mind that the staff' })).not.toBeVisible()
|
||||
|
||||
const getRuleLocator = (ruleId: string) =>
|
||||
page.getByRole('listitem').filter({ has: page.getByText(ruleId, { exact: true }) })
|
||||
|
||||
await expect(getRuleLocator('1')).not.toBeVisible()
|
||||
await expect(getRuleLocator('2')).not.toBeVisible()
|
||||
await expect(getRuleLocator('3')).not.toBeVisible()
|
||||
await page.getByRole('button', { name: 'Server rules' }).click()
|
||||
await expect(getRuleLocator('1')).toBeVisible()
|
||||
await expect(getRuleLocator('1')).toContainText(
|
||||
'Sexually explicit or violent media must be marked as sensitive when posting'
|
||||
)
|
||||
await expect(getRuleLocator('2')).toBeVisible()
|
||||
await expect(getRuleLocator('2')).toContainText('No racism, sexism, homophobia, transphobia, xenophobia, or casteism')
|
||||
await expect(getRuleLocator('3')).toBeVisible()
|
||||
await expect(getRuleLocator('3')).toContainText('No incitement of violence or promotion of violent ideologies')
|
||||
})
|
|
@ -71,6 +71,17 @@ test.describe('Presence of appropriate SEO metadata across the application', ()
|
|||
})
|
||||
})
|
||||
|
||||
// To unskip when we enable the about page
|
||||
test.skip('in about page', async ({ page }) => {
|
||||
await page.goto('http://127.0.0.1:8788/about')
|
||||
await checkPageSeoData(page, {
|
||||
title: 'About - Test Wildebeest',
|
||||
description: 'About page for the Test Wildebeest Mastodon instance',
|
||||
ogType: 'website',
|
||||
ogImage: 'https://imagedelivery.net/NkfPDviynOyTAOI79ar_GQ/b24caf12-5230-48c4-0bf7-2f40063bd400/thumbnail',
|
||||
})
|
||||
})
|
||||
|
||||
test('in non-existent page', async ({ page }) => {
|
||||
await page.goto('http://127.0.0.1:8788/@NonExistent')
|
||||
await checkPageSeoData(page, {
|
||||
|
|
Ładowanie…
Reference in New Issue