diff --git a/app/(account)/confirm-email/[token]/actions.ts b/app/(account)/confirm-email/[token]/actions.ts new file mode 100644 index 0000000..d555853 --- /dev/null +++ b/app/(account)/confirm-email/[token]/actions.ts @@ -0,0 +1,28 @@ +"use server" + +import {isFieldError} from "../../../../lib/auth/guards"; + +export const postMailConfirmationToken = async (token: string): Promise => { + try { + const response = await fetch(`${process.env.CC_API_URL}/accounts/confirm-account`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({token}), + }); + + if (!response.ok) { + const errorResponse = await response.json(); + if (isFieldError(errorResponse) && errorResponse.error.token) { + return {error: errorResponse.error.token.join(' ')}; + } + + return {error: "An unexpected error occurred."}; + } + + return {success: true}; + } catch (error) { + return {error: "An unexpected error occurred."}; + } +}; diff --git a/app/confirm-email/[token]/page.tsx b/app/(account)/confirm-email/[token]/page.tsx similarity index 84% rename from app/confirm-email/[token]/page.tsx rename to app/(account)/confirm-email/[token]/page.tsx index 771a6de..acb087a 100644 --- a/app/confirm-email/[token]/page.tsx +++ b/app/(account)/confirm-email/[token]/page.tsx @@ -1,13 +1,14 @@ "use client" -import {usePathname} from "next/navigation"; +import {useParams} from "next/navigation"; import {postMailConfirmationToken} from "./actions"; import React, {useEffect, useState} from "react"; -import Panel from "../../common/uiLibrary/panel"; -import ContactInformation from "../../(home)/contactInformation"; +import Panel from "../../../common/uiLibrary/panel"; +import ContactInformation from "../../../(home)/contactInformation"; + const MailConfirmationPage = () => { const [response, setResponse] = useState<{ success?: boolean; error?: string }>({}); - const token = usePathname().split('/').filter(Boolean).pop(); + const {token} = useParams<{token: string}>(); useEffect(() => { const fetchData = async () => { diff --git a/app/(account)/login/page.tsx b/app/(account)/login/page.tsx index bde0fa6..9204921 100644 --- a/app/(account)/login/page.tsx +++ b/app/(account)/login/page.tsx @@ -11,6 +11,7 @@ import {AuthorizerContext} from "../../../context/AuthorizerContextProvider"; import {isFieldError, isGeneralError} from "../../../lib/auth/guards"; import GenericLoading from "../../common/uiLibrary/genericLoading"; import {redirect} from "next/navigation"; +import FullPage from "../../common/uiLibrary/Layouters/fullPage"; const LoginPage = () => { const {state, credentialsLogin} = useContext(AuthorizerContext); @@ -41,7 +42,7 @@ const LoginPage = () => { const loginForm = () => { return ( -
+
{isLoading &&
@@ -89,14 +90,15 @@ const LoginPage = () => { links={ [ {link: '/register', linkText: 'Don\'t have an account?'}, - {link: '/reset-password', linkText: 'Forgot your password?'} + {link: '/reset-password', linkText: 'Forgot your password?'}, + {link: '/resend-confirm-email', linkText: 'Haven\'t received confirmation email yet?'}, ] } />
-
+
) } diff --git a/app/(account)/register/page.tsx b/app/(account)/register/page.tsx index 2060643..f318007 100644 --- a/app/(account)/register/page.tsx +++ b/app/(account)/register/page.tsx @@ -108,12 +108,10 @@ const RegisterPage = () => { placeholder='********' shadow required - helperText={(state.error && isFieldError(state.error) && state.error?.error.password2) ? + helperText={(state.error && isFieldError(state.error) && state.error?.error.password2) &&
{state.error?.error.password2}
- : - usernameHelperText() } /> diff --git a/app/(account)/resend-confirm-email/actions.ts b/app/(account)/resend-confirm-email/actions.ts new file mode 100644 index 0000000..b27851b --- /dev/null +++ b/app/(account)/resend-confirm-email/actions.ts @@ -0,0 +1,53 @@ +"use server" + +import {z} from 'zod'; +import {FieldError, GeneralError, isFieldError} from "../../../lib/auth/guards"; + +export interface ResendConfirmationMailRequest { + success: boolean; + error?: GeneralError | FieldError; +} + +export const postResendConfirmationMail = async (_: ResendConfirmationMailRequest, formData: FormData): Promise => { + const schema = z.object({ + email: z.string().email() + }); + + const parsed = schema.safeParse({ + email: formData.get('email') + }); + + if (!parsed.success) { + const fieldErrors = parsed.error.errors.reduce((acc, error) => { + return {...acc, [error.path[0]]: error.message} + }, {}); + + return {success: false, error: {status: 400, error: fieldErrors}}; + } + + try { + const response = await fetch(`${process.env.CC_API_URL}/accounts/resend-account-confirmation`, { + method: "POST", + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + email: parsed.data.email + }) + }); + + if (!response.ok) { + const error = await response.json(); + if (isFieldError(error)) { + return {success: false, error: error}; + } + + return {success: false, error: {error: 'An unexpected error happened', status: 500}}; + } + + } catch (e) { + return {success: false, error: {error: 'An unexpected error happened', status: 500}}; + } + + return {success: true} +} \ No newline at end of file diff --git a/app/(account)/resend-confirm-email/page.tsx b/app/(account)/resend-confirm-email/page.tsx new file mode 100644 index 0000000..86f6777 --- /dev/null +++ b/app/(account)/resend-confirm-email/page.tsx @@ -0,0 +1,70 @@ +"use client" + + +import FormContainer from "../../common/uiLibrary/Layouters/formContainer"; +import TextField from "../../common/uiLibrary/forms/textField"; +import Button from "../../common/uiLibrary/Button"; +import ContactInformation from "../../(home)/contactInformation"; +import React from "react"; +import FullPage from "../../common/uiLibrary/Layouters/fullPage"; +import {useFormState} from "react-dom"; +import {postResendConfirmationMail, ResendConfirmationMailRequest} from "./actions"; + +const ResendConfirmationMail = () => { + const initialState: ResendConfirmationMailRequest = { + success: false, + } + + const [state, formAction] = useFormState(postResendConfirmationMail, initialState); + + const resendForm = () => { + return ( + <> + {!state.success && state.error && errorMessage()} + + + + + ) + } + const errorMessage = () => { + return ( +
+

Oops!

+

There was an unexpected error while trying to resend the confirmation email.

+

Please try again later or contact us.

+
+ ) + } + + const successMessage = () => ( +
+

Success!

+

Your request has been processed successfully. If your account is found in our system and the email + address you provided matches our records, we have sent a confirmation email to that address.

+

Please check your inbox for the confirmation email. If you don't receive it within a few minutes, check + your spam or junk folder. For further assistance, don't hesitate to contact us.

+
+ ); + + return ( + +
+ + {state.success ? successMessage() : resendForm()} + +
+ +
+ ) +}; + +export default ResendConfirmationMail; \ No newline at end of file diff --git a/app/(account)/reset-password/[token]/actions.ts b/app/(account)/reset-password/[token]/actions.ts new file mode 100644 index 0000000..f233447 --- /dev/null +++ b/app/(account)/reset-password/[token]/actions.ts @@ -0,0 +1,75 @@ +"use server" + +import {FieldError, GeneralError, isFieldError, isGeneralError} from "../../../../lib/auth/guards"; +import {z} from "zod"; + +export interface ResetPasswordStep2Response { + success: boolean; + error?: GeneralError | FieldError; +} + +export const postPasswordReset = async (_: ResetPasswordStep2Response, formData: FormData): Promise => { + const schema = z.object({ + password: z.string().min(6), + token: z.string().min(1) + }); + + const parsed = schema.safeParse({ + password: formData.get('password'), + password2: formData.get('password2'), + token: formData.get('token') + }); + + if (!parsed.success) { + const fieldErrors = parsed.error.errors.reduce((acc, error) => { + return {...acc, [error.path[0]]: error.message} + }, {}); + + return {success: false, error: {status: 400, error: fieldErrors}}; + } + + const password2 = formData.get('password2'); + if (password2 !== parsed.data.password) { + return { + success: false, + error: + { + status: 400, + error: { + password2: ['Passwords do not match'] + } + } + } + } + + const body = { + password: parsed.data.password, + token: parsed.data.token + } + + try { + const response = await fetch(`${process.env.CC_API_URL}/accounts/reset-password/${parsed.data.token}`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(body), + }); + + if (!response.ok) { + if (isFieldError(response) && response.error.token) { + return {success: false, error: {status: 400, error: "Your link has expired. Please request a new one."}}; + } + + if (isFieldError(response) || isGeneralError(response)) { + return {success: false, error: response}; + } + + return { success: false, error: {status: 400, error: "An unexpected error occurred"}}; + } + + return { success: true}; + } catch (error) { + return { success: false, error: {status: 400, error: "An unexpected error occurred"}}; + } +} \ No newline at end of file diff --git a/app/(account)/reset-password/[token]/page.tsx b/app/(account)/reset-password/[token]/page.tsx new file mode 100644 index 0000000..b52e520 --- /dev/null +++ b/app/(account)/reset-password/[token]/page.tsx @@ -0,0 +1,93 @@ +"use client" + + +import {useParams} from "next/navigation"; +import React from "react"; +import FormContainer from "../../../common/uiLibrary/Layouters/formContainer"; +import TextField from "../../../common/uiLibrary/forms/textField"; +import Button from "../../../common/uiLibrary/Button"; +import {useFormState} from "react-dom"; +import {postPasswordReset, ResetPasswordStep2Response} from "./actions"; +import {isFieldError} from "../../../../lib/auth/guards"; +import FullPage from "../../../common/uiLibrary/Layouters/fullPage"; + +const ResetPasswordPageStep2 = () => { + const {token} = useParams<{ token: string }>(); + const initialState: ResetPasswordStep2Response = { + success: false, + error: undefined + } + + const [state, formAction] = useFormState(postPasswordReset, initialState); + + const successMessage = () => ( +
+

Success!

+

Your password has been reset successfully.

+

You can now log in using your new password.

+
+ ); + + const errorMessage = () => { + return ( +
+

Oops!

+

There was an error while trying to reset your password. Your password-reset token might be invalid or expired.

+

Please try requesting a new password reset or contact us.

+
+ ) + } + + const resetPasswordForm = () => { + return ( + <> + {state.error && errorMessage()} + + {state.error?.error.password} +
+ } + /> + + + {state.error?.error.password2} + + } + /> + + + + + + ) + } + + return ( + +
+ + {state.success ? successMessage() : resetPasswordForm()} + +
+
+ ) +} + +export default ResetPasswordPageStep2; \ No newline at end of file diff --git a/app/(account)/reset-password/actions.ts b/app/(account)/reset-password/actions.ts index 3ed3af8..675c059 100644 --- a/app/(account)/reset-password/actions.ts +++ b/app/(account)/reset-password/actions.ts @@ -3,14 +3,14 @@ import {revalidatePath} from "next/cache"; import {z} from "zod"; -export interface ResendMailResponse { +export interface ResetPassowrdStep1 { success: boolean; email?: string; message?: string; fieldErrors?: { [key: string]: string }; } -export const resendMailConfirmationToken = async (prevState: ResendMailResponse, formData : FormData): Promise => { +export const requestAPasswordReset = async (_: ResetPassowrdStep1, formData : FormData): Promise => { const schema = z.object({ email: z.string().email(), }); @@ -26,7 +26,7 @@ export const resendMailConfirmationToken = async (prevState: ResendMailResponse, const email = parsed.data.email; try { - const response = await fetch(`${process.env.CC_API_URL}/accounts/resend-account-confirmation/asd`, { + const response = await fetch(`${process.env.CC_API_URL}/accounts/reset-password/`, { method: "POST", headers: { "Content-Type": "application/json", diff --git a/app/(account)/reset-password/page.tsx b/app/(account)/reset-password/page.tsx index 6dba675..ff32b60 100644 --- a/app/(account)/reset-password/page.tsx +++ b/app/(account)/reset-password/page.tsx @@ -4,25 +4,25 @@ import Button from "../../common/uiLibrary/Button"; import React from "react"; import FormContainer from "../../common/uiLibrary/Layouters/formContainer"; import TextField from "../../common/uiLibrary/forms/textField"; -import {resendMailConfirmationToken, ResendMailResponse} from "./actions"; +import {requestAPasswordReset, ResetPassowrdStep1} from "./actions"; import {useFormState} from "react-dom"; import ContactInformation from "../../(home)/contactInformation"; -const initialState: ResendMailResponse = { +const initialState: ResetPassowrdStep1 = { success: false, message: undefined, email: '' }; const ResetPasswordPage = () => { - const [state, formAction] = useFormState(resendMailConfirmationToken, initialState); + const [state, formAction] = useFormState(requestAPasswordReset, initialState); const successMessage = () => (

Success!

An email has been sent with instructions to reset your password.

Please check your inbox and follow the instructions to complete the process.

-

Didn't receive the email? Check your Spam folder or try resending the email. Ensure your email address is +

Didn't receive the email? Check your Spam folder or try requesting a password reset again. Ensure your email address is entered correctly.

); diff --git a/app/(home)/page.tsx b/app/(home)/page.tsx index 2207cc2..ba71221 100644 --- a/app/(home)/page.tsx +++ b/app/(home)/page.tsx @@ -49,7 +49,7 @@ const fetchLatestBlogPost = async (): Promise => { return resPage1.results.concat(resPage2.results); } -const HomePage: () => Promise = async () => { +const HomePage = async () => { const latestBlogPosts: BlogPost[] = await fetchLatestBlogPost(); const NoSsrHeroImageClient = dynamic(() => import('./HeroRandomImageClient'), {ssr: false}); diff --git a/app/common/defaultNavbar.tsx b/app/common/defaultNavbar.tsx index d1fb833..14da603 100644 --- a/app/common/defaultNavbar.tsx +++ b/app/common/defaultNavbar.tsx @@ -16,9 +16,10 @@ export default function DefaultNavbar() { const loggedOptions = () => ( <> - My Account + {/* hidden while we work on it */} + {/*My Account*/} - Logout + Logout ) @@ -26,11 +27,11 @@ export default function DefaultNavbar() { const notLoggedOptions = () => ( <> - Login/Register + Login/Register - Reset password + Reset password ) @@ -50,10 +51,10 @@ export default function DefaultNavbar() {

Home

- +

Blog

- +

Changelog

diff --git a/app/common/uiLibrary/Layouters/fullPage.tsx b/app/common/uiLibrary/Layouters/fullPage.tsx new file mode 100644 index 0000000..ef34bfa --- /dev/null +++ b/app/common/uiLibrary/Layouters/fullPage.tsx @@ -0,0 +1,12 @@ +import layoutChildren from "../../../../types/layoutChildren"; + +// makes the view occupy the full page, excluding the header. +const FullPage = (props: layoutChildren) => { + return ( +
+ {props.children} +
+ ) +} + +export default FullPage; \ No newline at end of file diff --git a/app/confirm-email/[token]/actions.ts b/app/confirm-email/[token]/actions.ts deleted file mode 100644 index 0ebe69b..0000000 --- a/app/confirm-email/[token]/actions.ts +++ /dev/null @@ -1,31 +0,0 @@ -"use server" - -export const postMailConfirmationToken = async (token: string): Promise => { - try { - const response = await fetch(`${process.env.CC_API_URL}/accounts/confirm-account`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ token }), - }); - - if (!response.ok) { - const errorResponse = await response.json(); - if (errorResponse.error) { - if (typeof errorResponse.error === 'string') { - return { error: errorResponse.error }; - } else if (errorResponse.error.token) { - return { error: errorResponse.error.token.join(' ') }; - } else if (errorResponse.error.non_field_errors) { - return { error: errorResponse.error.non_field_errors.join(' ') }; - } - } - return { error: "An unexpected error occurred." }; - } - - return { success: true }; - } catch (error) { - return { error: "An unexpected error occurred." }; - } -}; diff --git a/app/hub-rules/EnforcementProcedure.tsx b/app/hub-rules/EnforcementProcedure.tsx new file mode 100644 index 0000000..79a8d6b --- /dev/null +++ b/app/hub-rules/EnforcementProcedure.tsx @@ -0,0 +1,20 @@ +import React from 'react'; + +const EnforcementProcedure = () => { + return ( +
+

Enforcement Procedure

+

+ Depending on the scope and nature of the violation in question we will try to contact you privately and resolve things cleanly without a lasting strike. This depends on the kind offense in question and how cooperative you were to resolve the matter. +

+

+ If we do decide to hand out a strike, we expect you to resolve the problem within a reasonable timeframe. Failing this we may delist your server until the root problem is solved, regardless of total strikes remaining. +

+

+ We expect to be able to contact you about matters of the hub rules. You should have some form of contact information on your server: something as simple as a Discord or website link we can follow as a paper trail is good enough. If we are unable to contact you we may be forced to go immediately to delisting. +

+
+ ); +}; + +export default EnforcementProcedure; diff --git a/app/hub-rules/HowRulesApplied.tsx b/app/hub-rules/HowRulesApplied.tsx new file mode 100644 index 0000000..ea967e1 --- /dev/null +++ b/app/hub-rules/HowRulesApplied.tsx @@ -0,0 +1,17 @@ +import React from 'react'; +import StrikeSystem from './StrikeSystem'; +import ScopeOfRules from './ScopeOfRules'; +import EnforcementProcedure from './EnforcementProcedure'; + +const HowRulesApplied = () => { + return ( +
+

How These Rules Are Applied

+ + + +
+ ); +}; + +export default HowRulesApplied; \ No newline at end of file diff --git a/app/hub-rules/Rule.tsx b/app/hub-rules/Rule.tsx new file mode 100644 index 0000000..49e1dc4 --- /dev/null +++ b/app/hub-rules/Rule.tsx @@ -0,0 +1,36 @@ +import React from 'react'; + +export interface RuleProps { + number: string; + text: string; + description?: string; + subRules?: string[]; + postNotes?: string[]; +} + +const Rule: React.FC = ({ number, text, description, subRules, postNotes }) => { + return ( + <> +

{number}. {text}

+ {description && ( +

{description}

+ )} + {subRules && ( +
    + {subRules.map((subRule, index) => ( +
  • {subRule}
  • + ))} +
+ )} + {postNotes && ( +
    + {postNotes.map((note, index) => ( +
  • {note}
  • + ))} +
+ )} + + ); +}; + +export default Rule; \ No newline at end of file diff --git a/app/hub-rules/RuleCategory.tsx b/app/hub-rules/RuleCategory.tsx new file mode 100644 index 0000000..d4c2f79 --- /dev/null +++ b/app/hub-rules/RuleCategory.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import Rule, { RuleProps } from './Rule'; + +interface RuleCategoryProps { + title: string; + description: string; + rules: RuleProps[]; +} + +const RuleCategory: React.FC = ({ title, description, rules }) => { + return ( + <> +

{title}

+

{description}

+ {rules.map((rule, index) => ( + + ))} +
+ + ); +}; + +export default RuleCategory; diff --git a/app/hub-rules/RulesIntroduction.tsx b/app/hub-rules/RulesIntroduction.tsx new file mode 100644 index 0000000..c06291f --- /dev/null +++ b/app/hub-rules/RulesIntroduction.tsx @@ -0,0 +1,16 @@ +import React from 'react'; + +const RulesIntroduction = () => { + return ( + <> +

+ This document specifies the rules that need to be followed when listing your community on our official server hub. If you are not sure whether something is allowed, or you think that something that is not allowed by the current rules should be allowed, feel free to contact us. We will clarify and possibly amend the rules. +

+

+ We may change these rules from time to time, for example to add clarifications. When we do, it will be announced on our discord server. +

+ + ); +}; + +export default RulesIntroduction; \ No newline at end of file diff --git a/app/hub-rules/RulesSection.tsx b/app/hub-rules/RulesSection.tsx new file mode 100644 index 0000000..aaecc83 --- /dev/null +++ b/app/hub-rules/RulesSection.tsx @@ -0,0 +1,149 @@ +// RulesSection.js +import React from 'react'; +import RuleCategory from './RuleCategory'; + +const RulesSection = () => { + const section1Rules = [ + { + number: "1.1", + text: "Do not harass, bully, brigade, defame, or doxx players, developers, or communities.", + subRules: [ + "Sharing IPs or hardware IDs with other server hosts for the purpose of banning and moderation is allowed.", + "Some guidance on what \"harassment\" constitutes: if somebody wants you to stop talking to or about them, you probably should." + ] + }, + { + number: "1.2", + text: "Do not attempt anything that is illegal in the European Union.", + description: "In order to keep Unitystation as a project and global community safe, we follow EU laws and regulations. We expect you to comply with them as well regardless of where you live. Actions that are considered illegal may include, but are not limited to:", + subRules: [ + "Hosting content or facilitating activities that promote discrimination, hostility, or violence against individuals or groups based on identity or individual characteristics such as gender, race, ethnicity, religion, or sexual orientation,", + "Network attacks such as distributed denial of service or unauthorized access,", + "Collecting data without user consent,", + "Distribution of malware,", + "Phishing and scams.", + ], + postNotes: [ + "If applicable, we may report you to government authorities.", + "Security vulnerabilities can be reported to our security team by joining our Discord and pinging or talking to someone with the @Engineers or @Lead Developer role.", + "Do not violate software licenses of other servers on the hub. This includes re-distributing the source code of secret repos or other license violations." + ], + }, + { + number: "1.3", + text: "Do not abuse the hardware or internet connection of players for purposes that are not directly related to playing the game or collecting telemetry, even with consent.", + subRules: [ + "For example, do not mine cryptocurrency using the players' hardware.", + "Redirecting the players' connection to another server you control or have permission to redirect to is allowed, as long as you have player consent." + ] + }, + { + number: "1.4", + text: "Servers containing mature elements such as erotic roleplay MUST clearly be marked as being 18+.", + subRules: [ + "Do this by putting [18+] clearly in the server name and tags." + ] + }, + { + number: "1.5", + text: "If your server or community as a whole is 18+, you must do due diligence to remove underage players", + subRules: [ + "You must ensure that minors do not have an easy way to access your community by using age verification methods that are suitable for your community and moderation capabilities.", + "Upon learning that one of your community members is underage, you must remove them from your community immediately.", + "If you do not have mature themes (rule 1.4) and choose to mark your server as 18+ anyways, you still cannot make exceptions here." + ] + }, + { + number: "1.6", + text: "Server staff members must not have a record of grooming or other predatory behavior.", + subRules: [ + "This counts for all servers, not just exclusively 18+ servers." + ] + }, + { + number: "1.7", + text: "Do not send false information to the hub.", + subRules: [ + "For example, do not lie about player count or about what region your server is in." + ] + }, + { + number: "1.8", + text: "Do not attempt to abuse loopholes in the hub rules or circumvent rulings", + subRules: [ + "For example: trying to circumvent strikes by making a \"new\" community." + ] + } + ]; + + const section2Rules = [ + { + number: "2.1", + text: "Keep your server names and descriptions clean", + subRules: [ + "They are visible to every player who uses the hub.", + "Keep it safe for work and free from vulgarity.", + "No bigotry, hate speech, discrimination, etc..." + ] + }, + { + number: "2.2", + text: "Do not impersonate other servers, developers, or organizations.", + subRules: [ + "If there is a reasonable way for players to be confused about the affiliations of your server, you're probably breaking this." + ] + }, + { + number: "2.3", + text: "Don't intentionally cause any sort of technical trouble for other communities, servers, the hub, or players." + } + ]; + + const section3Rules = [ + { + number: "3.1", + text: "No advertising in the server name.", + description: "Server names are for describing a game, server, or community, and not for advertising unrelated services, causes, or other information. For Example:", + subRules: [ + "A name consisting of only \"Hosted by XXX\" is not allowed, because it is advertising a hosting service.", + "A name such as \"Foo Station | Now with new and improved X and Y | Running map Z\" is allowed, because it describes Foo Station.", + "\"Advertisements\" as described in this rule are permitted in the server description (inside the foldout)." + ] + }, + { + number: "3.2", + text: "You may not use any of Unitystation's services to keep up with the downfall of a civilization.", + description: "During the event a of a widespread viral infection transmitted via bites or contact with bodily fluids that causes human corpses to reanimate and seek to consume living human flesh, blood, brain or nerve tissue and is likely to result in the fall of organized civilization, You may not use any of Unitystation's services to spread updates or information about the event. Services include but are not limited to:", + subRules: [ + "Stationhub", + "The official discord server*", + ], + postNotes: [ + "*: The secret nsfw admin channel is exempt from this rule." + ] + } + ]; + + return ( +
+

The Rules

+ + + +
+ ); +}; + +export default RulesSection; \ No newline at end of file diff --git a/app/hub-rules/ScopeOfRules.tsx b/app/hub-rules/ScopeOfRules.tsx new file mode 100644 index 0000000..62be06c --- /dev/null +++ b/app/hub-rules/ScopeOfRules.tsx @@ -0,0 +1,34 @@ +import React from 'react'; + +const ScopeOfRules = () => { + return ( +
+

Scope of Rules

+

+ These rules apply to any communities that have at least one server listed on the server hub. Our jurisdiction is as far as to remove your server(s) from the hub, and nothing more. +

+

+ We expect you to enforce some of these rules in other places than simply on-hub game servers. For example, 18+ rules may need to be enforced in your Discord or on any off-hub servers you have. +

+

+ We draw the line at what encapsulates a “community” on a case-by-case basis. This is based on shared branding, community spaces (e.g., Discord), infrastructure, and staff. For example, if you are a host that merely provides infrastructure for another community, you will get some degree of separation from said community. However, severe enough offenses may still make us come to you. +

+

+ When deciding whether an issue is your community's fault, we will look at factors such as: +

+
    +
  • The status of the perpetrators in your community
  • +
  • The severity of the issue
  • +
  • Whether your community facilitates the issue or punished people for it
  • +
+

Some examples:

+
    +
  • Not accountable: a random player trash-talking another server after being banned there.
  • +
  • Accountable: a staff member perpetuating lies about another server to the point it becomes an issue for them.
  • +
  • Accountable: harassment perpetuated by a large group of members, with staff aware but letting it happen.
  • +
+
+ ); +}; + +export default ScopeOfRules; diff --git a/app/hub-rules/StrikeSystem.tsx b/app/hub-rules/StrikeSystem.tsx new file mode 100644 index 0000000..f470a12 --- /dev/null +++ b/app/hub-rules/StrikeSystem.tsx @@ -0,0 +1,17 @@ +import React from 'react'; + +const StrikeSystem = () => { + return ( +
+

Strike System

+

+ Breaking some rules will lead to your community receiving one or more strikes. Strikes expire a year after their cause or causes are resolved. If you have three or more active strikes, your community will be permanently removed from the hub. +

+

+ You will be allowed to be relisted after enough strikes have expired to put you below three again. You must also have resolved the issue that caused the last strike. Contact us when this is the case, as we may not automatically relist you ourselves. +

+
+ ); +}; + +export default StrikeSystem; diff --git a/app/hub-rules/layout.tsx b/app/hub-rules/layout.tsx new file mode 100644 index 0000000..a93046b --- /dev/null +++ b/app/hub-rules/layout.tsx @@ -0,0 +1,8 @@ +import React from 'react'; +import FullPage from "../common/uiLibrary/Layouters/fullPage"; + +const PageLayout = ({ children }) => { + return {children}; +}; + +export default PageLayout; \ No newline at end of file diff --git a/app/hub-rules/page.tsx b/app/hub-rules/page.tsx new file mode 100644 index 0000000..570ea81 --- /dev/null +++ b/app/hub-rules/page.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import PageLayout from './layout'; +import RulesIntroduction from './RulesIntroduction'; +import HowRulesApplied from './HowRulesApplied'; +import RulesSection from './RulesSection'; + +const HubRules = () => { + return ( + +
+
+

Stationhub Rules

+ + + +
+
+
+ ); +}; + +export default HubRules; \ No newline at end of file