Skip to content

Commit

Permalink
Merge pull request #8 from timia2109/feature/ui-improvements
Browse files Browse the repository at this point in the history
UI improvements
  • Loading branch information
timia2109 committed Aug 7, 2024
2 parents 31bb3a3 + 0e36ef1 commit 8f62f2d
Show file tree
Hide file tree
Showing 21 changed files with 263 additions and 74 deletions.
15 changes: 5 additions & 10 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
# Since .env is gitignored, you can use .env.example to build a new `.env` file when you clone the repo.
# Keep this file up-to-date when you add new variables to `.env`.

# This file will be committed to version control, so make sure not to have any secrets in it.
# If you are cloning this repo, create a copy of this file named `.env` and populate it with your secrets.

# When adding additional env variables, the schema in /env/schema.mjs should be updated accordingly

# Prisma
DATABASE_URL=file:./db.sqlite
DATABASE_URL=mysql://root:root@db/simple-meal-plan
INVITATION_VALIDITY=P30D
NEXTAUTH_SECRET=A_SECRET
ROOT_URL=https://example.com
NEXT_PUBLIC_PRIVACY_URL=https://example.com
25 changes: 18 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,27 @@
# Simple Meal Plan

This is a very simple meal planner which I build in two hours.
I want to replace the Excel Sheet, my girlfriend was using.
Currently there a no authentication build in. So this should only deployed on private networks.
Work in Progress.
This is a very simple meal planner. It's main purpose is to host it as a free SaaS solution.
You can use it [here](https://example.com). Anyway you can deploy it using Docker.

I want to replace the Excel Sheet, my girlfriend was using.

## Features

![Example Screenshot](public/example.png)

- Plan Meal for any date
- (Tablet, Desktop): See a complete calendar of the month which is editable
- Share meal plans with magic links to other users to collaborate (household, etc.)
- Manage multiple meal plans per user

## Techstack

- [Next.js](https://nextjs.org)
- [Prisma](https://prisma.io)
- [Tailwind CSS](https://tailwindcss.com)
- [tRPC](https://trpc.io)
- [DaisyUI](https://daisyui.com)

## Deployment

## Screenshot
![Example Screenshot](docs/example.png)
You'll need a MySQL / MariaDB database.
Have a look at [`.env.example`](./.env.example) or at the [schema](./src/env/schema.mjs)
Binary file removed docs/example.png
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- DropConstraint
ALTER TABLE Account DROP KEY Account_userId_key;
4 changes: 2 additions & 2 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ model User {
emailVerified DateTime?
image String?
Session Session[]
Account Account?
Account Account[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
Expand All @@ -70,7 +70,7 @@ model User {

model Account {
id String @id @default(cuid()) @db.Char(25)
userId String @unique @db.Char(25)
userId String @db.Char(25)
type String
provider String
providerAccountId String
Expand Down
Binary file added public/example.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions public/undraw_eating_together_re_ux62.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions public/undraw_real_time_sync_re_nky7.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
40 changes: 40 additions & 0 deletions src/app/[locale]/(landing)/FeatureBox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import Image from "next/image";
import type { FC } from "react";
import type { Feature } from "./Features";
import { isImageFeature } from "./Features";

type Props = {
feature: Feature;
};

export const FeatureBox: FC<Props> = ({ feature }) => (
<div className="card bg-base-300 shadow-2xl">
{isImageFeature(feature) && (
<figure>
<Image
src={feature.image}
alt={feature.alt}
width={feature.width}
height={feature.height}
/>
</figure>
)}
<div className="card-body">
<h2 className="card-title flex justify-center">{feature.heading}</h2>
{feature.body.map((paragraph, index) => (
<p key={index}>{paragraph}</p>
))}
{feature.link && (
<div className="card-actions justify-end">
<a
href={feature.link}
target="_blank"
className="btn btn-outline btn-secondary"
>
More Infos
</a>
</div>
)}
</div>
</div>
);
63 changes: 63 additions & 0 deletions src/app/[locale]/(landing)/Features.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// In this files we define the features of the landing page

import { getScopedI18n } from "@/locales/server";

type Description = string;

type PlainFeature = {
heading: Description;
body: Description[];
link?: string;
};

type ImageFeature = PlainFeature & {
image: string;
alt: string;
width: number;
height: number;
};

export type Feature = PlainFeature | ImageFeature;

export const getFeatures: () => Promise<Feature[]> = async () => {
const t = await getScopedI18n("features");

return [
{
heading: t("featureA"),
image: "/example.png",
alt: t("featureAImgAlt"),
width: 288,
height: 119,
body: [t("featureADescription")],
},
{
heading: t("featureB"),
body: [t("featureBDescription1"), t("featureBDescription2")],
image: "undraw_real_time_sync_re_nky7.svg",
alt: t("featureBImgAlt"),
height: 200,
width: 200,
},
{
heading: t("featureC"),
body: [t("featureCDescription")],
image: "undraw_eating_together_re_ux62.svg",
alt: t("featureCImgAlt"),
height: 200,
width: 200,
},
{
heading: t("featureD"),
body: [t("featureDDescription")],
},
{
heading: t("featureE"),
body: [t("featureEDescription")],
link: "https://github.com/timia2109/simple-meal-plan",
},
];
};

export const isImageFeature = (feature: Feature): feature is ImageFeature =>
"image" in feature;
6 changes: 2 additions & 4 deletions src/app/[locale]/(landing)/SignInButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,10 @@ export const SignInButton: FC<{ id: string; label: string }> = ({
}) => {
return (
<button
className="btn-outline-primary btn btn-lg bg-gradient-to-r from-pink-500 to-purple-600 shadow-2xl"
className="btn btn-outline btn-primary join-item btn-lg"
onClick={() => signIn(id)}
>
<span className="px-16 text-xl font-semibold text-purple-950">
{label}
</span>
{label}
</button>
);
};
2 changes: 1 addition & 1 deletion src/app/[locale]/(landing)/SignInButtons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export async function SignInButtons() {
const t = await getScopedI18n("landing");

return (
<div className="flex flex-wrap justify-between gap-4">
<div className="join">
{Object.values(authConfig.providers as OAuth2Config<unknown>[]).map(
(d) => (
<SignInButton key={d.id} id={d.id} label={t("signinWith", d)} />
Expand Down
31 changes: 11 additions & 20 deletions src/app/[locale]/(landing)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { InvitationHeader } from "@/components/invitation/InvitationHeader";
import { getInvitation } from "@/dal/user/getInvitation";
import { getScopedI18n } from "@/locales/server";
import { redirectRoute } from "@/routes";
import { FeatureBox } from "./FeatureBox";
import { getFeatures } from "./Features";
import { SignInButtons } from "./SignInButtons";

type Props = {
Expand Down Expand Up @@ -36,13 +38,16 @@ export default async function LandingPage({ searchParams }: Props) {
);
if (currentUser != null && invitation == null) redirectRoute("mealPlan");

const features = await getFeatures();

return (
<div className="p-12">
<div className="fw-bolder flex flex-col items-center justify-center gap-16">
<div className="flex flex-col items-center justify-center gap-8 ">
<title>{t("title")}</title>
<div className="fw-bolder flex flex-col items-center justify-center gap-8">
<div className="flex flex-col items-center justify-center gap-4">
<h1 className="text-6xl">
{t("welcome")}
<span className="bg-gradient-to-r from-pink-500 to-purple-600 bg-clip-text font-semibold text-transparent">
<span className="bg-gradient-to-r from-primary to-secondary bg-clip-text font-semibold text-transparent">
{" "}
{t("title")}
</span>
Expand All @@ -57,23 +62,9 @@ export default async function LandingPage({ searchParams }: Props) {

<SignInButtons />
<div className="grid grid-cols-1 gap-8 md:grid-cols-3">
<div className="card w-96 bg-base-300 shadow-2xl">
<div className="card-body">
<h2 className="card-title flex justify-center">
Feature Heading
</h2>
<p>Feature Box</p>
</div>
</div>

<div className="card w-96 bg-base-300 shadow-2xl">
<div className="card-body">
<h2 className="card-title flex justify-center">Search Params</h2>
<p>
<code>{JSON.stringify(searchParams)}</code>
</p>
</div>
</div>
{features.map((feature, index) => (
<FeatureBox key={index} feature={feature} />
))}
</div>
</div>
</div>
Expand Down
33 changes: 26 additions & 7 deletions src/auth.config.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,31 @@
import type { NextAuthConfig } from "next-auth";
import GoogleProvider from "next-auth/providers/google";
import { Provider } from "next-auth/providers";
import Facebook from "next-auth/providers/facebook";
import Google from "next-auth/providers/google";
import { env } from "./env/server.mjs";

const providers: Provider[] = [];

if (env.AUTH_GOOGLE_ID && env.AUTH_GOOGLE_SECRET) {
providers.push(
Google({
clientId: env.AUTH_GOOGLE_ID,
clientSecret: env.AUTH_GOOGLE_SECRET,
allowDangerousEmailAccountLinking: env.ALLOW_ACCOUNT_LINKING === "true",
})
);
}

if (env.AUTH_FACEBOOK_ID && env.AUTH_FACEBOOK_SECRET) {
providers.push(
Facebook({
clientId: env.AUTH_FACEBOOK_ID,
clientSecret: env.AUTH_FACEBOOK_SECRET,
allowDangerousEmailAccountLinking: env.ALLOW_ACCOUNT_LINKING === "true",
})
);
}

export const authConfig: NextAuthConfig = {
secret: env.NEXTAUTH_SECRET,
session: {
Expand All @@ -15,10 +39,5 @@ export const authConfig: NextAuthConfig = {
return session;
},
},
providers: [
GoogleProvider({
clientId: env.GOOGLE_CLIENT_ID,
clientSecret: env.GOOGLE_CLIENT_SECRET,
}),
],
providers,
};
7 changes: 4 additions & 3 deletions src/components/common/Footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,15 @@ export async function Footer() {
href="https://timitt.dev"
target="_blank"
rel="noreferrer"
className="underline underline-offset-4 transition-all hover:font-bold
hover:text-orange-500"
className="link-hover link ms-1"
>
{t("author")}
</a>
</div>
{env.NEXT_PUBLIC_PRIVACY_URL && (
<a href={env.NEXT_PUBLIC_PRIVACY_URL}>{t("privacy")}</a>
<a className="link-hover" href={env.NEXT_PUBLIC_PRIVACY_URL}>
{t("privacy")}
</a>
)}
<div>
<a
Expand Down
4 changes: 3 additions & 1 deletion src/components/common/NavBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,9 @@ export async function NavBar() {
</ul>
</div>
<Link href={getRoute("mealPlan")} className="btn btn-ghost text-xl">
{t("landing.title")}
<span className="bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent">
{t("landing.title")}
</span>
</Link>
</div>
<div className="navbar-center hidden lg:flex">
Expand Down
2 changes: 1 addition & 1 deletion src/components/mealPlan/MealPlanComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export async function MealPlanComponent({
)}
</Link>

<div className="md:justify-self-end lg:justify-self-center lg:last:justify-self-end">
<div className="md:justify-self-end lg:justify-self-start lg:last:justify-self-end">
<div className="avatar-group -space-x-6 rtl:space-x-reverse">
{users.map((user) => (
<ProfileImage key={user.id} user={user} />
Expand Down
42 changes: 29 additions & 13 deletions src/env/schema.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,43 @@
import { z } from "zod";

/**
* Specify your server-side environment variables schema here.
* This way you can ensure the app isn't built with invalid env vars.
* This are the environment variables for the server.
* You need to set them
*/
export const serverSchema = z.object({
DATABASE_URL: z.string(),
DATABASE_URL: z.string().describe("The URL to the database"),
NODE_ENV: z.enum(["development", "test", "production"]),
GOOGLE_CLIENT_SECRET: z.string(),
GOOGLE_CLIENT_ID: z.string(),
INVITATION_VALIDITY: z.string().default("P30D"),
SESSION_VALIDITY_IN_SECONDS: z.number().default(60 * 60 * 24 * 30), // 30 days
NEXTAUTH_SECRET: z.string(),
ROOT_URL: z.string().url().optional(),
INVITATION_VALIDITY: z
.string()
.default("P30D")
.describe("The duration of the invitation token"),
SESSION_VALIDITY_IN_SECONDS: z
.number()
.default(60 * 60 * 24 * 30)
.describe("Session validity"), // 30 days
NEXTAUTH_SECRET: z.string().describe("The secret for next-auth"),
ROOT_URL: z
.string()
.url()
.optional()
.describe("The root URL of the server. Used to generate invitation links"),
ALLOW_ACCOUNT_LINKING: z.enum(["true", "false"]).default("false"),

// auth-js Providers
AUTH_GOOGLE_ID: z.string().optional(),
AUTH_GOOGLE_SECRET: z.string().optional(),
AUTH_FACEBOOK_ID: z.string().optional(),
AUTH_FACEBOOK_SECRET: z.string().optional(),
});

/**
* Specify your client-side environment variables schema here.
* This way you can ensure the app isn't built with invalid env vars.
* To expose them to the client, prefix them with `NEXT_PUBLIC_`.
* This are the environment variables for the client.
*/
export const clientSchema = z.object({
NEXT_PUBLIC_PRIVACY_URL: z.string().optional(),
NEXT_PUBLIC_PRIVACY_URL: z
.string()
.optional()
.describe("The URL to the privacy policy. Adds a point to the footer"),
});

/**
Expand Down
Loading

0 comments on commit 8f62f2d

Please sign in to comment.