Skip to content

Commit

Permalink
Sessions (lukevella#162)
Browse files Browse the repository at this point in the history
  • Loading branch information
lukevella authored May 9, 2022
1 parent 1d7bcdd commit 5c991d7
Show file tree
Hide file tree
Showing 83 changed files with 2,448 additions and 1,163 deletions.
12 changes: 10 additions & 2 deletions .eslintrc.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
{
"extends": "next/core-web-vitals",
"plugins": ["simple-import-sort"],
"extends": ["next/core-web-vitals"],
"plugins": ["simple-import-sort", "@typescript-eslint"],
"overrides": [
{
"files": ["**/*.ts", "**/*.tsx"],
"parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint"],
"extends": ["plugin:@typescript-eslint/recommended"]
}
],
"rules": {
"simple-import-sort/imports": "error",
"simple-import-sort/exports": "error",
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ jobs:
- name: Set environment variables
run: |
echo "DATABASE_URL=postgresql://postgres:password@localhost:5432/db" >> $GITHUB_ENV
echo "SECRET_PASSWORD=abcdefghijklmnopqrstuvwxyz1234567890" >> $GITHUB_ENV
- name: Install dependencies
run: yarn install --frozen-lockfile
Expand Down
44 changes: 19 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
[![License: AGPL v3](https://img.shields.io/badge/License-AGPL_v3-orange.svg)](https://www.gnu.org/licenses/agpl-3.0)
[![Donate](https://img.shields.io/badge/Donate-PayPal-blue.svg)](https://www.paypal.com/donate/?hosted_button_id=7QXP2CUBLY88E)


![hero](./docs/images/hero-image.png)

Rallly is a free group meeting scheduling tool – built with [Next.js](https://github.com/vercel/next.js/), [Prisma](https://github.com/prisma/prisma) & [TailwindCSS](https://github.com/tailwindlabs/tailwindcss)
Expand All @@ -18,18 +17,13 @@ git clone https://github.com/lukevella/rallly.git
cd rallly
```

_optional_: Configure your SMTP server. Without this, Rallly won't be able to send out emails. You can set the following environment variables in a `.env` in the root of the project
Once inside the directory create a `.env` file where you can set your environment variables. There is a `sample.env` that you can use as a reference.

```bash
cp sample.env .env
```
# support email - used as FROM email by SMTP server
[email protected]
# SMTP server - required if you want to send emails
SMTP_HOST=your-smtp-server
SMTP_PORT=587
SMTP_SECURE="false"
SMTP_USER=your-smtp-user
SMTP_PWD=your-smtp-password
```

_See [configuration](#-configuration) to see what parameters are availble._

Build and run with `docker-compose`

Expand All @@ -54,20 +48,7 @@ Copy the sample `.env` file then open it and set the variables.
cp sample.env .env
```

Fill in the required environment variables.

```
# postgres database - not needed if running with docker-compose
DATABASE_URL=postgres://your-database/db
# support email - used as FROM email by SMTP server
[email protected]
# SMTP server - required if you want to send emails
SMTP_HOST=your-smtp-server
SMTP_PORT=587
SMTP_SECURE="false"
SMTP_USER=your-smtp-user
SMTP_PWD=your-smtp-password
```
_See [configuration](#-configuration) to see what parameters are availble._

Install dependencies

Expand All @@ -91,6 +72,19 @@ yarn build
yarn start
```

## ⚙️ Configuration

| Parameter | Default | Description |
| --------------- | ---------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| DATABASE_URL | postgres://postgres:postgres@rallly_db:5432/db | A postgres database URL. Leave out if using the docker-compose file since it will spin up and connect to its own database instance. |
| SECRET_PASSWORD | - | A long string (minimum 25 characters) that is used to encrypt session data. |
| SUPPORT_EMAIL | - | An email address that will appear as the FROM email for all emails being sent out. |
| SMTP_HOST | - | Host name of your SMTP server |
| SMTP_PORT | - | Port of your SMTP server |
| SMTP_SECURE | false | Set to "true" if SSL is enabled for your SMTP connection |
| SMTP_USER | - | Username to use for your SMTP connection |
| SMTP_PWD | - | Password to use for your SMTP connection |

## 👨‍💻 Contributors

If you would like to contribute to the development of the project please reach out first before spending significant time on it.
Expand Down
26 changes: 26 additions & 0 deletions components/badge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import clsx from "clsx";
import React from "react";

const Badge: React.VoidFunctionComponent<{
children?: React.ReactNode;
color?: "gray" | "amber" | "green";
className?: string;
}> = ({ children, color = "gray", className }) => {
return (
<div
className={clsx(
"inline-flex h-5 items-center rounded-md px-1 text-xs",
{
"bg-slate-200 text-slate-500": color === "gray",
"bg-amber-100 text-amber-500": color === "amber",
"bg-green-100/50 text-green-500": color === "green",
},
className,
)}
>
{children}
</div>
);
};

export default Badge;
2 changes: 0 additions & 2 deletions components/button/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ export interface ButtonProps {
htmlType?: React.ButtonHTMLAttributes<HTMLButtonElement>["type"];
type?: "default" | "primary" | "danger" | "link";
form?: string;
href?: string;
rounded?: boolean;
title?: string;
onClick?: React.MouseEventHandler<HTMLButtonElement>;
Expand All @@ -27,7 +26,6 @@ const Button: React.ForwardRefRenderFunction<HTMLButtonElement, ButtonProps> = (
className,
icon,
disabled,
href,
rounded,
...passThroughProps
},
Expand Down
2 changes: 1 addition & 1 deletion components/compact-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const CompactButton: React.VoidFunctionComponent<CompactButtonProps> = ({
return (
<button
type="button"
className="inline-flex h-5 w-5 items-center justify-center rounded-full bg-gray-100 text-gray-400 transition-colors hover:bg-gray-200 active:bg-gray-300 active:text-gray-500"
className="inline-flex h-5 w-5 items-center justify-center rounded-full bg-slate-100 text-slate-400 transition-colors hover:bg-slate-200 active:bg-slate-300 active:text-slate-500"
onClick={onClick}
>
{Icon ? <Icon className="h-3 w-3" /> : children}
Expand Down
29 changes: 21 additions & 8 deletions components/create-poll.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,14 @@ import {
} from "../components/forms";
import StandardLayout from "../components/standard-layout";
import Steps from "../components/steps";
import { useUserName } from "../components/user-name-context";
import { encodeDateOption } from "../utils/date-time-utils";
import { SessionProps, useSession, withSession } from "./session";

type StepName = "eventDetails" | "options" | "userDetails";

const steps: StepName[] = ["eventDetails", "options", "userDetails"];

const required = <T extends unknown>(v: T | undefined): T => {
const required = <T,>(v: T | undefined): T => {
if (!v) {
throw new Error("Required value is missing");
}
Expand All @@ -38,16 +38,25 @@ const required = <T extends unknown>(v: T | undefined): T => {
const initialNewEventData: NewEventData = { currentStep: 0 };
const sessionStorageKey = "newEventFormData";

const Page: NextPage<{
export interface CreatePollPageProps extends SessionProps {
title?: string;
location?: string;
description?: string;
view?: "week" | "month";
}> = ({ title, location, description, view }) => {
}

const Page: NextPage<CreatePollPageProps> = ({
title,
location,
description,
view,
}) => {
const { t } = useTranslation("app");

const router = useRouter();

const session = useSession();

const [persistedFormData, setPersistedFormData] =
useSessionStorage<NewEventData>(sessionStorageKey, {
currentStep: 0,
Expand All @@ -59,6 +68,13 @@ const Page: NextPage<{
options: {
view,
},
userDetails:
session.user?.isGuest === false
? {
name: session.user.name,
contact: session.user.email,
}
: undefined,
});

const [formData, setTransientFormData] = React.useState(persistedFormData);
Expand All @@ -77,8 +93,6 @@ const Page: NextPage<{

const [isRedirecting, setIsRedirecting] = React.useState(false);

const [, setUserName] = useUserName();

const plausible = usePlausible();

const { mutate: createEventMutation, isLoading: isCreatingPoll } =
Expand All @@ -101,7 +115,6 @@ const Page: NextPage<{
{
onSuccess: (poll) => {
setIsRedirecting(true);
setUserName(poll.authorName);
plausible("Created poll", {
props: {
numberOfOptions: formData.options?.options?.length,
Expand Down Expand Up @@ -220,4 +233,4 @@ const Page: NextPage<{
);
};

export default Page;
export default withSession(Page);
53 changes: 32 additions & 21 deletions components/discussion/discussion.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,21 @@ import {
CreateCommentPayload,
} from "../../api-client/create-comment";
import { requiredString } from "../../utils/form-validation";
import Badge from "../badge";
import Button from "../button";
import CompactButton from "../compact-button";
import Dropdown, { DropdownItem } from "../dropdown";
import DotsHorizontal from "../icons/dots-horizontal.svg";
import Trash from "../icons/trash.svg";
import NameInput from "../name-input";
import TruncatedLinkify from "../poll/truncated-linkify";
import UserAvater from "../poll/user-avatar";
import UserAvatar from "../poll/user-avatar";
import { usePoll } from "../poll-context";
import { usePreferences } from "../preferences/use-preferences";
import { useUserName } from "../user-name-context";
import { useSession } from "../session";

export interface DiscussionProps {
pollId: string;
canDelete?: boolean;
}

interface CommentForm {
Expand All @@ -36,11 +37,9 @@ interface CommentForm {

const Discussion: React.VoidFunctionComponent<DiscussionProps> = ({
pollId,
canDelete,
}) => {
const { locale } = usePreferences();
const getCommentsQueryKey = ["poll", pollId, "comments"];
const [userName, setUserName] = useUserName();
const queryClient = useQueryClient();
const { data: comments } = useQuery(
getCommentsQueryKey,
Expand All @@ -64,6 +63,7 @@ const Discussion: React.VoidFunctionComponent<DiscussionProps> = ({
},
{
onSuccess: (newComment) => {
session.refresh();
queryClient.setQueryData(getCommentsQueryKey, (comments) => {
if (Array.isArray(comments)) {
return [...comments, newComment];
Expand All @@ -75,6 +75,8 @@ const Discussion: React.VoidFunctionComponent<DiscussionProps> = ({
},
);

const { poll } = usePoll();

const { mutate: deleteCommentMutation } = useMutation(
async (payload: { pollId: string; commentId: string }) => {
await axios.delete(`/api/poll/${pollId}/comments/${payload.commentId}`);
Expand All @@ -89,18 +91,16 @@ const Discussion: React.VoidFunctionComponent<DiscussionProps> = ({
},
);

const session = useSession();

const { register, setValue, control, handleSubmit, formState } =
useForm<CommentForm>({
defaultValues: {
authorName: userName,
authorName: "",
content: "",
},
});

React.useEffect(() => {
setValue("authorName", userName);
}, [setValue, userName]);

const handleDelete = React.useCallback(
(commentId: string) => {
deleteCommentMutation({ pollId, commentId });
Expand All @@ -124,6 +124,9 @@ const Discussion: React.VoidFunctionComponent<DiscussionProps> = ({
>
<AnimatePresence initial={false}>
{comments.map((comment) => {
const canDelete =
poll.role === "admin" || session.ownsObject(comment);

return (
<motion.div
transition={{ duration: 0.2 }}
Expand All @@ -137,12 +140,16 @@ const Discussion: React.VoidFunctionComponent<DiscussionProps> = ({
initial={{ scale: 0.8, y: 10 }}
animate={{ scale: 1, y: 0 }}
exit={{ scale: 0.8 }}
data-testid="comment"
className="w-fit rounded-xl border bg-white px-3 py-2 shadow-sm"
>
<div className="flex items-center space-x-2">
<UserAvater name={comment.authorName} />
<UserAvatar
name={comment.authorName}
showName={true}
isYou={session.ownsObject(comment)}
/>
<div className="mb-1">
<span className="mr-1">{comment.authorName}</span>
<span className="mr-1 text-slate-400">&bull;</span>
<span className="text-sm text-slate-500">
{formatRelative(
Expand Down Expand Up @@ -189,7 +196,6 @@ const Discussion: React.VoidFunctionComponent<DiscussionProps> = ({
},
{
onSuccess: () => {
setUserName(data.authorName);
setValue("content", "");
resolve(data);
},
Expand All @@ -201,23 +207,28 @@ const Discussion: React.VoidFunctionComponent<DiscussionProps> = ({
>
<textarea
id="comment"
placeholder="Add your comment…"
placeholder="Thanks for the invite!"
className="input w-full py-2 pl-3 pr-4"
{...register("content", { validate: requiredString })}
/>
<div className="mt-1 flex space-x-3">
<Controller
name="authorName"
control={control}
rules={{ validate: requiredString }}
render={({ field }) => <NameInput className="w-full" {...field} />}
/>
<div>
<Controller
name="authorName"
key={session.user?.id}
control={control}
rules={{ validate: requiredString }}
render={({ field }) => (
<NameInput {...field} className="w-full" />
)}
/>
</div>
<Button
htmlType="submit"
loading={formState.isSubmitting}
type="primary"
>
Send
Comment
</Button>
</div>
</form>
Expand Down
Loading

0 comments on commit 5c991d7

Please sign in to comment.