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 }) => {
Profile picture
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" },