diff --git a/frontend-react/src/components/plus/PlusPage.js b/frontend-react/src/components/plus/PlusPage.js
index 6a595dd5c..d4d620de4 100644
--- a/frontend-react/src/components/plus/PlusPage.js
+++ b/frontend-react/src/components/plus/PlusPage.js
@@ -95,7 +95,7 @@ const PlusPage = () => {
{!data.plusInfo.voting_ends && (
diff --git a/frontend-react/src/components/plus/Suggestions.js b/frontend-react/src/components/plus/Suggestions.js
index 610c31a3e..b9f05edb1 100644
--- a/frontend-react/src/components/plus/Suggestions.js
+++ b/frontend-react/src/components/plus/Suggestions.js
@@ -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
diff --git a/frontend-react/src/components/root/MainMenu.js b/frontend-react/src/components/root/MainMenu.js
index f878320aa..321fd6655 100644
--- a/frontend-react/src/components/root/MainMenu.js
+++ b/frontend-react/src/components/root/MainMenu.js
@@ -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",
diff --git a/frontend-react/src/components/user/Settings.js b/frontend-react/src/components/user/Settings.js
index 327dc416c..605557e40 100644
--- a/frontend-react/src/components/user/Settings.js
+++ b/frontend-react/src/components/user/Settings.js
@@ -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 }) => {
To add a profile picture you need to verify your Twitter on Discord and
log back in to sendou.ink
-
+
+ {customURLError}
+ {user.custom_url ? (
+
+ ) : (
+
+ setForms({ ...forms, custom_url: value })
+ }
+ error={Boolean(customURLError)}
+ />
+ )}
+
{
+ return (
+ <>
+ {!error && (
+
+ Please note that custom URL can't be changed once selected
+
+ )}
+
+ >
+ )
+}
+
+export default URLSelector
diff --git a/frontend-react/src/components/user/UserPage.js b/frontend-react/src/components/user/UserPage.js
index 3ceb54dda..3077b7022 100644
--- a/frontend-react/src/components/user/UserPage.js
+++ b/frontend-react/src/components/user/UserPage.js
@@ -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
@@ -134,7 +137,7 @@ const UserPage = () => {
}}
closeSettings={() => setShowSettings(false)}
handleSuccess={() => handleSuccess()}
- handleError={() => handleError()}
+ handleError={error => handleError(error)}
/>
) : (
{
diff --git a/graphql-schemas/user.js b/graphql-schemas/user.js
index 3473562f3..47cc4db63 100644
--- a/graphql-schemas/user.js
+++ b/graphql-schemas/user.js
@@ -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, {
diff --git a/index.js b/index.js
index 9c3ff81ba..a6d1a041d 100644
--- a/index.js
+++ b/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",
diff --git a/mongoose-models/user.js b/mongoose-models/user.js
index d2573e62e..0ff3b51bc 100644
--- a/mongoose-models/user.js
+++ b/mongoose-models/user.js
@@ -13,6 +13,7 @@ const userSchema = new mongoose.Schema({
},
weapons: [String],
top500: Boolean,
+ custom_url: String,
plus: {
membership_status: String,
vouch_status: String,
diff --git a/package.json b/package.json
index 2180b5057..2891b7096 100644
--- a/package.json
+++ b/package.json
@@ -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"
},