mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-09-29 22:58:18 -05:00
custom url feature
This commit is contained in:
@@ -95,7 +95,7 @@ const PlusPage = () => {
|
||||
{!data.plusInfo.voting_ends && (
|
||||
<Suggestions
|
||||
user={userData.user}
|
||||
plusServer={data.plusInfo.users_membership}
|
||||
plusServer={userData.user?.plus?.membership_status}
|
||||
showSuggestionForm={showSuggestionForm}
|
||||
setShowSuggestionForm={setShowSuggestionForm}
|
||||
/>
|
||||
|
||||
@@ -6,7 +6,6 @@ import SuggestionForm from "./SuggestionForm"
|
||||
import { suggestions } from "../../graphql/queries/suggestions"
|
||||
import Loading from "../common/Loading"
|
||||
import Error from "../common/Error"
|
||||
import { userLean } from "../../graphql/queries/userLean"
|
||||
|
||||
const SuggestionList = ({ suggestionsArray }) => {
|
||||
if (suggestionsArray.length === 0) return null
|
||||
|
||||
@@ -34,7 +34,10 @@ const MainMenu = () => {
|
||||
key: "user",
|
||||
text: "User Page",
|
||||
icon: "user",
|
||||
onClick: () => history.push(`/u/${user.discord_id}`),
|
||||
onClick: () =>
|
||||
history.push(
|
||||
user.custom_url ? `/u/${user.custom_url}` : `/u/${user.discord_id}`
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "sign-out",
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import React, { useState, useEffect } from "react"
|
||||
import { Header, Form, Select, Button } from "semantic-ui-react"
|
||||
import { Header, Form, Select, Button, Message, Label } from "semantic-ui-react"
|
||||
import CountryDropdown from "./CountryDropdown"
|
||||
import WeaponDropdown from "../common/WeaponDropdown"
|
||||
import { updateUser } from "../../graphql/mutations/updateUser"
|
||||
import { searchForUser } from "../../graphql/queries/searchForUser"
|
||||
import { useMutation } from "@apollo/react-hooks"
|
||||
import URLSelector from "./URLSelector"
|
||||
|
||||
const sensOptions = [
|
||||
-5,
|
||||
@@ -33,6 +34,7 @@ const sensOptions = [
|
||||
const Settings = ({ user, closeSettings, handleSuccess, handleError }) => {
|
||||
const [forms, setForms] = useState(user)
|
||||
const [submitDisabled, setSubmitDisabled] = useState(true)
|
||||
const [customURLError, setCustomURLError] = useState(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (JSON.stringify(user) === JSON.stringify(forms)) setSubmitDisabled(true)
|
||||
@@ -52,11 +54,25 @@ const Settings = ({ user, closeSettings, handleSuccess, handleError }) => {
|
||||
],
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
const url = forms.custom_url ?? ""
|
||||
if (url.length < 2)
|
||||
setCustomURLError("Custom URL has to be over 2 characters long")
|
||||
else if (url.length > 32)
|
||||
setCustomURLError("Custom URL has to be under 32 characters long")
|
||||
else if (!isNaN(url))
|
||||
setCustomURLError("Custom URL has to contain at least one letter")
|
||||
else if (!/^[a-z0-9]+$/i.test(url))
|
||||
setCustomURLError("Custom URL can only contain letters and numbers")
|
||||
else setCustomURLError("")
|
||||
}, [forms.custom_url])
|
||||
|
||||
const handleSubmit = async () => {
|
||||
const newProfile = { ...forms }
|
||||
if (newProfile.country === "") newProfile.country = null
|
||||
if (newProfile.stick_sens === "") newProfile.stick_sens = null
|
||||
if (newProfile.motion_sens === "") newProfile.motion_sens = null
|
||||
if (newProfile.custom_url === "") newProfile.custom_url = null
|
||||
|
||||
await editUserMutation({
|
||||
variables: { ...newProfile },
|
||||
@@ -79,7 +95,26 @@ const Settings = ({ user, closeSettings, handleSuccess, handleError }) => {
|
||||
<Header size="small">Profile picture</Header>
|
||||
To add a profile picture you need to verify your Twitter on Discord and
|
||||
log back in to sendou.ink
|
||||
<Form style={{ marginTop: "2em" }} onSubmit={handleSubmit}>
|
||||
<Form
|
||||
style={{ marginTop: "2em" }}
|
||||
onSubmit={handleSubmit}
|
||||
error={Boolean(customURLError)}
|
||||
>
|
||||
<Form.Field>
|
||||
<label>Custom URL</label>
|
||||
<Message error>{customURLError}</Message>
|
||||
{user.custom_url ? (
|
||||
<Label>https://sendou.ink/u/{user.custom_url}</Label>
|
||||
) : (
|
||||
<URLSelector
|
||||
value={forms.custom_url ?? ""}
|
||||
onChange={(e, { value }) =>
|
||||
setForms({ ...forms, custom_url: value })
|
||||
}
|
||||
error={Boolean(customURLError)}
|
||||
/>
|
||||
)}
|
||||
</Form.Field>
|
||||
<Form.Field>
|
||||
<label>Country</label>
|
||||
<CountryDropdown
|
||||
|
||||
22
frontend-react/src/components/user/URLSelector.js
Normal file
22
frontend-react/src/components/user/URLSelector.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import React from "react"
|
||||
import { Input, Message } from "semantic-ui-react"
|
||||
|
||||
const URLSelector = ({ value, onChange, error }) => {
|
||||
return (
|
||||
<>
|
||||
{!error && (
|
||||
<Message>
|
||||
Please note that custom URL can't be changed once selected
|
||||
</Message>
|
||||
)}
|
||||
<Input
|
||||
label="https://sendou.ink/u/"
|
||||
placeholder="custom URL of your choice"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default URLSelector
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState, useEffect } from "react"
|
||||
import { Tab, Image, Grid, Button, Message } from "semantic-ui-react"
|
||||
import { Redirect } from "react-router-dom"
|
||||
import { Redirect, useHistory } from "react-router-dom"
|
||||
import { useQuery } from "@apollo/react-hooks"
|
||||
import { useParams } from "react-router-dom"
|
||||
import { searchForUser } from "../../graphql/queries/searchForUser"
|
||||
@@ -15,9 +15,10 @@ import ProfileLists from "./ProfileLists"
|
||||
|
||||
const UserPage = () => {
|
||||
const { id } = useParams()
|
||||
const history = useHistory()
|
||||
const [tab, setTab] = useQueryParam("tab", NumberParam)
|
||||
const { data, error, loading } = useQuery(searchForUser, {
|
||||
variables: isNaN(id) ? { short_url: id } : { discord_id: id },
|
||||
variables: isNaN(id) ? { custom_url: id } : { discord_id: id },
|
||||
})
|
||||
const userLeanQuery = useQuery(userLean)
|
||||
|
||||
@@ -45,9 +46,11 @@ const UserPage = () => {
|
||||
useEffect(() => {
|
||||
if (loading || !data || !data.searchForUser) return
|
||||
document.title = `${data.searchForUser.username} - sendou.ink`
|
||||
if (data.searchForUser.custom_url)
|
||||
history.replace(`/u/${data.searchForUser.custom_url}`)
|
||||
|
||||
return () => setImageError(false)
|
||||
}, [loading, data])
|
||||
}, [loading, data, history])
|
||||
|
||||
if (loading || userLeanQuery.loading) return <Loading />
|
||||
|
||||
@@ -134,7 +137,7 @@ const UserPage = () => {
|
||||
}}
|
||||
closeSettings={() => setShowSettings(false)}
|
||||
handleSuccess={() => handleSuccess()}
|
||||
handleError={() => handleError()}
|
||||
handleError={error => handleError(error)}
|
||||
/>
|
||||
) : (
|
||||
<Tab
|
||||
|
||||
@@ -6,12 +6,14 @@ export const updateUser = gql`
|
||||
$motion_sens: Float
|
||||
$stick_sens: Float
|
||||
$weapons: [String]
|
||||
$custom_url: String
|
||||
) {
|
||||
updateUser(
|
||||
country: $country
|
||||
motion_sens: $motion_sens
|
||||
stick_sens: $stick_sens
|
||||
weapons: $weapons
|
||||
custom_url: $custom_url
|
||||
)
|
||||
}
|
||||
`
|
||||
|
||||
@@ -6,7 +6,6 @@ export const plusInfo = gql`
|
||||
plus_one_invite_link
|
||||
plus_two_invite_link
|
||||
voting_ends
|
||||
users_membership
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { gql } from "apollo-boost"
|
||||
|
||||
export const searchForUser = gql`
|
||||
query searchForUser($discord_id: String) {
|
||||
searchForUser(discord_id: $discord_id) {
|
||||
query searchForUser($discord_id: String, $custom_url: String) {
|
||||
searchForUser(discord_id: $discord_id, custom_url: $custom_url) {
|
||||
id
|
||||
username
|
||||
discriminator
|
||||
@@ -12,6 +12,7 @@ export const searchForUser = gql`
|
||||
country
|
||||
weapons
|
||||
top500
|
||||
custom_url
|
||||
sens {
|
||||
stick
|
||||
motion
|
||||
|
||||
@@ -7,6 +7,7 @@ export const userLean = gql`
|
||||
username
|
||||
twitter_name
|
||||
discord_id
|
||||
custom_url
|
||||
plus {
|
||||
membership_status
|
||||
}
|
||||
|
||||
@@ -37,7 +37,6 @@ const typeDef = gql`
|
||||
}
|
||||
|
||||
type PlusGeneralInfo {
|
||||
users_membership: String!
|
||||
plus_one_invite_link: String
|
||||
plus_two_invite_link: String!
|
||||
voting_ends: String
|
||||
@@ -136,7 +135,6 @@ const resolvers = {
|
||||
plus_server === "ONE" ? process.env.PLUS_ONE_LINK : null,
|
||||
plus_two_invite_link: process.env.PLUS_TWO_LINK,
|
||||
voting_ends: null,
|
||||
users_membership: plus_server,
|
||||
}
|
||||
},
|
||||
suggestions: (root, args, ctx) => {
|
||||
|
||||
@@ -10,7 +10,7 @@ const typeDef = gql`
|
||||
"Returns the current logged in user or null if not logged in."
|
||||
user: User
|
||||
"Returns user. Either discord_id or twitter has to provided or error is thrown."
|
||||
searchForUser(discord_id: String, twitter: String): User
|
||||
searchForUser(discord_id: String, twitter: String, custom_url: String): User
|
||||
"Returns all users"
|
||||
users: [User!]!
|
||||
}
|
||||
@@ -20,6 +20,7 @@ const typeDef = gql`
|
||||
motion_sens: Float
|
||||
stick_sens: Float
|
||||
weapons: [String]
|
||||
custom_url: String
|
||||
): Boolean
|
||||
}
|
||||
|
||||
@@ -29,7 +30,7 @@ const typeDef = gql`
|
||||
motion: Float
|
||||
}
|
||||
|
||||
"Represents user account. Also includes info regarding solo ladder."
|
||||
"Represents user account."
|
||||
type User {
|
||||
id: ID!
|
||||
"User's username. This is the same as their name on Discord. Updated on every log-in."
|
||||
@@ -43,6 +44,7 @@ const typeDef = gql`
|
||||
country: String
|
||||
sens: Sens
|
||||
weapons: [String]!
|
||||
custom_url: String
|
||||
top500: Boolean!
|
||||
}
|
||||
`
|
||||
@@ -87,6 +89,7 @@ const resolvers = {
|
||||
twitter_name: "sendouc",
|
||||
username: "Sendou",
|
||||
top500: true,
|
||||
//custom_url: "sendou",
|
||||
plus: {
|
||||
membership_status: "ONE",
|
||||
vouch_status: null,
|
||||
@@ -99,12 +102,13 @@ const resolvers = {
|
||||
let searchCriteria = {}
|
||||
if (args.twitter) searchCriteria = { twitter_name: args.twitter }
|
||||
else if (args.discord_id) searchCriteria = { discord_id: args.discord_id }
|
||||
else if (args.short_url)
|
||||
searchCriteria = { short_url: args.short_url.toLowerCase() }
|
||||
else if (args.custom_url)
|
||||
searchCriteria = { custom_url: args.custom_url.toLowerCase() }
|
||||
else
|
||||
throw new UserInputError("no twitter or discord id provided", {
|
||||
throw new UserInputError("no search criteria provided", {
|
||||
invalidArgs: args,
|
||||
})
|
||||
|
||||
return User.findOne(searchCriteria).catch(e => {
|
||||
throw new UserInputError(e.message, {
|
||||
invalidArgs: args,
|
||||
@@ -178,11 +182,36 @@ const resolvers = {
|
||||
}
|
||||
}
|
||||
|
||||
const user = await User.findById(ctx.user._id)
|
||||
if (!user)
|
||||
throw new UserInputError("No user found with the id", {
|
||||
invalidArgs: args,
|
||||
})
|
||||
const user = ctx.user
|
||||
|
||||
if (args.custom_url) {
|
||||
const url = args.custom_url.toLowerCase()
|
||||
if (user.custom_url && user.custom_url !== url)
|
||||
throw new UserInputError("Custom URL already set")
|
||||
if (
|
||||
url.length < 2 ||
|
||||
url.length > 32 ||
|
||||
!isNaN(url) ||
|
||||
!/^[a-z0-9]+$/i.test(url)
|
||||
) {
|
||||
throw new UserInputError("Invalid custom URL provided", {
|
||||
invalidArgs: args,
|
||||
})
|
||||
}
|
||||
|
||||
const userWithCustomUrl = await User.findOne({ custom_url: url }).catch(
|
||||
e => {
|
||||
throw new Error(error.message)
|
||||
}
|
||||
)
|
||||
|
||||
if (userWithCustomUrl && userWithCustomUrl._id !== user._id)
|
||||
throw new UserInputError(
|
||||
"Some other user already claimed this custom URL"
|
||||
)
|
||||
|
||||
args.custom_url = url
|
||||
}
|
||||
|
||||
await User.findByIdAndUpdate(ctx.user._id, { ...args }).catch(e => {
|
||||
throw new UserInputError(error.message, {
|
||||
|
||||
1
index.js
1
index.js
@@ -95,6 +95,7 @@ const server = new ApolloServer({
|
||||
twitch_name: "sendou",
|
||||
twitter_name: "sendouc",
|
||||
username: "Sendou",
|
||||
//custom_url: "sendou",
|
||||
weapons: [
|
||||
"Tenta Brella",
|
||||
"Range Blaster",
|
||||
|
||||
@@ -13,6 +13,7 @@ const userSchema = new mongoose.Schema({
|
||||
},
|
||||
weapons: [String],
|
||||
top500: Boolean,
|
||||
custom_url: String,
|
||||
plus: {
|
||||
membership_status: String,
|
||||
vouch_status: String,
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"scripts": {
|
||||
"start": "cross-env LOGGED_IN=TRUE node index.js",
|
||||
"loggedout": "node index.js",
|
||||
"watch": "nodemon index.js",
|
||||
"watch": "cross-env LOGGED_IN=TRUE nodemon index.js",
|
||||
"build": "cd frontend-react && npm install && npm run build && mv build ../build",
|
||||
"testbuild": "rmdir /s /q build && cd frontend-react && npm run build && move build ../build"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user