Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 94b261fa32 | |||
| c684b54aef | |||
| ffc037b854 | |||
| 5990d4ce2d | |||
| bae4cf1d4f | |||
| 4a0e75c6e5 | |||
| 3feeecdc1d |
@@ -12,11 +12,11 @@ export default function AnnouncementBar({ toggleAnnouncementBar }: Props) {
|
|||||||
<div className="w-fit font-semibold">
|
<div className="w-fit font-semibold">
|
||||||
🎉️ See what's new in{" "}
|
🎉️ See what's new in{" "}
|
||||||
<Link
|
<Link
|
||||||
href="https://blog.linkwarden.app/releases/v2.4"
|
href="https://blog.linkwarden.app/releases/v2.5"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
className="underline hover:opacity-50 duration-100"
|
className="underline hover:opacity-50 duration-100"
|
||||||
>
|
>
|
||||||
Linkwarden v2.4
|
Linkwarden v2.5
|
||||||
</Link>
|
</Link>
|
||||||
! 🥳️
|
! 🥳️
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -47,7 +47,10 @@ const CollectionListing = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (account.username) {
|
if (account.username) {
|
||||||
if (!account.collectionOrder || account.collectionOrder.length === 0)
|
if (
|
||||||
|
(!account.collectionOrder || account.collectionOrder.length === 0) &&
|
||||||
|
collections.length > 0
|
||||||
|
)
|
||||||
updateAccount({
|
updateAccount({
|
||||||
...account,
|
...account,
|
||||||
collectionOrder: collections
|
collectionOrder: collections
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export default function FilterSearchDropdown({
|
|||||||
>
|
>
|
||||||
<i className="bi-funnel text-neutral text-2xl"></i>
|
<i className="bi-funnel text-neutral text-2xl"></i>
|
||||||
</div>
|
</div>
|
||||||
<ul className="dropdown-content z-[30] menu shadow bg-base-200 border border-neutral-content rounded-box w-44 mt-1">
|
<ul className="dropdown-content z-[30] menu shadow bg-base-200 border border-neutral-content rounded-box w-56 mt-1">
|
||||||
<li>
|
<li>
|
||||||
<label
|
<label
|
||||||
className="label cursor-pointer flex justify-start"
|
className="label cursor-pointer flex justify-start"
|
||||||
@@ -84,27 +84,6 @@ export default function FilterSearchDropdown({
|
|||||||
<span className="label-text">Description</span>
|
<span className="label-text">Description</span>
|
||||||
</label>
|
</label>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
|
||||||
<label
|
|
||||||
className="label cursor-pointer flex justify-start"
|
|
||||||
tabIndex={0}
|
|
||||||
role="button"
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
name="search-filter-checkbox"
|
|
||||||
className="checkbox checkbox-primary"
|
|
||||||
checked={searchFilter.textContent}
|
|
||||||
onChange={() => {
|
|
||||||
setSearchFilter({
|
|
||||||
...searchFilter,
|
|
||||||
textContent: !searchFilter.textContent,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<span className="label-text">Full Content</span>
|
|
||||||
</label>
|
|
||||||
</li>
|
|
||||||
<li>
|
<li>
|
||||||
<label
|
<label
|
||||||
className="label cursor-pointer flex justify-start"
|
className="label cursor-pointer flex justify-start"
|
||||||
@@ -126,6 +105,29 @@ export default function FilterSearchDropdown({
|
|||||||
<span className="label-text">Tags</span>
|
<span className="label-text">Tags</span>
|
||||||
</label>
|
</label>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<label
|
||||||
|
className="label cursor-pointer flex justify-between"
|
||||||
|
tabIndex={0}
|
||||||
|
role="button"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
name="search-filter-checkbox"
|
||||||
|
className="checkbox checkbox-primary"
|
||||||
|
checked={searchFilter.textContent}
|
||||||
|
onChange={() => {
|
||||||
|
setSearchFilter({
|
||||||
|
...searchFilter,
|
||||||
|
textContent: !searchFilter.textContent,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span className="label-text">Full Content</span>
|
||||||
|
|
||||||
|
<div className="ml-auto badge badge-sm badge-neutral">Slower</div>
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
import LinkCard from "@/components/LinkViews/LinkCard";
|
import LinkCard from "@/components/LinkViews/LinkCard";
|
||||||
import { LinkIncludingShortenedCollectionAndTags } from "@/types/global";
|
import { LinkIncludingShortenedCollectionAndTags } from "@/types/global";
|
||||||
|
import { link } from "fs";
|
||||||
|
import { GridLoader } from "react-spinners";
|
||||||
|
|
||||||
export default function CardView({
|
export default function CardView({
|
||||||
links,
|
links,
|
||||||
showCheckbox = true,
|
|
||||||
editMode,
|
editMode,
|
||||||
|
isLoading,
|
||||||
}: {
|
}: {
|
||||||
links: LinkIncludingShortenedCollectionAndTags[];
|
links: LinkIncludingShortenedCollectionAndTags[];
|
||||||
showCheckbox?: boolean;
|
|
||||||
editMode?: boolean;
|
editMode?: boolean;
|
||||||
|
isLoading?: boolean;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="grid min-[1900px]:grid-cols-4 xl:grid-cols-3 sm:grid-cols-2 grid-cols-1 gap-5">
|
<div className="grid min-[1900px]:grid-cols-4 xl:grid-cols-3 sm:grid-cols-2 grid-cols-1 gap-5">
|
||||||
@@ -23,6 +25,15 @@ export default function CardView({
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
|
{isLoading && links.length > 0 && (
|
||||||
|
<GridLoader
|
||||||
|
color="oklch(var(--p))"
|
||||||
|
loading={true}
|
||||||
|
size={20}
|
||||||
|
className="fixed top-5 right-5 opacity-50 z-30"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
import LinkList from "@/components/LinkViews/LinkList";
|
import LinkList from "@/components/LinkViews/LinkList";
|
||||||
import { LinkIncludingShortenedCollectionAndTags } from "@/types/global";
|
import { LinkIncludingShortenedCollectionAndTags } from "@/types/global";
|
||||||
|
import { GridLoader } from "react-spinners";
|
||||||
|
|
||||||
export default function ListView({
|
export default function ListView({
|
||||||
links,
|
links,
|
||||||
editMode,
|
editMode,
|
||||||
|
isLoading,
|
||||||
}: {
|
}: {
|
||||||
links: LinkIncludingShortenedCollectionAndTags[];
|
links: LinkIncludingShortenedCollectionAndTags[];
|
||||||
editMode?: boolean;
|
editMode?: boolean;
|
||||||
|
isLoading?: boolean;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-1 flex-col">
|
<div className="flex gap-1 flex-col">
|
||||||
@@ -21,6 +24,15 @@ export default function ListView({
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
|
{isLoading && links.length > 0 && (
|
||||||
|
<GridLoader
|
||||||
|
color="oklch(var(--p))"
|
||||||
|
loading={true}
|
||||||
|
size={20}
|
||||||
|
className="fixed top-5 right-5 opacity-50 z-30"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -162,12 +162,18 @@ export default function LinkCard({ link, flipDropdown, editMode }: Props) {
|
|||||||
{unescapeString(link.name || link.description) || link.url}
|
{unescapeString(link.name || link.description) || link.url}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div title={link.url || ""} className="w-fit">
|
<Link
|
||||||
<div className="flex gap-1 item-center select-none text-neutral mt-1">
|
href={link.url || ""}
|
||||||
<i className="bi-link-45deg text-lg mt-[0.15rem] leading-none"></i>
|
target="_blank"
|
||||||
<p className="text-sm truncate">{shortendURL}</p>
|
title={link.url || ""}
|
||||||
</div>
|
onClick={(e) => {
|
||||||
</div>
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
className="flex gap-1 item-center select-none text-neutral mt-1 hover:opacity-70 duration-100"
|
||||||
|
>
|
||||||
|
<i className="bi-link-45deg text-lg mt-[0.10rem] leading-none"></i>
|
||||||
|
<p className="text-sm truncate">{shortendURL}</p>
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr className="divider mt-2 mb-1 last:hidden border-t border-neutral-content h-[1px]" />
|
<hr className="divider mt-2 mb-1 last:hidden border-t border-neutral-content h-[1px]" />
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import {
|
|||||||
CollectionIncludingMembersAndLinkCount,
|
CollectionIncludingMembersAndLinkCount,
|
||||||
LinkIncludingShortenedCollectionAndTags,
|
LinkIncludingShortenedCollectionAndTags,
|
||||||
} from "@/types/global";
|
} from "@/types/global";
|
||||||
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
@@ -15,12 +16,12 @@ export default function LinkCollection({
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<Link
|
||||||
|
href={`/collections/${link.collection.id}`}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
e.stopPropagation();
|
||||||
router.push(`/collections/${link.collection.id}`);
|
|
||||||
}}
|
}}
|
||||||
className="flex items-center gap-1 max-w-full w-fit hover:opacity-70 duration-100"
|
className="flex items-center gap-1 max-w-full w-fit hover:opacity-70 duration-100 select-none"
|
||||||
title={collection?.name}
|
title={collection?.name}
|
||||||
>
|
>
|
||||||
<i
|
<i
|
||||||
@@ -28,6 +29,6 @@ export default function LinkCollection({
|
|||||||
style={{ color: collection?.color }}
|
style={{ color: collection?.color }}
|
||||||
></i>
|
></i>
|
||||||
<p className="truncate capitalize">{collection?.name}</p>
|
<p className="truncate capitalize">{collection?.name}</p>
|
||||||
</div>
|
</Link>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -144,10 +144,18 @@ export default function LinkCardCompact({
|
|||||||
<LinkCollection link={link} collection={collection} />
|
<LinkCollection link={link} collection={collection} />
|
||||||
) : undefined}
|
) : undefined}
|
||||||
{link.url ? (
|
{link.url ? (
|
||||||
<div className="flex items-center gap-1 w-fit text-neutral truncate">
|
<Link
|
||||||
<i className="bi-link-45deg text-lg" />
|
href={link.url || ""}
|
||||||
<p className="truncate w-full select-none">{shortendURL}</p>
|
target="_blank"
|
||||||
</div>
|
title={link.url || ""}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
className="flex gap-1 item-center select-none text-neutral mt-1 hover:opacity-70 duration-100"
|
||||||
|
>
|
||||||
|
<i className="bi-link-45deg text-lg mt-[0.1rem] leading-none"></i>
|
||||||
|
<p className="text-sm truncate">{shortendURL}</p>
|
||||||
|
</Link>
|
||||||
) : (
|
) : (
|
||||||
<div className="badge badge-primary badge-sm my-1 select-none">
|
<div className="badge badge-primary badge-sm my-1 select-none">
|
||||||
{link.type}
|
{link.type}
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ export default function Navbar() {
|
|||||||
<ToggleDarkMode className="hidden sm:inline-grid" />
|
<ToggleDarkMode className="hidden sm:inline-grid" />
|
||||||
|
|
||||||
<div className="dropdown dropdown-end sm:inline-block hidden">
|
<div className="dropdown dropdown-end sm:inline-block hidden">
|
||||||
<div className="tooltip tooltip-bottom z-10" data-tip="Create New...">
|
<div className="tooltip tooltip-bottom" data-tip="Create New...">
|
||||||
<div
|
<div
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
role="button"
|
role="button"
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useRouter } from "next/router";
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
|
|
||||||
export default function SettingsSidebar({ className }: { className?: string }) {
|
export default function SettingsSidebar({ className }: { className?: string }) {
|
||||||
const LINKWARDEN_VERSION = "v2.5.0";
|
const LINKWARDEN_VERSION = "v2.5.1";
|
||||||
|
|
||||||
const { collections } = useCollectionStore();
|
const { collections } = useCollectionStore();
|
||||||
|
|
||||||
|
|||||||
+9
-1
@@ -1,5 +1,5 @@
|
|||||||
import { LinkRequestQuery } from "@/types/global";
|
import { LinkRequestQuery } from "@/types/global";
|
||||||
import { useEffect } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import useDetectPageBottom from "./useDetectPageBottom";
|
import useDetectPageBottom from "./useDetectPageBottom";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import useLinkStore from "@/store/links";
|
import useLinkStore from "@/store/links";
|
||||||
@@ -22,6 +22,8 @@ export default function useLinks(
|
|||||||
useLinkStore();
|
useLinkStore();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
|
||||||
const { reachedBottom, setReachedBottom } = useDetectPageBottom();
|
const { reachedBottom, setReachedBottom } = useDetectPageBottom();
|
||||||
|
|
||||||
const getLinks = async (isInitialCall: boolean, cursor?: number) => {
|
const getLinks = async (isInitialCall: boolean, cursor?: number) => {
|
||||||
@@ -61,10 +63,14 @@ export default function useLinks(
|
|||||||
basePath = "/api/v1/public/collections/links";
|
basePath = "/api/v1/public/collections/links";
|
||||||
} else basePath = "/api/v1/links";
|
} else basePath = "/api/v1/links";
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
const response = await fetch(`${basePath}?${queryString}`);
|
const response = await fetch(`${basePath}?${queryString}`);
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
|
setIsLoading(false);
|
||||||
|
|
||||||
if (response.ok) setLinks(data.response, isInitialCall);
|
if (response.ok) setLinks(data.response, isInitialCall);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -92,4 +98,6 @@ export default function useLinks(
|
|||||||
|
|
||||||
setReachedBottom(false);
|
setReachedBottom(false);
|
||||||
}, [reachedBottom]);
|
}, [reachedBottom]);
|
||||||
|
|
||||||
|
return { isLoading };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,11 +32,12 @@ export default async function checkSubscriptionByEmail(email: string) {
|
|||||||
customer.subscriptions?.data.some((subscription) => {
|
customer.subscriptions?.data.some((subscription) => {
|
||||||
subscription.current_period_end;
|
subscription.current_period_end;
|
||||||
|
|
||||||
active = subscription.items.data.some(
|
active =
|
||||||
(e) =>
|
subscription.items.data.some(
|
||||||
(e.price.id === MONTHLY_PRICE_ID && e.price.active === true) ||
|
(e) =>
|
||||||
(e.price.id === YEARLY_PRICE_ID && e.price.active === true)
|
(e.price.id === MONTHLY_PRICE_ID && e.price.active === true) ||
|
||||||
);
|
(e.price.id === YEARLY_PRICE_ID && e.price.active === true)
|
||||||
|
) || false;
|
||||||
stripeSubscriptionId = subscription.id;
|
stripeSubscriptionId = subscription.id;
|
||||||
currentPeriodStart = subscription.current_period_start * 1000;
|
currentPeriodStart = subscription.current_period_start * 1000;
|
||||||
currentPeriodEnd = subscription.current_period_end * 1000;
|
currentPeriodEnd = subscription.current_period_end * 1000;
|
||||||
@@ -44,7 +45,7 @@ export default async function checkSubscriptionByEmail(email: string) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
active,
|
active: active || false,
|
||||||
stripeSubscriptionId,
|
stripeSubscriptionId,
|
||||||
currentPeriodStart,
|
currentPeriodStart,
|
||||||
currentPeriodEnd,
|
currentPeriodEnd,
|
||||||
|
|||||||
@@ -16,8 +16,11 @@ export default async function paymentCheckout(
|
|||||||
|
|
||||||
const isExistingCustomer = listByEmail?.data[0]?.id || undefined;
|
const isExistingCustomer = listByEmail?.data[0]?.id || undefined;
|
||||||
|
|
||||||
|
console.log("isExistingCustomer", listByEmail?.data[0]);
|
||||||
|
|
||||||
const NEXT_PUBLIC_TRIAL_PERIOD_DAYS =
|
const NEXT_PUBLIC_TRIAL_PERIOD_DAYS =
|
||||||
process.env.NEXT_PUBLIC_TRIAL_PERIOD_DAYS;
|
Number(process.env.NEXT_PUBLIC_TRIAL_PERIOD_DAYS) || 14;
|
||||||
|
|
||||||
const session = await stripe.checkout.sessions.create({
|
const session = await stripe.checkout.sessions.create({
|
||||||
customer: isExistingCustomer ? isExistingCustomer : undefined,
|
customer: isExistingCustomer ? isExistingCustomer : undefined,
|
||||||
line_items: [
|
line_items: [
|
||||||
@@ -34,9 +37,9 @@ export default async function paymentCheckout(
|
|||||||
enabled: true,
|
enabled: true,
|
||||||
},
|
},
|
||||||
subscription_data: {
|
subscription_data: {
|
||||||
trial_period_days: NEXT_PUBLIC_TRIAL_PERIOD_DAYS
|
trial_period_days: isExistingCustomer
|
||||||
? Number(NEXT_PUBLIC_TRIAL_PERIOD_DAYS)
|
? undefined
|
||||||
: 14,
|
: NEXT_PUBLIC_TRIAL_PERIOD_DAYS,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -17,15 +17,7 @@ export default async function verifySubscription(
|
|||||||
|
|
||||||
const currentDate = new Date();
|
const currentDate = new Date();
|
||||||
|
|
||||||
if (
|
if (!subscription?.active || currentDate > subscription.currentPeriodEnd) {
|
||||||
subscription &&
|
|
||||||
currentDate > subscription.currentPeriodEnd &&
|
|
||||||
!subscription.active
|
|
||||||
) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!subscription || currentDate > subscription.currentPeriodEnd) {
|
|
||||||
const {
|
const {
|
||||||
active,
|
active,
|
||||||
stripeSubscriptionId,
|
stripeSubscriptionId,
|
||||||
@@ -59,15 +51,21 @@ export default async function verifySubscription(
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
.catch((err) => console.log(err));
|
.catch((err) => console.log(err));
|
||||||
}
|
} else if (!active) {
|
||||||
|
const subscription = await prisma.subscription.findFirst({
|
||||||
|
where: {
|
||||||
|
userId: user.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
if (!active) {
|
if (subscription)
|
||||||
if (user.username)
|
await prisma.subscription.delete({
|
||||||
// await prisma.user.update({
|
where: {
|
||||||
// where: { id: user.id },
|
userId: user.id,
|
||||||
// data: { username: null },
|
},
|
||||||
// });
|
});
|
||||||
return null;
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ export default async function verifyUser({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (STRIPE_SECRET_KEY) {
|
if (STRIPE_SECRET_KEY) {
|
||||||
const subscribedUser = verifySubscription(user);
|
const subscribedUser = await verifySubscription(user);
|
||||||
|
|
||||||
if (!subscribedUser) {
|
if (!subscribedUser) {
|
||||||
res.status(401).json({
|
res.status(401).json({
|
||||||
|
|||||||
+4
-3
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "linkwarden",
|
"name": "linkwarden",
|
||||||
"version": "2.5.0",
|
"version": "2.5.1",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"repository": "https://github.com/linkwarden/linkwarden.git",
|
"repository": "https://github.com/linkwarden/linkwarden.git",
|
||||||
"author": "Daniel31X13 <daniel31x13@gmail.com>",
|
"author": "Daniel31X13 <daniel31x13@gmail.com>",
|
||||||
@@ -10,10 +10,10 @@
|
|||||||
"seed": "node ./prisma/seed.js"
|
"seed": "node ./prisma/seed.js"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "concurrently -k \"next dev\" \"yarn worker:dev\"",
|
"dev": "concurrently -k -P \"next dev {@}\" \"yarn worker:dev\" --",
|
||||||
"worker:dev": "nodemon --skip-project scripts/worker.ts",
|
"worker:dev": "nodemon --skip-project scripts/worker.ts",
|
||||||
"worker:prod": "ts-node --transpile-only --skip-project scripts/worker.ts",
|
"worker:prod": "ts-node --transpile-only --skip-project scripts/worker.ts",
|
||||||
"start": "concurrently \"next start\" \"yarn worker:prod\"",
|
"start": "concurrently -k -P \"next start {@}\" \"yarn worker:prod\" --",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"lint": "next lint",
|
"lint": "next lint",
|
||||||
"format": "prettier --write \"**/*.{ts,tsx,js,json,md}\""
|
"format": "prettier --write \"**/*.{ts,tsx,js,json,md}\""
|
||||||
@@ -61,6 +61,7 @@
|
|||||||
"react-hot-toast": "^2.4.1",
|
"react-hot-toast": "^2.4.1",
|
||||||
"react-image-file-resizer": "^0.4.8",
|
"react-image-file-resizer": "^0.4.8",
|
||||||
"react-select": "^5.7.4",
|
"react-select": "^5.7.4",
|
||||||
|
"react-spinners": "^0.13.8",
|
||||||
"socks-proxy-agent": "^8.0.2",
|
"socks-proxy-agent": "^8.0.2",
|
||||||
"stripe": "^12.13.0",
|
"stripe": "^12.13.0",
|
||||||
"vaul": "^0.8.8",
|
"vaul": "^0.8.8",
|
||||||
|
|||||||
+1
-5
@@ -168,10 +168,7 @@ export default function Dashboard() {
|
|||||||
>
|
>
|
||||||
{links[0] ? (
|
{links[0] ? (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<LinkComponent
|
<LinkComponent links={links.slice(0, showLinks)} />
|
||||||
links={links.slice(0, showLinks)}
|
|
||||||
showCheckbox={false}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div
|
<div
|
||||||
@@ -282,7 +279,6 @@ export default function Dashboard() {
|
|||||||
{links.some((e) => e.pinnedBy && e.pinnedBy[0]) ? (
|
{links.some((e) => e.pinnedBy && e.pinnedBy[0]) ? (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<LinkComponent
|
<LinkComponent
|
||||||
showCheckbox={false}
|
|
||||||
links={links
|
links={links
|
||||||
.filter((e) => e.pinnedBy && e.pinnedBy[0])
|
.filter((e) => e.pinnedBy && e.pinnedBy[0])
|
||||||
.slice(0, showLinks)}
|
.slice(0, showLinks)}
|
||||||
|
|||||||
@@ -60,8 +60,8 @@ export default function PublicCollections() {
|
|||||||
name: true,
|
name: true,
|
||||||
url: true,
|
url: true,
|
||||||
description: true,
|
description: true,
|
||||||
textContent: true,
|
|
||||||
tags: true,
|
tags: true,
|
||||||
|
textContent: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const [sortBy, setSortBy] = useState<Sort>(Sort.DateNewestFirst);
|
const [sortBy, setSortBy] = useState<Sort>(Sort.DateNewestFirst);
|
||||||
|
|||||||
+21
-7
@@ -5,12 +5,13 @@ import MainLayout from "@/layouts/MainLayout";
|
|||||||
import useLinkStore from "@/store/links";
|
import useLinkStore from "@/store/links";
|
||||||
import { Sort, ViewMode } from "@/types/global";
|
import { Sort, ViewMode } from "@/types/global";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import React, { useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import ViewDropdown from "@/components/ViewDropdown";
|
import ViewDropdown from "@/components/ViewDropdown";
|
||||||
import CardView from "@/components/LinkViews/Layouts/CardView";
|
import CardView from "@/components/LinkViews/Layouts/CardView";
|
||||||
// import GridView from "@/components/LinkViews/Layouts/GridView";
|
// import GridView from "@/components/LinkViews/Layouts/GridView";
|
||||||
import ListView from "@/components/LinkViews/Layouts/ListView";
|
import ListView from "@/components/LinkViews/Layouts/ListView";
|
||||||
import PageHeader from "@/components/PageHeader";
|
import PageHeader from "@/components/PageHeader";
|
||||||
|
import { GridLoader, PropagateLoader } from "react-spinners";
|
||||||
|
|
||||||
export default function Search() {
|
export default function Search() {
|
||||||
const { links } = useLinkStore();
|
const { links } = useLinkStore();
|
||||||
@@ -21,8 +22,8 @@ export default function Search() {
|
|||||||
name: true,
|
name: true,
|
||||||
url: true,
|
url: true,
|
||||||
description: true,
|
description: true,
|
||||||
textContent: true,
|
|
||||||
tags: true,
|
tags: true,
|
||||||
|
textContent: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const [viewMode, setViewMode] = useState<string>(
|
const [viewMode, setViewMode] = useState<string>(
|
||||||
@@ -30,7 +31,7 @@ export default function Search() {
|
|||||||
);
|
);
|
||||||
const [sortBy, setSortBy] = useState<Sort>(Sort.DateNewestFirst);
|
const [sortBy, setSortBy] = useState<Sort>(Sort.DateNewestFirst);
|
||||||
|
|
||||||
useLinks({
|
const { isLoading } = useLinks({
|
||||||
sort: sortBy,
|
sort: sortBy,
|
||||||
searchQueryString: decodeURIComponent(router.query.q as string),
|
searchQueryString: decodeURIComponent(router.query.q as string),
|
||||||
searchByName: searchFilter.name,
|
searchByName: searchFilter.name,
|
||||||
@@ -40,6 +41,10 @@ export default function Search() {
|
|||||||
searchByTags: searchFilter.tags,
|
searchByTags: searchFilter.tags,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log("isLoading", isLoading);
|
||||||
|
}, [isLoading]);
|
||||||
|
|
||||||
const linkView = {
|
const linkView = {
|
||||||
[ViewMode.Card]: CardView,
|
[ViewMode.Card]: CardView,
|
||||||
// [ViewMode.Grid]: GridView,
|
// [ViewMode.Grid]: GridView,
|
||||||
@@ -51,7 +56,7 @@ export default function Search() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<MainLayout>
|
<MainLayout>
|
||||||
<div className="p-5 flex flex-col gap-5 w-full">
|
<div className="p-5 flex flex-col gap-5 w-full h-full">
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<PageHeader icon={"bi-search"} title={"Search Results"} />
|
<PageHeader icon={"bi-search"} title={"Search Results"} />
|
||||||
|
|
||||||
@@ -67,15 +72,24 @@ export default function Search() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{links[0] ? (
|
{!isLoading && !links[0] ? (
|
||||||
<LinkComponent links={links} />
|
|
||||||
) : (
|
|
||||||
<p>
|
<p>
|
||||||
Nothing found.{" "}
|
Nothing found.{" "}
|
||||||
<span className="font-bold text-xl" title="Shruggie">
|
<span className="font-bold text-xl" title="Shruggie">
|
||||||
¯\_(ツ)_/¯
|
¯\_(ツ)_/¯
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
|
) : links[0] ? (
|
||||||
|
<LinkComponent links={links} isLoading={isLoading} />
|
||||||
|
) : (
|
||||||
|
isLoading && (
|
||||||
|
<GridLoader
|
||||||
|
color="oklch(var(--p))"
|
||||||
|
loading={true}
|
||||||
|
size={20}
|
||||||
|
className="m-auto py-10"
|
||||||
|
/>
|
||||||
|
)
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</MainLayout>
|
</MainLayout>
|
||||||
|
|||||||
+20
-10
@@ -5,6 +5,7 @@ import { useRouter } from "next/router";
|
|||||||
import CenteredForm from "@/layouts/CenteredForm";
|
import CenteredForm from "@/layouts/CenteredForm";
|
||||||
import { Plan } from "@/types/global";
|
import { Plan } from "@/types/global";
|
||||||
import AccentSubmitButton from "@/components/AccentSubmitButton";
|
import AccentSubmitButton from "@/components/AccentSubmitButton";
|
||||||
|
import useAccountStore from "@/store/account";
|
||||||
|
|
||||||
export default function Subscribe() {
|
export default function Subscribe() {
|
||||||
const [submitLoader, setSubmitLoader] = useState(false);
|
const [submitLoader, setSubmitLoader] = useState(false);
|
||||||
@@ -12,6 +13,8 @@ export default function Subscribe() {
|
|||||||
|
|
||||||
const [plan, setPlan] = useState<Plan>(1);
|
const [plan, setPlan] = useState<Plan>(1);
|
||||||
|
|
||||||
|
const { account } = useAccountStore();
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
async function submit() {
|
async function submit() {
|
||||||
@@ -27,9 +30,13 @@ export default function Subscribe() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<CenteredForm
|
<CenteredForm
|
||||||
text={`Start with a ${
|
text={
|
||||||
process.env.NEXT_PUBLIC_TRIAL_PERIOD_DAYS || 14
|
account.username
|
||||||
}-day free trial, cancel anytime!`}
|
? ""
|
||||||
|
: `Start with a ${
|
||||||
|
process.env.NEXT_PUBLIC_TRIAL_PERIOD_DAYS || 14
|
||||||
|
}-day free trial, cancel anytime!`
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<div className="p-4 mx-auto flex flex-col gap-3 justify-between max-w-[30rem] min-w-80 w-full bg-base-200 rounded-2xl shadow-md border border-neutral-content">
|
<div className="p-4 mx-auto flex flex-col gap-3 justify-between max-w-[30rem] min-w-80 w-full bg-base-200 rounded-2xl shadow-md border border-neutral-content">
|
||||||
<p className="sm:text-3xl text-2xl text-center font-extralight">
|
<p className="sm:text-3xl text-2xl text-center font-extralight">
|
||||||
@@ -37,7 +44,6 @@ export default function Subscribe() {
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="divider my-0"></div>
|
<div className="divider my-0"></div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<p>
|
<p>
|
||||||
You will be redirected to Stripe, feel free to reach out to us at{" "}
|
You will be redirected to Stripe, feel free to reach out to us at{" "}
|
||||||
@@ -47,7 +53,6 @@ export default function Subscribe() {
|
|||||||
in case of any issue.
|
in case of any issue.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex gap-3 border border-solid border-neutral-content w-4/5 mx-auto p-1 rounded-xl relative">
|
<div className="flex gap-3 border border-solid border-neutral-content w-4/5 mx-auto p-1 rounded-xl relative">
|
||||||
<button
|
<button
|
||||||
onClick={() => setPlan(Plan.monthly)}
|
onClick={() => setPlan(Plan.monthly)}
|
||||||
@@ -74,7 +79,6 @@ export default function Subscribe() {
|
|||||||
25% Off
|
25% Off
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-2 justify-center items-center">
|
<div className="flex flex-col gap-2 justify-center items-center">
|
||||||
<p className="text-3xl">
|
<p className="text-3xl">
|
||||||
${plan === Plan.monthly ? "4" : "3"}
|
${plan === Plan.monthly ? "4" : "3"}
|
||||||
@@ -89,13 +93,20 @@ export default function Subscribe() {
|
|||||||
</legend>
|
</legend>
|
||||||
|
|
||||||
<p className="text-sm">
|
<p className="text-sm">
|
||||||
{process.env.NEXT_PUBLIC_TRIAL_PERIOD_DAYS}-day free trial, then $
|
{account.username
|
||||||
{plan === Plan.monthly ? "4 per month" : "36 annually"}
|
? ""
|
||||||
|
: `${process.env.NEXT_PUBLIC_TRIAL_PERIOD_DAYS}-day free trial, then `}
|
||||||
|
${plan === Plan.monthly ? "4 per month" : "36 annually"}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm">+ VAT if applicable</p>
|
<p className="text-sm">+ VAT if applicable</p>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</div>
|
|
||||||
|
|
||||||
|
<p className="text-sm mb-5">
|
||||||
|
{account.username
|
||||||
|
? "Please note that since your trial has been previously ended, your subscription will start immediately. You can cancel anytime."
|
||||||
|
: ""}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
<AccentSubmitButton
|
<AccentSubmitButton
|
||||||
type="button"
|
type="button"
|
||||||
label="Complete Subscription!"
|
label="Complete Subscription!"
|
||||||
@@ -103,7 +114,6 @@ export default function Subscribe() {
|
|||||||
onClick={submit}
|
onClick={submit}
|
||||||
loading={submitLoader}
|
loading={submitLoader}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
onClick={() => signOut()}
|
onClick={() => signOut()}
|
||||||
className="w-fit mx-auto cursor-pointer text-neutral font-semibold "
|
className="w-fit mx-auto cursor-pointer text-neutral font-semibold "
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "Collection_ownerId_idx" ON "Collection"("ownerId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "UsersAndCollections_userId_idx" ON "UsersAndCollections"("userId");
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "Tag_ownerId_idx" ON "Tag"("ownerId");
|
||||||
@@ -93,6 +93,8 @@ model Collection {
|
|||||||
links Link[]
|
links Link[]
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @default(now()) @updatedAt
|
updatedAt DateTime @default(now()) @updatedAt
|
||||||
|
|
||||||
|
@@index([ownerId])
|
||||||
}
|
}
|
||||||
|
|
||||||
model UsersAndCollections {
|
model UsersAndCollections {
|
||||||
@@ -107,6 +109,7 @@ model UsersAndCollections {
|
|||||||
updatedAt DateTime @default(now()) @updatedAt
|
updatedAt DateTime @default(now()) @updatedAt
|
||||||
|
|
||||||
@@id([userId, collectionId])
|
@@id([userId, collectionId])
|
||||||
|
@@index([userId])
|
||||||
}
|
}
|
||||||
|
|
||||||
model Link {
|
model Link {
|
||||||
@@ -139,6 +142,7 @@ model Tag {
|
|||||||
updatedAt DateTime @default(now()) @updatedAt
|
updatedAt DateTime @default(now()) @updatedAt
|
||||||
|
|
||||||
@@unique([name, ownerId])
|
@@unique([name, ownerId])
|
||||||
|
@@index([ownerId])
|
||||||
}
|
}
|
||||||
|
|
||||||
model Subscription {
|
model Subscription {
|
||||||
|
|||||||
@@ -5211,6 +5211,11 @@ react-select@^5.7.4:
|
|||||||
react-transition-group "^4.3.0"
|
react-transition-group "^4.3.0"
|
||||||
use-isomorphic-layout-effect "^1.1.2"
|
use-isomorphic-layout-effect "^1.1.2"
|
||||||
|
|
||||||
|
react-spinners@^0.13.8:
|
||||||
|
version "0.13.8"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-spinners/-/react-spinners-0.13.8.tgz#5262571be0f745d86bbd49a1e6b49f9f9cb19acc"
|
||||||
|
integrity sha512-3e+k56lUkPj0vb5NDXPVFAOkPC//XyhKPJjvcGjyMNPWsBKpplfeyialP74G7H7+It7KzhtET+MvGqbKgAqpZA==
|
||||||
|
|
||||||
react-style-singleton@^2.2.1:
|
react-style-singleton@^2.2.1:
|
||||||
version "2.2.1"
|
version "2.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/react-style-singleton/-/react-style-singleton-2.2.1.tgz#f99e420492b2d8f34d38308ff660b60d0b1205b4"
|
resolved "https://registry.yarnpkg.com/react-style-singleton/-/react-style-singleton-2.2.1.tgz#f99e420492b2d8f34d38308ff660b60d0b1205b4"
|
||||||
|
|||||||
Reference in New Issue
Block a user