Skip to content

Commit

Permalink
Reviews app: change weights for rejections. Applications app: ability…
Browse files Browse the repository at this point in the history
… to save before submitting.
  • Loading branch information
simonkernel committed Oct 19, 2022
1 parent 3ce93b0 commit 44b62aa
Show file tree
Hide file tree
Showing 5 changed files with 114 additions and 42 deletions.
2 changes: 1 addition & 1 deletion packages/applications/src/components/Intro.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const Intro = () => (
<Paragraph>This application is the start of your Kernel Unprofile. Rather than project what you think others want to hear, we ask that you speak the truth with love. This is what serves our core goal, which is to <strong>learn together</strong>.</Paragraph>
<Paragraph>What you write here will be visible to a group of current fellows who review applications. We value honesty, sincerity, simplicity and a diversity of skills and interests. <a className='text-blue-600 visited:text-purple-600' href='https://www.kernel.community/en/start/principled-people/' target='_blank' rel='noreferrer'>Our principles</a> are <strong>do no harm</strong> and <strong>play, infinitely</strong>. The purpose of this form is to start getting to know each other. Don't worry about proving anything, we really just want to understand what you care about.</Paragraph>
<Paragraph>If you are accepted, you’ll be able to update it continuously. It is your data and you are in control. This also means you are responsible for how it appears. The interplay of control and responsibility is one of the <a className='text-blue-600 visited:text-purple-600' href='https://www.kernel.community/en/learn/module-0/play-of-pattern/' target='_blank' rel='noreferrer'>complementary opposites</a> on which Kernel is based.</Paragraph>
<Paragraph>We save the fields in this form automatically, so feel free to take your time and don't worry if something seems to go wrong.</Paragraph>
<Paragraph>You can save your application and submit it whenever you are ready.</Paragraph>
</div>
</>
)
Expand Down
109 changes: 78 additions & 31 deletions packages/applications/src/views/Application.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,34 +17,33 @@ const FORM_INPUT = {
name: { label: 'Name', tip: 'What can we call you?', tag: 'input' },
email: { label: 'Email', tip: 'So we can send you updates.', tag: 'input' },
reason: { label: 'Reason', tip: 'Why do you want to be in Kernel?', tag: 'textarea' },
interests: { label: 'Interests', tip: 'At Kernel, we learn through conversations oganised by any fellow. What topics most inspire you to talk and listen? What are you really passionate about?', tag: 'textarea' },
interests: { label: 'Interests', tip: 'At Kernel, we learn through conversations organised by any fellow. What topics most inspire you to talk and listen? What are you really passionate about?', tag: 'textarea' },
activities: { label: 'Activities', tip: 'What do you love doing? What makes you grateful to be alive?', tag: 'textarea' },
urls: { label: 'Links', tip: 'Please share any links which best represent you (can be a song you like, a project you work on, or anything else between).', tag: 'textarea' }
}

const INITIAL_FORM_KEYS = ['wallet'].concat(Object.keys(FORM_INPUT))
const INITIAL_FORM_KEYS = [].concat(Object.keys(FORM_INPUT))
const INITIAL_FORM_FIELDS_STATE = INITIAL_FORM_KEYS
.reduce((acc, key) => Object.assign(acc, { [key]: '' }), {})

const INITIAL_FORM_SUBMISSION_STATE = {
formStatus: 'clean',
errorMessage: null
errorMessage: null,
editable: true
}

const INITIAL_STATE = { ...INITIAL_FORM_FIELDS_STATE, ...INITIAL_FORM_SUBMISSION_STATE }

const actions = {}

// initializes an actions object in the form:
// { bio: (state, value) => Object.assign({}, state, bio: value}) }
// each field's action updates the state with the given value
const LOGICAL_STATE = ['referralId', 'member', 'members', 'memberId', 'applications', 'applicationId', 'taskService', 'editable']

Object.keys(INITIAL_STATE)
.concat(['referralId', 'members', 'memberId', 'applications', 'applicationId', 'taskService'])
.concat(LOGICAL_STATE)
.forEach((key) => {
actions[key] = (state, value) => Object.assign({}, state, { [key]: value })
})

// tries to call the given action
const reducer = (state, action) => {
try {
return actions[action.type](state, action.payload)
Expand All @@ -54,7 +53,6 @@ const reducer = (state, action) => {
}
}

// tries to get the payload out of the event and dispatch it
const change = (dispatch, type, e) => {
try {
const target = e.target
Expand All @@ -69,7 +67,7 @@ const value = (state, type) => state[type]

const save = async (user, state, dispatch, e) => {
e.preventDefault()
dispatch({ type: 'formStatus', payload: 'submitting' })
dispatch({ type: 'formStatus', payload: 'saving' })
dispatch({ type: 'errorMessage', payload: null })

if (user.role < AppConfig.minRole) {
Expand All @@ -78,36 +76,69 @@ const save = async (user, state, dispatch, e) => {
return
}

const keysToExclude = [
'applications',
'members',
'applicationId',
'wallet',
'taskService'
].concat(Object.keys(INITIAL_FORM_SUBMISSION_STATE))
const { applications, member, members, memberId, applicationId } = state

if (member.data.reviewId) {
dispatch({ type: 'formStatus', payload: 'error' })
dispatch({ type: 'errorMessage', payload: 'You have already submitted your application.' })
return
}

const { taskService, applications, memberId, applicationId } = state
const keysToExclude = LOGICAL_STATE.concat(Object.keys(INITIAL_FORM_SUBMISSION_STATE))
const data = Object.keys(state)
.filter(key => !keysToExclude.includes(key))
.reduce((acc, key) => Object.assign(acc, { [key]: state[key] }), {})

try {
if (applicationId && memberId) {
if (applicationId) {
const patched = await applications.patch(applicationId, data)
dispatch({ type: 'formStatus', payload: 'success' })
dispatch({ type: 'formStatus', payload: 'saved' })
return patched
}

await taskService.submitApplication({ data })
const application = await applications.create(data)
dispatch({ type: 'applicationId', payload: application.id })

const member = await members.patch(memberId, { applicationId: application.id })
dispatch({ type: 'member', payload: member })
dispatch({ type: 'formStatus', payload: 'saved' })
} catch (error) {
dispatch({ type: 'formStatus', payload: 'error' })
dispatch({ type: 'errorMessage', payload: error.message })
}
}

const submit = async (user, state, dispatch, e) => {
e.preventDefault()
dispatch({ type: 'formStatus', payload: 'submitting' })
dispatch({ type: 'errorMessage', payload: null })

if (user.role < AppConfig.minRole) {
dispatch({ type: 'formStatus', payload: 'error' })
dispatch({ type: 'errorMessage', payload: 'You are already a member.' })
return
}

const { taskService, member, applicationId } = state

if (member.data.reviewId) {
dispatch({ type: 'formStatus', payload: 'error' })
dispatch({ type: 'errorMessage', payload: 'You have already submitted an application.' })
return
}

try {
await taskService.submitApplication({ applicationId })
dispatch({ type: 'editable', payload: false })
dispatch({ type: 'formStatus', payload: 'success' })
} catch (error) {
dispatch({ type: 'formStatus', payload: 'error' })
dispatch({ type: 'errorMessage', payload: error.message })
}
}

const Input = ({ fieldName, label, tip, tag, editable = true, state, dispatch }) => {
const disabled = !editable
const Input = ({ fieldName, label, tip, tag, state, dispatch }) => {
const disabled = !state.editable
const bgColorClass = disabled ? 'bg-gray-200' : ''
const InputField = tag

Expand All @@ -118,7 +149,7 @@ const Input = ({ fieldName, label, tip, tag, editable = true, state, dispatch })
</label>
<p className='text-sm italic'>{tip || ''}</p>
<InputField
type='text' disabled={!editable} className={`border-1 rounded w-full ${bgColorClass}`}
type='text' disabled={disabled} className={`border-1 rounded w-full ${bgColorClass}`}
value={value(state, fieldName)} onChange={change.bind(null, dispatch, fieldName)}
/>
</div>
Expand All @@ -127,12 +158,16 @@ const Input = ({ fieldName, label, tip, tag, editable = true, state, dispatch })

const PageAlert = ({ formStatus, errorMessage }) => {
switch (formStatus) {
case 'submitting':
case 'saving':
return <Alert type='transparent'>Saving your changes...</Alert>
case 'submitting':
return <Alert type='transparent'>Submitting your application...</Alert>
case 'saved':
return <Alert type='success'>Saved</Alert>
case 'success':
return <Alert type='success'>Your changes have been saved!</Alert>
return <Alert type='success'>Your application has been submitted! We will reach out with information about next steps.</Alert>
case 'error':
return <Alert type='danger'>Something went wrong. {errorMessage}</Alert>
return <Alert type='danger'>{errorMessage}</Alert>
default:
return <Alert type='transparent' />
}
Expand All @@ -158,6 +193,8 @@ const Application = () => {

useEffect(() => {
(async () => {
dispatch({ type: 'formStatus', payload: 'clean' })

const memberId = user.iss
dispatch({ type: 'memberId', payload: memberId })

Expand All @@ -171,9 +208,12 @@ const Application = () => {
dispatch({ type: 'members', payload: members })

const member = await members.get(memberId)
dispatch({ type: 'wallet', payload: member.data.wallet })
dispatch({ type: 'member', payload: member })

const { data: { applicationId } } = member
const { data: { applicationId, reviewId } } = member
if (reviewId) {
dispatch({ type: 'editable', payload: false })
}
if (applicationId) {
dispatch({ type: 'applicationId', payload: applicationId })
const application = await applications.get(applicationId)
Expand Down Expand Up @@ -202,9 +242,16 @@ const Application = () => {
)
})}
<button
disabled={state.formStatus === 'submitting'}
disabled={state.formStatus === 'submitting' || !state.editable}
onClick={save.bind(null, user, state, dispatch)}
className={`mt-6 mb-4 px-6 py-4 ${state.formStatus === 'submitting' ? 'bg-gray-300' : 'bg-kernel-green-dark'} text-kernel-white w-full rounded font-bold`}
className={`mt-6 mb-4 px-6 py-4 ${state.formStatus === 'submitting' || !state.editable ? 'bg-gray-300' : 'bg-kernel-green-dark'} text-kernel-white w-full rounded font-bold`}
>
Save
</button>
<button
disabled={state.formStatus === 'submitting'}
onClick={submit.bind(null, user, state, dispatch)}
className={`mt-6 mb-4 px-6 py-4 ${state.formStatus === 'submitting' || !state.editable ? 'bg-gray-300' : 'bg-kernel-green-dark'} text-kernel-white w-full rounded font-bold`}
>
Submit
</button>
Expand Down
2 changes: 1 addition & 1 deletion packages/common/src/services/task.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const build = async ({ rpcClient }) => {
const voteProposal = async ({ proposalId, choice }) => call('voteProposal', { proposalId, choice })
const voteReview = async ({ reviewId, choice }) => call('voteReview', { reviewId, choice })
// External
const submitApplication = async ({ data }) => call('submitApplication', { data })
const submitApplication = async ({ applicationId }) => call('submitApplication', { applicationId })
const ethereumFaucet = async ({ chainId }) => call('ethereumFaucet', { chainId })

return {
Expand Down
6 changes: 5 additions & 1 deletion packages/reviews/src/views/Review.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const CHOICES = [
{ emoji: 'ok_hand', code: '128076', weight: 4 },
{ emoji: 'clap', code: '128079', weight: 3 },
{ emoji: 'thumbs up', code: '128077', weight: 2 },
{ emoji: 'thumbs down', code: '128078', weight: 1 }
{ emoji: 'thumbs down', code: '128078', weight: -1 }
]
const WEIGHTS = CHOICES.reduce((acc, { code, weight }) => ({ ...acc, [code]: weight }), {})

Expand Down Expand Up @@ -103,9 +103,12 @@ const View = () => {
<div>
{entity &&
<>
<p><b>Status: </b>{entity.data.status}</p>
<hr className='my-4' />
{Object.entries(application.data)
.filter(([key, _]) => !key.includes('Id'))
.map(([key, value]) => (<p key={key}><b>{key}: </b>{value}</p>))}
<hr className='my-4' />
<p>{CHOICES.map(({ code, weight }, i) =>
<button
key={i}
Expand All @@ -116,6 +119,7 @@ const View = () => {
{String.fromCodePoint(code)}
</button>)}
</p>
<hr className='my-4' />
<Tally votes={entity.data.votes} />
<p />
<hr />
Expand Down
37 changes: 29 additions & 8 deletions packages/task/src/services/taskQueue.js
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ const build = async ({ projectId, seed, serviceAccount, infuraId, faucetAmount,
{ emoji: 'ok_hand', code: '128076', weight: 4 },
{ emoji: 'clap', code: '128079', weight: 3 },
{ emoji: 'thumbs up', code: '128077', weight: 2 },
{ emoji: 'thumbs down', code: '128078', weight: 1 }
{ emoji: 'thumbs down', code: '128078', weight: -1 }
]

const WEIGHTS = CHOICES.reduce((acc, { code, weight }) => ({ ...acc, [code]: weight }), {})
Expand Down Expand Up @@ -224,19 +224,40 @@ const build = async ({ projectId, seed, serviceAccount, infuraId, faucetAmount,
}

// External
const submitApplication = async ({ iss, role }, { data }) => {
const submitApplication = async ({ iss, role }, { applicationId }) => {
if (role < EXTERNAL_ROLE) {
console.debug(`Already a member ${iss}, ${role}`)
return
}
const { data: { applicationId } } = await members.get(iss)
if (applicationId) {
const patched = await applications.patch(applicationId, data)
return { application: patched }
const member = await members.get(iss)
if (member.data.applicationId !== applicationId) {
console.debug(`Application mismatch ${iss}, ${applicationId}`)
return
}
if (member.data.reviewId) {
console.debug(`Already submitted an application ${iss}, ${applicationId}`)
return
}
const application = await applications.create(data, { owner: iss })
const application = await applications.get(applicationId)
const review = await reviews.create({ applicationId: application.id, status: 'new' })
const member = await members.patch(iss, { applicationId: application.id })
await members.patch(iss, { reviewId: review.id })

const { data: { email, name }} = application
if (email) {
const subject = `${name}'s Kernel Community Application Confirmation`
// TODO: move to separate content package
const message = `Hi ${name},
Thank you for applying to the KERNEL Community! We’ve received your application and are reviewing it on a rolling basis.
You can always visit the application app to see the latest status and we will also notify you via email when your status changes.
If you have any questions, you can just respond to this email.
Our Best,
The KERNEL Stewards`
await googleServices.sendEmail(generateEmail(email, subject, message))
}

return { application }
}
Expand Down

0 comments on commit 44b62aa

Please sign in to comment.