Skip to content

Commit 6f35c1a

Browse files
committed
qr code specimen sharing & bump to 1.7.3
1 parent d21c1c6 commit 6f35c1a

File tree

15 files changed

+1148
-298
lines changed

15 files changed

+1148
-298
lines changed

android/app/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ android {
88
minSdkVersion rootProject.ext.minSdkVersion
99
targetSdkVersion rootProject.ext.targetSdkVersion
1010
versionCode 1
11-
versionName "1.7"
11+
versionName "1.7.5"
1212
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
1313
aaptOptions {
1414
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.

android/app/src/main/AndroidManifest.xml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,14 @@
2222
<category android:name="android.intent.category.LAUNCHER" />
2323
</intent-filter>
2424

25+
<!-- Handle custom Moltly scheme deep links -->
26+
<intent-filter android:autoVerify="false">
27+
<action android:name="android.intent.action.VIEW" />
28+
<category android:name="android.intent.category.DEFAULT" />
29+
<category android:name="android.intent.category.BROWSABLE" />
30+
<data android:scheme="moltly" />
31+
</intent-filter>
32+
2533
</activity>
2634

2735
<provider

app/api/specimens/copy/route.ts

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
export const dynamic = "force-dynamic";
2+
export const runtime = "nodejs";
3+
4+
import { NextResponse } from "next/server";
5+
import { getServerSession } from "next-auth";
6+
import { authOptions } from "@/lib/auth-options";
7+
import { connectMongoose } from "@/lib/mongoose";
8+
import MoltEntry from "@/models/MoltEntry";
9+
import HealthEntry from "@/models/HealthEntry";
10+
import BreedingEntry from "@/models/BreedingEntry";
11+
import SpecimenCover from "@/models/SpecimenCover";
12+
import { Types } from "mongoose";
13+
14+
const escapeRegex = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
15+
16+
export async function POST(request: Request) {
17+
const session = await getServerSession(authOptions);
18+
if (!session?.user?.id) {
19+
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
20+
}
21+
22+
try {
23+
const { specimen, ownerId } = (await request.json()) as { specimen?: string; ownerId?: string };
24+
if (!specimen || typeof specimen !== "string" || !specimen.trim()) {
25+
return NextResponse.json({ error: "Invalid specimen" }, { status: 400 });
26+
}
27+
if (!ownerId || typeof ownerId !== "string" || !ownerId.trim()) {
28+
return NextResponse.json({ error: "Invalid owner" }, { status: 400 });
29+
}
30+
31+
let ownerObjectId: Types.ObjectId;
32+
try {
33+
ownerObjectId = new Types.ObjectId(ownerId.trim());
34+
} catch {
35+
return NextResponse.json({ error: "Invalid owner" }, { status: 400 });
36+
}
37+
38+
await connectMongoose();
39+
const match = new RegExp(`^${escapeRegex(specimen.trim())}$`, "i");
40+
41+
const [moltEntries, healthEntries, breedingEntries, cover] = await Promise.all([
42+
MoltEntry.find({ userId: ownerObjectId, specimen: match }).lean(),
43+
HealthEntry.find({ userId: ownerObjectId, specimen: match }).lean(),
44+
BreedingEntry.find({
45+
userId: ownerObjectId,
46+
$or: [{ femaleSpecimen: match }, { maleSpecimen: match }],
47+
}).lean(),
48+
SpecimenCover.findOne({ userId: ownerObjectId, key: match }).lean(),
49+
]);
50+
51+
const now = new Date();
52+
const mappedMolt = moltEntries.map((e) => {
53+
const { _id, userId, createdAt, updatedAt, ...rest } = e as any;
54+
return {
55+
...rest,
56+
userId: session.user!.id,
57+
createdAt: now,
58+
updatedAt: now,
59+
};
60+
});
61+
62+
const mappedHealth = healthEntries.map((e) => {
63+
const { _id, userId, createdAt, updatedAt, ...rest } = e as any;
64+
return {
65+
...rest,
66+
userId: session.user!.id,
67+
createdAt: now,
68+
updatedAt: now,
69+
};
70+
});
71+
72+
const mappedBreeding = breedingEntries.map((e) => {
73+
const { _id, userId, createdAt, updatedAt, ...rest } = e as any;
74+
return {
75+
...rest,
76+
userId: session.user!.id,
77+
createdAt: now,
78+
updatedAt: now,
79+
};
80+
});
81+
82+
const results = await Promise.all([
83+
mappedMolt.length ? MoltEntry.insertMany(mappedMolt) : [],
84+
mappedHealth.length ? HealthEntry.insertMany(mappedHealth) : [],
85+
mappedBreeding.length ? BreedingEntry.insertMany(mappedBreeding) : [],
86+
cover
87+
? SpecimenCover.updateOne(
88+
{ userId: session.user.id, key: specimen.trim() },
89+
{ $set: { imageUrl: (cover as any).imageUrl } },
90+
{ upsert: true }
91+
)
92+
: null,
93+
]);
94+
95+
return NextResponse.json({
96+
copied: {
97+
molt: mappedMolt.length,
98+
health: mappedHealth.length,
99+
breeding: mappedBreeding.length,
100+
cover: Boolean(cover),
101+
},
102+
});
103+
} catch (err) {
104+
console.error("Failed to copy specimen", err);
105+
return NextResponse.json({ error: "Failed to copy specimen" }, { status: 500 });
106+
}
107+
}

app/api/specimens/shared/route.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
export const dynamic = "force-dynamic";
2+
export const runtime = "nodejs";
3+
4+
import { NextResponse } from "next/server";
5+
import { connectMongoose } from "@/lib/mongoose";
6+
import MoltEntry from "@/models/MoltEntry";
7+
import HealthEntry from "@/models/HealthEntry";
8+
import BreedingEntry from "@/models/BreedingEntry";
9+
import SpecimenCover from "@/models/SpecimenCover";
10+
import { Types } from "mongoose";
11+
12+
const escapeRegex = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
13+
14+
export async function GET(request: Request) {
15+
const { searchParams } = new URL(request.url);
16+
const specimen = (searchParams.get("specimen") || "").trim();
17+
const ownerId = (searchParams.get("owner") || "").trim();
18+
if (!specimen || !ownerId) {
19+
return NextResponse.json({ error: "Missing specimen or owner" }, { status: 400 });
20+
}
21+
22+
try {
23+
let ownerObjectId: Types.ObjectId;
24+
try {
25+
ownerObjectId = new Types.ObjectId(ownerId);
26+
} catch {
27+
return NextResponse.json({ error: "Invalid owner id" }, { status: 400 });
28+
}
29+
await connectMongoose();
30+
const match = new RegExp(`^${escapeRegex(specimen)}$`, "i");
31+
32+
const [moltEntries, healthEntries, breedingEntries, cover] = await Promise.all([
33+
MoltEntry.find({ userId: ownerObjectId, specimen: match }).sort({ date: -1 }).lean(),
34+
HealthEntry.find({ userId: ownerObjectId, specimen: match }).sort({ date: -1 }).lean(),
35+
BreedingEntry.find({
36+
userId: ownerObjectId,
37+
$or: [{ femaleSpecimen: match }, { maleSpecimen: match }],
38+
})
39+
.sort({ pairingDate: -1 })
40+
.lean(),
41+
SpecimenCover.findOne({ userId: ownerObjectId, key: match }).lean(),
42+
]);
43+
44+
const normalize = (doc: any) => ({
45+
...doc,
46+
id: doc._id?.toString() ?? "",
47+
_id: undefined,
48+
userId: undefined,
49+
});
50+
51+
return NextResponse.json({
52+
entries: moltEntries.map(normalize),
53+
health: healthEntries.map(normalize),
54+
breeding: breedingEntries.map(normalize),
55+
cover: cover?.imageUrl ?? null,
56+
});
57+
} catch (err) {
58+
console.error("Failed to load shared specimen", err);
59+
return NextResponse.json({ error: "Failed to load shared specimen" }, { status: 500 });
60+
}
61+
}

app/layout.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ export const metadata: Metadata = {
66
title: "Moltly",
77
description: "Moltly (moltly.xyz) keeps every tarantula molt, reminder, and husbandry detail in sync.",
88
manifest: "/manifest.webmanifest",
9-
themeColor: "#0B0B0B",
109
appleWebApp: {
1110
capable: true,
1211
statusBarStyle: "black-translucent",

app/page.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
1+
import { Suspense } from "react";
12
import MobileDashboard from "../components/MobileDashboard";
23

34
export default async function HomePage() {
4-
return <MobileDashboard />;
5+
return (
6+
<Suspense fallback={null}>
7+
<MobileDashboard />
8+
</Suspense>
9+
);
510
}

bun.lock

Lines changed: 8 additions & 12 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)