kaneo (minimalist kanban) fork to experiment adding a tangled integration github.com/usekaneo/kaneo
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge pull request #1206 from tinsever/feat/1201-allow-selecting-first-day-of-week

feat: add configurable first day of week

authored by

Andrej and committed by
GitHub
73e71cd4 7bdd2754

+148 -12
+6
apps/web/src/components/ui/calendar.tsx
··· 9 9 import { DayPicker } from "react-day-picker"; 10 10 11 11 import { cn } from "@/lib/cn"; 12 + import { useUserPreferencesStore } from "@/store/user-preferences"; 12 13 13 14 const buttonClassNames = 14 15 "relative flex size-(--cell-size) text-base sm:text-sm items-center justify-center rounded-lg text-foreground not-in-data-selected:hover:bg-accent disabled:pointer-events-none disabled:opacity-64 [&_svg:not([class*='opacity-'])]:opacity-80 [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0"; ··· 18 19 classNames, 19 20 showOutsideDays = true, 20 21 components: userComponents, 22 + weekStartsOn: weekStartsOnProp, 21 23 ...props 22 24 }: React.ComponentProps<typeof DayPicker>) { 25 + const preferredWeekStartsOn = useUserPreferencesStore( 26 + (state) => state.weekStartsOn, 27 + ); 23 28 const defaultClassNames = { 24 29 button_next: buttonClassNames, 25 30 button_previous: buttonClassNames, ··· 129 134 }} 130 135 mode="single" 131 136 showOutsideDays={showOutsideDays} 137 + weekStartsOn={weekStartsOnProp ?? preferredWeekStartsOn} 132 138 {...props} 133 139 /> 134 140 );
+11 -5
apps/web/src/hooks/use-task-filters-with-labels-support.ts
··· 1 1 import { useQueryClient } from "@tanstack/react-query"; 2 2 import { addWeeks, endOfWeek, isWithinInterval, startOfWeek } from "date-fns"; 3 3 import { useCallback, useEffect, useMemo, useState } from "react"; 4 + import { useUserPreferencesStore } from "@/store/user-preferences"; 4 5 import type { ProjectWithTasks } from "@/types/project"; 5 6 import type Task from "@/types/task"; 6 7 import { type BoardFilters, DUE_DATE_FILTER_VALUES } from "./use-task-filters"; ··· 46 47 textQuery?: string, 47 48 ) { 48 49 const queryClient = useQueryClient(); 50 + const weekStartsOn = useUserPreferencesStore((state) => state.weekStartsOn); 49 51 const storageKey = projectId ? `kaneo:board-filters:${projectId}` : null; 50 52 const [filters, setFilters] = useState<BoardFilters>(DEFAULT_FILTERS); 51 53 ··· 139 141 140 142 switch (dueDateFilter) { 141 143 case DUE_DATE_FILTER_VALUES.dueThisWeek: { 142 - const weekStart = startOfWeek(today); 143 - const weekEnd = endOfWeek(today); 144 + const weekStart = startOfWeek(today, { weekStartsOn }); 145 + const weekEnd = endOfWeek(today, { weekStartsOn }); 144 146 return isWithinInterval(taskDate, { 145 147 start: weekStart, 146 148 end: weekEnd, 147 149 }); 148 150 } 149 151 case DUE_DATE_FILTER_VALUES.dueNextWeek: { 150 - const nextWeekStart = startOfWeek(addWeeks(today, 1)); 151 - const nextWeekEnd = endOfWeek(addWeeks(today, 1)); 152 + const nextWeekStart = startOfWeek(addWeeks(today, 1), { 153 + weekStartsOn, 154 + }); 155 + const nextWeekEnd = endOfWeek(addWeeks(today, 1), { 156 + weekStartsOn, 157 + }); 152 158 return isWithinInterval(taskDate, { 153 159 start: nextWeekStart, 154 160 end: nextWeekEnd, ··· 182 188 return true; 183 189 }); 184 190 }, 185 - [filters, getTaskLabels, textQuery], 191 + [filters, getTaskLabels, textQuery, weekStartsOn], 186 192 ); 187 193 188 194 const filteredProject = useMemo(() => {
+10 -4
apps/web/src/hooks/use-task-filters.ts
··· 1 1 import { addWeeks, endOfWeek, isWithinInterval, startOfWeek } from "date-fns"; 2 2 import { useEffect, useState } from "react"; 3 + import { useUserPreferencesStore } from "@/store/user-preferences"; 3 4 import type { ProjectWithTasks } from "@/types/project"; 4 5 import type Task from "@/types/task"; 5 6 ··· 56 57 project: ProjectWithTasks | null | undefined, 57 58 projectId?: string, 58 59 ) { 60 + const weekStartsOn = useUserPreferencesStore((state) => state.weekStartsOn); 59 61 const storageKey = projectId ? `kaneo:board-filters:${projectId}` : null; 60 62 const [filters, setFilters] = useState<BoardFilters>(DEFAULT_FILTERS); 61 63 ··· 122 124 123 125 switch (dueDateFilter) { 124 126 case DUE_DATE_FILTER_VALUES.dueThisWeek: { 125 - const weekStart = startOfWeek(today); 126 - const weekEnd = endOfWeek(today); 127 + const weekStart = startOfWeek(today, { weekStartsOn }); 128 + const weekEnd = endOfWeek(today, { weekStartsOn }); 127 129 return isWithinInterval(taskDate, { 128 130 start: weekStart, 129 131 end: weekEnd, 130 132 }); 131 133 } 132 134 case DUE_DATE_FILTER_VALUES.dueNextWeek: { 133 - const nextWeekStart = startOfWeek(addWeeks(today, 1)); 134 - const nextWeekEnd = endOfWeek(addWeeks(today, 1)); 135 + const nextWeekStart = startOfWeek(addWeeks(today, 1), { 136 + weekStartsOn, 137 + }); 138 + const nextWeekEnd = endOfWeek(addWeeks(today, 1), { 139 + weekStartsOn, 140 + }); 135 141 return isWithinInterval(taskDate, { 136 142 start: nextWeekStart, 137 143 end: nextWeekEnd,
+45
apps/web/src/routes/_layout/_authenticated/dashboard/settings/account/preferences.tsx
··· 56 56 const { 57 57 theme, 58 58 setTheme, 59 + weekStartsOn, 60 + setWeekStartsOn, 59 61 viewMode, 60 62 setViewMode, 61 63 showTaskNumbers, ··· 82 84 const viewLabels: Record<string, string> = { 83 85 board: t("settings:preferencesPage.board"), 84 86 list: t("settings:preferencesPage.list"), 87 + }; 88 + const weekStartLabels: Record<"0" | "1", string> = { 89 + "0": t("settings:preferencesPage.weekStartsOnSunday"), 90 + "1": t("settings:preferencesPage.weekStartsOnMonday"), 85 91 }; 86 92 87 93 const selectedLocale: AppLocale = locale ?? defaultLocale; ··· 174 180 {getLocaleLabel(supportedLocale)} 175 181 </SelectItem> 176 182 ))} 183 + </SelectContent> 184 + </Select> 185 + </div> 186 + 187 + <Separator /> 188 + 189 + <div className="flex items-center justify-between"> 190 + <div className="space-y-0.5"> 191 + <Label className="text-sm font-medium"> 192 + {t("settings:preferencesPage.firstDayOfWeek")} 193 + </Label> 194 + <p className="text-xs text-muted-foreground"> 195 + {t("settings:preferencesPage.firstDayOfWeekDescription")} 196 + </p> 197 + </div> 198 + <Select 199 + value={String(weekStartsOn)} 200 + onValueChange={(value) => { 201 + if (value === "0" || value === "1") { 202 + setWeekStartsOn(Number(value) as 0 | 1); 203 + } 204 + }} 205 + > 206 + <SelectTrigger size="sm" className="w-40"> 207 + <SelectValue 208 + placeholder={t( 209 + "settings:preferencesPage.selectFirstDayOfWeek", 210 + )} 211 + > 212 + {weekStartLabels[String(weekStartsOn) as "0" | "1"]} 213 + </SelectValue> 214 + </SelectTrigger> 215 + <SelectContent> 216 + <SelectItem value="0"> 217 + {t("settings:preferencesPage.weekStartsOnSunday")} 218 + </SelectItem> 219 + <SelectItem value="1"> 220 + {t("settings:preferencesPage.weekStartsOnMonday")} 221 + </SelectItem> 177 222 </SelectContent> 178 223 </Select> 179 224 </div>
+5 -3
apps/web/src/routes/_layout/_authenticated/dashboard/workspace/$workspaceId/project/$projectId/gantt.tsx
··· 24 24 import { useIsMobile } from "@/hooks/use-mobile"; 25 25 import { cn } from "@/lib/cn"; 26 26 import { getStatusLabel } from "@/lib/i18n/domain"; 27 + import { useUserPreferencesStore } from "@/store/user-preferences"; 27 28 28 29 type GanttSearchParams = { 29 30 taskId?: string; ··· 50 51 const { taskId } = Route.useSearch(); 51 52 const navigate = useNavigate(); 52 53 const { data: project } = useGetTasks(projectId); 54 + const weekStartsOn = useUserPreferencesStore((state) => state.weekStartsOn); 53 55 const [searchQuery, setSearchQuery] = useState(""); 54 56 const isMobile = useIsMobile(); 55 57 const [isTaskRailOpen, setIsTaskRailOpen] = useState(false); ··· 135 137 136 138 // Week-aligned bounds around task dates, then pad with extra days so bars can 137 139 // be resized or moved past the current last task without running out of grid. 138 - const weekStart = startOfWeek(earliest, { weekStartsOn: 1 }); 139 - const weekEnd = endOfWeek(latest, { weekStartsOn: 1 }); 140 + const weekStart = startOfWeek(earliest, { weekStartsOn }); 141 + const weekEnd = endOfWeek(latest, { weekStartsOn }); 140 142 const rangeStart = subDays(weekStart, 7); 141 143 const rangeEnd = addDays(weekEnd, 28); 142 144 ··· 148 150 gridTemplateColumns: `repeat(${days.length}, minmax(${dayColumnWidthRem}rem, ${dayColumnWidthRem}rem))`, 149 151 timelineMinWidthRem: days.length * dayColumnWidthRem, 150 152 }; 151 - }, [parsedTasks, dayColumnWidthRem]); 153 + }, [parsedTasks, dayColumnWidthRem, weekStartsOn]); 152 154 153 155 useLayoutEffect(() => { 154 156 const element = timelineTrackRef.current;
+6
apps/web/src/store/user-preferences.ts
··· 33 33 34 34 sidebarDefaultOpen: boolean; 35 35 setSidebarDefaultOpen: (open: boolean) => void; 36 + 37 + weekStartsOn: 0 | 1; 38 + setWeekStartsOn: (weekStartsOn: 0 | 1) => void; 36 39 }; 37 40 38 41 export const useUserPreferencesStore = create<UserPreferencesStore>()( ··· 102 105 103 106 sidebarDefaultOpen: true, 104 107 setSidebarDefaultOpen: (open) => set({ sidebarDefaultOpen: open }), 108 + 109 + weekStartsOn: 0, 110 + setWeekStartsOn: (weekStartsOn) => set({ weekStartsOn }), 105 111 }), 106 112 { 107 113 name: "user-preferences",
+5
i18n/de-DE.json
··· 424 424 "language": "Sprache", 425 425 "languageDescription": "Wähle deine bevorzugte Oberflächensprache", 426 426 "selectLanguage": "Sprache auswählen", 427 + "firstDayOfWeek": "Erster Wochentag", 428 + "firstDayOfWeekDescription": "Lege fest, ob Kalender und Wochenbereiche am Sonntag oder Montag beginnen", 429 + "selectFirstDayOfWeek": "Ersten Tag auswählen", 430 + "weekStartsOnSunday": "Sonntag", 431 + "weekStartsOnMonday": "Montag", 427 432 "defaultView": "Standardansicht", 428 433 "defaultViewDescription": "Wähle deine bevorzugte Aufgabenansicht", 429 434 "selectViewMode": "Ansicht auswählen",
+5
i18n/el-GR.json
··· 424 424 "language": "Γλώσσα", 425 425 "languageDescription": "Επιλέξτε τη γλώσσα διεπαφής που προτιμάτε", 426 426 "selectLanguage": "Επιλέξτε γλώσσα", 427 + "firstDayOfWeek": "Πρώτη ημέρα της εβδομάδας", 428 + "firstDayOfWeekDescription": "Επιλέξτε αν τα ημερολόγια και οι εβδομάδες ξεκινούν την Κυριακή ή τη Δευτέρα", 429 + "selectFirstDayOfWeek": "Επιλέξτε την πρώτη ημέρα", 430 + "weekStartsOnSunday": "Κυριακή", 431 + "weekStartsOnMonday": "Δευτέρα", 427 432 "defaultView": "Προεπιλεγμένη προβολή", 428 433 "defaultViewDescription": "Επιλέξτε τον προτιμώμενο τρόπο προβολής εργασιών", 429 434 "selectViewMode": "Επιλέξτε τρόπο προβολής",
+5
i18n/en-US.json
··· 424 424 "language": "Language", 425 425 "languageDescription": "Choose your preferred interface language", 426 426 "selectLanguage": "Select a language", 427 + "firstDayOfWeek": "First Day of Week", 428 + "firstDayOfWeekDescription": "Choose whether calendars and weekly ranges start on Sunday or Monday", 429 + "selectFirstDayOfWeek": "Select the first day", 430 + "weekStartsOnSunday": "Sunday", 431 + "weekStartsOnMonday": "Monday", 427 432 "defaultView": "Default View", 428 433 "defaultViewDescription": "Choose your preferred task view mode", 429 434 "selectViewMode": "Select a view mode",
+5
i18n/es-ES.json
··· 334 334 "language": "Idioma", 335 335 "languageDescription": "Elige el idioma de la interfaz", 336 336 "selectLanguage": "Seleccionar un idioma", 337 + "firstDayOfWeek": "Primer día de la semana", 338 + "firstDayOfWeekDescription": "Elige si los calendarios y las semanas empiezan en domingo o lunes", 339 + "selectFirstDayOfWeek": "Selecciona el primer día", 340 + "weekStartsOnSunday": "Domingo", 341 + "weekStartsOnMonday": "Lunes", 337 342 "defaultView": "Vista por defecto", 338 343 "defaultViewDescription": "Elige la vista por defecto para las tareas", 339 344 "selectViewMode": "Elige una vista por defecto",
+5
i18n/fr-FR.json
··· 424 424 "language": "Langue", 425 425 "languageDescription": "Choisissez la langue de l'interface", 426 426 "selectLanguage": "Sélectionnez une langue", 427 + "firstDayOfWeek": "Premier jour de la semaine", 428 + "firstDayOfWeekDescription": "Choisissez si les calendriers et les semaines commencent le dimanche ou le lundi", 429 + "selectFirstDayOfWeek": "Sélectionnez le premier jour", 430 + "weekStartsOnSunday": "Dimanche", 431 + "weekStartsOnMonday": "Lundi", 427 432 "defaultView": "Vue par défaut", 428 433 "defaultViewDescription": "Choisissez votre mode d'affichage des tâches préféré", 429 434 "selectViewMode": "Sélectionnez un mode d'affichage",
+5
i18n/mk-MK.json
··· 424 424 "language": "Јазик", 425 425 "languageDescription": "Избери го претпочитаниот јазик на интерфејсот", 426 426 "selectLanguage": "Избери јазик", 427 + "firstDayOfWeek": "Прв ден од неделата", 428 + "firstDayOfWeekDescription": "Избери дали календарите и неделите почнуваат во недела или понеделник", 429 + "selectFirstDayOfWeek": "Избери прв ден", 430 + "weekStartsOnSunday": "Недела", 431 + "weekStartsOnMonday": "Понеделник", 427 432 "defaultView": "Стандарден приказ", 428 433 "defaultViewDescription": "Избери го претпочитаниот режим на приказ на задачи", 429 434 "selectViewMode": "Избери режим на приказ",
+5
i18n/nl-NL.json
··· 334 334 "language": "Taal", 335 335 "languageDescription": "Kies je gewenste taal voor de interface", 336 336 "selectLanguage": "Selecteer een taal", 337 + "firstDayOfWeek": "Eerste dag van de week", 338 + "firstDayOfWeekDescription": "Kies of kalenders en weekoverzichten op zondag of maandag beginnen", 339 + "selectFirstDayOfWeek": "Selecteer de eerste dag", 340 + "weekStartsOnSunday": "Zondag", 341 + "weekStartsOnMonday": "Maandag", 337 342 "defaultView": "Standaardweergave", 338 343 "defaultViewDescription": "Kies de weergavemodus voor taken", 339 344 "selectViewMode": "Selecteer een weergavemodus",
+5
i18n/ru-RU.json
··· 424 424 "language": "Язык", 425 425 "languageDescription": "Выберите предпочтительный язык интерфейса", 426 426 "selectLanguage": "Выберите язык", 427 + "firstDayOfWeek": "Первый день недели", 428 + "firstDayOfWeekDescription": "Выберите, начинаются ли календари и недели с воскресенья или понедельника", 429 + "selectFirstDayOfWeek": "Выберите первый день", 430 + "weekStartsOnSunday": "Воскресенье", 431 + "weekStartsOnMonday": "Понедельник", 427 432 "defaultView": "Вид по умолчанию", 428 433 "defaultViewDescription": "Выберите предпочтительный режим отображения задач", 429 434 "selectViewMode": "Выберите режим просмотра",
+20
i18n/schema.json
··· 1660 1660 "selectLanguage": { 1661 1661 "type": "string" 1662 1662 }, 1663 + "firstDayOfWeek": { 1664 + "type": "string" 1665 + }, 1666 + "firstDayOfWeekDescription": { 1667 + "type": "string" 1668 + }, 1669 + "selectFirstDayOfWeek": { 1670 + "type": "string" 1671 + }, 1672 + "weekStartsOnSunday": { 1673 + "type": "string" 1674 + }, 1675 + "weekStartsOnMonday": { 1676 + "type": "string" 1677 + }, 1663 1678 "defaultView": { 1664 1679 "type": "string" 1665 1680 }, ··· 1735 1750 "language", 1736 1751 "languageDescription", 1737 1752 "selectLanguage", 1753 + "firstDayOfWeek", 1754 + "firstDayOfWeekDescription", 1755 + "selectFirstDayOfWeek", 1756 + "weekStartsOnSunday", 1757 + "weekStartsOnMonday", 1738 1758 "defaultView", 1739 1759 "defaultViewDescription", 1740 1760 "selectViewMode",
+5
i18n/uk-UA.json
··· 424 424 "language": "Мова", 425 425 "languageDescription": "Оберіть бажану мову інтерфейсу", 426 426 "selectLanguage": "Обрати мову", 427 + "firstDayOfWeek": "Перший день тижня", 428 + "firstDayOfWeekDescription": "Оберіть, чи календарі та тижні починаються в неділю або понеділок", 429 + "selectFirstDayOfWeek": "Обрати перший день", 430 + "weekStartsOnSunday": "Неділя", 431 + "weekStartsOnMonday": "Понеділок", 427 432 "defaultView": "Перегляд за замовчуванням", 428 433 "defaultViewDescription": "Оберіть бажаний режим перегляду завдань", 429 434 "selectViewMode": "Обрати режим перегляду",