Refactor validation

Signed-off-by: Nikolaos Karaolidis <nick@karaolidis.com>
This commit is contained in:
2023-07-21 16:34:44 +03:00
parent b7dd1c77af
commit 3c303e0304
17 changed files with 256 additions and 151 deletions

View File

@@ -5,6 +5,11 @@ import { useDimensions } from '../../contexts';
import LoadingView from '../loadingView'; import LoadingView from '../loadingView';
import { MemeTagSelector } from '.'; import { MemeTagSelector } from '.';
import { Tag } from '../../database'; import { Tag } from '../../database';
import {
StringValidationResult,
validateMemeDescription,
validateMemeTitle,
} from '../../utilities';
const MemeEditor = ({ const MemeEditor = ({
imageUri, imageUri,
@@ -14,18 +19,14 @@ const MemeEditor = ({
setMemeDescription, setMemeDescription,
memeTags, memeTags,
setMemeTags, setMemeTags,
memeTitleError,
setMemeTitleError,
}: { }: {
imageUri: string[]; imageUri: string[];
memeTitle: string; memeTitle: StringValidationResult;
setMemeTitle: (name: string) => void; setMemeTitle: (name: StringValidationResult) => void;
memeDescription: string; memeDescription: StringValidationResult;
setMemeDescription: (description: string) => void; setMemeDescription: (description: StringValidationResult) => void;
memeTags: Map<string, Tag>; memeTags: Map<string, Tag>;
setMemeTags: (tags: Map<string, Tag>) => void; setMemeTags: (tags: Map<string, Tag>) => void;
memeTitleError: string | undefined;
setMemeTitleError: (error: string | undefined) => void;
}) => { }) => {
const { dimensions, fixed, responsive } = useDimensions(); const { dimensions, fixed, responsive } = useDimensions();
@@ -40,16 +41,6 @@ const MemeEditor = ({
}); });
}, [dimensions.width, imageUri]); }, [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 />; if (!imageWidth || !imageHeight) return <LoadingView />;
return ( return (
@@ -57,13 +48,13 @@ const MemeEditor = ({
<TextInput <TextInput
mode="outlined" mode="outlined"
label="Title" label="Title"
value={memeTitle} value={memeTitle.raw}
onChangeText={handleMemeTitleChange} onChangeText={title => setMemeTitle(validateMemeTitle(title))}
error={!!memeTitleError} error={!memeTitle.valid}
selectTextOnFocus selectTextOnFocus
/> />
<HelperText type="error" visible={!!memeTitleError}> <HelperText type="error" visible={!memeTitle.valid}>
{memeTitleError} {memeTitle.error}
</HelperText> </HelperText>
<Image <Image
source={{ uri: imageUri[0] }} source={{ uri: imageUri[0] }}
@@ -89,8 +80,10 @@ const MemeEditor = ({
style={{ style={{
marginBottom: responsive.verticalScale(15), marginBottom: responsive.verticalScale(15),
}} }}
value={memeDescription} value={memeDescription.raw}
onChangeText={setMemeDescription} onChangeText={description =>
setMemeDescription(validateMemeDescription(description))
}
/> />
</> </>
); );

View File

@@ -1,4 +1,4 @@
import React, { useRef, useState } from 'react'; import React, { useEffect, useRef, useState } from 'react';
import { TagChip } from '../tags'; import { TagChip } from '../tags';
import { Tag } from '../../database'; import { Tag } from '../../database';
import { useQuery, useRealm } from '@realm/react'; import { useQuery, useRealm } from '@realm/react';
@@ -8,6 +8,7 @@ import { StyleSheet } from 'react-native';
import { useDimensions } from '../../contexts'; import { useDimensions } from '../../contexts';
import styles from '../../styles'; import styles from '../../styles';
import { FlashList } from '@shopify/flash-list'; import { FlashList } from '@shopify/flash-list';
import { validateTagName } from '../../utilities';
const memeTagSearchModalStyles = StyleSheet.create({ const memeTagSearchModalStyles = StyleSheet.create({
modal: { modal: {
@@ -34,6 +35,11 @@ const MemeTagSearchModal = ({
const flashListRef = useRef<FlashList<Tag>>(null); const flashListRef = useRef<FlashList<Tag>>(null);
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
const [tagName, setTagName] = useState(validateTagName(search));
useEffect(() => {
setTagName(validateTagName(search));
}, [search]);
const handleSearch = (newSearch: string) => { const handleSearch = (newSearch: string) => {
flashListRef.current?.scrollToOffset({ offset: 0 }); flashListRef.current?.scrollToOffset({ offset: 0 });
@@ -42,10 +48,20 @@ const MemeTagSearchModal = ({
const tags = useQuery<Tag>( const tags = useQuery<Tag>(
Tag.schema.name, Tag.schema.name,
collection => collectionIn => {
collection let collection = collectionIn;
.filtered(`name CONTAINS[c] "${search}"`)
.sorted(tagSortQuery(TAG_SORT.DATE_MODIFIED), true), if (search) {
collection = collection.filtered('name CONTAINS[c] $0', search);
}
collection = collection.sorted(
tagSortQuery(TAG_SORT.DATE_MODIFIED),
true,
);
return collection;
},
[search], [search],
); );
@@ -114,8 +130,10 @@ const MemeTagSearchModal = ({
<Chip <Chip
icon="plus" icon="plus"
mode="outlined" mode="outlined"
onPress={() => handleCreateTag(search.replaceAll(/\s+/g, ''))}> onPress={() =>
Create Tag #{search.replaceAll(/\s+/g, '')} handleCreateTag(tagName.valid ? tagName.parsed : 'newTag')
}>
Create Tag #{tagName.valid ? tagName.parsed : 'newTag'}
</Chip> </Chip>
)} )}
/> />

View File

@@ -1,77 +1,52 @@
import React from 'react'; import React, { useRef } from 'react';
import { HelperText, TextInput } from 'react-native-paper'; import { HelperText, TextInput } from 'react-native-paper';
import TagPreview from './tagPreview'; import TagPreview from './tagPreview';
import { generateRandomColor, isValidColor } from '../../utilities'; import {
StringValidationResult,
generateRandomColor,
validateColor,
validateTagName,
} from '../../utilities';
const TagEditor = ({ const TagEditor = ({
tagName, tagName,
setTagName, setTagName,
tagColor, tagColor,
setTagColor, setTagColor,
validatedTagColor,
setValidatedTagColor,
tagNameError,
setTagNameError,
tagColorError,
setTagColorError,
}: { }: {
tagName: string; tagName: StringValidationResult;
setTagName: (name: string) => void; setTagName: (name: StringValidationResult) => void;
tagColor: string; tagColor: StringValidationResult;
setTagColor: (color: string) => void; setTagColor: (color: StringValidationResult) => void;
validatedTagColor: string;
setValidatedTagColor: (color: string) => void;
tagNameError: string | undefined;
setTagNameError: (error: string | undefined) => void;
tagColorError: string | undefined;
setTagColorError: (error: string | undefined) => void;
}) => { }) => {
const handleTagNameChange = (name: string) => { const lastValidTagColor = useRef(tagColor.parsed);
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 handleTagColorChange = (color: string) => { const handleTagColorChange = (color: string) => {
setTagColor(color); setTagColor(validateColor(color));
if (tagColor.valid) lastValidTagColor.current = tagColor.parsed;
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');
}
}; };
return ( return (
<> <>
<TagPreview name={tagName} color={validatedTagColor} /> <TagPreview name={tagName.parsed} color={lastValidTagColor.current} />
<TextInput <TextInput
mode="outlined" mode="outlined"
label="Name" label="Name"
value={tagName} value={tagName.raw}
onChangeText={handleTagNameChange} onChangeText={name => setTagName(validateTagName(name))}
error={!!tagNameError} error={!tagName.valid}
autoCapitalize="none" autoCapitalize="none"
selectTextOnFocus selectTextOnFocus
/> />
<HelperText type="error" visible={!!tagNameError}> <HelperText type="error" visible={!tagName.valid}>
{tagNameError} {tagName.error}
</HelperText> </HelperText>
<TextInput <TextInput
mode="outlined" mode="outlined"
label="Color" label="Color"
value={tagColor} value={tagColor.raw}
onChangeText={handleTagColorChange} onChangeText={handleTagColorChange}
error={!!tagColorError} error={!tagColor.valid}
autoCorrect={false} autoCorrect={false}
right={ right={
<TextInput.Icon <TextInput.Icon
@@ -80,8 +55,8 @@ const TagEditor = ({
/> />
} }
/> />
<HelperText type="error" visible={!!tagColorError}> <HelperText type="error" visible={!tagColor.valid}>
{tagColorError} {tagColor.error}
</HelperText> </HelperText>
</> </>
); );

View File

@@ -13,7 +13,11 @@ import styles from '../styles';
import { ROUTE, RootStackParamList } from '../types'; import { ROUTE, RootStackParamList } from '../types';
import { MEME_TYPE, Meme, Tag } from '../database'; import { MEME_TYPE, Meme, Tag } from '../database';
import { RootState } from '../state'; import { RootState } from '../state';
import { getMemeType } from '../utilities'; import {
getMemeType,
validateMemeDescription,
validateMemeTitle,
} from '../utilities';
import { MemeEditor } from '../components'; import { MemeEditor } from '../components';
const AddMeme = ({ const AddMeme = ({
@@ -34,13 +38,13 @@ const AddMeme = ({
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
uri.length > 1 ? MEME_TYPE.ALBUM : getMemeType(uri[0].type!); uri.length > 1 ? MEME_TYPE.ALBUM : getMemeType(uri[0].type!);
const [memeTitle, setMemeTitle] = useState('New Meme'); const [memeTitle, setMemeTitle] = useState(validateMemeTitle('New Meme'));
const [memeDescription, setMemeDescription] = useState(''); const [memeDescription, setMemeDescription] = useState(
validateMemeDescription(''),
);
const [memeIsFavorite, setMemeIsFavorite] = useState(false); const [memeIsFavorite, setMemeIsFavorite] = useState(false);
const [memeTags, setMemeTags] = useState(new Map<string, Tag>()); const [memeTags, setMemeTags] = useState(new Map<string, Tag>());
const [memeTitleError, setMemeTitleError] = useState<string | undefined>();
const [isSaving, setIsSaving] = useState(false); const [isSaving, setIsSaving] = useState(false);
const handleSave = async () => { const handleSave = async () => {
@@ -72,8 +76,8 @@ const AddMeme = ({
uri: savedUri, uri: savedUri,
size, size,
hash, hash,
title: memeTitle, title: memeTitle.parsed,
description: memeDescription, description: memeDescription.parsed,
isFavorite: memeIsFavorite, isFavorite: memeIsFavorite,
tags: [...memeTags.values()], tags: [...memeTags.values()],
tagsLength: memeTags.size, tagsLength: memeTags.size,
@@ -119,8 +123,6 @@ const AddMeme = ({
setMemeDescription={setMemeDescription} setMemeDescription={setMemeDescription}
memeTags={memeTags} memeTags={memeTags}
setMemeTags={setMemeTags} setMemeTags={setMemeTags}
memeTitleError={memeTitleError}
setMemeTitleError={setMemeTitleError}
/> />
</View> </View>
<View style={[styles.flex, styles.justifyEnd]}> <View style={[styles.flex, styles.justifyEnd]}>
@@ -128,7 +130,7 @@ const AddMeme = ({
mode="contained" mode="contained"
icon="floppy" icon="floppy"
onPress={handleSave} onPress={handleSave}
disabled={!!memeTitleError || isSaving} disabled={!memeTitle.valid || !memeDescription.valid || isSaving}
loading={isSaving}> loading={isSaving}>
Save Save
</Button> </Button>

View File

@@ -4,7 +4,11 @@ import { Appbar, Button, useTheme } from 'react-native-paper';
import { useNavigation } from '@react-navigation/native'; import { useNavigation } from '@react-navigation/native';
import { useRealm } from '@realm/react'; import { useRealm } from '@realm/react';
import styles from '../styles'; import styles from '../styles';
import { generateRandomColor } from '../utilities'; import {
generateRandomColor,
validateColor,
validateTagName,
} from '../utilities';
import { useDimensions } from '../contexts'; import { useDimensions } from '../contexts';
import { Tag } from '../database'; import { Tag } from '../database';
import { TagEditor } from '../components'; import { TagEditor } from '../components';
@@ -15,18 +19,16 @@ const AddTag = () => {
const { orientation } = useDimensions(); const { orientation } = useDimensions();
const realm = useRealm(); const realm = useRealm();
const [tagName, setTagName] = useState('newTag'); const [tagName, setTagName] = useState(validateTagName('newTag'));
const [tagColor, setTagColor] = useState(generateRandomColor()); const [tagColor, setTagColor] = useState(
const [validatedTagColor, setValidatedTagColor] = useState(tagColor); validateColor(generateRandomColor()),
);
const [tagNameError, setTagNameError] = useState<string | undefined>();
const [tagColorError, setTagColorError] = useState<string | undefined>();
const handleSave = () => { const handleSave = () => {
realm.write(() => { realm.write(() => {
realm.create(Tag.schema.name, { realm.create(Tag.schema.name, {
name: tagName, name: tagName.parsed,
color: tagColor, color: tagColor.parsed,
}); });
}); });
@@ -54,12 +56,6 @@ const AddTag = () => {
setTagName={setTagName} setTagName={setTagName}
tagColor={tagColor} tagColor={tagColor}
setTagColor={setTagColor} setTagColor={setTagColor}
validatedTagColor={validatedTagColor}
setValidatedTagColor={setValidatedTagColor}
tagNameError={tagNameError}
setTagNameError={setTagNameError}
tagColorError={tagColorError}
setTagColorError={setTagColorError}
/> />
</View> </View>
<View style={[styles.flex, styles.justifyEnd]}> <View style={[styles.flex, styles.justifyEnd]}>
@@ -67,7 +63,7 @@ const AddTag = () => {
mode="contained" mode="contained"
icon="floppy" icon="floppy"
onPress={handleSave} onPress={handleSave}
disabled={!!tagNameError || !!tagColorError}> disabled={!tagName.valid || !tagColor.valid}>
Save Save
</Button> </Button>
</View> </View>

View File

@@ -10,6 +10,7 @@ import styles from '../styles';
import { useDimensions } from '../contexts'; import { useDimensions } from '../contexts';
import { ROUTE, RootStackParamList } from '../types'; import { ROUTE, RootStackParamList } from '../types';
import { Tag } from '../database'; import { Tag } from '../database';
import { validateColor, validateTagName } from '../utilities';
const EditTag = ({ const EditTag = ({
route, route,
@@ -25,17 +26,13 @@ const EditTag = ({
BSON.UUID.createFromHexString(route.params.id), BSON.UUID.createFromHexString(route.params.id),
)!; )!;
const [tagName, setTagName] = useState(tag.name); const [tagName, setTagName] = useState(validateTagName(tag.name));
const [tagColor, setTagColor] = useState(tag.color); const [tagColor, setTagColor] = useState(validateColor(tag.color));
const [validatedTagColor, setValidatedTagColor] = useState(tagColor);
const [tagNameError, setTagNameError] = useState<string | undefined>();
const [tagColorError, setTagColorError] = useState<string | undefined>();
const handleSave = () => { const handleSave = () => {
realm.write(() => { realm.write(() => {
tag.name = tagName; tag.name = tagName.parsed;
tag.color = tagColor; tag.color = tagColor.parsed;
tag.dateModified = new Date(); tag.dateModified = new Date();
}); });
@@ -80,12 +77,6 @@ const EditTag = ({
setTagName={setTagName} setTagName={setTagName}
tagColor={tagColor} tagColor={tagColor}
setTagColor={setTagColor} setTagColor={setTagColor}
validatedTagColor={validatedTagColor}
setValidatedTagColor={setValidatedTagColor}
tagNameError={tagNameError}
setTagNameError={setTagNameError}
tagColorError={tagColorError}
setTagColorError={setTagColorError}
/> />
</View> </View>
<View style={[styles.flex, styles.justifyEnd]}> <View style={[styles.flex, styles.justifyEnd]}>
@@ -93,7 +84,7 @@ const EditTag = ({
mode="contained" mode="contained"
icon="floppy" icon="floppy"
onPress={handleSave} onPress={handleSave}
disabled={!!tagNameError || !!tagColorError}> disabled={!tagName.valid || !tagColor.valid}>
Save Save
</Button> </Button>
</View> </View>

View File

@@ -12,7 +12,7 @@ import {
} from 'react-native-paper'; } from 'react-native-paper';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import styles from '../styles'; 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 { getSortIcon, getViewIcon } from '../utilities';
import { import {
RootState, RootState,
@@ -72,7 +72,27 @@ const Memes = () => {
}; };
const [search, setSearch] = useState(''); 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 ( return (
<View <View

View File

@@ -13,7 +13,11 @@ import { openDocumentTree } from 'react-native-scoped-storage';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import type {} from 'redux-thunk/extend-redux'; import type {} from 'redux-thunk/extend-redux';
import styles from '../styles'; import styles from '../styles';
import { RootState, updateNoMedia, updateStorageUri } from '../state'; import {
RootState,
setNoMedia,
setStorageUri,
} from '../state';
import { useDimensions } from '../contexts'; import { useDimensions } from '../contexts';
const settingsScreenStyles = StyleSheet.create({ const settingsScreenStyles = StyleSheet.create({
@@ -68,7 +72,7 @@ const SettingsScreen = () => {
}} }}
onPress={async () => { onPress={async () => {
const { uri } = await openDocumentTree(true); const { uri } = await openDocumentTree(true);
void dispatch(updateStorageUri(uri)); void dispatch(setStorageUri(uri));
}}> }}>
Change External Storage Path Change External Storage Path
</Button> </Button>
@@ -84,7 +88,7 @@ const SettingsScreen = () => {
<Switch <Switch
value={noMedia} value={noMedia}
onValueChange={value => { onValueChange={value => {
void dispatch(updateNoMedia(value)); void dispatch(setNoMedia(value));
}} }}
/> />
</View> </View>

View File

@@ -99,13 +99,20 @@ const Tags = () => {
const tags = useQuery<Tag>( const tags = useQuery<Tag>(
Tag.schema.name, Tag.schema.name,
collection => collectionIn => {
collection let collection = collectionIn;
.filtered(`name CONTAINS[c] "${search}"`)
.sorted( if (search) {
collection = collection.filtered('name CONTAINS[c] $0', search);
}
collection = collection.sorted(
tagSortQuery(sort), tagSortQuery(sort),
sortDirection === SORT_DIRECTION.DESCENDING, sortDirection === SORT_DIRECTION.DESCENDING,
), );
return collection;
},
[search, sort, sortDirection], [search, sort, sortDirection],
); );

View File

@@ -5,7 +5,7 @@ import { useDispatch } from 'react-redux';
import { openDocumentTree } from 'react-native-scoped-storage'; import { openDocumentTree } from 'react-native-scoped-storage';
import styles from '../styles'; import styles from '../styles';
import { noOp } from '../utilities'; import { noOp } from '../utilities';
import { updateStorageUri } from '../state'; import { setStorageUri } from '../state';
import { useDimensions } from '../contexts'; import { useDimensions } from '../contexts';
const Welcome = ({ onWelcomeComplete }: { onWelcomeComplete: () => void }) => { const Welcome = ({ onWelcomeComplete }: { onWelcomeComplete: () => void }) => {
@@ -16,7 +16,7 @@ const Welcome = ({ onWelcomeComplete }: { onWelcomeComplete: () => void }) => {
const selectStorageLocation = async () => { const selectStorageLocation = async () => {
const uri = await openDocumentTree(true).catch(noOp); const uri = await openDocumentTree(true).catch(noOp);
if (!uri) return; if (!uri) return;
await dispatch(updateStorageUri(uri.uri)); await dispatch(setStorageUri(uri.uri));
onWelcomeComplete(); onWelcomeComplete();
}; };

View File

@@ -52,8 +52,8 @@ const persistor = persistStore(store);
export { type RootState, store, persistor }; export { type RootState, store, persistor };
export { export {
type SettingsState, type SettingsState,
updateStorageUri, setStorageUri,
updateNoMedia, setNoMedia,
validateSettings, validateSettings,
} from './settings'; } from './settings';
export { export {

View File

@@ -109,8 +109,8 @@ const validateSettings = createAsyncThunk(
export { export {
type SettingsState, type SettingsState,
updateStorageUri, updateStorageUri as setStorageUri,
updateNoMedia, updateNoMedia as setNoMedia,
validateSettings, validateSettings,
}; };
export default settingsSlice.reducer; export default settingsSlice.reducer;

View File

@@ -38,7 +38,7 @@ const styles = StyleSheet.create({
centerText: { centerText: {
textAlign: 'center', textAlign: 'center',
}, },
selfCenter: { centerSelf: {
alignSelf: 'center', alignSelf: 'center',
}, },
flex: { flex: {

View File

@@ -23,10 +23,6 @@ const isRgbColor = (color: string) => {
return /^rgb\((\d{1,3}), ?(\d{1,3}), ?(\d{1,3})\)$/i.test(color); 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 rgbToHex = (rgb: string) => {
const [r, g, b] = rgb const [r, g, b] = rgb
.replaceAll(/[^\d,]/g, '') .replaceAll(/[^\d,]/g, '')
@@ -37,10 +33,22 @@ const rgbToHex = (rgb: string) => {
}; };
const generateRandomColor = () => { const generateRandomColor = () => {
const r = Math.floor(Math.random() * 256).toString(16).padStart(2, '0'); const r = Math.floor(Math.random() * 256)
const g = Math.floor(Math.random() * 256).toString(16).padStart(2, '0'); .toString(16)
const b = Math.floor(Math.random() * 256).toString(16).padStart(2, '0'); .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}`; return `#${r}${g}${b}`;
}; };
export { getContrastColor, isHexColor, isRgbColor, isValidColor, rgbToHex, generateRandomColor }; export {
getContrastColor,
isHexColor,
isRgbColor,
rgbToHex,
generateRandomColor,
};

View File

@@ -1,5 +1,5 @@
const multipleIdQuery = (ids: string[]) => { const multipleIdQuery = (ids: string[]) => {
return `id in {${ids.map(id => `uuid(${id})`).join(',')}}`; return `id IN {${ids.map(id => `uuid(${id})`).join(',')}}`;
}; };
export { multipleIdQuery }; export { multipleIdQuery };

View File

@@ -2,7 +2,6 @@ export {
getContrastColor, getContrastColor,
isHexColor, isHexColor,
isRgbColor, isRgbColor,
isValidColor,
rgbToHex, rgbToHex,
generateRandomColor, generateRandomColor,
} from './color'; } from './color';
@@ -16,3 +15,10 @@ export {
} from './filesystem'; } from './filesystem';
export { isPermissionForPath, clearPermissions } from './permissions'; export { isPermissionForPath, clearPermissions } from './permissions';
export { getSortIcon, getViewIcon } from './icon'; export { getSortIcon, getViewIcon } from './icon';
export {
type StringValidationResult,
validateMemeTitle,
validateMemeDescription,
validateTagName,
validateColor,
} from './validation';

View 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,
};