custom url feature

This commit is contained in:
Sendou
2019-12-30 03:05:32 +02:00
parent d8b5854747
commit 82b84b3e26
15 changed files with 119 additions and 25 deletions

View File

@@ -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}
/>

View File

@@ -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

View File

@@ -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",

View File

@@ -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

View 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

View File

@@ -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

View File

@@ -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
)
}
`

View File

@@ -6,7 +6,6 @@ export const plusInfo = gql`
plus_one_invite_link
plus_two_invite_link
voting_ends
users_membership
}
}
`

View File

@@ -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

View File

@@ -7,6 +7,7 @@ export const userLean = gql`
username
twitter_name
discord_id
custom_url
plus {
membership_status
}

View File

@@ -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) => {

View File

@@ -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, {

View File

@@ -95,6 +95,7 @@ const server = new ApolloServer({
twitch_name: "sendou",
twitter_name: "sendouc",
username: "Sendou",
//custom_url: "sendou",
weapons: [
"Tenta Brella",
"Range Blaster",

View File

@@ -13,6 +13,7 @@ const userSchema = new mongoose.Schema({
},
weapons: [String],
top500: Boolean,
custom_url: String,
plus: {
membership_status: String,
vouch_status: String,

View File

@@ -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"
},