Merge branch 'load_more'

This commit is contained in:
2021-12-08 22:09:06 +00:00
17 changed files with 461 additions and 138 deletions

View File

@@ -266,11 +266,16 @@ export function AccountForm(props) {
<Form.Field>
<label>Participate in "Last Scanned" member list?</label>
<Checkbox
<Form.Checkbox
label='Yes, show me'
name='allow_last_scanned'
onChange={handleCheck}
checked={input.allow_last_scanned}
error={error.allow_last_scanned ?
{ content: error.allow_last_scanned, pointing: 'left' }
:
false
}
/>
</Form.Field>

View File

@@ -16,6 +16,7 @@ import { Training } from './Training.js';
import { AdminTransactions } from './AdminTransactions.js';
import { Admin } from './Admin.js';
import { Paste } from './Paste.js';
import { Sign } from './Sign.js';
import { Courses, CourseDetail } from './Courses.js';
import { Classes, ClassDetail } from './Classes.js';
import { Members, MemberDetail } from './Members.js';
@@ -167,9 +168,9 @@ function App() {
to='/classes'
/>
<Dropdown.Item
content='Transporter'
content='Utilities'
as={Link}
to='/paste'
to='/utils'
/>
<Dropdown.Item
content='Charts'
@@ -215,10 +216,14 @@ function App() {
<PasswordReset />
</Route>
<Route path='/paste'>
<Route path='/utils'>
<Paste token={token} />
</Route>
<Route path='/sign'>
<Sign token={token} />
</Route>
<Route path='/charts'>
<Charts />
</Route>

View File

@@ -7,6 +7,7 @@ import { Container, Divider, Dropdown, Form, Grid, Header, Icon, Image, Menu, Me
import { statusColor, BasicTable, siteUrl, staticUrl, requester, isAdmin } from './utils.js';
import { LoginForm, SignupForm } from './LoginSignup.js';
import { AccountForm } from './Account.js';
import { SignForm } from './Sign.js';
import { PayPalSubscribeDeal } from './PayPal.js';
function MemberInfo(props) {
@@ -202,9 +203,6 @@ export function Home(props) {
</Grid.Column>
<Grid.Column>
<Segment>
<Header size='medium'>Home</Header>
<p>Welcome to the Protospace member portal! Here you can view member info, join classes, and manage your membership.</p>
<Header size='medium'>Quick Links</Header>
<p><a href='http://protospace.ca/' target='_blank' rel='noopener noreferrer'>Main Website</a></p>
<p><a href='http://wiki.protospace.ca/Welcome_to_Protospace' target='_blank' rel='noopener noreferrer'>Protospace Wiki</a> <Link to='/auth/wiki'>[register]</Link></p>
@@ -279,10 +277,26 @@ export function Home(props) {
} trigger={<a>[more]</a>} />
</p>
<p>
Precix availability: {getTrackStat('CNC-PRECIX')} <Popup content={
<React.Fragment>
<p>
Last use:<br />
{getTrackLast('CNC-PRECIX')}<br />
{getTrackAgo('CNC-PRECIX')}<br />
by {getTrackName('CNC-PRECIX')}
</p>
</React.Fragment>
} trigger={<a>[more]</a>} />
</p>
{user && <p>Alarm status: {alarmStat()}{doorOpenStat()}</p>}
</div>
<SignForm token={token} />
</Segment>
</Grid.Column>
</Grid>
</Container>

View File

@@ -77,12 +77,19 @@ export function SignupForm(props) {
const handleValues = (e, v) => setInput({ ...input, [v.name]: v.value });
const handleChange = (e) => handleValues(e, e.currentTarget);
const genUsername = () => (
input.first_name && input.last_name ?
(input.first_name + '.' + input.last_name).toLowerCase().replace(/ /g, '.')
:
''
);
const genUsername = () => {
if (input.first_name && input.last_name) {
let first_name = input.first_name.trim().toLowerCase();
let last_name = input.last_name.trim().toLowerCase();
first_name = first_name.replace(/[^a-z- ]+/g, '');
last_name = last_name.replace(/[^a-z- ]+/g, '');
first_name = first_name.replace(/[ -]/g, '.');
last_name = last_name.replace(/[ -]/g, '.');
return first_name + '.' + last_name;
} else {
return '';
}
};
const handleSubmit = (e) => {
if (loading) return;

View File

@@ -16,11 +16,12 @@ const memberSorts = {
//newest_overall: 'Newest Overall',
oldest_active: 'Oldest',
//oldest_overall: 'Oldest Overall',
recently_inactive: 'Inactive',
recently_inactive: 'Recently Inactive',
is_director: 'Directors',
is_instructor: 'Instructors',
due: 'Due',
overdue: 'Overdue',
everyone: 'Everyone',
};
export function MembersDropdown(props) {
@@ -65,28 +66,94 @@ export function MembersDropdown(props) {
);
};
let numShowCache = 20;
let responseCache = false;
let pageCache = 0;
let sortCache = '';
let searchCache = '';
const loadMoreStrings = [
'Load More',
'Load EVEN More',
'Load WAY More',
'Why did you stop? LOAD MORE!',
'GIVE ME MORE NAMES!!',
'Shower me with names, baby',
'I don\'t care about the poor server, MORE NAMES!',
'Names make me hotter than two rats in a wool sock',
'Holy shit, I can\'t get enough names',
'I don\'t have anything better to do than LOAD NAMES!',
'I need names because I love N̶a̸M̸E̵S̴ it\'s not to late to stop but I can\'t because it feels so good god help me',
'The One who loads the names will liquify the NERVES of the sentient whilst I o̴̭̐b̴̙̾s̷̺͝ē̶̟r̷̦̓v̸͚̐ę̸̈́ ̷̞̒t̸͘ͅh̴͂͜e̵̜̕i̶̾͜r̷̃͜ ̵̹͊Ḷ̷͝Ȍ̸͚Ä̶̘́D̴̰́I̸̧̚N̵͖̎G̷̣͒',
'The Song of Names will will e̶̟̤͋x̷̜̀͘͜t̴̳̀i̸̪͑̇n̷̘̍g̵̥̗̓ṳ̴̑̈́i̷͚̿s̸̨̪̓ḣ̶̡̓ ̷̲͊ṫ̴̫h̸̙͕͗ḛ̸̡̃̈́ ̷̘̫̉̏v̸̧̟͗̕o̴͕̾͜i̷̢͛̿ͅc̴͕̥̈́̂ȅ̵͕s̶̹͋̀ ̶̰́͜͠ǒ̷̰̯f̵̛̥̊ ̸̟̟̒͝m̸̯̀̂o̶̝͛̌͜r̸̞̀ṫ̴̥͗ä̶̢́l̶̯̄͘ ̵̫̈́m̷̦̑̂ą̶͕͝ṋ̴̎͝ from the sphere I can see it can you see it it is beautiful',
'The final suffering of T̷̯̂͝H̴̰̏̉Ḛ̸̀̓ ̷̟̒ͅN̷̠̾Ą̵̟̈́M̶̡̾͝E̸̥̟̐͐S̸̖̍ are lies all is lost the pony he come h̷̲̺͂̾͒̔͝ḙ̶̻͒͠ ̷̙̘͈̬̰̽̽̈́̒͘c̵͎̺̞̰͝ơ̷͚̱̺̰̺͐̏͑͠m̴̖̰̓̈͝ĕ̷̜s̶̛̹̤̦͉̓͝ the í̵̠̞̙̦̱̠̅̊͒̌͊̓͠͠c̴̻̺̙͕̲͚͔̩̥͑ḩ̷̦̰̠̯̳̖̘́̉̾̾͠o̴͈̯̟̣̲͙̦̖̖͍̞̞̻̎͐̊͊̇͋̒͛̅͆̌͂̈̕r̷̡̝̲̜͇͉̣̹̖͕̻̐̑̉̋͋̉͒͋̍́̒͐͐͘ͅ ̵̳̖͕̩̝̮͈̻̣̤͎̟͓̜̄̿̓̈́p̴̰̝͓̣͍̫̞͓̑͌͊͑̓̂̽͑͝e̶̛̪̜̐̋́̆͊͌̋̄́͘r̶̫̬͈͌̔̽m̶̛̱̣͍͌̈́͋̾̈̀͑̽̋̏̊͋͝ę̶̋̀̈̃͠ą̵̡̣̫̮͙͈͚̞̰̠̥͇̣̽̿̉́̔̒͌̓͌̂̌̕͜͠t̷̯͚̭̮̠̐͋͆́͛̿́̏̆̚ě̶̢̨̩̞ş̸̢͍̱̻͕̪̗̻͖͇̱̳̽̈́̚͠ ̴͉̝̖̤͚̖̩̻̪̒ͅà̸̙̥̩̠̝̪̰͋́̊̓͌́͒̕͝ĺ̵̖̖͚̱͎̤̟̲̺͎͑͋̐̈́̓͂͆̅̈́̎̆̋̇l̸̢̧̟͉̞͇̱͉̙͇͊̏͐͠ͅ',
];
export function Members(props) {
const history = useHistory();
const qs = useLocation().search;
const params = new URLSearchParams(qs);
const sort = params.get('sort') || 'recently_vetted';
const search = params.get('q') || '';
const [response, setResponse] = useState(false);
const [numShow, setNumShow] = useState(numShowCache);
const [response, setResponse] = useState(responseCache);
const [loading, setLoading] = useState(false);
const [page, setPage] = useState(pageCache);
const [sort, setSort] = useState(sortCache);
const [search, setSearch] = useState(searchCache);
const [controller, setController] = useState(false);
const { token, user } = props;
const doSearch = (q) => {
console.log('doing search', q);
if (q.length) {
const qs = queryString.stringify({ 'q': q });
history.replace('/members?' + qs);
const makeRequest = ({loadPage, q, sort_key}) => {
let pageNum = 0;
if (loadPage) {
pageNum = page + 1;
setPage(pageNum);
pageCache = pageNum;
} else {
setResponse(false);
history.replace('/members');
setPage(0);
pageCache = 0;
}
if (controller) {
controller.abort();
}
const ctl = new AbortController();
setController(ctl);
const signal = ctl.signal;
const data = {page: pageNum};
if (q) data.q = q;
if (sort_key) data.sort = sort_key;
requester('/search/', 'POST', token, data, signal)
.then(res => {
const r = loadPage ? {...response, results: [...response.results, ...res.results]} : res;
setResponse(r);
responseCache = r;
setLoading(false);
})
.catch(err => {
console.log('Aborted.');
});
}
const loadMore = () => {
setLoading(true);
makeRequest({loadPage: true, q: search, sort_key: sort});
};
const doSort = (sort_key) => {
setSort(sort_key);
sortCache = sort_key;
setSearch('');
searchCache = '';
makeRequest({loadPage: false, sort_key: sort_key});
};
const doSearch = (q) => {
if (q) {
setSearch(q);
searchCache = q;
setSort('');
sortCache = '';
makeRequest({loadPage: false, q: q});
} else {
doSort('recently_vetted');
}
};
@@ -96,26 +163,10 @@ export function Members(props) {
};
useEffect(() => {
if (controller) {
controller.abort();
if (!responseCache) {
doSort('recently_vetted');
}
const ctl = new AbortController();
setController(ctl);
const signal = ctl.signal;
const data = {q: search, sort: sort};
requester('/search/', 'POST', token, data, signal)
.then(res => {
setResponse(res);
})
.catch(err => {
;
});
}, [search, sort]);
useEffect(() => {
setResponse(false);
}, [sort]);
}, []);
return (
<Container>
@@ -144,7 +195,7 @@ export function Members(props) {
Sort by{' '}
{Object.entries(memberSorts).map((x, i) =>
<>
<Link to={'/members?sort='+x[0]} replace>{x[1]}</Link>
<a href='javascript:void(0)' onClick={() => doSort(x[0])}>{x[1]}</a>
{i < Object.keys(memberSorts).length - 1 && ', '}
</>
)}.
@@ -164,9 +215,11 @@ export function Members(props) {
{response ?
<>
<p>{response.total} results:</p>
<Item.Group unstackable divided>
{response.results.length ?
response.results.slice(0, numShow).map((x, i) =>
{!!response.results.length &&
response.results.map((x, i) =>
<Item key={x.member.id} as={Link} to={'/members/'+x.member.id}>
<div className='list-num'>{i+1}</div>
<Item.Image size='tiny' src={x.member.photo_small ? staticUrl + '/' + x.member.photo_small : '/nophoto.png'} />
@@ -181,16 +234,11 @@ export function Members(props) {
</Item.Content>
</Item>
)
:
<p>No Results</p>
}
</Item.Group>
{response.results.length > 20 && numShow !== 100 ?
<Button
content='Load More'
onClick={() => {setNumShow(100); numShowCache = 100;}}
/> : ''
{!search && response.total !== response.results.length &&
<Button content={loading ? 'Reticulating splines...' : loadMoreStrings[page]} onClick={loadMore} disabled={loading} />
}
</>
:

View File

@@ -41,7 +41,7 @@ function PasteForm(props) {
<Form onSubmit={handleSubmit}>
<Form.TextArea
maxLength={20000}
rows={20}
rows={15}
{...makeProps('paste')}
/>
@@ -53,6 +53,91 @@ function PasteForm(props) {
);
};
function LabelForm(props) {
const [error, setError] = useState(false);
const [input, setInput] = useState({ id: '107', size: '2' });
const [label, setLabel] = useState(false);
const [loading, setLoading] = useState(false);
const [success, setSuccess] = useState(false);
const handleValues = (e, v) => setInput({ ...input, [v.name]: v.value });
const handleChange = (e) => handleValues(e, e.currentTarget);
const handleSubmit = (e) => {
if (loading) return;
setLoading(true);
fetch('https://labels.protospace.ca/?' + new URLSearchParams(input))
.then(res => {
if (res.ok) {
return res.blob();
} else {
return res.text().then(text => {throw new Error(text)});
}
})
.then(res => {
setLoading(false);
setSuccess(true);
setError(false);
const imageObjectURL = URL.createObjectURL(res);
setLabel(imageObjectURL);
})
.catch(err => {
setLabel(false);
setLoading(false);
console.log(err);
setError(err);
});
};
const makeProps = (name) => ({
name: name,
onChange: handleChange,
value: input[name] || '',
});
const sizeOptions = [
{ key: '0', text: '1.0', value: '1' },
{ key: '1', text: '1.5', value: '1.5' },
{ key: '2', text: '2.0', value: '2' },
{ key: '3', text: '2.5', value: '2.5' },
{ key: '4', text: '3.0', value: '3' },
{ key: '5', text: '3.5', value: '3.5' },
{ key: '6', text: '4.0', value: '4' },
];
return (
<Form onSubmit={handleSubmit} error={!!error}>
<Form.Group widths='equal'>
<Form.Input
fluid
label='Wiki ID #'
{...makeProps('id')}
/>
<Form.Select
fluid
label='Size'
options={sizeOptions}
{...makeProps('size')}
onChange={handleValues}
/>
</Form.Group>
<Message
error
header='Label Error'
content={error.message}
/>
<Form.Button loading={loading}>
Submit
</Form.Button>
{label && <img src={label} />}
</Form>
);
};
let pasteCache = 'Loading...';
export function Paste(props) {
@@ -73,6 +158,14 @@ export function Paste(props) {
return (
<Container>
<Header size='large'>Label Generator</Header>
<p>Use this to generate QR code labels for tools at Protospace.</p>
<p>Choose a tool from here: <a href='https://wiki.protospace.ca/Category:Tools' target='_blank'>https://wiki.protospace.ca/Category:Tools</a></p>
<LabelForm />
<Header size='large'>Transporter</Header>
<p>

64
webclient/src/Sign.js Normal file
View File

@@ -0,0 +1,64 @@
import React, { useState, useEffect, useReducer, useContext } from 'react';
import { BrowserRouter as Router, Switch, Route, Link, useParams, useHistory } from 'react-router-dom';
import { Button, Container, Checkbox, Dimmer, Divider, Dropdown, Form, Grid, Header, Icon, Image, Menu, Message, Segment, Table } from 'semantic-ui-react';
import { apiUrl, statusColor, BasicTable, staticUrl, requester } from './utils.js';
import { NotFound } from './Misc.js';
export function SignForm(props) {
const { token } = props;
const [error, setError] = useState({});
const [sign, setSign] = useState('');
const [loading, setLoading] = useState(false);
const [success, setSuccess] = useState(false);
const handleValues = (e, v) => setSign(v.value);
const handleChange = (e) => handleValues(e, e.currentTarget);
const handleSubmit = (e) => {
if (loading) return;
setLoading(true);
const data = {sign: sign};
requester('/stats/sign/', 'POST', token, data)
.then(res => {
setLoading(false);
setSuccess(true);
setError({});
setSign('');
})
.catch(err => {
setLoading(false);
console.log(err);
setError(err.data);
});
};
return (
<Form onSubmit={handleSubmit}>
<p>Send a message to the sign:</p>
<Form.Input
name='sign'
onChange={handleChange}
value={sign}
error={error.sign}
/>
<Form.Button loading={loading} error={error.non_field_errors}>
Submit
</Form.Button>
{success && <div>Success!</div>}
</Form>
);
};
export function Sign(props) {
const { token } = props;
return (
<Container>
<Header size='large'>Protospace Sign</Header>
<SignForm token={token} />
</Container>
);
};