...
 
Commits (5)
body {
background: darkslategrey;
}
.App {
text-align: center;
text-align: center;
}
.App-logo {
animation: App-logo-spin infinite 20s linear;
height: 40vmin;
pointer-events: none;
animation: App-logo-spin infinite 20s linear;
height: 40vmin;
pointer-events: none;
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #61dafb;
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
import React from 'react';
import logo from './logo.svg';
import './App.css';
import SearchBar from './Search/searchBar';
import SearchBar from './Search/SearchBar';
// console.clear();
function App() {
return (
<div className='App'>
Welcome to RecomMovie v0.1!
<SearchBar query='Title' />
{/* <SearchBar query='Title' />
<SearchBar query='Year' />
<SearchBar query='Date' />
<SearchBar query='Date' /> */}
<SearchBar />
<hr />
</div>
);
......
input[type='number']::-webkit-inner-spin-button,
input[type='number']::-webkit-outer-spin-button {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
margin: 0;
}
input[type='number'] {
-moz-appearance: textfield;
}
import React from 'react';
import PropTypes from 'prop-types';
import './NumberInput.css';
import makeCharacterRegex from '../utilities/makeRegex';
import { isNonNegativeInteger } from '../utilities/IntegerChecker';
//TODO: Have an opinion about preceding 0 for decimal values?
//TODO: Remove inputRef (currently 2 line boilerplate)
//FYI: So, since the <input type="number"> only fires a change event when the value of the string in the field changes, so that "DD" and "DD." are the same, but "DD.0" is not (because float != integer || 12. != valid number). If you need to access the exact string, use an 'input' event, and get evt.target.
//README: Default widths for text and number inputs differ within the same browswer, and those defaults vary across browsers. For example, on my computer: {
/* firefox: {
text: 144.467,
number: 150.467,
border: 0.8,
padding: 1.6,//4.8px total
height: 20.8 //16 inner height
},
chrome: {
text: 160.8,
number: 58.4,
border: 2,
padding: 1 0,//4hor or 6vert px
height: 22 //16 inner height
}
}*/
// README: Behavior defaults to all integers. If you want to allow decimal places, or limit the numbers to positive values only, there are boolean prop flags for that.
// README: input.value = the string representation of the number (localization left up to the user), input.data-numeric-value = the string representation of the JS float (using periods instead of commas).
//Event order: Keydown, Input (value updated by now), Change, Keyup
class NumberInput extends React.Component {
constructor(props) {
super(props);
this.state = {
placeholder:
this.props.placeholder ||
(this.props.allowedDecimalPlaces
? 'Enter a number'
: 'Enter a whole number'),
dataNumericValue: 0,
hasFocus: false,
justGainedFocus: false
};
this.inputRef = React.createRef();
console.log(
'props.allowNegatives',
this.props.allowNegatives,
'props.maxIntegerPlaces',
props.maxIntegerPlaces
);
}
toggleType = evt => {
const gainedFocus = evt.type === 'focus';
console.log(
'on ' + evt.type + ' prior to change',
this.state.hasFocus,
typeof evt.type,
'===?',
// evt.type === 'focus'
gainedFocus
);
// this.setState(prevState => ({ hasFocus: !prevState.hasFocus }));
// this.inputRef.current.
this.setState(prevState => ({
hasFocus: gainedFocus,
justGainedFocus: gainedFocus && !prevState.justGainedFocus //need to check this b/c switching the input's type from text to numeric makes it lose the focus.
}));
};
componentDidUpdate = () => {
console.log(
'%cdid update. has focus?',
'background: darkpurple',
this.inputRef.current === document.activeElement,
document.hasFocus(),
'state.focus?',
this.state.hasFocus,
'justGainedFocus?',
this.state.justGainedFocus,
this.inputRef.current.type
);
if (this.state.justGainedFocus) this.inputRef.current.focus();
};
nextInputValue = (
changedText,
{ target: { value, selectionStart, selectionEnd } }
) =>
value.slice(0, Math.min(selectionStart, selectionEnd)) +
changedText +
value.slice(Math.max(selectionStart, selectionEnd));
//handleCut of leading zero
handlePaste = evt => {};
//TODO: Decide whether to delete this wrapper. (only needed if we're keeping track of lastValue)
handleChange = evt => {
const { value } = evt.target;
// this.setState((prevState, props) => ({
// dataNumericValue: parseFloat(value.replace(',', '.'))
// }));
console.log(
'Logged Output: NumberInput -> dataNumericValue',
this.state.dataNumericValue,
'parsed value:',
parseFloat(value.replace(',', '.'))
);
// evt.target.setAttribute('data-numeric-value', )
// setTimeout(() => {
this.props.onChange(evt);
// }, 0);
};
handleKeyDown = evt => {
//TODO: simplify these tests with regex on valueBuffer + key.
//TODO: replace evt.key with key
const { value, selectionStart, selectionEnd } = evt.target;
const { key } = evt;
const AllowableModifierKeyIsPressed =
evt.ctrlKey ||
evt.altKey ||
evt.metaKey ||
evt.getModifierState('OS');
const allowedPattern =
'^' +
(this.props.allowNegatives ? '-?' : '') +
'\\d' +
(this.props.requireLeadingZero ? '+' : '*') +
'[\\.,]?\\d*$';
const r = new RegExp(allowedPattern);
//testing against key length excludes modifier keys and Backspace/Del. The former set doesn't immediately change the value, and the latter produces a provably-valid value if there was originally a valid value (removing the negative, any digit, or the decimal place → another valid number—excepting, of course, certain flags being on.)
const newValue = this.nextInputValue(key.length === 1 ? key : '', evt);
const validKeyPress = r.test(newValue);
console.log(
'%cValue',
'background:darkred',
'requireLeadingZero',
this.props.requireLeadingZero,
value,
'selstart, selend',
selectionStart,
selectionEnd,
'selection:',
selectionEnd != selectionStart
? value.substring(selectionStart, selectionEnd)
: '',
'newValue',
newValue,
r,
'valid?',
validKeyPress
);
// evt.target.setAttribute('type', 'number');
console.log('In Key Down', 'key', key, 'target.value', value);
// if (/^[\.,]/.test(evt.target.newValue));
if (
evt.key.length === 1 &&
// /^[\.,]/.test(newValue) &&
!(validKeyPress || AllowableModifierKeyIsPressed)
) {
console.log('PREVENTING KEY');
evt.preventDefault();
} else {
this.setState((prevState, props) => ({
dataNumericValue: parseFloat(newValue.replace(',', '.'))
}));
}
};
render() {
console.log(
'Rendering. Has Focus?',
this.state.hasFocus,
'type:',
this.inputRef.current
? this.inputRef.current.type
: 'inputRef not yet initialized.'
);
// setTimeout(() => {
// console.log(
// 'Timeout cb: type:',
// this.inputRef.current
// ? this.inputRef.current.type
// : 'inputRef not yet initialized.'
// );
// }, 100);
return (
<input
ref={this.inputRef}
// type='text'
type={this.state.hasFocus ? 'text' : 'number'}
value={this.props.value}
placeholder={this.state.placeholder}
data-numeric-value={this.state.dataNumericValue}
id={this.props.id}
min={this.props.min}
max={this.props.max}
onKeyDown={this.handleKeyDown}
onChange={this.handleChange}
onInput={this.props.onInput}
onFocus={this.toggleType}
onBlur={this.toggleType}
onSubmit={this.handleSubmit}
inputMode={
this.props.allowedDecimalPlaces ? 'decimal' : 'numeric'
}
/>
);
}
}
function nonNegativeIntegerPropTypeFactory(isRequired) {
return function(props, propName, componentName) {
if (props[propName] === undefined) {
if (isRequired) {
return new Error(
`The prop \`${propName}\` is marked as required in \`${componentName}\`, but its value is \`${
props[propName]
}\`.`
);
}
} else if (!isNonNegativeInteger(props[propName])) {
return new Error(
`Invalid value for prop \`${propName}\` (${
props[propName]
}) supplied to \`${componentName}\`. Please supply a non-negative integer.`
);
}
};
}
//TODO: Custom validators article/package for numbers.
//TODO: Create Number
const nonNegativeIntegerPropType = nonNegativeIntegerPropTypeFactory(false);
nonNegativeIntegerPropType.isRequired = nonNegativeIntegerPropTypeFactory(true);
const minPropValidator = (props, propName, componentName) => {
if (!props.allowNegatives && props.min < 0) {
return new Error(
`Value of props.min is negative while props.allowNegatives is false in ${componentName}.`
);
} else if (props.min > props.max) {
return new Error(
`Value of props.min is greater than props.max in ${componentName}.`
);
}
};
//TODO: Issue a warning if the min/max values don't include negative numbers and yet the range implied by them does.
console.log('PropTypes.string', PropTypes.string);
//TODO: Look at errors for failed PropTypes
NumberInput.propTypes = {
value: PropTypes.string,
placeholder: PropTypes.string,
id: PropTypes.string,
min: minPropValidator,
max: PropTypes.number,
// max: PropTypes.string,
onChange: PropTypes.func.isRequired,
onInput: PropTypes.func.isRequired,
allowNegatives: PropTypes.bool,
maxIntegerPlaces: nonNegativeIntegerPropType, //minimum is about form validation, not input-restriction.
maxDecimalPlaces: nonNegativeIntegerPropType,
requireLeadingZeroForDecimals: PropTypes.bool //only makes a difference if decimals are allowed.
};
NumberInput.defaultProps = {
allowNegatives: true,
maxDecimalPlaces: 0
};
export default NumberInput;
import React from 'react';
import PropTypes from 'prop-types';
class Input extends React.Component {
constructor(props) {
super(props);
this.state = {
// value: props.children
value: ''
};
this.handleChange = this.handleChange.bind(this);
}
handleChange(evt) {
// console.log(evt.target.value);
this.setState({ value: evt.target.value });
}
render() {
return (
<input
placeholder={this.props.placeholder}
value={this.state.value}
onChange={this.handleChange}
/>
);
}
}
Input.propTypes = {
placeholder: PropTypes.string
};
Input.defaultProps = {
placeholder: 'enter text here'
};
export default Input;
import React from 'react';
import PropTypes from 'prop-types';
import Input from './input';
import NumberInput from './NumberInput';
import makeCharacterRegex from '../utilities/makeRegex';
// import makeCharacterRegex from '../utilities/makeRegex';
//TODO: One single search bar, comprising the three query parameters: inputs for title & year (validated input/dropdown?) & dropdown for type
const QUERY_TYPES = {
......@@ -9,34 +11,110 @@ const QUERY_TYPES = {
type: 'type'
};
const isValidQueryType = query =>
QUERY_TYPES.hasOwnProperty(query.toLowerCase());
function SearchBar(props) {
return (
<div>
<Input placeholder={`Enter ${props.query}`} />
<button
// disabled={!isValidQueryType(props.query).toString()}
disabled={!isValidQueryType(props.query)}
>{`Search by ${props.query}`}</button>
</div>
);
}
const isValidYear = yearString => {
const result = yearString.match(/[0-9]{4}/)[0];
console.log('Logged Output: result', result);
};
const MAX_YEAR = new Date().getFullYear() + 10;
console.log('Logged Output: MAX_YEAR', MAX_YEAR);
// const isValidQuery = () => this.state.title || this.state.year || this.state.type.
//Todone: custom validator, based on query possibilities?
SearchBar.propTypes = {
query: props => {
if (!isValidQueryType(props['query'])) {
return new Error(
'Invalid value for prop `query` (' +
props['query'] +
') supplied to `SearchBar`. Please choose from among: [' +
Object.keys(QUERY_TYPES).join(', ') +
']'
);
}
class SearchBar extends React.Component {
constructor(props) {
super(props);
this.state = {
title: '',
year: '',
type: undefined,
yearFloat: 0
};
}
};
handleTitleChange = evt => {
console.log(evt.target.value);
this.setState({ title: evt.target.value });
};
handleYearChange = evt => {
const numValue = parseFloat(evt.target.value);
console.log(
'%cLogged Output: in change, newValue',
'background: teal',
numValue,
'target',
evt.target,
'data-value-buffer',
evt.target.getAttribute('data-numeric-value')
);
// console.trace();
const match = numValue.toString().match(/\d{0,4}/)[0];
console.log('Logged Output: in change, match', match);
// if (match) {
this.setState({
year: evt.target.value,
yearFloat: parseFloat(evt.target['data-numeric-value'])
});
// }
};
render() {
return (
<form>
<input
placeholder='Enter Title'
value={this.state.title}
onChange={this.handleTitleChange}
/>
<br />
{/* <input
value={this.state.year}
type='number'
min='1901'
max={MAX_YEAR}
onChange={this.handleYearChange}
/> */}
<br />
<NumberInput
placeholder='Enter Year'
value={this.state.year}
maxDecimalPlaces={4}
requireLeadingZero={true}
min={1901}
max={MAX_YEAR}
// onKeyDown={this.handleYearKeyDown}
onChange={this.handleYearChange}
/>
{/* <Input placeholder={`Enter Title`} />
<Input placeholder={`Enter Year`} />
<Input placeholder={`Enter Type`} /> */}
{/* <button
// disabled={!isValidQueryType(props.query).toString()}
disabled={!isValidQueryType(props.query)}
>{`Search by ${props.query}`}</button> */}
<br />
<input type='submit' />
</form>
);
}
}
// //Todone: custom validator, based on query possibilities?
// SearchBar.propTypes = {
// query: props => {
// if (!isValidQueryType(props['query'])) {
// return new Error(
// 'Invalid value for prop `query` (' +
// props['query'] +
// ') supplied to `SearchBar`. Please choose from among: [' +
// Object.keys(QUERY_TYPES).join(', ') +
// ']'
// );
// }
// }
// };
export default SearchBar;
//TODO: Run some testing to see whether doing the comparison then the function call would be faster.
export const isInteger = variableName =>
typeof variableName === 'number' && parseInt(variableName) === variableName;
export const isPositiveInteger = variableName =>
isInteger(variableName) && variableName > 0;
export const isNegativeInteger = variableName =>
isInteger(variableName) && variableName < 0;
export const isNonNegativeInteger = variableName =>
isInteger(variableName) && variableName >= 0;
export const isNonPositiveInteger = variableName =>
isInteger(variableName) && variableName <= 0;
const IntegerChecker = {
isInteger,
isPositiveInteger,
isNegativeInteger,
isNonNegativeInteger,
isNonPositiveInteger
};
function testIntegerChecker() {
const testNumbers = [-2.3, -2, 0, 3, 4.6];
const tester = {};
for (const func in IntegerChecker) {
tester[func] = testNumbers.reduce((funcResults, num) => {
funcResults[num] = IntegerChecker[func](num);
return funcResults;
}, {});
}
console.table(tester);
}
// testIntegerChecker();
export default IntegerChecker;
//TODO: FOR README: This package can make some highly-inefficient regexes.
const OPTION_TO_PATTERN = {
word: '\\w',
letter: 'A-Za-z',
digit: '\\d',
whitespace: '\\s',
special: '`~!@#$%^&*()\\-_=+{}[\\];:\'",.<>/?\\\\|',
bracket: '(){}[\\]<>',
punctuation: '!&()\\-_[\\];:\'",.<>/?\\\\|',
quote: '`\'"'
};
function makeCharacterRegex(optionString, minRepeats, maxRepeats) {
const options = optionString
.split(' ')
.reduce((redundantOptions, currentOptionToken) => {
if (OPTION_TO_PATTERN.hasOwnProperty(currentOptionToken)) {
redundantOptions[currentOptionToken] = true;
} else {
console.log(
`Error: in makeCharacterRegex(), option argument "${currentOptionToken}" is not recognized. Skipping it...`
);
}
return redundantOptions;
}, {});
let possiblyRedundantPattern = Object.keys(options).reduce(
(pattern, option) => pattern + (OPTION_TO_PATTERN[option] || ''),
''
);
let patternAtoms = {};
for (let i = 0; i < possiblyRedundantPattern.length; i++) {
let character = possiblyRedundantPattern[i];
if (possiblyRedundantPattern[i] === '\\') {
//deal with escaped characters
character += possiblyRedundantPattern[++i];
} else if (possiblyRedundantPattern[i + 1] === '-') {
//deal with character ranges
character += possiblyRedundantPattern.slice(i + 1, i + 3);
i += 2;
}
patternAtoms[character] = true;
}
// console.log('min max', minRepeats, maxRepeats);
let repeatString = '';
if (parseInt(minRepeats) === minRepeats) {
let maxRepeatsErrorCondition = '';
if (maxRepeats === undefined) {
repeatString = `{${minRepeats}}`;
} else if (parseInt(maxRepeats) === maxRepeats) {
if (maxRepeats >= minRepeats) {
if (minRepeats === 0 && maxRepeats === 1) repeatString = '?';
else repeatString = `{${minRepeats},${maxRepeats}}`;
} else {
maxRepeatsErrorCondition = `smaller than minRepeats argument "${minRepeats}"`;
}
} else if (maxRepeats === Infinity) {
if (minRepeats === 0) {
repeatString = `*`;
} else if (minRepeats === 1) {
repeatString = `+`;
} else {
repeatString = `{${minRepeats},}`;
}
} else {
maxRepeatsErrorCondition = 'not an integer';
}
if (maxRepeatsErrorCondition) {
console.log(
`Error: in makeCharacterRegex(), maxRepeats argument "${maxRepeats}" is ${maxRepeatsErrorCondition}. Returning character pattern with exact quantification of: ${minRepeats}...`
);
repeatString = `{${minRepeats}}`;
}
} else if (!(minRepeats === undefined && maxRepeats === undefined)) {
console.log(
`Error: in makeCharacterRegex(), minRepeats "${minRepeats}" is not a valid integer argument. Returning character pattern without quantifiers...`
);
}
return new RegExp(`[${Object.keys(patternAtoms).join('')}]${repeatString}`);
// return `/[${Object.keys(patternAtoms).join('')}]${repeatString}/`;
}
function testMakeCharacterRegex() {
let minMax = [
[0],
[1],
[0, Infinity],
[1, Infinity],
[3, Infinity],
[0, 1],
[3, 4],
[4, 3],
[3, 'four'],
['3', 4],
[Infinity]
];
for (let i = 0; i < minMax.length; i++) {
console.log('[min, max] repeats:', minMax[i]);
let testCaseRegex = makeCharacterRegex(
'word whitespace digit quote punctuation bracket',
minMax[i][0],
minMax[i][1]
);
console.log(testCaseRegex);
}
}
// testMakeCharacterRegex();
export default makeCharacterRegex;