Compare commits

...

8 Commits

Author SHA1 Message Date
Hafiz a99e6f097a remove unused test 2026-05-07 12:20:43 -05:00
Hafiz 66fee441f2 scheduling warning and summary UI updates 2026-05-07 12:14:29 -05:00
Hafiz d13cc871a2 formatting 2026-04-02 17:37:05 -05:00
Hafiz a2be823817 dailies monthly weeks of month summary
update monthly day-of-week scheduling summary, add scheduling summary to task form, and add 5th week warning
2026-04-02 17:30:02 -05:00
Phillip Thelen e4b76bd212 remove trailing space 2026-04-02 17:25:16 -05:00
Phillip Thelen 764cbfc6c3 fix lint 2026-04-02 17:25:16 -05:00
Phillip Thelen de31a8c5b8 Add Web UI to set email if apple does not provide one 2026-04-02 17:25:16 -05:00
Phillip Thelen af0d80bef8 apply email passed via body if it is missing from apple profile 2026-04-02 17:25:16 -05:00
8 changed files with 210 additions and 48 deletions
-15
View File
@@ -54,19 +54,4 @@ describe('armoire', () => {
const febuaryItems = armoire.all;
expect(febuaryItems.length).to.equal(384);
});
it('sets have at least 2 items', () => {
const setMap = {};
forEach(armoire.all, item => {
// Gotta have one outlier
if (!item.set || item.set.startsWith('armoire-')) return;
if (setMap[item.set] === undefined) {
setMap[item.set] = 0;
}
setMap[item.set] += 1;
});
Object.keys(setMap).forEach(set => {
expect(setMap[set], set).to.be.at.least(2);
});
});
});
@@ -0,0 +1,10 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_3344_18)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M9 12H7V10H9V12ZM16 2V14C16 15.1 15.1 16 14 16H2C0.9 16 0 15.1 0 14V2C0 0.9 0.9 0 2 0H14C15.1 0 16 0.9 16 2ZM14 2H2V14H14V2ZM9 4H7V9H9V4Z" fill="#4E4A57"/>
</g>
<defs>
<clipPath id="clip0_3344_18">
<rect width="16" height="16" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 450 B

@@ -43,6 +43,14 @@
<p class="purple-600">
{{ $t('usernameLimitations') }}
</p>
<input
v-if="needsEmailField"
id="emailInput"
v-model="email"
class="form-control dark"
type="text"
:placeholder="$t('email')"
>
<div class="custom-control custom-checkbox mb-4">
<input
id="privacyTOS"
@@ -165,6 +173,7 @@ export default {
registrationMethod: null,
username: '',
usernameIssues: [],
needsEmailField: false,
};
},
computed: {
@@ -183,22 +192,30 @@ export default {
},
},
mounted () {
if (window.sessionStorage.getItem('apple-token')) {
this.registrationMethod = 'apple';
} else if (!this.$store.state.registrationOptions.registrationMethod) {
this.$router.push('/');
} else {
this.registrationMethod = this.$store.state.registrationOptions.registrationMethod;
}
this.authData = this.$store.state.registrationOptions.authData;
this.email = this.$store.state.registrationOptions.email;
this.username = this.$store.state.registrationOptions.username;
this.password = this.$store.state.registrationOptions.password;
this.passwordConfirm = this.$store.state.registrationOptions.passwordConfirm;
if (!this.email) {
if (window.sessionStorage.getItem('apple-token')) {
this.registrationMethod = 'apple';
if (!this.email) {
this.email = window.sessionStorage.getItem('apple-email');
}
} else if (!this.$store.state.registrationOptions.registrationMethod) {
this.$router.push('/');
} else {
this.registrationMethod = this.$store.state.registrationOptions.registrationMethod;
}
if (!this.email && this.registrationMethod !== 'apple') {
return;
}
if ((!this.email || this.email === '') && this.registrationMethod === 'apple') {
this.needsEmailField = true;
}
const usernameToCheck = this.email.split('@')[0].replace(/[^a-zA-Z0-9\-_]/g, '');
this.$store.dispatch('auth:verifyUsername', {
username: usernameToCheck,
@@ -237,6 +254,7 @@ export default {
idToken: window.sessionStorage.getItem('apple-token'),
name: window.sessionStorage.getItem('apple-name'),
username: this.username,
email: this.email,
allowRegister: true,
});
} else {
@@ -37,6 +37,7 @@ export default {
window.location.href = '/';
} else {
window.sessionStorage.setItem('apple-token', response.idToken);
window.sessionStorage.setItem('apple-email', response.email);
window.location.href = '/username';
}
},
@@ -382,6 +382,25 @@
</div>
</div>
</div>
<p
v-if="task.type === 'daily' && schedulingSummary"
class="scheduling-summary mt-2 mb-0"
>
{{ schedulingSummary }}
</p>
<div
v-if="task.type === 'daily' && schedulingWarning"
class="scheduling-warning mt-2"
>
<span
class="scheduling-warning-icon"
v-html="icons.exclamationInfo"
></span>
<span
class="scheduling-warning-text"
v-html="schedulingWarning"
></span>
</div>
<div
v-if="!groupId"
class="tags-select option mt-3"
@@ -1065,6 +1084,42 @@
height: 1rem;
}
.scheduling-summary {
font-family: 'Roboto', sans-serif;
font-weight: 400;
font-style: normal;
font-size: 12px;
line-height: 16px;
color: $gray-50;
text-align: left;
}
.scheduling-warning {
display: flex;
align-items: flex-start;
font-family: 'Roboto', sans-serif;
font-weight: 400;
font-style: normal;
font-size: 12px;
line-height: 16px;
color: $gray-50;
}
.scheduling-warning-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
flex-shrink: 0;
margin-right: 6px;
margin-top: -1px;
}
.scheduling-warning-text {
flex: 1;
}
label {
display: inline-flex;
align-items: center;
@@ -1195,6 +1250,7 @@ import goldIcon from '@/assets/svg/gold.svg?raw';
import chevronIcon from '@/assets/svg/chevron.svg?raw';
import calendarIcon from '@/assets/svg/calendar.svg?raw';
import gripIcon from '@/assets/svg/grip.svg?raw';
import exclamationInfoIcon from '@/assets/svg/exclaimation_info.svg?raw';
import InformationIcon from '@/components/ui/informationIcon.vue';
export default {
@@ -1231,6 +1287,7 @@ export default {
streak: streakIcon,
calendar: calendarIcon,
grip: gripIcon,
exclamationInfo: exclamationInfoIcon,
}),
members: [],
membersNameAndId: [],
@@ -1326,6 +1383,87 @@ export default {
}
return null;
},
schedulingSummary () {
if (!this.task || this.task.type !== 'daily') return '';
const { task } = this;
const everyXValue = +task.everyX;
let interval;
if (task.frequency === 'daily') {
interval = everyXValue === 1 ? this.$t('everyDay') : this.$t('everyXDays', { count: everyXValue });
} else if (task.frequency === 'weekly') {
interval = everyXValue === 1 ? this.$t('everyWeek') : this.$t('everyXWeeks', { count: everyXValue });
} else if (task.frequency === 'monthly') {
interval = everyXValue === 1 ? this.$t('everyMonth') : this.$t('everyXMonths', { count: everyXValue });
} else if (task.frequency === 'yearly') {
interval = everyXValue === 1 ? this.$t('everyYear') : this.$t('everyXYears', { count: everyXValue });
} else {
return '';
}
let details = '';
if (task.frequency === 'weekly') {
const dayNames = {
su: 'Sunday',
m: 'Monday',
t: 'Tuesday',
w: 'Wednesday',
th: 'Thursday',
f: 'Friday',
s: 'Saturday',
};
const activeDays = Object.keys(task.repeat || {}).filter(d => task.repeat[d]);
if (activeDays.length > 0) {
details = ` on ${activeDays.map(d => dayNames[d]).join(', ')}`;
}
} else if (task.frequency === 'monthly' && task.startDate) {
const dayOfMonth = moment(task.startDate).date();
if (task.weeksOfMonth && task.weeksOfMonth.length > 0) {
const weekNum = task.weeksOfMonth[0] + 1;
const weekStr = String(weekNum);
const lastDigit = weekStr.slice(-1);
let suffix = 'th';
if (lastDigit === '1' && weekStr !== '11') suffix = 'st';
if (lastDigit === '2' && weekStr !== '12') suffix = 'nd';
if (lastDigit === '3' && weekStr !== '13') suffix = 'rd';
const dayName = moment(task.startDate).format('dddd');
details = ` on the ${weekNum}${suffix} ${dayName} of the month`;
} else if (task.daysOfMonth && task.daysOfMonth.length > 0) {
const dom = task.daysOfMonth[0];
const domStr = String(dom);
const lastDigit = domStr.slice(-1);
let suffix = 'th';
if (lastDigit === '1' && domStr !== '11') suffix = 'st';
if (lastDigit === '2' && domStr !== '12') suffix = 'nd';
if (lastDigit === '3' && domStr !== '13') suffix = 'rd';
details = ` on the ${dom}${suffix}`;
} else {
const domStr = String(dayOfMonth);
const lastDigit = domStr.slice(-1);
let suffix = 'th';
if (lastDigit === '1' && domStr !== '11') suffix = 'st';
if (lastDigit === '2' && domStr !== '12') suffix = 'nd';
if (lastDigit === '3' && domStr !== '13') suffix = 'rd';
details = ` on the ${dayOfMonth}${suffix}`;
}
} else if (task.frequency === 'yearly' && task.startDate) {
details = ` on ${moment(task.startDate).format('MMMM Do')}`;
}
return `${this.$t('repeats')} ${interval}${details}`;
},
schedulingWarning () {
if (!this.task || this.task.type !== 'daily') return '';
const { task } = this;
if (task.frequency === 'monthly'
&& task.weeksOfMonth && task.weeksOfMonth.length > 0
&& task.weeksOfMonth[0] === 4
&& task.startDate) {
const dayName = moment(task.startDate).format('dddd');
return this.$t('fifthWeekWarning', { day: dayName });
}
return '';
},
repeatsOn: {
get () {
let repeatsOn = 'dayOfMonth';
@@ -222,14 +222,22 @@ export default {
return usernames;
},
summarySentence () {
let fifthWeekWarning = '';
if (this.task.type === 'daily' && this.task.frequency === 'monthly'
&& this.task.weeksOfMonth && this.task.weeksOfMonth.length > 0
&& this.task.weeksOfMonth[0] === 4) {
const activeDays = keys(pickBy(this.task.repeat, value => value === true));
const dayName = this.expandDayString[activeDays[0]];
fifthWeekWarning = ` ${this.$t('fifthWeekWarning', { day: dayName })}`;
}
if (this.task.type === 'daily' && moment().isBefore(this.task.startDate)) {
return `This is ${this.formattedDifficulty(this.task.priority)} task that will repeat
${this.formattedRepeatInterval(this.task.frequency, this.task.everyX)}${this.formattedDays(this.task.frequency, this.task.repeat, this.task.daysOfMonth, this.task.weeksOfMonth, this.task.startDate)}
starting on <strong>${moment(this.task.startDate).format('MM/DD/YYYY')}</strong>.`;
starting on <strong>${moment(this.task.startDate).format('MM/DD/YYYY')}</strong>.${fifthWeekWarning}`;
}
if (this.task.type === 'daily') {
return `This is ${this.formattedDifficulty(this.task.priority)} task that repeats
${this.formattedRepeatInterval(this.task.frequency, this.task.everyX)}${this.formattedDays(this.task.frequency, this.task.repeat, this.task.daysOfMonth, this.task.weeksOfMonth, this.task.startDate)}.`;
${this.formattedRepeatInterval(this.task.frequency, this.task.everyX)}${this.formattedDays(this.task.frequency, this.task.repeat, this.task.daysOfMonth, this.task.weeksOfMonth, this.task.startDate)}.${fifthWeekWarning}`;
}
if (this.task.date) {
return `This is ${this.formattedDifficulty(this.task.priority)} task that is due <strong>${moment(this.task.date).format('MM/DD/YYYY')}.`;
@@ -287,25 +295,14 @@ export default {
});
dayStringArray.push('</strong>');
} else if (weeksOfMonth.length > 0) {
switch (weeksOfMonth[0]) {
case 0:
dayStringArray.push('first');
break;
case 1:
dayStringArray.push('second');
break;
case 2:
dayStringArray.push('third');
break;
case 3:
dayStringArray.push('fourth');
break;
case 4:
dayStringArray.push('fifth');
break;
default:
break;
}
const weekNum = weeksOfMonth[0] + 1;
const weekNumStr = String(weekNum);
const lastDigit = weekNumStr.slice(-1);
let ordinalSuffix = 'th';
if (lastDigit === '1' && weekNumStr !== '11') ordinalSuffix = 'st';
if (lastDigit === '2' && weekNumStr !== '12') ordinalSuffix = 'nd';
if (lastDigit === '3' && weekNumStr !== '13') ordinalSuffix = 'rd';
dayStringArray.push(`${weekNum}${ordinalSuffix}`);
activeDays = keys(pickBy(repeat, value => value === true));
dayStringArray.push(` ${this.expandDayString[activeDays[0]]} of the month</strong>`);
}
@@ -343,9 +340,8 @@ export default {
if (numericX === 2) return '<strong>every other week</strong>';
return `<strong>every ${numericX} weeks</strong>`;
case 'monthly':
if (numericX === 1) return '<strong>every month</strong>';
if (numericX === 2) return '<strong>every other month</strong>';
return `<strong>every ${numericX} months</strong>`;
if (numericX === 1) return `<strong>${this.$t('everyMonth')}</strong>`;
return `<strong>${this.$t('everyXMonths', { count: numericX })}</strong>`;
case 'yearly':
if (numericX === 1) return '<strong>every year</strong>';
return `<strong>every ${everyX} years</strong>`;
+10
View File
@@ -123,6 +123,16 @@
"dayOfMonth": "Day of the Month",
"month": "Month",
"months": "Months",
"every": "every",
"everyDay": "every day",
"everyXDays": "every <%= count %> days",
"everyWeek": "every week",
"everyXWeeks": "every <%= count %> weeks",
"everyMonth": "every month",
"everyXMonths": "every <%= count %> months",
"everyYear": "every year",
"everyXYears": "every <%= count %> years",
"fifthWeekWarning": "This task <strong>will not</strong> appear due during months with fewer <%= day %>s",
"week": "Week",
"weeks": "Weeks",
"year": "Year",
+5 -1
View File
@@ -44,9 +44,13 @@ export async function appleProfile (req) {
const verifiedPayload = await jwt.verify(idToken, applePublicKey, { algorithms: 'RS256' });
let { email } = verifiedPayload;
if ((!email || email === '') && req.body.email) {
email = req.body.email;
}
return {
id: verifiedPayload.sub,
emails: [{ value: verifiedPayload.email }],
emails: [{ value: email }],
name: verifiedPayload.name || req.body.name || req.query.name,
idToken,
};