Skip to content

Commit 948580e

Browse files
committed
feat: user settings export
1 parent 61fa1b6 commit 948580e

16 files changed

+105
-30
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { apiClient } from "@/app/apiClient";
2+
3+
export const GET = async (_request: Request) => {
4+
const { my_user: loggedInUser } = await apiClient.getSite();
5+
6+
if (!loggedInUser) {
7+
return Response.json({ status: "not_authorized" }, { status: 401 });
8+
}
9+
10+
const res = await apiClient.exportSettings();
11+
12+
const fileName = `export_${new URL(loggedInUser.local_user_view.person.actor_id).hostname}_${loggedInUser.local_user_view.person.name}_${Number(new Date())}.json`;
13+
return Response.json(res, {
14+
headers: { "Content-Disposition": `attachment; filename=${fileName}` },
15+
});
16+
};

src/app/(ui)/StyledLink.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import Link, { LinkProps } from "next/link";
55
type Props = {
66
readonly children?: React.ReactNode;
77
readonly className?: string;
8+
readonly download?: string;
89
} & LinkProps &
910
React.RefAttributes<HTMLAnchorElement>;
1011

src/app/(ui)/button/ButtonLink.tsx

+6-1
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,16 @@ type ButtonLinkProps = Pick<
77
"className" | "children" | "size" | "color"
88
> & {
99
readonly href: Route;
10+
readonly download?: string;
1011
};
1112

1213
export const ButtonLink = (props: ButtonLinkProps) => {
1314
return (
14-
<StyledLink className={getButtonClassnames(props)} href={props.href}>
15+
<StyledLink
16+
className={getButtonClassnames(props)}
17+
download={props.download}
18+
href={props.href}
19+
>
1520
{props.children}
1621
</StyledLink>
1722
);

src/app/(ui)/markdown/Prose.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ export const Prose = (props: Props) => {
1414
`min-w-none prose prose-sm prose-neutral prose-invert max-w-full break-words
1515
prose-p:mx-0 prose-p:my-1 prose-p:break-words prose-p:leading-snug
1616
prose-a:text-primary-400 prose-a:no-underline hover:prose-a:text-primary-300
17-
prose-ul:list-disc prose-li:leading-snug prose-hr:border-neutral-700`,
17+
prose-ul:list-disc prose-li:leading-snug prose-hr:border-dashed
18+
prose-hr:border-neutral-600`,
1819
props.className,
1920
)}
2021
>

src/app/Navbar.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import classNames from "classnames";
99
import { NavbarCollapsibleLinks } from "@/app/NavbarCollapsibleLinks";
1010
import { MagnifyingGlassIcon } from "@heroicons/react/16/solid";
1111
import { LoggedInUserIcons } from "@/app/u/LoggedInUserIcons";
12-
import { getUnreadCounts } from "@/app/settings/loggedInUserActions";
12+
import { getUnreadCounts } from "@/app/settings/userActions";
1313

1414
export const Navbar = async () => {
1515
const { site_view: siteView, my_user: loggedInUser } =

src/app/settings/2fa/disable/page.tsx

+1-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
import { apiClient } from "@/app/apiClient";
2-
import { StyledLink } from "@/app/(ui)/StyledLink";
3-
import { disable2faAction } from "@/app/settings/loggedInUserActions";
1+
import { disable2faAction } from "@/app/settings/userActions";
42
import { Input } from "@/app/(ui)/form/Input";
53
import { SubmitButton } from "@/app/(ui)/button/SubmitButton";
64

src/app/settings/2fa/enable/page.tsx

+1-4
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
import { apiClient } from "@/app/apiClient";
2-
import {
3-
disable2faAction,
4-
enable2faAction,
5-
} from "@/app/settings/loggedInUserActions";
2+
import { enable2faAction } from "@/app/settings/userActions";
63
import { Input } from "@/app/(ui)/form/Input";
74
import { SubmitButton } from "@/app/(ui)/button/SubmitButton";
85
import * as QRCode from "qrcode";

src/app/settings/AuthForm.tsx

+2-3
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,10 @@
22

33
import { SettingsInputWithLabel } from "@/app/settings/SettingsInputWithLabel";
44
import { MyUserInfo } from "lemmy-js-client";
5-
import { postListSortOptions } from "@/app/post/postListSortOptions";
65
import {
76
changePasswordAction,
87
updateEmailAction,
9-
updateSettingsAction,
10-
} from "@/app/settings/loggedInUserActions";
8+
} from "@/app/settings/userActions";
119
import { SubmitButton } from "@/app/(ui)/button/SubmitButton";
1210
import { ButtonLink } from "@/app/(ui)/button/ButtonLink";
1311

@@ -43,6 +41,7 @@ export const AuthForm = (props: { readonly loggedInUser: MyUserInfo }) => {
4341
className={"hidden"}
4442
id={"username"}
4543
name={"username"}
44+
readOnly={true}
4645
type={"text"}
4746
value={props.loggedInUser.local_user_view.person.name}
4847
/>

src/app/settings/BlocksForm.tsx

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
"use client";
2+
3+
import { MyUserInfo } from "lemmy-js-client";
4+
import { updateSettingsAction } from "@/app/settings/userActions";
5+
6+
export const BlocksForm = (props: { readonly loggedInUser: MyUserInfo }) => {
7+
return (
8+
<form
9+
action={updateSettingsAction}
10+
className={"flex max-w-96 flex-col gap-4"}
11+
>
12+
<h1 className={"text-xl font-bold"}>{"Blocks"}</h1>
13+
<p>{"(NOT IMPLEMENTED YET)"}</p>
14+
</form>
15+
);
16+
};

src/app/settings/ExportImportForm.tsx

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
"use client";
2+
3+
import { MyUserInfo } from "lemmy-js-client";
4+
import { importSettingsAction } from "@/app/settings/userActions";
5+
import { SubmitButton } from "@/app/(ui)/button/SubmitButton";
6+
import { Input } from "@/app/(ui)/form/Input";
7+
import { ButtonLink } from "@/app/(ui)/button/ButtonLink";
8+
9+
export const ExportImportForm = (props: {
10+
readonly loggedInUser: MyUserInfo;
11+
}) => {
12+
return (
13+
<form
14+
action={importSettingsAction}
15+
className={"flex max-w-96 flex-col gap-4"}
16+
>
17+
<h1 className={"text-xl font-bold"}>{"Import/export settings"}</h1>
18+
<p>{"Import and export your account settings as JSON"}</p>
19+
<label>
20+
{"Export"}
21+
<ButtonLink
22+
download={`export_${new URL(props.loggedInUser.local_user_view.person.actor_id).hostname}_${props.loggedInUser.local_user_view.person.name}_${Number(new Date())}.json`}
23+
href={"/next/api/settings/export.json"}
24+
>
25+
{"Export"}
26+
</ButtonLink>
27+
</label>
28+
<div className={"border-t border-dashed border-neutral-700 pt-2"}>
29+
<label>{"Import (NOT IMPLEMENTED YET)"}</label>
30+
<Input
31+
disabled={true}
32+
id={"imported_file"}
33+
name={"imported_file"}
34+
required={true}
35+
type={"file"}
36+
/>
37+
<SubmitButton className={"mt-2 w-full"} disabled={true}>
38+
{"Import"}
39+
</SubmitButton>
40+
</div>
41+
</form>
42+
);
43+
};

src/app/settings/ProfileForm.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import { SettingsInputWithLabel } from "@/app/settings/SettingsInputWithLabel";
44
import { MyUserInfo } from "lemmy-js-client";
5-
import { updateProfileAction } from "@/app/settings/loggedInUserActions";
5+
import { updateProfileAction } from "@/app/settings/userActions";
66
import { SubmitButton } from "@/app/(ui)/button/SubmitButton";
77
import { ProfileImageInput } from "@/app/settings/ProfileImageInput";
88

src/app/settings/ProfileImageInput.tsx

+2-6
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
import classNames from "classnames";
22
import { Input } from "@/app/(ui)/form/Input";
3-
import { Select } from "@/app/(ui)/form/Select";
4-
import { MarkdownTextArea } from "@/app/(ui)/markdown/MarkdownTextArea";
53
import { Image } from "@/app/(ui)/Image";
64
import { ChangeEvent, MouseEvent, useState } from "react";
75
import {
@@ -10,10 +8,7 @@ import {
108
} from "@/app/(ui)/markdown/imageActions";
119
import { Button } from "@/app/(ui)/button/Button";
1210
import { TrashIcon } from "@heroicons/react/16/solid";
13-
import {
14-
removeUserAvatar,
15-
removeUserBanner,
16-
} from "@/app/settings/loggedInUserActions";
11+
import { removeUserAvatar, removeUserBanner } from "@/app/settings/userActions";
1712
import { Spinner } from "@/app/(ui)/Spinner";
1813

1914
type Props = {
@@ -166,6 +161,7 @@ export const ProfileImageInput = (props: Props) => {
166161
className={"hidden"}
167162
id={props.inputId}
168163
name={props.inputId}
164+
readOnly={true}
169165
value={url}
170166
/>
171167
</div>

src/app/settings/SettingsForm.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,14 @@
33
import { SettingsInputWithLabel } from "@/app/settings/SettingsInputWithLabel";
44
import { MyUserInfo } from "lemmy-js-client";
55
import { postListSortOptions } from "@/app/post/postListSortOptions";
6-
import { updateSettingsAction } from "@/app/settings/loggedInUserActions";
6+
import { updateSettingsAction } from "@/app/settings/userActions";
77
import { SubmitButton } from "@/app/(ui)/button/SubmitButton";
88

99
export const SettingsForm = (props: { readonly loggedInUser: MyUserInfo }) => {
1010
return (
1111
<form
1212
action={updateSettingsAction}
13-
className={"flex max-w-96 flex-col gap-4"}
13+
className={"flex w-full max-w-96 flex-col gap-4"}
1414
>
1515
<h1 className={"text-xl font-bold"}>{"Preferences"}</h1>
1616
<SettingsInputWithLabel

src/app/settings/page.tsx

+6-4
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@ import { apiClient } from "@/app/apiClient";
66
import { ProfileForm } from "@/app/settings/ProfileForm";
77
import { SettingsForm } from "@/app/settings/SettingsForm";
88
import { AuthForm } from "@/app/settings/AuthForm";
9-
import { log } from "node:util";
109
import { ReactNode } from "react";
1110
import classNames from "classnames";
11+
import { ExportImportForm } from "@/app/settings/ExportImportForm";
12+
import { BlocksForm } from "@/app/settings/BlocksForm";
1213

1314
const SettingsPage = async () => {
1415
const { my_user: loggedInUser } = await apiClient.getSite();
@@ -44,10 +45,10 @@ const SettingsPage = async () => {
4445
<AuthForm loggedInUser={loggedInUser} />
4546
</Section>
4647
<Section className={"border-l-0 lg:border-l xl:border-l-0"}>
47-
<h1 className={"text-xl"}>{"Blocks"}</h1>
48+
<BlocksForm loggedInUser={loggedInUser} />
4849
</Section>
4950
<Section className={"border-l-0 lg:border-l-0 xl:border-l"}>
50-
<h1 className={"text-xl"}>{"Data"}</h1>
51+
<ExportImportForm loggedInUser={loggedInUser} />
5152
</Section>
5253
<Section className={"border-l-0 lg:border-l xl:border-l"}>
5354
{null}
@@ -64,7 +65,8 @@ const Section = (props: {
6465
return (
6566
<div
6667
className={classNames(
67-
"w-full border-l border-t border-neutral-700 p-10",
68+
`flex w-full justify-center border-l border-t border-neutral-700 p-4 lg:p-10
69+
xl:justify-start`,
6870
props.className,
6971
)}
7072
>

src/app/settings/loggedInUserActions.ts src/app/settings/userActions.ts

+4
Original file line numberDiff line numberDiff line change
@@ -142,3 +142,7 @@ export const removeUserBanner = async () => {
142142
revalidatePath("/settings");
143143
revalidatePath("/");
144144
};
145+
146+
export const importSettingsAction = async (settings: object) => {
147+
await apiClient.importSettings(settings);
148+
};

src/app/u/LoggedInUserIcons.tsx

+1-4
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,7 @@ import {
99
import { ClipboardIcon } from "@heroicons/react/20/solid";
1010
import { MyUserInfo } from "lemmy-js-client";
1111
import classNames from "classnames";
12-
import {
13-
getUnreadCounts,
14-
UnreadCounts,
15-
} from "@/app/settings/loggedInUserActions";
12+
import { getUnreadCounts, UnreadCounts } from "@/app/settings/userActions";
1613
import { useInterval } from "usehooks-ts";
1714
import { useState } from "react";
1815

0 commit comments

Comments
 (0)