Compare commits
51 Commits
main
...
app-store-
Author | SHA1 | Date | |
---|---|---|---|
|
ee9bde6d5b | ||
|
d6e9bfbe9f | ||
|
cfe667b4d9 | ||
|
794556ded3 | ||
|
a726daad0d | ||
|
ec897f46c6 | ||
|
44643a49f9 | ||
|
81c84ee5ed | ||
|
7e1a6f026d | ||
|
32839befa3 | ||
|
1d73558bdd | ||
|
5281b4f4d4 | ||
|
e7f1a829fd | ||
|
8455945761 | ||
|
7857128791 | ||
|
8123e94f33 | ||
|
1b1846e157 | ||
|
b341257607 | ||
|
96aff4a3af | ||
|
0132e4a241 | ||
|
80d0ee62d9 | ||
|
c575ecaf92 | ||
|
5794a6d9f9 | ||
|
fca281a09b | ||
|
4bfc397292 | ||
|
313a93dab5 | ||
|
0dc5da588a | ||
|
e8d44e9689 | ||
|
342e94d64e | ||
|
b0d8cfec59 | ||
|
7a53806500 | ||
|
1132acc960 | ||
|
91085c8a86 | ||
|
bc494f728f | ||
|
92fa33fc17 | ||
|
4de5d31145 | ||
|
f6f86da64c | ||
|
644d549cc3 | ||
|
15aecf4b7d | ||
|
caef2c7828 | ||
|
357331890b | ||
|
ae7f843edb | ||
|
1c86ae14a8 | ||
|
2899d2a156 | ||
|
2f16b94ecb | ||
|
27bbbb3b5c | ||
|
8a4454cb3a | ||
|
e4397a2fe1 | ||
|
3a4813e4b0 | ||
|
f3588a35e3 | ||
|
80a0ad98b1 |
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit e82b40a50e8128739b9572fb8774ce89eb7efe12
|
|
@ -5,16 +5,21 @@ import {
|
||||||
FlagIcon,
|
FlagIcon,
|
||||||
MailIcon,
|
MailIcon,
|
||||||
ShieldCheckIcon,
|
ShieldCheckIcon,
|
||||||
|
StarIcon,
|
||||||
} from "@heroicons/react/outline";
|
} from "@heroicons/react/outline";
|
||||||
|
import { StarIcon as FilledStarIcon } from "@heroicons/react/solid";
|
||||||
import { ChevronLeftIcon } from "@heroicons/react/solid";
|
import { ChevronLeftIcon } from "@heroicons/react/solid";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
|
|
||||||
import { InstallAppButton } from "@calcom/app-store/components";
|
import { InstallAppButton } from "@calcom/app-store/components";
|
||||||
|
import classNames from "@calcom/lib/classNames";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { App as AppType } from "@calcom/types/App";
|
import { App as AppType } from "@calcom/types/App";
|
||||||
import { Button } from "@calcom/ui";
|
import { Button } from "@calcom/ui";
|
||||||
|
|
||||||
|
import { trpc } from "@lib/trpc";
|
||||||
|
|
||||||
import Shell from "@components/Shell";
|
import Shell from "@components/Shell";
|
||||||
import Badge from "@components/ui/Badge";
|
import Badge from "@components/ui/Badge";
|
||||||
|
|
||||||
|
@ -34,6 +39,7 @@ export default function App({
|
||||||
email,
|
email,
|
||||||
tos,
|
tos,
|
||||||
privacy,
|
privacy,
|
||||||
|
slug,
|
||||||
}: {
|
}: {
|
||||||
name: string;
|
name: string;
|
||||||
type: AppType["type"];
|
type: AppType["type"];
|
||||||
|
@ -51,9 +57,22 @@ export default function App({
|
||||||
email: string; // required
|
email: string; // required
|
||||||
tos?: string;
|
tos?: string;
|
||||||
privacy?: string;
|
privacy?: string;
|
||||||
|
slug: string;
|
||||||
}) {
|
}) {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
|
|
||||||
|
const [rating, setRating] = useState(0);
|
||||||
|
const [hover, setHover] = useState(0);
|
||||||
|
const [comment, setComment] = useState("");
|
||||||
|
const totalStars = 5;
|
||||||
|
|
||||||
|
const postMutation = trpc.useMutation("viewer.appReviews.post");
|
||||||
|
|
||||||
|
const onSubmitReview = async (slug: string, rating: number, comment: string) => {
|
||||||
|
console.log("This triggers");
|
||||||
|
postMutation.mutate({ slug, rating, comment });
|
||||||
|
};
|
||||||
|
|
||||||
const priceInDollar = Intl.NumberFormat("en-US", {
|
const priceInDollar = Intl.NumberFormat("en-US", {
|
||||||
style: "currency",
|
style: "currency",
|
||||||
currency: "USD",
|
currency: "USD",
|
||||||
|
@ -237,6 +256,35 @@ export default function App({
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3>Review</h3>
|
||||||
|
<div className="flex">
|
||||||
|
{[...new Array(totalStars)].map((arr, index) => {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
key={index}
|
||||||
|
onClick={() => setRating(index)}
|
||||||
|
onMouseEnter={() => setHover(index)}
|
||||||
|
onMouseLeave={() => setHover(rating)}>
|
||||||
|
<FilledStarIcon
|
||||||
|
className={classNames(
|
||||||
|
"ml-1 mt-0.5 h-4 w-4",
|
||||||
|
index <= (rating || hover) ? "text-yellow-600" : "text-yellow-200"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<textarea
|
||||||
|
id="comment"
|
||||||
|
name="comment"
|
||||||
|
rows={3}
|
||||||
|
onChange={(event) => setComment(event.target.value)}
|
||||||
|
className="my-1 block rounded-sm border-gray-300 py-2 pb-2 shadow-sm sm:text-sm"></textarea>
|
||||||
|
<Button onClick={() => onSubmitReview(slug, rating, comment)}>{t("submit")}</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Shell>
|
</Shell>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -19,6 +19,7 @@ export default function AllApps({ apps }: { apps: App[] }) {
|
||||||
logo={app.logo}
|
logo={app.logo}
|
||||||
rating={app.rating}
|
rating={app.rating}
|
||||||
reviews={app.reviews}
|
reviews={app.reviews}
|
||||||
|
installs={app.installs}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { StarIcon } from "@heroicons/react/solid";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
import Button from "@calcom/ui/Button";
|
import Button from "@calcom/ui/Button";
|
||||||
|
@ -10,9 +11,12 @@ interface AppCardProps {
|
||||||
description: string;
|
description: string;
|
||||||
rating: number;
|
rating: number;
|
||||||
reviews?: number;
|
reviews?: number;
|
||||||
|
installs?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AppCard(props: AppCardProps) {
|
export default function AppCard(props: AppCardProps) {
|
||||||
|
console.log("🚀 ~ file: AppCard.tsx ~ line 16 ~ installs", props.installs);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link href={"/apps/" + props.slug}>
|
<Link href={"/apps/" + props.slug}>
|
||||||
<a
|
<a
|
||||||
|
@ -33,10 +37,11 @@ export default function AppCard(props: AppCardProps) {
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<h3 className="font-medium">{props.name}</h3>
|
<h3 className="font-medium">{props.name}</h3>
|
||||||
{/* TODO: add reviews <div className="flex text-sm text-gray-800">
|
{/* TODO: add reviews */}
|
||||||
|
<div className="flex text-sm text-gray-800">
|
||||||
<span>{props.rating} stars</span> <StarIcon className="ml-1 mt-0.5 h-4 w-4 text-yellow-600" />
|
<span>{props.rating} stars</span> <StarIcon className="ml-1 mt-0.5 h-4 w-4 text-yellow-600" />
|
||||||
<span className="pl-1 text-gray-500">{props.reviews} reviews</span>
|
<span className="pl-1 text-gray-500">{props.installs} installs</span>
|
||||||
</div> */}
|
</div>
|
||||||
<p className="mt-2 truncate text-sm text-gray-500">{props.description}</p>
|
<p className="mt-2 truncate text-sm text-gray-500">{props.description}</p>
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
|
@ -30,6 +30,7 @@ const TrendingAppsSlider = <T extends App>({ items }: { items: T[] }) => {
|
||||||
logo={app.logo}
|
logo={app.logo}
|
||||||
rating={app.rating}
|
rating={app.rating}
|
||||||
reviews={app.reviews}
|
reviews={app.reviews}
|
||||||
|
installs={app.installs}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -61,6 +61,7 @@ function SingleAppPage({ data, source }: inferSSRProps<typeof getStaticProps>) {
|
||||||
docs={data.docsUrl}
|
docs={data.docsUrl}
|
||||||
website={data.url}
|
website={data.url}
|
||||||
email={data.email}
|
email={data.email}
|
||||||
|
slug={data.slug}
|
||||||
// tos="https://zoom.us/terms"
|
// tos="https://zoom.us/terms"
|
||||||
// privacy="https://zoom.us/privacy"
|
// privacy="https://zoom.us/privacy"
|
||||||
body={<MDXRemote {...source} components={components} />}
|
body={<MDXRemote {...source} components={components} />}
|
||||||
|
@ -83,7 +84,20 @@ export const getStaticProps = async (ctx: GetStaticPropsContext) => {
|
||||||
|
|
||||||
const app = await prisma.app.findUnique({
|
const app = await prisma.app.findUnique({
|
||||||
where: { slug: ctx.params.slug },
|
where: { slug: ctx.params.slug },
|
||||||
|
include: {
|
||||||
|
reviews: {
|
||||||
|
select: {
|
||||||
|
user: {
|
||||||
|
select: {
|
||||||
|
name: true,
|
||||||
|
avatar: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
console.log("🚀 ~ file: index.tsx ~ line 100 ~ getStaticProps ~ app", app.reviews);
|
||||||
|
|
||||||
if (!app) return { notFound: true };
|
if (!app) return { notFound: true };
|
||||||
|
|
||||||
|
|
|
@ -25,15 +25,31 @@ export default function Apps({ appStore, categories }: InferGetStaticPropsType<t
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getStaticProps = async () => {
|
export const getStaticProps = async () => {
|
||||||
const appStore = await getAppRegistry();
|
const appMetaData = await getAppRegistry();
|
||||||
|
|
||||||
const categoryQuery = await prisma.app.findMany({
|
const appsQuery = await prisma.app.findMany({
|
||||||
select: {
|
select: {
|
||||||
|
slug: true,
|
||||||
categories: true,
|
categories: true,
|
||||||
|
_count: {
|
||||||
|
select: {
|
||||||
|
credentials: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const categories = categoryQuery.reduce((c, app) => {
|
console.log("🚀 ~ file: index.tsx ~ line 40 ~ getStaticProps ~ appsQuery", appsQuery);
|
||||||
for (const category of app.categories) {
|
|
||||||
|
const appStore = appMetaData.map((app) => {
|
||||||
|
const installs = appsQuery.filter((query) => query.slug === app.slug);
|
||||||
|
console.log("🚀 ~ file: index.tsx ~ line 45 ~ appStore ~ installs", installs[0]._count.credentials);
|
||||||
|
|
||||||
|
return { ...app, installs: installs[0]._count.credentials };
|
||||||
|
});
|
||||||
|
|
||||||
|
const categoriesArray = appsQuery.map((app) => app.categories);
|
||||||
|
const categories = categoriesArray.reduce((c, app) => {
|
||||||
|
for (const category of app) {
|
||||||
c[category] = c[category] ? c[category] + 1 : 1;
|
c[category] = c[category] ? c[category] + 1 : 1;
|
||||||
}
|
}
|
||||||
return c;
|
return c;
|
||||||
|
|
|
@ -25,6 +25,7 @@ import {
|
||||||
import slugify from "@lib/slugify";
|
import slugify from "@lib/slugify";
|
||||||
|
|
||||||
import { apiKeysRouter } from "@server/routers/viewer/apiKeys";
|
import { apiKeysRouter } from "@server/routers/viewer/apiKeys";
|
||||||
|
import { appReviewsRouter } from "@server/routers/viewer/appReviews";
|
||||||
import { availabilityRouter } from "@server/routers/viewer/availability";
|
import { availabilityRouter } from "@server/routers/viewer/availability";
|
||||||
import { eventTypesRouter } from "@server/routers/viewer/eventTypes";
|
import { eventTypesRouter } from "@server/routers/viewer/eventTypes";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
|
@ -928,4 +929,5 @@ export const viewerRouter = createRouter()
|
||||||
.merge("availability.", availabilityRouter)
|
.merge("availability.", availabilityRouter)
|
||||||
.merge("teams.", viewerTeamsRouter)
|
.merge("teams.", viewerTeamsRouter)
|
||||||
.merge("webhook.", webhookRouter)
|
.merge("webhook.", webhookRouter)
|
||||||
.merge("apiKeys.", apiKeysRouter);
|
.merge("apiKeys.", apiKeysRouter)
|
||||||
|
.merge("appReviews.", appReviewsRouter);
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
import { PrismaAdapter } from "@next-auth/prisma-adapter";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { createProtectedRouter } from "@server/createRouter";
|
||||||
|
import { TRPCError } from "@trpc/server";
|
||||||
|
|
||||||
|
export const appReviewsRouter = createProtectedRouter().mutation("post", {
|
||||||
|
input: z.object({
|
||||||
|
slug: z.string(),
|
||||||
|
rating: z.number(),
|
||||||
|
comment: z.string(),
|
||||||
|
}),
|
||||||
|
async resolve({ ctx, input }) {
|
||||||
|
const { slug, rating, comment } = input;
|
||||||
|
const { prisma, user } = ctx;
|
||||||
|
|
||||||
|
await prisma.appReview.create({
|
||||||
|
data: {
|
||||||
|
slug: slug,
|
||||||
|
date: dayjs().toISOString(),
|
||||||
|
user: {
|
||||||
|
connect: {
|
||||||
|
id: user.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rating: rating,
|
||||||
|
comment: comment,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
|
@ -172,6 +172,7 @@ model User {
|
||||||
apiKeys ApiKey[]
|
apiKeys ApiKey[]
|
||||||
accounts Account[]
|
accounts Account[]
|
||||||
sessions Session[]
|
sessions Session[]
|
||||||
|
reviews AppReview[]
|
||||||
|
|
||||||
Feedback Feedback[]
|
Feedback Feedback[]
|
||||||
@@map(name: "users")
|
@@map(name: "users")
|
||||||
|
@ -477,6 +478,18 @@ model App {
|
||||||
credentials Credential[]
|
credentials Credential[]
|
||||||
Webhook Webhook[]
|
Webhook Webhook[]
|
||||||
ApiKey ApiKey[]
|
ApiKey ApiKey[]
|
||||||
|
reviews AppReview[]
|
||||||
|
}
|
||||||
|
|
||||||
|
model AppReview {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
slug String @unique
|
||||||
|
app App @relation(fields: [slug], references: [slug], onDelete: Cascade)
|
||||||
|
userId Int
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
date DateTime
|
||||||
|
rating Int
|
||||||
|
comment String
|
||||||
}
|
}
|
||||||
|
|
||||||
model Feedback {
|
model Feedback {
|
||||||
|
|
|
@ -73,4 +73,6 @@ export interface App {
|
||||||
price?: number;
|
price?: number;
|
||||||
/** only required for "usage-based" billing. % of commission for paid bookings */
|
/** only required for "usage-based" billing. % of commission for paid bookings */
|
||||||
commission?: number;
|
commission?: number;
|
||||||
|
/** Query from db, count the number of credentials */
|
||||||
|
installs?: number;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user