mirror of
				https://github.com/superseriousbusiness/gotosocial.git
				synced 2025-10-31 13:32:25 -05:00 
			
		
		
		
	[feature/frogend] modify local emoji (#1143)
* update danger button red * emoji category and image modification * debug bundles in dev * fix linting error
This commit is contained in:
		
					parent
					
						
							
								b6dbe21026
							
						
					
				
			
			
				commit
				
					
						665d902fd7
					
				
			
		
					 11 changed files with 322 additions and 82 deletions
				
			
		|  | @ -46,6 +46,7 @@ $blue3: #89caff; /* hover/selected accent to $blue2, can be used with $gray1 (7. | ||||||
| 
 | 
 | ||||||
| $error1: #860000; /* Error border/foreground text, can be used with $error2 (5.0), $white1 (10), $white2 (5.1) */ | $error1: #860000; /* Error border/foreground text, can be used with $error2 (5.0), $white1 (10), $white2 (5.1) */ | ||||||
| $error2: #ff9796; /* Error background text, can be used with $error1 (5.0), $gray1 (6.6), $gray2 (5.3), $gray3 (4.8) */ | $error2: #ff9796; /* Error background text, can be used with $error1 (5.0), $gray1 (6.6), $gray2 (5.3), $gray3 (4.8) */ | ||||||
|  | $error3: #dd2c2c; /* Error button background text, can be used with $white1 (4.51) */ | ||||||
| $error-link: #185F8C; /* Error link text, can be used with $error2 (5.54) */ | $error-link: #185F8C; /* Error link text, can be used with $error2 (5.54) */ | ||||||
| 
 | 
 | ||||||
| $fg: $white1; | $fg: $white1; | ||||||
|  | @ -69,9 +70,9 @@ $button-bg: $blue2; | ||||||
| $button-fg: $gray1; | $button-fg: $gray1; | ||||||
| $button-hover-bg: $blue3; | $button-hover-bg: $blue3; | ||||||
| 
 | 
 | ||||||
| $button-danger-bg: $orange1; | $button-danger-bg: $error3; | ||||||
| $button-danger-fg: $gray1; | $button-danger-fg: $white1; | ||||||
| $button-danger-hover-bg: $orange2; | $button-danger-hover-bg: $error2; | ||||||
| 
 | 
 | ||||||
| $toot-focus-bg: $gray5; | $toot-focus-bg: $gray5; | ||||||
| $toot-unfocus-bg: $gray2; | $toot-unfocus-bg: $gray2; | ||||||
|  |  | ||||||
|  | @ -172,6 +172,16 @@ main { | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	&:disabled { | ||||||
|  | 		color: $white2; | ||||||
|  | 		background: $gray2; | ||||||
|  | 		cursor: auto; | ||||||
|  | 
 | ||||||
|  | 		&:hover { | ||||||
|  | 			background: $gray3; | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	&:hover { | 	&:hover { | ||||||
| 		background: $button-hover-bg; | 		background: $button-hover-bg; | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | @ -66,7 +66,6 @@ skulk({ | ||||||
| 			], | 			], | ||||||
| 		}, | 		}, | ||||||
| 		settings: { | 		settings: { | ||||||
| 			debug: false, |  | ||||||
| 			entryFile: "settings", | 			entryFile: "settings", | ||||||
| 			outputFile: "settings.js", | 			outputFile: "settings.js", | ||||||
| 			prodCfg: prodCfg, | 			prodCfg: prodCfg, | ||||||
|  |  | ||||||
							
								
								
									
										96
									
								
								web/source/settings/admin/emoji/category-select.jsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										96
									
								
								web/source/settings/admin/emoji/category-select.jsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,96 @@ | ||||||
|  | /* | ||||||
|  | 	 GoToSocial | ||||||
|  | 	 Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org | ||||||
|  | 
 | ||||||
|  | 	 This program is free software: you can redistribute it and/or modify | ||||||
|  | 	 it under the terms of the GNU Affero General Public License as published by | ||||||
|  | 	 the Free Software Foundation, either version 3 of the License, or | ||||||
|  | 	 (at your option) any later version. | ||||||
|  | 
 | ||||||
|  | 	 This program is distributed in the hope that it will be useful, | ||||||
|  | 	 but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  | 	 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||||
|  | 	 GNU Affero General Public License for more details. | ||||||
|  | 
 | ||||||
|  | 	 You should have received a copy of the GNU Affero General Public License | ||||||
|  | 	 along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||||
|  | */ | ||||||
|  | 
 | ||||||
|  | "use strict"; | ||||||
|  | 
 | ||||||
|  | const React = require("react"); | ||||||
|  | const splitFilterN = require("split-filter-n"); | ||||||
|  | const syncpipe = require('syncpipe'); | ||||||
|  | const { matchSorter } = require("match-sorter"); | ||||||
|  | 
 | ||||||
|  | const query = require("../../lib/query"); | ||||||
|  | 
 | ||||||
|  | const ComboBox = require("../../components/combo-box"); | ||||||
|  | 
 | ||||||
|  | function useEmojiByCategory(emoji) { | ||||||
|  | 	// split all emoji over an object keyed by the category names (or Unsorted) | ||||||
|  | 	return React.useMemo(() => splitFilterN( | ||||||
|  | 		emoji, | ||||||
|  | 		[], | ||||||
|  | 		(entry) => entry.category ?? "Unsorted" | ||||||
|  | 	), [emoji]); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function CategorySelect({value, categoryState, setIsNew=() => {}, children}) { | ||||||
|  | 	const { | ||||||
|  | 		data: emoji = [], | ||||||
|  | 		isLoading, | ||||||
|  | 		isSuccess, | ||||||
|  | 		error | ||||||
|  | 	} = query.useGetAllEmojiQuery({filter: "domain:local"}); | ||||||
|  | 
 | ||||||
|  | 	const emojiByCategory = useEmojiByCategory(emoji); | ||||||
|  | 
 | ||||||
|  | 	const categories = React.useMemo(() => new Set(Object.keys(emojiByCategory)), [emojiByCategory]); | ||||||
|  | 
 | ||||||
|  | 	// data used by the ComboBox element to select an emoji category | ||||||
|  | 	const categoryItems = React.useMemo(() => { | ||||||
|  | 		return syncpipe(emojiByCategory, [ | ||||||
|  | 			(_) => Object.keys(_),            // just emoji category names | ||||||
|  | 			(_) => matchSorter(_, value, {threshold: matchSorter.rankings.NO_MATCH}),  // sorted by complex algorithm | ||||||
|  | 			(_) => _.map((categoryName) => [  // map to input value, and selectable element with icon | ||||||
|  | 				categoryName, | ||||||
|  | 				<> | ||||||
|  | 					<img src={emojiByCategory[categoryName][0].static_url} aria-hidden="true"></img> | ||||||
|  | 					{categoryName} | ||||||
|  | 				</> | ||||||
|  | 			]) | ||||||
|  | 		]); | ||||||
|  | 	}, [emojiByCategory, value]); | ||||||
|  | 
 | ||||||
|  | 	React.useEffect(() => { | ||||||
|  | 		if (value != undefined && isSuccess && value.trim().length > 0) { | ||||||
|  | 			setIsNew(!categories.has(value.trim())); | ||||||
|  | 		} | ||||||
|  | 	}, [categories, value, setIsNew, isSuccess]); | ||||||
|  | 
 | ||||||
|  | 	if (error) { // fall back to plain text input, but this would almost certainly have caused a bigger error message elsewhere | ||||||
|  | 		return ( | ||||||
|  | 			<> | ||||||
|  | 				<input type="text" placeholder="e.g., reactions" onChange={(e) => {categoryState.value = e.target.value;}}/>; | ||||||
|  | 			</> | ||||||
|  | 		); | ||||||
|  | 	} else if (isLoading) { | ||||||
|  | 		return <input type="text" value="Loading categories..." disabled={true}/>; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return ( | ||||||
|  | 		<ComboBox | ||||||
|  | 			state={categoryState} | ||||||
|  | 			items={categoryItems} | ||||||
|  | 			label="Category" | ||||||
|  | 			placeHolder="e.g., reactions" | ||||||
|  | 			children={children} | ||||||
|  | 		/> | ||||||
|  | 	); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | module.exports = { | ||||||
|  | 	useEmojiByCategory, | ||||||
|  | 	CategorySelect | ||||||
|  | }; | ||||||
|  | @ -22,48 +22,130 @@ const React = require("react"); | ||||||
| 
 | 
 | ||||||
| const { useRoute, Link, Redirect } = require("wouter"); | const { useRoute, Link, Redirect } = require("wouter"); | ||||||
| 
 | 
 | ||||||
| const BackButton = require("../../components/back-button"); | const { CategorySelect } = require("./category-select"); | ||||||
|  | const { useComboBoxInput, useFileInput } = require("../../components/form"); | ||||||
| 
 | 
 | ||||||
| const query = require("../../lib/query"); | const query = require("../../lib/query"); | ||||||
|  | const FakeToot = require("../../components/fake-toot"); | ||||||
| 
 | 
 | ||||||
| const base = "/settings/admin/custom-emoji"; | const base = "/settings/admin/custom-emoji"; | ||||||
| 
 | 
 | ||||||
| /* We wrap the component to generate formFields with a setter depending on the domain | module.exports = function EmojiDetailRoute() { | ||||||
| 	 if formFields() is used inside the same component that is re-rendered with their state, | 	let [_match, params] = useRoute(`${base}/:emojiId`); | ||||||
| 	 inputs get re-created on every change, causing them to lose focus, and bad performance | 	if (params?.emojiId == undefined) { | ||||||
| */ | 		return <Redirect to={base}/>; | ||||||
| module.exports = function EmojiDetailWrapped() { | 	} else { | ||||||
| 	let [_match, {emojiId}] = useRoute(`${base}/:emojiId`); | 		return ( | ||||||
| 	const {currentData: emoji, isLoading, error} = query.useGetEmojiQuery(emojiId); | 			<div className="emoji-detail"> | ||||||
| 
 | 				<Link to={base}><a>< go back</a></Link> | ||||||
| 	return (<> | 				<EmojiDetailData emojiId={params.emojiId}/> | ||||||
| 		{error && <div className="error accent">{error.status}: {error.data.error}</div>} | 			</div> | ||||||
| 		{isLoading | 		); | ||||||
| 			? "Loading..." | 	} | ||||||
| 			: <EmojiDetail emoji={emoji}/> |  | ||||||
| 		} |  | ||||||
| 	</>); |  | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | function EmojiDetailData({emojiId}) { | ||||||
|  | 	const {currentData: emoji, isLoading, error} = query.useGetEmojiQuery(emojiId); | ||||||
|  | 
 | ||||||
|  | 	if (error) { | ||||||
|  | 		return ( | ||||||
|  | 			<div className="error accent"> | ||||||
|  | 				{error.status}: {error.data.error} | ||||||
|  | 			</div> | ||||||
|  | 		); | ||||||
|  | 	} else if (isLoading) { | ||||||
|  | 		return "Loading..."; | ||||||
|  | 	} else { | ||||||
|  | 		return <EmojiDetail emoji={emoji}/>; | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
| function EmojiDetail({emoji}) { | function EmojiDetail({emoji}) { | ||||||
| 	if (emoji == undefined) { | 	const [modifyEmoji, modifyResult] = query.useEditEmojiMutation(); | ||||||
| 		return (<> | 
 | ||||||
| 			<Link to={base}> | 	const [isNewCategory, setIsNewCategory] = React.useState(false); | ||||||
| 				<a className="button">go back</a> | 
 | ||||||
| 			</Link> | 	const [categoryState, _resetCategory, { category }] = useComboBoxInput("category", {defaultValue: emoji.category}); | ||||||
| 		</>); | 
 | ||||||
|  | 	const [onFileChange, _resetFile, { image, imageURL, imageInfo }] = useFileInput("image", { | ||||||
|  | 		withPreview: true, | ||||||
|  | 		maxSize: 50 * 1024 | ||||||
|  | 	}); | ||||||
|  | 
 | ||||||
|  | 	function modifyCategory() { | ||||||
|  | 		modifyEmoji({id: emoji.id, category: category.trim()}); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	function modifyImage() { | ||||||
|  | 		modifyEmoji({id: emoji.id, image: image}); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	React.useEffect(() => { | ||||||
|  | 		if (category != emoji.category && !categoryState.open && !isNewCategory && emoji.category != undefined) { | ||||||
|  | 			console.log("updating to", category); | ||||||
|  | 			modifyEmoji({id: emoji.id, category: category.trim()}); | ||||||
|  | 		} | ||||||
|  | 	}, [isNewCategory, category, categoryState.open, emoji.category, emoji.id, modifyEmoji]); | ||||||
|  | 
 | ||||||
| 	return ( | 	return ( | ||||||
| 		<div> | 		<> | ||||||
| 			<h1><BackButton to={base}/> Custom Emoji: {emoji.shortcode}</h1> | 			<div className="emoji-header"> | ||||||
| 			<DeleteButton id={emoji.id}/> | 				<img src={emoji.url} alt={emoji.shortcode} title={emoji.shortcode}/> | ||||||
| 			<p> | 				<div> | ||||||
| 				Editing custom emoji isn't implemented yet.<br/> | 					<h2>{emoji.shortcode}</h2> | ||||||
| 				<a target="_blank" rel="noreferrer" href="https://github.com/superseriousbusiness/gotosocial/issues/797">View implementation progress.</a> | 					<DeleteButton id={emoji.id}/> | ||||||
| 			</p> | 				</div> | ||||||
| 			<img src={emoji.url} alt={emoji.shortcode} title={`:${emoji.shortcode}:`}/> | 			</div> | ||||||
| 		</div> | 
 | ||||||
|  | 			<div className="left-border"> | ||||||
|  | 				<h2>Modify this emoji {modifyResult.isLoading && "(processing..)"}</h2> | ||||||
|  | 
 | ||||||
|  | 				{modifyResult.error && <div className="error"> | ||||||
|  | 					{modifyResult.error.status}: {modifyResult.error.data.error} | ||||||
|  | 				</div>} | ||||||
|  | 
 | ||||||
|  | 				<div className="update-category"> | ||||||
|  | 					<CategorySelect | ||||||
|  | 						value={category} | ||||||
|  | 						categoryState={categoryState} | ||||||
|  | 						setIsNew={setIsNewCategory} | ||||||
|  | 					> | ||||||
|  | 						<button style={{visibility: (isNewCategory ? "initial" : "hidden")}} onClick={modifyCategory}> | ||||||
|  | 							Create | ||||||
|  | 						</button> | ||||||
|  | 					</CategorySelect> | ||||||
|  | 				</div> | ||||||
|  | 
 | ||||||
|  | 				<div className="update-image"> | ||||||
|  | 					<b>Image</b> | ||||||
|  | 					<div className="form-field file"> | ||||||
|  | 						<label className="file-input button" htmlFor="image"> | ||||||
|  | 							Browse | ||||||
|  | 						</label> | ||||||
|  | 						{imageInfo} | ||||||
|  | 						<input | ||||||
|  | 							className="hidden" | ||||||
|  | 							type="file" | ||||||
|  | 							id="image" | ||||||
|  | 							name="Image" | ||||||
|  | 							accept="image/png,image/gif" | ||||||
|  | 							onChange={onFileChange} | ||||||
|  | 						/> | ||||||
|  | 					</div> | ||||||
|  | 
 | ||||||
|  | 					<button onClick={modifyImage} disabled={image == undefined}>Replace image</button> | ||||||
|  | 
 | ||||||
|  | 					<FakeToot> | ||||||
|  | 						Look at this new custom emoji <img | ||||||
|  | 							className="emoji" | ||||||
|  | 							src={imageURL ?? emoji.url} | ||||||
|  | 							title={`:${emoji.shortcode}:`} | ||||||
|  | 							alt={emoji.shortcode} | ||||||
|  | 						/> isn't it cool? | ||||||
|  | 					</FakeToot> | ||||||
|  | 				</div> | ||||||
|  | 			</div> | ||||||
|  | 		</> | ||||||
| 	); | 	); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -71,9 +153,9 @@ function DeleteButton({id}) { | ||||||
| 	// TODO: confirmation dialog?
 | 	// TODO: confirmation dialog?
 | ||||||
| 	const [deleteEmoji, deleteResult] = query.useDeleteEmojiMutation(); | 	const [deleteEmoji, deleteResult] = query.useDeleteEmojiMutation(); | ||||||
| 
 | 
 | ||||||
| 	let text = "Delete this emoji"; | 	let text = "Delete"; | ||||||
| 	if (deleteResult.isLoading) { | 	if (deleteResult.isLoading) { | ||||||
| 		text = "processing..."; | 		text = "Deleting..."; | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if (deleteResult.isSuccess) { | 	if (deleteResult.isSuccess) { | ||||||
|  | @ -81,6 +163,6 @@ function DeleteButton({id}) { | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	return ( | 	return ( | ||||||
| 		<button onClick={() => deleteEmoji(id)} disabled={deleteResult.isLoading}>{text}</button> | 		<button className="danger" onClick={() => deleteEmoji(id)} disabled={deleteResult.isLoading}>{text}</button> | ||||||
| 	); | 	); | ||||||
| } | } | ||||||
|  | @ -20,11 +20,9 @@ | ||||||
| 
 | 
 | ||||||
| const Promise = require('bluebird'); | const Promise = require('bluebird'); | ||||||
| const React = require("react"); | const React = require("react"); | ||||||
| const { matchSorter } = require("match-sorter"); |  | ||||||
| 
 | 
 | ||||||
| const FakeToot = require("../../components/fake-toot"); | const FakeToot = require("../../components/fake-toot"); | ||||||
| const MutateButton = require("../../components/mutation-button"); | const MutateButton = require("../../components/mutation-button"); | ||||||
| const ComboBox = require("../../components/combo-box"); |  | ||||||
| 
 | 
 | ||||||
| const { | const { | ||||||
| 	useTextInput, | 	useTextInput, | ||||||
|  | @ -33,9 +31,9 @@ const { | ||||||
| } = require("../../components/form"); | } = require("../../components/form"); | ||||||
| 
 | 
 | ||||||
| const query = require("../../lib/query"); | const query = require("../../lib/query"); | ||||||
| const syncpipe = require('syncpipe'); | const { CategorySelect } = require('./category-select'); | ||||||
| 
 | 
 | ||||||
| module.exports = function NewEmojiForm({ emoji, emojiByCategory }) { | module.exports = function NewEmojiForm({ emoji }) { | ||||||
| 	const emojiCodes = React.useMemo(() => { | 	const emojiCodes = React.useMemo(() => { | ||||||
| 		return new Set(emoji.map((e) => e.shortcode)); | 		return new Set(emoji.map((e) => e.shortcode)); | ||||||
| 	}, [emoji]); | 	}, [emoji]); | ||||||
|  | @ -57,21 +55,6 @@ module.exports = function NewEmojiForm({ emoji, emojiByCategory }) { | ||||||
| 
 | 
 | ||||||
| 	const [categoryState, resetCategory, { category }] = useComboBoxInput("category"); | 	const [categoryState, resetCategory, { category }] = useComboBoxInput("category"); | ||||||
| 
 | 
 | ||||||
| 	// data used by the ComboBox element to select an emoji category
 |  | ||||||
| 	const categoryItems = React.useMemo(() => { |  | ||||||
| 		return syncpipe(emojiByCategory, [ |  | ||||||
| 			(_) => Object.keys(_),            // just emoji category names
 |  | ||||||
| 			(_) => matchSorter(_, category),  // sorted by complex algorithm
 |  | ||||||
| 			(_) => _.map((categoryName) => [  // map to input value, and selectable element with icon
 |  | ||||||
| 				categoryName, |  | ||||||
| 				<> |  | ||||||
| 					<img src={emojiByCategory[categoryName][0].static_url} aria-hidden="true"></img> |  | ||||||
| 					{categoryName} |  | ||||||
| 				</> |  | ||||||
| 			]) |  | ||||||
| 		]); |  | ||||||
| 	}, [emojiByCategory, category]); |  | ||||||
| 
 |  | ||||||
| 	React.useEffect(() => { | 	React.useEffect(() => { | ||||||
| 		if (shortcode.length == 0) { | 		if (shortcode.length == 0) { | ||||||
| 			if (image != undefined) { | 			if (image != undefined) { | ||||||
|  | @ -152,11 +135,9 @@ module.exports = function NewEmojiForm({ emoji, emojiByCategory }) { | ||||||
| 					/> | 					/> | ||||||
| 				</div> | 				</div> | ||||||
| 
 | 
 | ||||||
| 				<ComboBox | 				<CategorySelect | ||||||
| 					state={categoryState} | 					value={category} | ||||||
| 					items={categoryItems} | 					categoryState={categoryState} | ||||||
| 					label="Category" |  | ||||||
| 					placeHolder="e.g., reactions" |  | ||||||
| 				/> | 				/> | ||||||
| 
 | 
 | ||||||
| 				<MutateButton text="Upload emoji" result={result} /> | 				<MutateButton text="Upload emoji" result={result} /> | ||||||
|  |  | ||||||
|  | @ -20,11 +20,11 @@ | ||||||
| 
 | 
 | ||||||
| const React = require("react"); | const React = require("react"); | ||||||
| const {Link} = require("wouter"); | const {Link} = require("wouter"); | ||||||
| const splitFilterN = require("split-filter-n"); |  | ||||||
| 
 | 
 | ||||||
| const NewEmojiForm = require("./new-emoji"); | const NewEmojiForm = require("./new-emoji"); | ||||||
| 
 | 
 | ||||||
| const query = require("../../lib/query"); | const query = require("../../lib/query"); | ||||||
|  | const { useEmojiByCategory } = require("./category-select"); | ||||||
| 
 | 
 | ||||||
| const base = "/settings/admin/custom-emoji"; | const base = "/settings/admin/custom-emoji"; | ||||||
| 
 | 
 | ||||||
|  | @ -35,13 +35,6 @@ module.exports = function EmojiOverview() { | ||||||
| 		error | 		error | ||||||
| 	} = query.useGetAllEmojiQuery({filter: "domain:local"}); | 	} = query.useGetAllEmojiQuery({filter: "domain:local"}); | ||||||
| 
 | 
 | ||||||
| 	// split all emoji over an object keyed by the category names (or Unsorted)
 |  | ||||||
| 	const emojiByCategory = React.useMemo(() => splitFilterN( |  | ||||||
| 		emoji, |  | ||||||
| 		[], |  | ||||||
| 		(entry) => entry.category ?? "Unsorted" |  | ||||||
| 	), [emoji]); |  | ||||||
| 
 |  | ||||||
| 	return ( | 	return ( | ||||||
| 		<> | 		<> | ||||||
| 			<h1>Custom Emoji</h1> | 			<h1>Custom Emoji</h1> | ||||||
|  | @ -51,15 +44,17 @@ module.exports = function EmojiOverview() { | ||||||
| 			{isLoading | 			{isLoading | ||||||
| 				? "Loading..." | 				? "Loading..." | ||||||
| 				: <> | 				: <> | ||||||
| 					<EmojiList emoji={emoji} emojiByCategory={emojiByCategory}/> | 					<EmojiList emoji={emoji}/> | ||||||
| 					<NewEmojiForm emoji={emoji} emojiByCategory={emojiByCategory}/> | 					<NewEmojiForm emoji={emoji}/> | ||||||
| 				</> | 				</> | ||||||
| 			} | 			} | ||||||
| 		</> | 		</> | ||||||
| 	); | 	); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| function EmojiList({emoji, emojiByCategory}) { | function EmojiList({emoji}) { | ||||||
|  | 	const emojiByCategory = useEmojiByCategory(emoji); | ||||||
|  | 
 | ||||||
| 	return ( | 	return ( | ||||||
| 		<div> | 		<div> | ||||||
| 			<h2>Overview</h2> | 			<h2>Overview</h2> | ||||||
|  |  | ||||||
|  | @ -26,16 +26,19 @@ const { | ||||||
| 	ComboboxPopover, | 	ComboboxPopover, | ||||||
| } = require("ariakit/combobox"); | } = require("ariakit/combobox"); | ||||||
| 
 | 
 | ||||||
| module.exports = function ComboBox({state, items, label, placeHolder}) { | module.exports = function ComboBox({state, items, label, placeHolder, children}) { | ||||||
| 	return ( | 	return ( | ||||||
| 		<div className="form-field combobox-wrapper"> | 		<div className="form-field combobox-wrapper"> | ||||||
| 			<label> | 			<label> | ||||||
| 				{label} | 				{label} | ||||||
| 				<Combobox | 				<div className="row"> | ||||||
| 					state={state} | 					<Combobox | ||||||
| 					placeholder={placeHolder} | 						state={state} | ||||||
| 					className="combobox input" | 						placeholder={placeHolder} | ||||||
| 				/> | 						className="combobox input" | ||||||
|  | 					/> | ||||||
|  | 					{children} | ||||||
|  | 				</div> | ||||||
| 			</label> | 			</label> | ||||||
| 			<ComboboxPopover state={state} className="popover"> | 			<ComboboxPopover state={state} className="popover"> | ||||||
| 				{items.map(([key, value]) => ( | 				{items.map(([key, value]) => ( | ||||||
|  |  | ||||||
|  | @ -20,11 +20,15 @@ | ||||||
| 
 | 
 | ||||||
| const { useComboboxState } = require("ariakit/combobox"); | const { useComboboxState } = require("ariakit/combobox"); | ||||||
| 
 | 
 | ||||||
| module.exports = function useComboBoxInput({name, Name}, {validator} = {}) { | module.exports = function useComboBoxInput({name, Name}, {validator, defaultValue} = {}) { | ||||||
| 	const state = useComboboxState({ gutter: 0, sameWidth: true }); | 	const state = useComboboxState({ | ||||||
|  | 		defaultValue, | ||||||
|  | 		gutter: 0, | ||||||
|  | 		sameWidth: true | ||||||
|  | 	}); | ||||||
| 
 | 
 | ||||||
| 	function reset() { | 	function reset() { | ||||||
| 		state.value = ""; | 		state.setValue(""); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	return [ | 	return [ | ||||||
|  |  | ||||||
|  | @ -54,6 +54,23 @@ const endpoints = (build) => ({ | ||||||
| 				? [{type: "Emojis", id: "LIST"}, {type: "Emojis", id: res.id}] | 				? [{type: "Emojis", id: "LIST"}, {type: "Emojis", id: res.id}] | ||||||
| 				: [{type: "Emojis", id: "LIST"}] | 				: [{type: "Emojis", id: "LIST"}] | ||||||
| 	}), | 	}), | ||||||
|  | 	editEmoji: build.mutation({ | ||||||
|  | 		query: ({id, ...patch}) => { | ||||||
|  | 			return { | ||||||
|  | 				method: "PATCH", | ||||||
|  | 				url: `/api/v1/admin/custom_emojis/${id}`, | ||||||
|  | 				asForm: true, | ||||||
|  | 				body: { | ||||||
|  | 					type: "modify", | ||||||
|  | 					...patch | ||||||
|  | 				} | ||||||
|  | 			}; | ||||||
|  | 		}, | ||||||
|  | 		invalidatesTags: (res) =>  | ||||||
|  | 			res | ||||||
|  | 				? [{type: "Emojis", id: "LIST"}, {type: "Emojis", id: res.id}] | ||||||
|  | 				: [{type: "Emojis", id: "LIST"}] | ||||||
|  | 	}), | ||||||
| 	deleteEmoji: build.mutation({ | 	deleteEmoji: build.mutation({ | ||||||
| 		query: (id) => ({ | 		query: (id) => ({ | ||||||
| 			method: "DELETE", | 			method: "DELETE", | ||||||
|  |  | ||||||
|  | @ -544,4 +544,56 @@ span.form-info { | ||||||
| .combobox-item[data-active-item] { | .combobox-item[data-active-item] { | ||||||
|   background: $button-hover-bg; |   background: $button-hover-bg; | ||||||
|   color: hsl(204 20% 100%); |   color: hsl(204 20% 100%); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .row { | ||||||
|  | 	display: flex; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .emoji-detail { | ||||||
|  | 	display: flex; | ||||||
|  | 	flex-direction: column; | ||||||
|  | 	gap: 1rem !important; | ||||||
|  | 
 | ||||||
|  | 	& > a { | ||||||
|  | 		align-self: flex-start; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	.emoji-header { | ||||||
|  | 		display: flex; | ||||||
|  | 		align-items: center; | ||||||
|  | 		gap: 0.5rem; | ||||||
|  | 
 | ||||||
|  | 		img { | ||||||
|  | 			height: 8.5rem; | ||||||
|  | 			width: 8.5rem; | ||||||
|  | 			border: 0.2rem solid $border-accent; | ||||||
|  | 			object-fit: contain; | ||||||
|  | 			padding: 0.5rem; | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	.update-category { | ||||||
|  | 		margin-bottom: 1rem; | ||||||
|  | 		.combobox-wrapper button { | ||||||
|  | 			font-size: 1rem; | ||||||
|  | 			margin: 0.15rem 0; | ||||||
|  | 		} | ||||||
|  | 	 | ||||||
|  | 		.row { | ||||||
|  | 			margin-top: 0.4rem; | ||||||
|  | 			gap: 0.5rem; | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	.update-image { | ||||||
|  | 		display: flex; | ||||||
|  | 		flex-direction: column; | ||||||
|  | 		gap: 0.5rem; | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .left-border { | ||||||
|  | 	border-left: 0.2rem solid $border-accent; | ||||||
|  | 	padding-left: 0.4rem; | ||||||
| } | } | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue