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 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))
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@@ -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>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
@@ -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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@@ -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>
|
||||||
|
@@ -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>
|
||||||
|
@@ -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>
|
||||||
|
@@ -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
|
||||||
|
@@ -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>
|
||||||
|
@@ -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) {
|
||||||
tagSortQuery(sort),
|
collection = collection.filtered('name CONTAINS[c] $0', search);
|
||||||
sortDirection === SORT_DIRECTION.DESCENDING,
|
}
|
||||||
),
|
|
||||||
|
collection = collection.sorted(
|
||||||
|
tagSortQuery(sort),
|
||||||
|
sortDirection === SORT_DIRECTION.DESCENDING,
|
||||||
|
);
|
||||||
|
|
||||||
|
return collection;
|
||||||
|
},
|
||||||
[search, sort, sortDirection],
|
[search, sort, sortDirection],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@@ -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();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -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 {
|
||||||
|
@@ -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;
|
||||||
|
@@ -38,7 +38,7 @@ const styles = StyleSheet.create({
|
|||||||
centerText: {
|
centerText: {
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
},
|
},
|
||||||
selfCenter: {
|
centerSelf: {
|
||||||
alignSelf: 'center',
|
alignSelf: 'center',
|
||||||
},
|
},
|
||||||
flex: {
|
flex: {
|
||||||
|
@@ -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,
|
||||||
|
};
|
||||||
|
@@ -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 };
|
||||||
|
@@ -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';
|
||||||
|
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