Refactor validation
Signed-off-by: Nikolaos Karaolidis <nick@karaolidis.com>
This commit is contained in:
@@ -5,6 +5,11 @@ import { useDimensions } from '../../contexts';
|
||||
import LoadingView from '../loadingView';
|
||||
import { MemeTagSelector } from '.';
|
||||
import { Tag } from '../../database';
|
||||
import {
|
||||
StringValidationResult,
|
||||
validateMemeDescription,
|
||||
validateMemeTitle,
|
||||
} from '../../utilities';
|
||||
|
||||
const MemeEditor = ({
|
||||
imageUri,
|
||||
@@ -14,18 +19,14 @@ const MemeEditor = ({
|
||||
setMemeDescription,
|
||||
memeTags,
|
||||
setMemeTags,
|
||||
memeTitleError,
|
||||
setMemeTitleError,
|
||||
}: {
|
||||
imageUri: string[];
|
||||
memeTitle: string;
|
||||
setMemeTitle: (name: string) => void;
|
||||
memeDescription: string;
|
||||
setMemeDescription: (description: string) => void;
|
||||
memeTitle: StringValidationResult;
|
||||
setMemeTitle: (name: StringValidationResult) => void;
|
||||
memeDescription: StringValidationResult;
|
||||
setMemeDescription: (description: StringValidationResult) => void;
|
||||
memeTags: Map<string, Tag>;
|
||||
setMemeTags: (tags: Map<string, Tag>) => void;
|
||||
memeTitleError: string | undefined;
|
||||
setMemeTitleError: (error: string | undefined) => void;
|
||||
}) => {
|
||||
const { dimensions, fixed, responsive } = useDimensions();
|
||||
|
||||
@@ -40,16 +41,6 @@ const MemeEditor = ({
|
||||
});
|
||||
}, [dimensions.width, imageUri]);
|
||||
|
||||
const handleMemeTitleChange = (name: string) => {
|
||||
setMemeTitle(name);
|
||||
if (name.length === 0) {
|
||||
setMemeTitleError('Meme title cannot be empty');
|
||||
} else {
|
||||
// eslint-disable-next-line unicorn/no-useless-undefined
|
||||
setMemeTitleError(undefined);
|
||||
}
|
||||
};
|
||||
|
||||
if (!imageWidth || !imageHeight) return <LoadingView />;
|
||||
|
||||
return (
|
||||
@@ -57,13 +48,13 @@ const MemeEditor = ({
|
||||
<TextInput
|
||||
mode="outlined"
|
||||
label="Title"
|
||||
value={memeTitle}
|
||||
onChangeText={handleMemeTitleChange}
|
||||
error={!!memeTitleError}
|
||||
value={memeTitle.raw}
|
||||
onChangeText={title => setMemeTitle(validateMemeTitle(title))}
|
||||
error={!memeTitle.valid}
|
||||
selectTextOnFocus
|
||||
/>
|
||||
<HelperText type="error" visible={!!memeTitleError}>
|
||||
{memeTitleError}
|
||||
<HelperText type="error" visible={!memeTitle.valid}>
|
||||
{memeTitle.error}
|
||||
</HelperText>
|
||||
<Image
|
||||
source={{ uri: imageUri[0] }}
|
||||
@@ -89,8 +80,10 @@ const MemeEditor = ({
|
||||
style={{
|
||||
marginBottom: responsive.verticalScale(15),
|
||||
}}
|
||||
value={memeDescription}
|
||||
onChangeText={setMemeDescription}
|
||||
value={memeDescription.raw}
|
||||
onChangeText={description =>
|
||||
setMemeDescription(validateMemeDescription(description))
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import React, { useRef, useState } from 'react';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { TagChip } from '../tags';
|
||||
import { Tag } from '../../database';
|
||||
import { useQuery, useRealm } from '@realm/react';
|
||||
@@ -8,6 +8,7 @@ import { StyleSheet } from 'react-native';
|
||||
import { useDimensions } from '../../contexts';
|
||||
import styles from '../../styles';
|
||||
import { FlashList } from '@shopify/flash-list';
|
||||
import { validateTagName } from '../../utilities';
|
||||
|
||||
const memeTagSearchModalStyles = StyleSheet.create({
|
||||
modal: {
|
||||
@@ -34,6 +35,11 @@ const MemeTagSearchModal = ({
|
||||
const flashListRef = useRef<FlashList<Tag>>(null);
|
||||
|
||||
const [search, setSearch] = useState('');
|
||||
const [tagName, setTagName] = useState(validateTagName(search));
|
||||
|
||||
useEffect(() => {
|
||||
setTagName(validateTagName(search));
|
||||
}, [search]);
|
||||
|
||||
const handleSearch = (newSearch: string) => {
|
||||
flashListRef.current?.scrollToOffset({ offset: 0 });
|
||||
@@ -42,10 +48,20 @@ const MemeTagSearchModal = ({
|
||||
|
||||
const tags = useQuery<Tag>(
|
||||
Tag.schema.name,
|
||||
collection =>
|
||||
collection
|
||||
.filtered(`name CONTAINS[c] "${search}"`)
|
||||
.sorted(tagSortQuery(TAG_SORT.DATE_MODIFIED), true),
|
||||
collectionIn => {
|
||||
let collection = collectionIn;
|
||||
|
||||
if (search) {
|
||||
collection = collection.filtered('name CONTAINS[c] $0', search);
|
||||
}
|
||||
|
||||
collection = collection.sorted(
|
||||
tagSortQuery(TAG_SORT.DATE_MODIFIED),
|
||||
true,
|
||||
);
|
||||
|
||||
return collection;
|
||||
},
|
||||
[search],
|
||||
);
|
||||
|
||||
@@ -114,8 +130,10 @@ const MemeTagSearchModal = ({
|
||||
<Chip
|
||||
icon="plus"
|
||||
mode="outlined"
|
||||
onPress={() => handleCreateTag(search.replaceAll(/\s+/g, ''))}>
|
||||
Create Tag #{search.replaceAll(/\s+/g, '')}
|
||||
onPress={() =>
|
||||
handleCreateTag(tagName.valid ? tagName.parsed : 'newTag')
|
||||
}>
|
||||
Create Tag #{tagName.valid ? tagName.parsed : 'newTag'}
|
||||
</Chip>
|
||||
)}
|
||||
/>
|
||||
|
@@ -1,77 +1,52 @@
|
||||
import React from 'react';
|
||||
import React, { useRef } from 'react';
|
||||
import { HelperText, TextInput } from 'react-native-paper';
|
||||
import TagPreview from './tagPreview';
|
||||
import { generateRandomColor, isValidColor } from '../../utilities';
|
||||
import {
|
||||
StringValidationResult,
|
||||
generateRandomColor,
|
||||
validateColor,
|
||||
validateTagName,
|
||||
} from '../../utilities';
|
||||
|
||||
const TagEditor = ({
|
||||
tagName,
|
||||
setTagName,
|
||||
tagColor,
|
||||
setTagColor,
|
||||
validatedTagColor,
|
||||
setValidatedTagColor,
|
||||
tagNameError,
|
||||
setTagNameError,
|
||||
tagColorError,
|
||||
setTagColorError,
|
||||
}: {
|
||||
tagName: string;
|
||||
setTagName: (name: string) => void;
|
||||
tagColor: string;
|
||||
setTagColor: (color: string) => void;
|
||||
validatedTagColor: string;
|
||||
setValidatedTagColor: (color: string) => void;
|
||||
tagNameError: string | undefined;
|
||||
setTagNameError: (error: string | undefined) => void;
|
||||
tagColorError: string | undefined;
|
||||
setTagColorError: (error: string | undefined) => void;
|
||||
tagName: StringValidationResult;
|
||||
setTagName: (name: StringValidationResult) => void;
|
||||
tagColor: StringValidationResult;
|
||||
setTagColor: (color: StringValidationResult) => void;
|
||||
}) => {
|
||||
const handleTagNameChange = (name: string) => {
|
||||
setTagName(name);
|
||||
|
||||
if (name.length === 0) {
|
||||
setTagNameError('Tag name cannot be empty');
|
||||
} else if (name.includes(' ')) {
|
||||
setTagNameError('Tag name cannot contain spaces');
|
||||
} else {
|
||||
// eslint-disable-next-line unicorn/no-useless-undefined
|
||||
setTagNameError(undefined);
|
||||
}
|
||||
};
|
||||
const lastValidTagColor = useRef(tagColor.parsed);
|
||||
|
||||
const handleTagColorChange = (color: string) => {
|
||||
setTagColor(color);
|
||||
|
||||
if (isValidColor(color)) {
|
||||
setValidatedTagColor(color);
|
||||
// eslint-disable-next-line unicorn/no-useless-undefined
|
||||
setTagColorError(undefined);
|
||||
} else {
|
||||
setTagColorError('Color must be a valid hex or rgb value');
|
||||
}
|
||||
setTagColor(validateColor(color));
|
||||
if (tagColor.valid) lastValidTagColor.current = tagColor.parsed;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<TagPreview name={tagName} color={validatedTagColor} />
|
||||
<TagPreview name={tagName.parsed} color={lastValidTagColor.current} />
|
||||
<TextInput
|
||||
mode="outlined"
|
||||
label="Name"
|
||||
value={tagName}
|
||||
onChangeText={handleTagNameChange}
|
||||
error={!!tagNameError}
|
||||
value={tagName.raw}
|
||||
onChangeText={name => setTagName(validateTagName(name))}
|
||||
error={!tagName.valid}
|
||||
autoCapitalize="none"
|
||||
selectTextOnFocus
|
||||
/>
|
||||
<HelperText type="error" visible={!!tagNameError}>
|
||||
{tagNameError}
|
||||
<HelperText type="error" visible={!tagName.valid}>
|
||||
{tagName.error}
|
||||
</HelperText>
|
||||
<TextInput
|
||||
mode="outlined"
|
||||
label="Color"
|
||||
value={tagColor}
|
||||
value={tagColor.raw}
|
||||
onChangeText={handleTagColorChange}
|
||||
error={!!tagColorError}
|
||||
error={!tagColor.valid}
|
||||
autoCorrect={false}
|
||||
right={
|
||||
<TextInput.Icon
|
||||
@@ -80,8 +55,8 @@ const TagEditor = ({
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<HelperText type="error" visible={!!tagColorError}>
|
||||
{tagColorError}
|
||||
<HelperText type="error" visible={!tagColor.valid}>
|
||||
{tagColor.error}
|
||||
</HelperText>
|
||||
</>
|
||||
);
|
||||
|
@@ -13,7 +13,11 @@ import styles from '../styles';
|
||||
import { ROUTE, RootStackParamList } from '../types';
|
||||
import { MEME_TYPE, Meme, Tag } from '../database';
|
||||
import { RootState } from '../state';
|
||||
import { getMemeType } from '../utilities';
|
||||
import {
|
||||
getMemeType,
|
||||
validateMemeDescription,
|
||||
validateMemeTitle,
|
||||
} from '../utilities';
|
||||
import { MemeEditor } from '../components';
|
||||
|
||||
const AddMeme = ({
|
||||
@@ -34,13 +38,13 @@ const AddMeme = ({
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
uri.length > 1 ? MEME_TYPE.ALBUM : getMemeType(uri[0].type!);
|
||||
|
||||
const [memeTitle, setMemeTitle] = useState('New Meme');
|
||||
const [memeDescription, setMemeDescription] = useState('');
|
||||
const [memeTitle, setMemeTitle] = useState(validateMemeTitle('New Meme'));
|
||||
const [memeDescription, setMemeDescription] = useState(
|
||||
validateMemeDescription(''),
|
||||
);
|
||||
const [memeIsFavorite, setMemeIsFavorite] = useState(false);
|
||||
const [memeTags, setMemeTags] = useState(new Map<string, Tag>());
|
||||
|
||||
const [memeTitleError, setMemeTitleError] = useState<string | undefined>();
|
||||
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
|
||||
const handleSave = async () => {
|
||||
@@ -72,8 +76,8 @@ const AddMeme = ({
|
||||
uri: savedUri,
|
||||
size,
|
||||
hash,
|
||||
title: memeTitle,
|
||||
description: memeDescription,
|
||||
title: memeTitle.parsed,
|
||||
description: memeDescription.parsed,
|
||||
isFavorite: memeIsFavorite,
|
||||
tags: [...memeTags.values()],
|
||||
tagsLength: memeTags.size,
|
||||
@@ -119,8 +123,6 @@ const AddMeme = ({
|
||||
setMemeDescription={setMemeDescription}
|
||||
memeTags={memeTags}
|
||||
setMemeTags={setMemeTags}
|
||||
memeTitleError={memeTitleError}
|
||||
setMemeTitleError={setMemeTitleError}
|
||||
/>
|
||||
</View>
|
||||
<View style={[styles.flex, styles.justifyEnd]}>
|
||||
@@ -128,7 +130,7 @@ const AddMeme = ({
|
||||
mode="contained"
|
||||
icon="floppy"
|
||||
onPress={handleSave}
|
||||
disabled={!!memeTitleError || isSaving}
|
||||
disabled={!memeTitle.valid || !memeDescription.valid || isSaving}
|
||||
loading={isSaving}>
|
||||
Save
|
||||
</Button>
|
||||
|
@@ -4,7 +4,11 @@ import { Appbar, Button, useTheme } from 'react-native-paper';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { useRealm } from '@realm/react';
|
||||
import styles from '../styles';
|
||||
import { generateRandomColor } from '../utilities';
|
||||
import {
|
||||
generateRandomColor,
|
||||
validateColor,
|
||||
validateTagName,
|
||||
} from '../utilities';
|
||||
import { useDimensions } from '../contexts';
|
||||
import { Tag } from '../database';
|
||||
import { TagEditor } from '../components';
|
||||
@@ -15,18 +19,16 @@ const AddTag = () => {
|
||||
const { orientation } = useDimensions();
|
||||
const realm = useRealm();
|
||||
|
||||
const [tagName, setTagName] = useState('newTag');
|
||||
const [tagColor, setTagColor] = useState(generateRandomColor());
|
||||
const [validatedTagColor, setValidatedTagColor] = useState(tagColor);
|
||||
|
||||
const [tagNameError, setTagNameError] = useState<string | undefined>();
|
||||
const [tagColorError, setTagColorError] = useState<string | undefined>();
|
||||
const [tagName, setTagName] = useState(validateTagName('newTag'));
|
||||
const [tagColor, setTagColor] = useState(
|
||||
validateColor(generateRandomColor()),
|
||||
);
|
||||
|
||||
const handleSave = () => {
|
||||
realm.write(() => {
|
||||
realm.create(Tag.schema.name, {
|
||||
name: tagName,
|
||||
color: tagColor,
|
||||
name: tagName.parsed,
|
||||
color: tagColor.parsed,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -54,12 +56,6 @@ const AddTag = () => {
|
||||
setTagName={setTagName}
|
||||
tagColor={tagColor}
|
||||
setTagColor={setTagColor}
|
||||
validatedTagColor={validatedTagColor}
|
||||
setValidatedTagColor={setValidatedTagColor}
|
||||
tagNameError={tagNameError}
|
||||
setTagNameError={setTagNameError}
|
||||
tagColorError={tagColorError}
|
||||
setTagColorError={setTagColorError}
|
||||
/>
|
||||
</View>
|
||||
<View style={[styles.flex, styles.justifyEnd]}>
|
||||
@@ -67,7 +63,7 @@ const AddTag = () => {
|
||||
mode="contained"
|
||||
icon="floppy"
|
||||
onPress={handleSave}
|
||||
disabled={!!tagNameError || !!tagColorError}>
|
||||
disabled={!tagName.valid || !tagColor.valid}>
|
||||
Save
|
||||
</Button>
|
||||
</View>
|
||||
|
@@ -10,6 +10,7 @@ import styles from '../styles';
|
||||
import { useDimensions } from '../contexts';
|
||||
import { ROUTE, RootStackParamList } from '../types';
|
||||
import { Tag } from '../database';
|
||||
import { validateColor, validateTagName } from '../utilities';
|
||||
|
||||
const EditTag = ({
|
||||
route,
|
||||
@@ -25,17 +26,13 @@ const EditTag = ({
|
||||
BSON.UUID.createFromHexString(route.params.id),
|
||||
)!;
|
||||
|
||||
const [tagName, setTagName] = useState(tag.name);
|
||||
const [tagColor, setTagColor] = useState(tag.color);
|
||||
const [validatedTagColor, setValidatedTagColor] = useState(tagColor);
|
||||
|
||||
const [tagNameError, setTagNameError] = useState<string | undefined>();
|
||||
const [tagColorError, setTagColorError] = useState<string | undefined>();
|
||||
const [tagName, setTagName] = useState(validateTagName(tag.name));
|
||||
const [tagColor, setTagColor] = useState(validateColor(tag.color));
|
||||
|
||||
const handleSave = () => {
|
||||
realm.write(() => {
|
||||
tag.name = tagName;
|
||||
tag.color = tagColor;
|
||||
tag.name = tagName.parsed;
|
||||
tag.color = tagColor.parsed;
|
||||
tag.dateModified = new Date();
|
||||
});
|
||||
|
||||
@@ -80,12 +77,6 @@ const EditTag = ({
|
||||
setTagName={setTagName}
|
||||
tagColor={tagColor}
|
||||
setTagColor={setTagColor}
|
||||
validatedTagColor={validatedTagColor}
|
||||
setValidatedTagColor={setValidatedTagColor}
|
||||
tagNameError={tagNameError}
|
||||
setTagNameError={setTagNameError}
|
||||
tagColorError={tagColorError}
|
||||
setTagColorError={setTagColorError}
|
||||
/>
|
||||
</View>
|
||||
<View style={[styles.flex, styles.justifyEnd]}>
|
||||
@@ -93,7 +84,7 @@ const EditTag = ({
|
||||
mode="contained"
|
||||
icon="floppy"
|
||||
onPress={handleSave}
|
||||
disabled={!!tagNameError || !!tagColorError}>
|
||||
disabled={!tagName.valid || !tagColor.valid}>
|
||||
Save
|
||||
</Button>
|
||||
</View>
|
||||
|
@@ -12,7 +12,7 @@ import {
|
||||
} from 'react-native-paper';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import styles from '../styles';
|
||||
import { MEME_SORT, SORT_DIRECTION } from '../types';
|
||||
import { MEME_SORT, SORT_DIRECTION, memesSortQuery } from '../types';
|
||||
import { getSortIcon, getViewIcon } from '../utilities';
|
||||
import {
|
||||
RootState,
|
||||
@@ -72,7 +72,27 @@ const Memes = () => {
|
||||
};
|
||||
|
||||
const [search, setSearch] = useState('');
|
||||
const memes = useQuery<Meme>(Meme.schema.name);
|
||||
|
||||
const memes = useQuery<Meme>(
|
||||
Meme.schema.name,
|
||||
collectionIn => {
|
||||
let collection = collectionIn;
|
||||
|
||||
if (favoritesOnly) collection = collection.filtered('isFavorite == true');
|
||||
if (filter) collection = collection.filtered('type == $0', filter);
|
||||
if (search) {
|
||||
collection = collection.filtered('title CONTAINS[c] $0', search);
|
||||
}
|
||||
|
||||
collection = collection.sorted(
|
||||
memesSortQuery(sort),
|
||||
sortDirection === SORT_DIRECTION.DESCENDING,
|
||||
);
|
||||
|
||||
return collection;
|
||||
},
|
||||
[sort, sortDirection, favoritesOnly, filter, search],
|
||||
);
|
||||
|
||||
return (
|
||||
<View
|
||||
|
@@ -13,7 +13,11 @@ import { openDocumentTree } from 'react-native-scoped-storage';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import type {} from 'redux-thunk/extend-redux';
|
||||
import styles from '../styles';
|
||||
import { RootState, updateNoMedia, updateStorageUri } from '../state';
|
||||
import {
|
||||
RootState,
|
||||
setNoMedia,
|
||||
setStorageUri,
|
||||
} from '../state';
|
||||
import { useDimensions } from '../contexts';
|
||||
|
||||
const settingsScreenStyles = StyleSheet.create({
|
||||
@@ -68,7 +72,7 @@ const SettingsScreen = () => {
|
||||
}}
|
||||
onPress={async () => {
|
||||
const { uri } = await openDocumentTree(true);
|
||||
void dispatch(updateStorageUri(uri));
|
||||
void dispatch(setStorageUri(uri));
|
||||
}}>
|
||||
Change External Storage Path
|
||||
</Button>
|
||||
@@ -84,7 +88,7 @@ const SettingsScreen = () => {
|
||||
<Switch
|
||||
value={noMedia}
|
||||
onValueChange={value => {
|
||||
void dispatch(updateNoMedia(value));
|
||||
void dispatch(setNoMedia(value));
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
|
@@ -99,13 +99,20 @@ const Tags = () => {
|
||||
|
||||
const tags = useQuery<Tag>(
|
||||
Tag.schema.name,
|
||||
collection =>
|
||||
collection
|
||||
.filtered(`name CONTAINS[c] "${search}"`)
|
||||
.sorted(
|
||||
tagSortQuery(sort),
|
||||
sortDirection === SORT_DIRECTION.DESCENDING,
|
||||
),
|
||||
collectionIn => {
|
||||
let collection = collectionIn;
|
||||
|
||||
if (search) {
|
||||
collection = collection.filtered('name CONTAINS[c] $0', search);
|
||||
}
|
||||
|
||||
collection = collection.sorted(
|
||||
tagSortQuery(sort),
|
||||
sortDirection === SORT_DIRECTION.DESCENDING,
|
||||
);
|
||||
|
||||
return collection;
|
||||
},
|
||||
[search, sort, sortDirection],
|
||||
);
|
||||
|
||||
|
@@ -5,7 +5,7 @@ import { useDispatch } from 'react-redux';
|
||||
import { openDocumentTree } from 'react-native-scoped-storage';
|
||||
import styles from '../styles';
|
||||
import { noOp } from '../utilities';
|
||||
import { updateStorageUri } from '../state';
|
||||
import { setStorageUri } from '../state';
|
||||
import { useDimensions } from '../contexts';
|
||||
|
||||
const Welcome = ({ onWelcomeComplete }: { onWelcomeComplete: () => void }) => {
|
||||
@@ -16,7 +16,7 @@ const Welcome = ({ onWelcomeComplete }: { onWelcomeComplete: () => void }) => {
|
||||
const selectStorageLocation = async () => {
|
||||
const uri = await openDocumentTree(true).catch(noOp);
|
||||
if (!uri) return;
|
||||
await dispatch(updateStorageUri(uri.uri));
|
||||
await dispatch(setStorageUri(uri.uri));
|
||||
onWelcomeComplete();
|
||||
};
|
||||
|
||||
|
@@ -52,8 +52,8 @@ const persistor = persistStore(store);
|
||||
export { type RootState, store, persistor };
|
||||
export {
|
||||
type SettingsState,
|
||||
updateStorageUri,
|
||||
updateNoMedia,
|
||||
setStorageUri,
|
||||
setNoMedia,
|
||||
validateSettings,
|
||||
} from './settings';
|
||||
export {
|
||||
|
@@ -109,8 +109,8 @@ const validateSettings = createAsyncThunk(
|
||||
|
||||
export {
|
||||
type SettingsState,
|
||||
updateStorageUri,
|
||||
updateNoMedia,
|
||||
updateStorageUri as setStorageUri,
|
||||
updateNoMedia as setNoMedia,
|
||||
validateSettings,
|
||||
};
|
||||
export default settingsSlice.reducer;
|
||||
|
@@ -38,7 +38,7 @@ const styles = StyleSheet.create({
|
||||
centerText: {
|
||||
textAlign: 'center',
|
||||
},
|
||||
selfCenter: {
|
||||
centerSelf: {
|
||||
alignSelf: 'center',
|
||||
},
|
||||
flex: {
|
||||
|
@@ -23,10 +23,6 @@ const isRgbColor = (color: string) => {
|
||||
return /^rgb\((\d{1,3}), ?(\d{1,3}), ?(\d{1,3})\)$/i.test(color);
|
||||
};
|
||||
|
||||
const isValidColor = (color: string) => {
|
||||
return isHexColor(color) || isRgbColor(color);
|
||||
};
|
||||
|
||||
const rgbToHex = (rgb: string) => {
|
||||
const [r, g, b] = rgb
|
||||
.replaceAll(/[^\d,]/g, '')
|
||||
@@ -37,10 +33,22 @@ const rgbToHex = (rgb: string) => {
|
||||
};
|
||||
|
||||
const generateRandomColor = () => {
|
||||
const r = Math.floor(Math.random() * 256).toString(16).padStart(2, '0');
|
||||
const g = Math.floor(Math.random() * 256).toString(16).padStart(2, '0');
|
||||
const b = Math.floor(Math.random() * 256).toString(16).padStart(2, '0');
|
||||
const r = Math.floor(Math.random() * 256)
|
||||
.toString(16)
|
||||
.padStart(2, '0');
|
||||
const g = Math.floor(Math.random() * 256)
|
||||
.toString(16)
|
||||
.padStart(2, '0');
|
||||
const b = Math.floor(Math.random() * 256)
|
||||
.toString(16)
|
||||
.padStart(2, '0');
|
||||
return `#${r}${g}${b}`;
|
||||
};
|
||||
|
||||
export { getContrastColor, isHexColor, isRgbColor, isValidColor, rgbToHex, generateRandomColor };
|
||||
export {
|
||||
getContrastColor,
|
||||
isHexColor,
|
||||
isRgbColor,
|
||||
rgbToHex,
|
||||
generateRandomColor,
|
||||
};
|
||||
|
@@ -1,5 +1,5 @@
|
||||
const multipleIdQuery = (ids: string[]) => {
|
||||
return `id in {${ids.map(id => `uuid(${id})`).join(',')}}`;
|
||||
return `id IN {${ids.map(id => `uuid(${id})`).join(',')}}`;
|
||||
};
|
||||
|
||||
export { multipleIdQuery };
|
||||
|
@@ -2,7 +2,6 @@ export {
|
||||
getContrastColor,
|
||||
isHexColor,
|
||||
isRgbColor,
|
||||
isValidColor,
|
||||
rgbToHex,
|
||||
generateRandomColor,
|
||||
} from './color';
|
||||
@@ -16,3 +15,10 @@ export {
|
||||
} from './filesystem';
|
||||
export { isPermissionForPath, clearPermissions } from './permissions';
|
||||
export { getSortIcon, getViewIcon } from './icon';
|
||||
export {
|
||||
type StringValidationResult,
|
||||
validateMemeTitle,
|
||||
validateMemeDescription,
|
||||
validateTagName,
|
||||
validateColor,
|
||||
} from './validation';
|
||||
|
85
src/utilities/validation.ts
Normal file
85
src/utilities/validation.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import { isHexColor, isRgbColor } from './color';
|
||||
|
||||
interface StringValidationResult {
|
||||
valid: boolean;
|
||||
raw: string;
|
||||
parsed: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
const validateMemeTitle = (title: string): StringValidationResult => {
|
||||
const parsedTitle = title.trim();
|
||||
|
||||
if (parsedTitle.length === 0) {
|
||||
return {
|
||||
valid: false,
|
||||
raw: title,
|
||||
parsed: parsedTitle,
|
||||
error: 'Title cannot be empty',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
valid: true,
|
||||
raw: title,
|
||||
parsed: parsedTitle,
|
||||
};
|
||||
};
|
||||
|
||||
const validateMemeDescription = (
|
||||
description: string,
|
||||
): StringValidationResult => {
|
||||
const parsedDescription = description.trim();
|
||||
|
||||
return {
|
||||
valid: true,
|
||||
raw: description,
|
||||
parsed: parsedDescription,
|
||||
};
|
||||
};
|
||||
|
||||
const validateTagName = (name: string): StringValidationResult => {
|
||||
const parsedName = name.trim();
|
||||
|
||||
if (parsedName.length === 0) {
|
||||
return {
|
||||
valid: false,
|
||||
raw: name,
|
||||
parsed: parsedName,
|
||||
error: 'Name cannot be empty',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
valid: true,
|
||||
raw: name,
|
||||
parsed: parsedName,
|
||||
};
|
||||
};
|
||||
|
||||
const validateColor = (color: string): StringValidationResult => {
|
||||
const parsedColor = color.trim().toLowerCase();
|
||||
|
||||
if (!isHexColor(parsedColor) && !isRgbColor(parsedColor)) {
|
||||
return {
|
||||
valid: false,
|
||||
raw: color,
|
||||
parsed: parsedColor,
|
||||
error: 'Color must be a valid hex or rgb value',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
valid: true,
|
||||
raw: color,
|
||||
parsed: parsedColor,
|
||||
};
|
||||
};
|
||||
|
||||
export {
|
||||
type StringValidationResult,
|
||||
validateMemeTitle,
|
||||
validateMemeDescription,
|
||||
validateTagName,
|
||||
validateColor,
|
||||
};
|
Reference in New Issue
Block a user