Skip to content

Commit b141ce3

Browse files
authored
Merge pull request #326 from fccview/develop
Lift off!
2 parents 7dc3290 + 1e413be commit b141ce3

64 files changed

Lines changed: 2327 additions & 970 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,13 @@ A self-hosted app for your checklists and notes.
1414

1515
<p align="center">
1616
<a href="https://discord.gg/invite/mMuk2WzVZu">
17-
<img width="40" src="https://skills.syvixor.com/api/icons?i=discord">
17+
<img width="40" src="public/repo-images/discord.svg">
1818
</a>
1919
<a href="https://www.reddit.com/r/jotty">
20-
<img width="40" src="https://skills.syvixor.com/api/icons?i=reddit">
20+
<img width="40" src="public/repo-images/reddit.svg">
2121
</a>
2222
<a href="https://t.me/jottypage">
23-
<img width="40" src="https://skills.syvixor.com/api/icons?i=telegram">
23+
<img width="40" src="public/repo-images/telegram.svg">
2424
</a>
2525
<br />
2626
<br />
@@ -86,6 +86,7 @@ A self-hosted app for your checklists and notes.
8686
## Getting Started
8787

8888
My recommended way to run `jotty·page` is with Docker. You can also use:
89+
8990
- The [Proxmox community script](https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/jotty.sh) for Proxmox VE
9091
- The [Unraid template](howto/UNRAID.md) for Unraid Community Applications
9192

@@ -252,11 +253,10 @@ I will always detail these migrations in the release notes. I _highly recommend_
252253

253254
## Translations
254255

255-
`jotty·page` can be translated in multiple languages, all translations are community driven and can be found in the [app/_translations](app/_translations) directory.
256+
`jotty·page` can be translated in multiple languages, all translations are community driven and can be found in the [app/\_translations](app/_translations) directory.
256257

257258
📖 **For the complete translations documentation, see [howto/TRANSLATIONS.md](howto/TRANSLATIONS.md)**
258259

259-
260260
<a id="custom-themes-and-emojis"></a>
261261

262262
## Custom Manifest
@@ -357,6 +357,14 @@ I would like to thank the following members for raising issues and help test/deb
357357
<a href="https://github.com/rcallison"><img width="100" height="100" src="https://avatars.githubusercontent.com/u/535687?s=100&v=4"><br />rcallison</a>
358358
</td>
359359
</tr>
360+
<tr>
361+
<td align="center" valign="top" width="20%">
362+
<a href="https://github.com/BaccanoMob"><img width="100" height="100" src="https://avatars.githubusercontent.com/u/82655889?s=100&v=4"><br />BaccanoMob</a>
363+
</td>
364+
<td align="center" valign="top" width="20%">
365+
<a href="https://github.com/DaxtonD"><img width="100" height="100" src="https://avatars.githubusercontent.com/u/96708688?s=100&v=4"><br />DaxtonD</a>
366+
</td>
367+
</tr>
360368
</tbody>
361369
</table>
362370

app/(loggedOutRoutes)/auth/login/page.tsx

Lines changed: 4 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import { redirect } from "next/navigation";
22
import { hasUsers } from "@/app/_server/actions/users";
3-
import LoginForm from "@/app/(loggedOutRoutes)/auth/login/login-form";
3+
import LoginForm from "@/app/_components/GlobalComponents/Auth/LoginForm";
44
import { AuthShell } from "@/app/_components/GlobalComponents/Auth/AuthShell";
55
import { getTranslations } from "next-intl/server";
6+
import { SsoOnlyLogin } from "@/app/_components/GlobalComponents/Auth/SsoOnlyLogin";
67

78
export const dynamic = "force-dynamic";
89

910
export default async function LoginPage() {
10-
const t = await getTranslations('auth');
11+
const t = await getTranslations("auth");
1112
const ssoEnabled = process.env.SSO_MODE === "oidc";
1213
const allowLocal =
1314
process.env.SSO_FALLBACK_LOCAL &&
@@ -23,26 +24,7 @@ export default async function LoginPage() {
2324
}
2425

2526
if (ssoEnabled && !allowLocal) {
26-
return (
27-
<AuthShell>
28-
<div className="space-y-6 text-center">
29-
<div className="space-y-2">
30-
<h1 className="text-2xl font-bold tracking-tight text-foreground">
31-
{t('welcomeBack')}
32-
</h1>
33-
<p className="text-md lg:text-sm text-muted-foreground">
34-
{t('signInWithOIDC')}
35-
</p>
36-
</div>
37-
<a
38-
className="inline-flex items-center justify-center rounded-jotty text-md lg:text-sm font-medium bg-primary text-primary-foreground hover:bg-primary/90 h-10 px-4 py-2 w-full"
39-
href="/api/oidc/login"
40-
>
41-
{t('signInButton')}
42-
</a>
43-
</div>
44-
</AuthShell>
45-
);
27+
return <SsoOnlyLogin />;
4628
}
4729

4830
return (
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
"use client";
2+
3+
import { useEffect, useState } from "react";
4+
import { useSearchParams } from "next/navigation";
5+
import { useTranslations } from "next-intl";
6+
import { useAppMode } from "@/app/_providers/AppModeProvider";
7+
import { AuthShell } from "@/app/_components/GlobalComponents/Auth/AuthShell";
8+
9+
export const SsoOnlyLogin = () => {
10+
const t = useTranslations("auth");
11+
const searchParams = useSearchParams();
12+
const [error, setError] = useState<string>("");
13+
const { appVersion } = useAppMode();
14+
15+
useEffect(() => {
16+
const errorParam = searchParams.get("error");
17+
if (errorParam === "unauthorized") {
18+
setError(t("notAuthorized"));
19+
}
20+
}, [searchParams, t]);
21+
22+
return (
23+
<AuthShell>
24+
<div className="space-y-6 text-center">
25+
<div className="space-y-2">
26+
<h1 className="text-2xl font-bold tracking-tight text-foreground">
27+
{t("welcomeBack")}
28+
</h1>
29+
<p className="text-md lg:text-sm text-muted-foreground">
30+
{t("signInWithOIDC")}
31+
</p>
32+
</div>
33+
34+
{error && (
35+
<div className="flex items-center gap-2 p-3 bg-destructive/10 border border-destructive/20 rounded-jotty">
36+
<span className="text-md lg:text-sm text-destructive">{error}</span>
37+
</div>
38+
)}
39+
40+
<a
41+
className="inline-flex items-center justify-center rounded-jotty text-md lg:text-sm font-medium bg-primary text-primary-foreground hover:bg-primary/90 h-10 px-4 py-2 w-full"
42+
href="/api/oidc/login"
43+
>
44+
{t("signInButton")}
45+
</a>
46+
47+
{appVersion && (
48+
<div className="text-center text-sm lg:text-xs text-muted-foreground">
49+
<a
50+
target="_blank"
51+
href={`https://github.com/fccview/jotty/releases/tag/${appVersion}`}
52+
>
53+
{t("version", { version: appVersion })}
54+
</a>
55+
</div>
56+
)}
57+
</div>
58+
</AuthShell>
59+
);
60+
};

app/(loggedOutRoutes)/auth/setup/page.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,15 @@ import { AuthShell } from "@/app/_components/GlobalComponents/Auth/AuthShell";
66
export const dynamic = "force-dynamic";
77

88
export default async function SetupPage() {
9+
const allowLocal =
10+
process.env.SSO_FALLBACK_LOCAL &&
11+
process.env.SSO_FALLBACK_LOCAL !== "no" &&
12+
process.env.SSO_FALLBACK_LOCAL !== "false";
13+
14+
if (!allowLocal) {
15+
redirect("/auth/login");
16+
}
17+
918
const hasExistingUsers = await hasUsers();
1019
if (hasExistingUsers) {
1120
redirect("/auth/login");

app/_components/FeatureComponents/Checklists/Checklist.tsx

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ export const ChecklistView = ({
8888
onEdit={() => onEdit?.(list)}
8989
/>
9090
<div className="flex-1 flex items-center justify-center">
91-
<p>{t('checklists.loadingChecklist')}</p>
91+
<p>{t("checklists.loadingChecklist")}</p>
9292
</div>
9393
</div>
9494
);
@@ -118,12 +118,12 @@ export const ChecklistView = ({
118118
<label className="block">
119119
Deleting {deletingItemsCount} item(s)
120120
</label>
121-
<label>{t('checklists.doNotRefresh')}</label>
121+
<label>{t("checklists.doNotRefresh")}</label>
122122
</>
123123
),
124124
},
125125
]}
126-
onRemove={() => { }}
126+
onRemove={() => {}}
127127
></ToastContainer>
128128
)}
129129

@@ -138,12 +138,12 @@ export const ChecklistView = ({
138138
<label className="block">
139139
Syncing {pendingTogglesCount} item(s)
140140
</label>
141-
<label>{t('checklists.doNotRefresh')}</label>
141+
<label>{t("checklists.doNotRefresh")}</label>
142142
</>
143143
),
144144
},
145145
]}
146-
onRemove={() => { }}
146+
onRemove={() => {}}
147147
></ToastContainer>
148148
)}
149149

@@ -157,8 +157,8 @@ export const ChecklistView = ({
157157
isLoading={isLoading}
158158
autoFocus={true}
159159
focusKey={focusKey}
160-
placeholder={t('checklists.addNewItem')}
161-
submitButtonText={t('checklists.addItem')}
160+
placeholder={t("checklists.addNewItem")}
161+
submitButtonText={t("checklists.addItem")}
162162
/>
163163
)}
164164

@@ -180,4 +180,3 @@ export const ChecklistView = ({
180180
</div>
181181
);
182182
};
183-

app/_components/FeatureComponents/Checklists/ChecklistsClient.tsx

Lines changed: 38 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,13 @@ interface ChecklistsClientProps {
1515
children: React.ReactNode;
1616
}
1717

18-
type ChecklistFilter = "all" | "completed" | "incomplete" | "pinned" | "task" | "simple";
18+
type ChecklistFilter =
19+
| "all"
20+
| "completed"
21+
| "incomplete"
22+
| "pinned"
23+
| "task"
24+
| "simple";
1925

2026
interface ChecklistsFilterContextType {
2127
checklistFilter: ChecklistFilter;
@@ -33,10 +39,13 @@ interface ChecklistsFilterContextType {
3339
onPageChange: (page: number) => void;
3440
onItemsPerPageChange: (items: number) => void;
3541
} | null;
36-
setPaginationInfo: (info: ChecklistsFilterContextType['paginationInfo']) => void;
42+
setPaginationInfo: (
43+
info: ChecklistsFilterContextType["paginationInfo"]
44+
) => void;
3745
}
3846

39-
const ChecklistsFilterContext = createContext<ChecklistsFilterContextType | null>(null);
47+
const ChecklistsFilterContext =
48+
createContext<ChecklistsFilterContextType | null>(null);
4049

4150
export const useChecklistsFilter = () => {
4251
const context = useContext(ChecklistsFilterContext);
@@ -51,22 +60,26 @@ export const ChecklistsClient = ({
5160
user,
5261
children,
5362
}: ChecklistsClientProps) => {
54-
const t = useTranslations('checklists');
55-
const { openSettings, openCreateChecklistModal, openCreateCategoryModal } = useShortcut();
63+
const t = useTranslations("checklists");
64+
const { openSettings, openCreateChecklistModal, openCreateCategoryModal } =
65+
useShortcut();
5666

57-
const [checklistFilter, setChecklistFilter] = useState<ChecklistFilter>(user?.defaultChecklistFilter || "all");
67+
const [checklistFilter, setChecklistFilter] = useState<ChecklistFilter>(
68+
user?.defaultChecklistFilter || "all"
69+
);
5870
const [selectedCategories, setSelectedCategories] = useState<string[]>([]);
5971
const [recursive, setRecursive] = useState(false);
6072
const [itemsPerPage, setItemsPerPage] = useState(12);
61-
const [paginationInfo, setPaginationInfo] = useState<ChecklistsFilterContextType['paginationInfo']>(null);
73+
const [paginationInfo, setPaginationInfo] =
74+
useState<ChecklistsFilterContextType["paginationInfo"]>(null);
6275

6376
const filterOptions = [
64-
{ id: "all", name: t('allChecklists') },
65-
{ id: "completed", name: t('completed') },
66-
{ id: "incomplete", name: t('incomplete') },
67-
{ id: "pinned", name: t('pinned') },
68-
{ id: "task", name: t('taskLists') },
69-
{ id: "simple", name: t('simpleLists') },
77+
{ id: "all", name: t("allChecklists") },
78+
{ id: "completed", name: t("completed") },
79+
{ id: "incomplete", name: t("incomplete") },
80+
{ id: "pinned", name: t("pinned") },
81+
{ id: "task", name: t("taskLists") },
82+
{ id: "simple", name: t("simpleLists") },
7083
];
7184

7285
const handleClearAllCategories = () => {
@@ -98,7 +111,7 @@ export const ChecklistsClient = ({
98111
<FiltersSidebar
99112
isOpen={isOpen}
100113
onClose={onClose}
101-
title={t('byStatus')}
114+
title={t("byStatus")}
102115
filterValue={checklistFilter}
103116
filterOptions={filterOptions}
104117
onFilterChange={(value) => {
@@ -113,14 +126,22 @@ export const ChecklistsClient = ({
113126
currentPage={paginationInfo?.currentPage}
114127
totalPages={paginationInfo?.totalPages}
115128
onPageChange={paginationInfo?.onPageChange}
116-
itemsPerPage={paginationInfo?.totalItems !== undefined ? itemsPerPage : undefined}
129+
itemsPerPage={
130+
paginationInfo?.totalItems !== undefined
131+
? itemsPerPage
132+
: undefined
133+
}
117134
onItemsPerPageChange={paginationInfo?.onItemsPerPageChange}
118135
totalItems={paginationInfo?.totalItems}
119136
/>
120137
)}
121138
>
122-
<MobileHeader user={user} onOpenSettings={openSettings} currentLocale={user?.preferredLocale || "en"} />
123-
139+
<MobileHeader
140+
user={user}
141+
onOpenSettings={openSettings}
142+
currentLocale={user?.preferredLocale || "en"}
143+
/>
144+
124145
<div className="w-full px-4 py-6 h-full overflow-y-auto jotty-scrollable-content">
125146
{children}
126147
</div>

app/_components/FeatureComponents/Checklists/Parts/ChecklistClient.tsx

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { ChecklistView } from "@/app/_components/FeatureComponents/Checklists/Ch
77
import { KanbanBoard } from "@/app/_components/FeatureComponents/Checklists/Parts/Kanban/KanbanBoard";
88
import { ChecklistHeader } from "@/app/_components/FeatureComponents/Checklists/Parts/Common/ChecklistHeader";
99
import { ShareModal } from "@/app/_components/GlobalComponents/Modals/SharingModals/ShareModal";
10-
import { ConversionConfirmModal } from "@/app/_components/GlobalComponents/Modals/ConfirmationModals/ConversionConfirmModal";
10+
import { ConfirmModal } from "@/app/_components/GlobalComponents/Modals/ConfirmationModals/ConfirmModal";
1111
import { EditChecklistModal } from "@/app/_components/GlobalComponents/Modals/ChecklistModals/EditChecklistModal";
1212
import { CreateListModal } from "@/app/_components/GlobalComponents/Modals/ChecklistModals/CreateListModal";
1313
import { CreateCategoryModal } from "@/app/_components/GlobalComponents/Modals/CategoryModals/CreateCategoryModal";
@@ -18,6 +18,7 @@ import { Modes } from "@/app/_types/enums";
1818
import { useShortcut } from "@/app/_providers/ShortcutsProvider";
1919
import { toggleArchive } from "@/app/_server/actions/dashboard";
2020
import { buildCategoryPath } from "@/app/_utils/global-utils";
21+
import { useTranslations } from "next-intl";
2122

2223
interface ChecklistClientProps {
2324
checklist: Checklist;
@@ -31,6 +32,7 @@ export const ChecklistClient = ({
3132
user,
3233
}: ChecklistClientProps) => {
3334
const router = useRouter();
35+
const t = useTranslations();
3436
const { checkNavigation } = useNavigationGuard();
3537
const [localChecklist, setLocalChecklist] = useState<Checklist>(checklist);
3638
const [showShareModal, setShowShareModal] = useState(false);
@@ -160,6 +162,7 @@ export const ChecklistClient = ({
160162
onOpenCreateModal={openCreateChecklistModal}
161163
onOpenCategoryModal={openCreateCategoryModal}
162164
user={user}
165+
extraClasses="jotty-checklist-page"
163166
>
164167
{renderContent()}
165168

@@ -174,12 +177,16 @@ export const ChecklistClient = ({
174177
)}
175178

176179
{showConversionModal && (
177-
<ConversionConfirmModal
180+
<ConfirmModal
178181
isOpen={showConversionModal}
179182
onClose={() => setShowConversionModal(false)}
180183
onConfirm={handleConfirmConversion}
181-
currentType={localChecklist.type}
182-
newType={getNewType(localChecklist.type)}
184+
title={t("checklists.convertChecklistType")}
185+
message={t("checklists.convertTypeConfirmation", {
186+
currentType: localChecklist.type === "simple" ? t("checklists.simpleChecklist") : t("checklists.taskProject"),
187+
newType: getNewType(localChecklist.type) === "simple" ? t("checklists.simpleChecklist") : t("checklists.taskProject")
188+
})}
189+
confirmText={t("checklists.convert")}
183190
/>
184191
)}
185192

0 commit comments

Comments
 (0)