diff --git a/README.md b/README.md index 3d6c38f..f3d7262 100644 --- a/README.md +++ b/README.md @@ -1,122 +1,160 @@ -# 🌐 Lambda: An open-source privacy-based alternative to traditional social media -_**NOTE:** This version of Lambda is a prototype, it's for validating the idea, gaining traction, and testing purposes only_ +# 🌐 Privatus: An open-source privacy-based alternative to traditional social media +_**NOTE:** This version of Privatus is a prototype, it's for validating the idea, gaining traction, and testing purposes only_ -## ❓ What is Lambda ❓ -Lambda is an Open-source privacy-focused social media app. It focuses not only on privacy but also on your well-being preventing you from excessive use of social media. -It solves all the problems that big social media platforms have made, from privacy concerns to well-being to targeted advertisements, with just one solution. +## ❓ What is Privatus ❓ -With Lambda you can have an online experience like Facebook, X(formally Twitter), or even Instagram, but with no concerns about your privacy. +Privatus is an open-sourced privacy based social media app created as a better alternative to traditional social media app. Privatus promotes privacy by not collecting any kind of data about the user, other then email address used only for notification and identification purpose. -## ❓ Why Lambda ❓ -But then why Facebook? Or why X? +## ❓ Why Privatus ❓ +Privatus isn't just a one word solution, it combines multiple(useful) ideas into one place in order to achieve these goals and be the best social media app - transparency, openness, trust, privacy, and overall well being of users. -Why do you use those? Why can't you use a decentralized platform like Mastodon? - -But Why only those? - -There are multiple reasons why not to use Facebook or X, but if you value your privacy and want to make sure that you don't get tracked and your data is shared with advertisers -so that they can show you relevant ads and make you buy stuff you don't even want to. Here are the reasons why to pick Lambda over traditional social media platforms: +Here is how Privatus tends to achieve these goals - ### Organic Feed -traditional social media platforms use AI models to show you the most relevant and most highly clicked posts. Lambda has concerns about that, Lambda wants to make your online presence -as healthy as it can, to prevent you from spending too much time on social media, but not entirely give up on it, lambda generates your feed using nothing more than a list of `followee`, this is nothing more then a list of users you follow if you are not following anyone, lambda will not generate a feed for you and you would need to follow someone to generate a feed. Lambda simply takes your `followee` list and gets the latest posts from each user you are following and shows you those, thus making sure you are not given promoted content, high click rate posts, or any kind of other stuff. +traditional social media platforms use AI models to show you the most relevant and most highly clicked posts, this leads to unhealthy social media usage. Since _overall well-being_ is one of our goals, Privatus wants to make your online presence as healthy as it can, to prevent you from spending too much time on social media, but not entirely give up on it, Privatus generates your feed using nothing more than a list of `following`, this is a list of users you follow, if you are not following anyone Privatus will not generate a feed for you and you would need to follow someone to generate a feed. Privatus simply takes your `following` list and gets the latest posts from each user you are following and shows you those, thus making sure you are not given promoted content, high click rate posts, or any kind of addicting content. --- -### Non-targeted advertisements(after incorporation) -Lambda believes that advertisers have too much control over what social media users can see on a platform, they are second in control after the company itself. The majority of social media runs on ads, which makes those platforms keep their advertisers more happy than their users, this leads to methods of using users as a way of making more money. +### Non-personalized advertisements + +Privatus believes in win-win, not win-lose, which most social media platforms follow, where the advertisers and platform wins, and the users lose. To create a win-win for everyone without hurting advertisers goal and users well-being, Privatus uses a non-personalized generalized advertisement model. -Lambda uses a unique way to advertise and make money. It uses the same technique used by DuckDuckGo alongside the main 2 ways of advertising. +This model is similar to the technique used by DuckDuckGo. When ever you look at a post, depending on what type of post it is the same type of ads you are shown instead of a more personalized ads which depends on your search history on the platform. -There are 2 ways an advertiser can advertise on lambda: +For ex - If a user uses the platform to search mainly dog food, but views a post about **Cars** than the ads will be also about **Cars** and not about dog food companies. -1. Bidded advertisement. +For Advertisers there are 2 ways in which they can advertise on Privatus: + +1. Bided advertisement. 2. Click advertisement. -#### Bidded advertisement +Advertisers aren't given option between these 2, rather depending on their revenue and budget the option is selected. + +#### Bided advertisement -This method is useful for companies with high revenue and can afford to burn cash. +This method is for companies with high revenue. -In bidded advertisement, advertisers have to bid amount, their bid of the total amount determines how much of ad space they get. For Ex- +In bided advertisement, advertisers have to bid amount, the percent of their bid to the total amount determines how much of ads space they get. For Ex- -Company **A** wants to advertise, they are a car company, **A** bidded $1M, of the total amount that was raised from car advertisers, **A** got 10% of ad space allocated to car ads. -Now every month **A** needs to pay 1/10th of the percent ad space they got, this would be 1%, **A** will pay 1% of their bid, now their budget, every month to keep their ad running. +Company **A** wants to advertise, they are a car company, **A** bided $10M, assuming that in total all biders in ads space related to cars bid a total of $100M, company **A** will get 10 * 100/100 = 10% of total ads space allotted for car topics. -**A** now get's insight into competitors, such as how much competitors are bidding, how has the highest bid in their topic, and click rate on their ads with previous performance. +Now every month **A** needs to pay 1% of the amount they bid, now their budget, every month to keep their ad running. + +This method also gives **A** insight into their competitors, such as how much competitors are bidding, who has the highest bid in their topic, and click rate on their ads with previous performance. #### Click advertisement -This method is useful for companies or startups with low revenue. +This method is for companies with low revenue. In this method, companies pay as their ads get clicked, the same usual way, except, they don't get info on competitors. For Ex- -Company **B** wants to advertise, they have an AI product, **B**'s advertisement budget is set to $500K, of the total amount that was raised from AI advertisers, **B** gets 2% of ad space allocated to AI ads. +Company **B** wants to advertise, they have an AI product, **B**'s advertisement budget is set to $500K, assuming the total amount spend by all AI advertisers was $25M, **B** gets 2% of ad space allocated to AI ads. Now **B** pays $0.1/click. -This method comes with loses, **B** doesn't get insight on how much competitors are paying and who their competitors are. No competitor insight for click advertisement. +**B** gets insight into how their ads is doing, such as click rate. + +This method comes with loses, **B** doesn't get insight on how much competitors are paying and who their competitors are. #### What's ad space? -All empty white spaces next to blogs on the right are ad spaces, each blog has an ad space on its right, it's usually a small portion of the actual content so won't be that destructive. + +empty white spaces left on the right hand side of only blogs are ads space. These as of now aren't utilized because the current version of Privatus is just a prototype for testing purposes. + +--- + +### Content Moderation + +AI mods wouldn't be used as they can be modified for the benefit of the company and not the users of the platform. + +Moderation would be done in a hierarchy system where there would be 4 levels of mods. Each with its own powers and limitations. + +There will be 4 mods, helper mods, head mods, lead mods, and super mods: + +1. **Helper mods:** Every user applying for first time is a new mod, these can only review content and make sure community policies are being followed. +2. **Head mods:** Once leveled up, head mods can temporarily ban users and can do what helper mods can do. +3. **Lead mods:** Lead mods can delete content which doesn't follow content policy and also do what helper and head mods can do. +4. **Super mods:** They can only promote mods, they can't do what other mods can do, but they can level up mods and remove mods, but not add mods. They can also permanently ban users, but with justifiable statement. + +Due to having human moderators, all the posts made by users with <500K trusty points will not be published until it has been checked and verified by a moderator. + +In order to let other users know that the post is verified and following content policy, each post will have a badge with each badge having a different meaning as follow - + +| Badges | Meaning | +| ------ | ------- | +| Verified - {type} | The post has been checked by a moderator of {type} | +| Unverified | The author of the post is trusted but the post isn't checked by a moderator yet | +| Trusty | The user can be trusted | --- -### Human mods +### Trusty Point + +Trusty point is a basic point based system in which users are given points when they receive likes. + +For receiving trusty points the following can be done -To moderate content no AI mods would be used, rather human mods, anyone can apply to be a mod, but new mods won't have much power to moderate content. +| Event | Points earned | +| ---------------- | :-----------: | +| Post liked | +10 | +| Comment liked | +50 | +| Post disliked | -20 | +| Comment disliked | -100 | -Moderation is in a hierarchy where the top mods can promote lower mods, lower mods can only implement policies. +With trusty points you get certain privileges because you only receive points when the community likes your work. These privileges are - -There will be 4 mods, new mods, helper mods, head mods, and super mods: +| Points earned | Privileges | +| ------------- | ---------- | +| +10K | Liking other's posts and comments contribute to their trusty points | +| +500K | Posts won't be stopped, but will have unverified badge on it | +| +10M | Posts won't have unverified badge, rather trusty badge | +| +100M | Given Lead mod status(optional) | +| +500M | Given trusty badge on Profile | -1. **New mods:** Every mod applying first time is a new mod, these can only report users and ban users temporarily. -2. **Helper mods:** Once leveled up, helper mods can review content, all new content is firstly paused before publishing for review to make sure no content policies are disobeyed. Helper mods can't do what new mods can do. -3. **Head mods:** They can do what both new and helper mods can do, and they can remove content voilating content policies. -4. **Super mods:** They can only promote mods, they can't do what other mods can do, but they can level up mods and remove mods, but not add mods. They can also permanently ban users, but review is required. +More privileges to come. --- -## 🔍 Comparison: Lambda vs. Nostr vs. Mastodon +## 🔍 Comparison: Privatus vs. Decentralized Decentralized platforms are great, individuals create them and are maintained by their users, this means that anyone can use a decentralized protocol such as Nostr or Mastodon and build a whole new platform that focuses on decentralizing social media. -But these have their downfalls and disadvantages. +But they too have their disadvantages. ### Reimagining social media -**Lambda:** Lambda want's to reimagine social media by making an all in one platform, that also focuses on privacy, well-being, no to censorship, human content moderators, and is community-driven. +**Privatus:** Privatus wants to reimagine social media by making an all in one platform, that focuses on privacy, well-being, no to censorship, human moderators, and is community-driven. -**Decentralized:** Decentralized social media platforms aim to remove censorship, they are made to compete with platforms like X, and they don't tend to focus much on large scale but act as small individual entities that can be used separately. They also don't tend to add much features on the platform because each platform can have a unique feature but follows the same protocol throughout. +**Decentralized:** Decentralized social media platforms aim to remove censorship, they are made as an alternative to platforms mainly like X, and they don't tend to focus much on large scale but act as small individual entities that can be used separately. --- ### Moderation -**Lambda:** Though Lambda is currently a prototype for gaining traction and validating the idea, it is planned to implement a moderation system in which anyone who applies, with certain criteria reached, will be given moderation access, this would allow the community to moderate content flow in the platform. +**Privatus:** Though Privatus is currently a prototype for gaining traction and validating the idea, it is planned to implement a moderation system in which anyone who applies, with certain criteria reached, will be given moderation access, this would allow the community to moderate content which it wants to flow in the platform. -**Decentralized:** Aim to avoid centralized content moderation, but because of this approach, it can be challenging to address illegal or harmful content. This can lead to the free flow of illegal content on the platforms. +**Decentralized:** Aim to avoid centralized content moderation, because of this approach, it can be challenging to address illegal or harmful content. This can lead to the free flow of illegal content on the platforms. --- ### Technical Complexity -**Lambda:** Lambda is for now as a prototype, but once it gains traction it will be made into a fully scalable platform with proper management of technical issues. Using Lambda isn't that hard, it just requires your email, an even better option is to use an email service that creates fake emails for you to use. +**Privatus:** Privatus is for now as a prototype, but once it gains traction it will be made into a fully scalable platform with proper management of technical issues. Using Privatus isn't that hard, it just requires your email, an even better option is to use an email service that creates fake emails for you to use. -**Decentralized:** To use nostr or any other decentralized platform it requires a high level of technical understanding. Users need to manage private keys, and wallet addresses, and understand blockchain technology. This can be intimidating and confusing for the majority of non-technical species of humans, thus limiting the platform’s accessibility. +**Decentralized:** To use any decentralized platform it requires a bit of technical understanding. Users need to manage private keys, and wallet addresses, and understand blockchain technology. This can be intimidating and confusing for the majority of non-technical humans, thus limiting the platform’s accessibility. --- ### Feature and user experiences -**Lambda:** As mentioned before lambda is a prototype of what the real product would be, it does not have all the functionalities of a fully functional platform. Though some features might be not available in the prototype, would be available in the production-ready version, such as community feature(from Reddit), short nuntius, Latin for message(from X), and many other features that would be added to the real product. +**Privatus:** As mentioned before Privatus is a prototype of what the real product would be, it does not have all the functionalities of a fully functional platform. Though some features might not be available in the prototype, would be available in the production-ready version, such as community group, microblogging, private chat, trusty point and many other features that would be added to the real product. **Decentralized:** Decentralized social media platforms do not offer the same level of features and user experiences as more established, centralized platforms do. This can make it less appealing for users who are accustomed to the convenience and polish of mainstream social media networks. Also taking into account, majority of these platforms act as alternatives only to X and not the entire social media market. -Lambda wants to make your online experience better, focus on the stuff you want, not what you don't want but still keep getting. +Privatus wants to make your online experience better, focus on the stuff you want, not what you don't want but still keep getting. --- ## How to contribute ❓ -Lambda functions because of its users, you give the idea, we implement the idea, +Privatus functions because of its users, you give the idea, we implement the idea, > _its for the people, by the people, of the people_ @@ -124,46 +162,18 @@ Lambda functions because of its users, you give the idea, we implement the idea, To get started just hope over to the issue tab and write down an issue! -If it's a code solution that you might have, just use the [contribute.md file](https://github.com/ezpie1/lambda-official/blob/main/CONTRIBUTING.md) +If it's a code solution that you might have, just use the [contribute.md file](https://github.com/ezpie1/Privatus-official/blob/main/CONTRIBUTING.md) --- -## What's new? +## How can you help? -To make Lambda a reality, lambda is following this 3 month long milestone, in which it would aim to get at least 5K users. +First of all, if you are reading this section, thanks a lot for showing interest and wanting to help Privatus become the next big thing. Privatus is nothing without its users. -### Why does this matter? - -It matters cause this would help in many ways. - -Because Lambda is a social media platform, it would be competing against pre-established platforms with a large user base and can make their users stick on their platforms for a longer time. -Lambda must gain recognition and traction. By gaining 5K users, Lambda plans to get help and support from those users in spreading the word about a far better social media alternative that benefits everyone. - -### How can you help? - -First of all, if you are reading this section, thanks a lot for showing interest and wanting to help Lambda reach this milestone. Lambda is nothing without its users. - -To help Lambda reach such a milestone, please go ahead and share about lambda to your network, and share it with your friends, family members, and co-workers. For every new sign-up the prototype version of lambda gets, it adds more and more trust and support to Lambda. +To help Privatus grow, please go ahead and share about Privatus to your network, and share it with your friends, family members, and co-workers. For every new sign-up the prototype version of Privatus gets, it adds more and more trust and support to Privatus. It's more the better. -If Lambda reaches at least 5K users before October. - Once again thanks for showing support 😃. -**Current number of signups:** _80_(13 Oct 24) - -## Contact -If you have any query, just drop it in issues tab. - - - - - - - - - - - - +**Current number of signups:** _78_(17 Feb 25) \ No newline at end of file diff --git a/cypress.config.ts b/cypress.config.ts deleted file mode 100644 index 78eed99..0000000 --- a/cypress.config.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { defineConfig } from "cypress"; - -export default defineConfig({ - e2e: { - baseUrl: "http://localhost:3000", - setupNodeEvents(on, config) { - // implement node event listeners here - }, - }, -}); diff --git a/cypress/e2e/Auth.cy.ts b/cypress/e2e/Auth.cy.ts deleted file mode 100644 index 314a320..0000000 --- a/cypress/e2e/Auth.cy.ts +++ /dev/null @@ -1,62 +0,0 @@ -/// - -describe("Authentication Tests", () => { - it("Creating user", () => { - // Intercept the supabase POST request to check weather the response - // status code is 200 or not - cy.intercept( - "http://127.0.0.1:54321/auth/v1/signup?redirect_to=http%3A%2F%2Flocalhost%3A3000%2Fauth%2Fcallback" - ).as("SignupResponse"); - - cy.visit("/signup"); - - // get input field with id `username` and type `tester` as username - cy.get("#username").type("tester"); - - // The value of the `username` field keeps disappearing, to prevent that - // the wait command is being used to allow the app to process the input - cy.wait(1000); - - // get input field with id `email` and type `tester@test.com` as email - cy.get("#email").type("tester@test.com"); - - // get input field with id `password` and type `test1234` as password - cy.get("#password").type("test1234"); - - // check if all field's have their values and hasn't disappeared - cy.get("#username").should("have.value", "tester"); - cy.get("#email").should("have.value", "tester@test.com"); - cy.get("#password").should("have.value", "test1234"); - - // submit the form - cy.get("form").submit(); - - // Check if the response status code is 200 or not - cy.wait("@SignupResponse").its("response.statusCode").should("eq", 200); - }); - - it.only("Logging in user", () => { - // intercept the login response - cy.intercept("http://127.0.0.1:54321/auth/v1/token?grant_type=password").as( - "LoginResponse" - ); - - cy.visit("/login"); - - // get input field with id `email` and type `tester@test.com` as email - cy.get("#email").type("tester@test.com"); - - // get input field with id `password` and type `test1234` as password - cy.get("#password").type("test1234"); - - // check if all field's have their values and hasn't disappeared - cy.get("#email").should("have.value", "tester@test.com"); - cy.get("#password").should("have.value", "test1234"); - - // submit form to login user - cy.get("form").submit(); - - // check if the response status code is 200 - cy.wait("@LoginResponse").its("response.statusCode").should("eq", 200); - }); -}); diff --git a/cypress/e2e/PostingBlog.cy.ts b/cypress/e2e/PostingBlog.cy.ts deleted file mode 100644 index e02caf1..0000000 --- a/cypress/e2e/PostingBlog.cy.ts +++ /dev/null @@ -1,23 +0,0 @@ -describe("Posting Blog", () => { - it("Post should be added", () => { - // Login the test user - cy.LoginUser(); - - // refresh the page to head back to the homepage - cy.reload(); - - // get the `New Post` button and click it - cy.GetByTest("newPost").click(); - - // get the input with id `content` - cy.get("#content").type( - "Lorem ipsum dolor, sit amet consectetur adipisicing elit. Animi, quisquam quasi! Repudiandae aliquam atque adipisci repellendus. Minima nihil, ullam odit veritatis voluptatem placeat esse maxime, odio in, aliquam perspiciatis. Aliquam." - ); - - // get the input with id `title` - cy.get("#title").type("This is a test post"); - - // submit the form to add a new post - cy.GetByTest("submitBtn").click(); - }); -}); diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts deleted file mode 100644 index 82c9241..0000000 --- a/cypress/support/commands.ts +++ /dev/null @@ -1,47 +0,0 @@ -/// - -// declarations used for type safety -declare namespace Cypress { - interface Chainable { - LoginUser(): Chainable>; - GetByTest(testAttribute: string): Chainable>; - } -} - -// Used for logging in the test user `tester` -Cypress.Commands.add("LoginUser", () => { - // intercept the login response - cy.intercept("http://127.0.0.1:54321/auth/v1/token?grant_type=password").as( - "LoginResponse" - ); - - cy.visit("/login"); - - // get input field with id `email` and type `tester@test.com` as email - cy.get("#email").type("tester@test.com"); - - // get input field with id `password` and type `test1234` as password - cy.get("#password").type("test1234"); - - // check if all field's have their values and hasn't disappeared - cy.get("#email").should("have.value", "tester@test.com"); - - // wait for the app to process data - cy.wait(1000); - - cy.get("#password").should("have.value", "test1234"); - - // submit form to login user - cy.get("form").submit(); - - // wait for the process to happen - cy.wait(1000); - - // check if the response status code is 200 - cy.wait("@LoginResponse").its("response.statusCode").should("eq", 200); -}); - -// used for getting html tags with the `test-data` attribute -Cypress.Commands.add("GetByTest", (selector) => { - return cy.get(`[test-data=${selector}]`); -}); diff --git a/cypress/support/e2e.ts b/cypress/support/e2e.ts deleted file mode 100644 index f80f74f..0000000 --- a/cypress/support/e2e.ts +++ /dev/null @@ -1,20 +0,0 @@ -// *********************************************************** -// This example support/e2e.ts is processed and -// loaded automatically before your test files. -// -// This is a great place to put global configuration and -// behavior that modifies Cypress. -// -// You can change the location of this file or turn off -// automatically serving support files with the -// 'supportFile' configuration option. -// -// You can read more here: -// https://on.cypress.io/configuration -// *********************************************************** - -// Import commands.js using ES2015 syntax: -import './commands' - -// Alternatively you can use CommonJS syntax: -// require('./commands') \ No newline at end of file diff --git a/next.config.js b/next.config.js index 658404a..24f1a90 100644 --- a/next.config.js +++ b/next.config.js @@ -1,4 +1,6 @@ /** @type {import('next').NextConfig} */ -const nextConfig = {}; +const nextConfig = { + reactStrictMode: false, +}; module.exports = nextConfig; diff --git a/package.json b/package.json index b3bff9b..8600524 100644 --- a/package.json +++ b/package.json @@ -6,8 +6,7 @@ "dev": "next dev", "build": "next build", "start": "next start", - "lint": "next lint", - "cypress:open": "npx cypress open" + "lint": "next lint" }, "dependencies": { "@supabase/auth-helpers-nextjs": "^0.8.1", @@ -34,4 +33,4 @@ "tailwindcss": "^3", "typescript": "^5" } -} +} \ No newline at end of file diff --git a/public/logos/logo.svg b/public/logos/logo.svg index ab9d46a..b35690c 100644 --- a/public/logos/logo.svg +++ b/public/logos/logo.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 569b836..0fbb5ee 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -16,6 +16,7 @@ export default async function RootLayout({ }) { // Used for checking if the user is authenticated or not. If yes then display the navbars const supabase = createServerComponentClient({ cookies }); + const { data: { session }, } = await supabase.auth.getSession(); @@ -24,11 +25,11 @@ export default async function RootLayout({ - Lambda + Privatus - + {session && } -
{children}
+
{children}
diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index fdd9664..c9a3866 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -6,7 +6,7 @@ import LogInForm from "@/components/Auth/LogInForm"; import Separator from "@/components/form/separator"; // StyleSheet used for styling the form -import "@/styles/form.css"; +import "@/styles/auth/auth.css"; // Tell's vercel that this is a dynamic function export const dynamic = "force-dynamic"; @@ -22,9 +22,20 @@ export default async function LogIn() { } return ( -
-
-

LogIn Using Your Lambda Account

+
+
+
+

Privatus

+

The Social Media App

+

That values privacy

+
+
+

It's Open-source

+

and

+

Privacy based

+
+
+
diff --git a/src/app/page.tsx b/src/app/page.tsx index 5afac57..0abf0f6 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -3,7 +3,7 @@ import { cookies } from "next/headers"; import { redirect } from "next/navigation"; import { createServerComponentClient } from "@supabase/auth-helpers-nextjs"; -import Posts from "@/components/HomePage/DisplayPost"; +import HomePage from "@/components/HomePage/HomePageRenderer"; // Tell's vercel that this is a dynamic function export const dynamic = "force-dynamic"; @@ -21,31 +21,25 @@ export default async function Home() { redirect("/login"); } - // get the posts in latest order - const { data: latestPosts } = await supabase - .from("Blogs") - .select("*, author: profiles(*)") - .order("created_at", { ascending: false }); - // get the posts in most liked order const { data: popularPosts } = await supabase .from("Blogs") .select("*, author: profiles(*)") .order("likes", { ascending: false }); - /* eslint-disable max-len */ - return ( - - ); -} + // get current logged in username + const { data: loggedInUser } = await supabase.auth.getUser(); + const userID = loggedInUser.user?.id + const { data: loggedInUsername } = await supabase + .from("profiles") + .select("username") + .eq("id", String(userID)) + .single(); -function RenderedContent({latestPostsList, popularPostsList}: {latestPostsList: postWithAuthor[], popularPostsList: postWithAuthor[]}) { + const username = String(loggedInUsername?.username) + + /* eslint-disable max-len */ return ( -
- -
+ ); -} +} \ No newline at end of file diff --git a/src/app/post/[postId]/PostPageRenderer.tsx b/src/app/post/[postId]/PostPageRenderer.tsx new file mode 100644 index 0000000..aef3a6d --- /dev/null +++ b/src/app/post/[postId]/PostPageRenderer.tsx @@ -0,0 +1,53 @@ +"use client"; + +import Link from "next/link"; + +import Like from "@/components/Post/LikeBtn"; +import Formatter from "@/components/Post/MarkupFormatter"; +import SideBar from "@/components/Banners/SideBar"; + +import "@/styles/PostPage/PostPage.css"; + +interface Props { + post: postWithAuthor; + loggedInUsername: string | undefined; +} + +export default function PostRenderer({ post, loggedInUsername }: Props) { + + return ( + <> + +
+
+

+ {post.title} +

+
+ {post.content && } +
+
+
+ +
+
+
+

+ + {post.author?.username} + +

+

+ {post.author?.description} +

+
+ + ) +} \ No newline at end of file diff --git a/src/app/post/[postId]/page.tsx b/src/app/post/[postId]/page.tsx index 6be3223..02b8f3a 100644 --- a/src/app/post/[postId]/page.tsx +++ b/src/app/post/[postId]/page.tsx @@ -1,14 +1,8 @@ -// import necessary libraries import { createServerComponentClient } from "@supabase/auth-helpers-nextjs"; import { cookies } from "next/headers"; -import Link from "next/link"; -// Import the LikeBtn and CommentSection components -import Like from "@/components/Post/LikeBtn"; import CommentSection from "@/components/Post/CommentSection"; -import Formatter from "@/components/Post/MarkupFormatter"; -// importing stylesheet -import "@/styles/PostPage.css"; +import PostRenderer from "@/app/post/[postId]/PostPageRenderer"; // Tell's vercel that this is a dynamic function export const dynamic = "force-dynamic"; @@ -44,6 +38,17 @@ export default async function Page({ params }: { params: { postId: string } }) { likes: post.Likes.length, })) ?? []; + // used by the PostRenderer component + const { data: currentLoggedInUsernameArray } = await supabase + .from("profiles") + .select("username") + .eq("id", String(session?.user.id)) + .single(); + + // the returned data is an object - {username: name} + // For example - {username: "tester1"} + const currentLoggedInUsername = currentLoggedInUsernameArray?.username; + if (postInfo) { const post = postInfo[0]; @@ -51,31 +56,9 @@ export default async function Page({ params }: { params: { postId: string } }) { return (
-
-
-

- {post.title} -

-
- {post.content && } -
-
-
- -
-
-
-

- - {post.author?.username} - -

-

- {post.author?.description} -

-
+
-
+
diff --git a/src/app/search/[query]/SearchResultPosts.tsx b/src/app/search/[query]/SearchResultPosts.tsx index db51cd2..2e58f2c 100644 --- a/src/app/search/[query]/SearchResultPosts.tsx +++ b/src/app/search/[query]/SearchResultPosts.tsx @@ -35,13 +35,15 @@ export default function SearchPosts({ posts }: { posts: postWithAuthor[] }) { return ( <> {posts.map((post) => ( -
- +
+

- {post.author?.username} + + {post.author?.username} +

-

+

{post.title}

diff --git a/src/app/settings/profile/page.tsx b/src/app/settings/profile/page.tsx index 5b7e678..5226edc 100644 --- a/src/app/settings/profile/page.tsx +++ b/src/app/settings/profile/page.tsx @@ -32,8 +32,8 @@ export default async function Page() { .single(); return ( -

-
+
+
{userInfo != null && }
diff --git a/src/app/signup/page.tsx b/src/app/signup/page.tsx index 4d7df8a..e51d569 100644 --- a/src/app/signup/page.tsx +++ b/src/app/signup/page.tsx @@ -2,13 +2,24 @@ import SignUpForm from "@/components/Auth/SignUpForm"; import Separator from "@/components/form/separator"; // Stylesheet for styling the form with custom styles -import "@/styles/form.css"; +import "@/styles/auth/auth.css"; export default function SignUp() { return ( -
-
-

SignUp for a Lambda Account

+
+
+
+

Privatus

+

The Social Media App

+

That values privacy

+
+
+

It's Open-source

+

and

+

Privacy based

+
+
+
diff --git a/src/app/user/[username]/page.tsx b/src/app/user/[username]/page.tsx index 804a98f..bbbb7b2 100644 --- a/src/app/user/[username]/page.tsx +++ b/src/app/user/[username]/page.tsx @@ -1,12 +1,13 @@ -// importing necessary libraries and hooks import { createServerComponentClient } from "@supabase/auth-helpers-nextjs"; import { cookies } from "next/headers"; -import "@/styles/profilePage.css"; // stylesheet import Link from "next/link"; import Image from "next/image"; import Formatter from "@/components/Post/MarkupFormatter"; import FollowCoreBtn from "@/components/Profile/FollowBtnHandler"; +import SideBar from "@/components/Banners/SideBar"; + +import "@/styles/ProfilePage/profilePage.css"; // eslint-disable-line // Tell's vercel that this is a dynamic function export const dynamic = "force-dynamic"; @@ -35,10 +36,14 @@ export default async function UserProfile({ // getting the number of followers the current profile user has const { data: currentProfileUserFollowers } = await supabase - .from("profiles") - .select("followers") - .eq("username", params.username) - .single(); + .from("profiles") + .select("followers") + .eq("username", params.username) + .single(); + + // get current logged in username for the sidebar + const { data: loggedInUser } = await supabase.auth.getUser() + const currentLoggedInUsername = loggedInUser.user?.user_metadata.username // If we have the information then continue if (user) { @@ -51,44 +56,51 @@ export default async function UserProfile({ /* eslint-disable max-len */ return ( -
-
-
-
- User avatar -
-
-
-
-

- {user?.username} -

-

{user?.description}

-

- {currentProfileUserFollowers?.followers?.length} Followers -

+
+ +
+
+
+
+ User avatar +
-
- +
+
+

+ {user?.username} + + + +

+

{user?.description}

+

+ {currentProfileUserFollowers?.followers?.length} Follower(s) +

+
-
-
- {posts?.map((post) => ( - -
-

{post.title}

-

{post.content && }

-
- - ))} -
+
+ {posts?.map((post) => ( + +
+

{post.title}

+

{post.content && }

+
+ + ))} +
+
); } diff --git a/src/components/Auth/LogInForm.tsx b/src/components/Auth/LogInForm.tsx index ebffc72..ea52892 100644 --- a/src/components/Auth/LogInForm.tsx +++ b/src/components/Auth/LogInForm.tsx @@ -42,12 +42,12 @@ export default function LogInForm() { return (
handleLogIn(event)} > -

+


setUserEmail(e.target.value)} value={userEmail} + placeholder="Your Email..." />

-

+


setUserPassword(e.target.value)} value={userPassword} + placeholder="********" />

- +
); diff --git a/src/components/Auth/SignUpForm.tsx b/src/components/Auth/SignUpForm.tsx index af196ba..e8928c0 100644 --- a/src/components/Auth/SignUpForm.tsx +++ b/src/components/Auth/SignUpForm.tsx @@ -4,6 +4,8 @@ import { createClientComponentClient } from "@supabase/auth-helpers-nextjs"; import React, { useState } from "react"; +import { ValidateEmail, ValidateUsername } from "./Validator"; + /** * The signup form is used for creating a new user * @@ -18,6 +20,17 @@ export default function SignUpForm() { // Connect to supabase const supabase = createClientComponentClient(); + const isDataValid = async () => { + const isUsernameValid = await ValidateUsername(username); + const isEmailValid = await ValidateEmail(userEmail); + + if (isUsernameValid && isEmailValid) { + return true; + } + + return false; + } + /** * handle's creating a new user with supabase * @@ -27,36 +40,39 @@ export default function SignUpForm() { // Prevent the default functioning of the form event.preventDefault(); - // Create a new user with their email and password, store their provided - // username and email as meta data - const { data } = await supabase.auth.signUp({ - email: userEmail, - password: userPassword, - options: { - emailRedirectTo: `${location.origin}/auth/callback`, - data: { - username: username, - email: userEmail, + // Create a new user with their email and password + // store the provided username as meta data + if (await isDataValid()) { + const { data } = await supabase.auth.signUp({ + email: userEmail, + password: userPassword, + options: { + emailRedirectTo: `${location.origin}/auth/callback`, + data: { + username: username, + email: userEmail, + }, }, - }, - }); + }); - // If signup was successful then inform the user to check their email - if (data) { - alert("Check your Email for verification"); - } else { - alert("Ops! Try again"); + // If signup was successful then inform the user to check their email + if (data) { + alert("Check your Email for verification"); + } else { + alert("Ops! Try again"); + } } + }; return (
handleSignUp(event)} > -

+

{" "}
setUsername(e.target.value)} value={username} + placeholder="User_name" />

-

+

{" "}
setUserEmail(e.target.value)} value={userEmail} + placeholder="Your Email..." />

-

+

{" "}
setUserPassword(e.target.value)} value={userPassword} + placeholder="********" />

-
diff --git a/src/components/Auth/Validator.ts b/src/components/Auth/Validator.ts new file mode 100644 index 0000000..4a8745a --- /dev/null +++ b/src/components/Auth/Validator.ts @@ -0,0 +1,47 @@ +"use client"; + +import { createClientComponentClient } from "@supabase/auth-helpers-nextjs"; + +const supabase = createClientComponentClient(); + +export async function ValidateUsername(username: string) { + + if (/\s/.test(username)) { + alert("Invalid username. Try again") + return false; + } + + if (username == "") { + alert("Please provide username") + return false; + } + + + const { data } = await supabase + .from("profiles") + .select("username") + .eq("username", username) + .single(); + + if (data != null) { + alert("Username taken") + return false; + } + + return true; +} + +export async function ValidateEmail(email: string) { + const { data } = await supabase + .from("profiles") + .select("email") + .eq("email", email) + .single(); + + if (data != null) { + alert("Email already taken") + return false; + } + + return true; +} \ No newline at end of file diff --git a/src/components/Banners/NavBar.tsx b/src/components/Banners/NavBar.tsx index 778024d..66e54c5 100644 --- a/src/components/Banners/NavBar.tsx +++ b/src/components/Banners/NavBar.tsx @@ -1,4 +1,4 @@ -import "@/styles/navbar.css"; +import "@/styles/Banner/navbar.css"; // Import necessary libraries import { createServerComponentClient } from "@supabase/auth-helpers-nextjs"; @@ -50,16 +50,16 @@ export default async function NavBar() {
- - - + + +
- User Avatar
    - + -
    - + User Avatar{userInfo.username}
    - -
  • +
  • + Profile -
  • - - -
  • + +
  • +
  • + Settings -
  • - + +
diff --git a/src/components/Banners/SideBar.tsx b/src/components/Banners/SideBar.tsx new file mode 100644 index 0000000..de4b276 --- /dev/null +++ b/src/components/Banners/SideBar.tsx @@ -0,0 +1,149 @@ +"use client"; + +import { createClientComponentClient } from "@supabase/auth-helpers-nextjs"; +import Image from "next/image"; +import Link from "next/link"; +import { useEffect, useState } from "react"; + +import "@/styles/Banner/sidebar.css" + +interface Props { + ShowPopularPosts?: (isSelected: boolean) => void; + loggedInUsername: string; +} + +export default function SideBar({ ShowPopularPosts, loggedInUsername }: Props) { + const [isFeedSelected, setFeedSelected] = useState(true); + const [isPopularSelected, setPopularSelected] = useState(false); + const [followingArray, setFollowingArray] = useState([]); + + const supabase = createClientComponentClient(); + + const getFollowingArray = async () => { + const { data } = await supabase + .from("profiles") + .select("following") + .eq("username", loggedInUsername) + .single(); + + if (data?.following) { + setFollowingArray(data.following as string[]) + } + } + + useEffect(() => { + getFollowingArray(); + }) + + if (ShowPopularPosts) { + + return ( + <> +
+
    +
  • { + setFeedSelected(true); + setPopularSelected(false); + ShowPopularPosts(false); + } + }> + Feed +
  • +
  • { + setFeedSelected(false); + setPopularSelected(true); + ShowPopularPosts(true); + } + }> + Popular +
  • +
  • + + Profile + +
  • +
  • + + Settings + +
  • +
+
+
+

Following

+
    + {followingArray.length > 0 ? ( + followingArray.map((username: string, index: number) => ( + +
  • + This profile pic + {username} +
  • +
    + )) + ) : ( +

    You’re not following anyone.

    + )} +
+
+ + ) + } else { + return ( + <> +
+
    +
  • + + Home + +
  • +
  • + + Profile + +
  • +
  • + + Settings + +
  • +
+
+
+

Following

+
    + {followingArray.length > 0 ? ( + followingArray.map((username: string, index: number) => ( + +
  • + This profile pic + {username} +
  • +
    + )) + ) : ( +

    You’re not following anyone.

    + )} +
+
+ + ) + } +} \ No newline at end of file diff --git a/src/components/HomePage/DisplayPost.tsx b/src/components/HomePage/DisplayPost.tsx index fce9d48..0a3dc3a 100644 --- a/src/components/HomePage/DisplayPost.tsx +++ b/src/components/HomePage/DisplayPost.tsx @@ -1,100 +1,39 @@ "use client"; // Importing necessary libraries and hooks -import { createClientComponentClient } from "@supabase/auth-helpers-nextjs"; -import { useRouter } from "next/navigation"; -import { Suspense, useEffect, useState } from "react"; +import { Suspense } from "react"; -// Style -import "@/styles/postStyle.css" import UserOrganicFeed from "./OrganicFeedGenerator"; import PostDisplayFunction from "./PostDisplayRenderer"; // an interface for latest and popular posts types interface Props { - latestPosts: postWithAuthor[]; popularPosts: postWithAuthor[]; + isPopularSelected: boolean; } /** * Used for displaying all the posts in the Blogs table * - * @param {postWithAuthor[]} latestPosts - All the posts in latest to oldest order * @param {postWithAuthor[]} popularPosts - All the posts in most liked to least liked * * @returns JSX.Element */ -export default function Posts({ latestPosts, popularPosts }: Props) { - const [latestSelected, setLatestSelected] = useState(false); - const [feedSelected, setFeedSelected] = useState(true); - - // Setup supabase and router - const supabase = createClientComponentClient(); - const router = useRouter(); - - // Subscribe to realtime update for the Blogs table - useEffect(() => { - const channel = supabase - .channel("realtime posts") - .on( - "postgres_changes", - { - event: "*", - schema: "public", - table: "Blogs", - }, - () => { - router.refresh(); - } - ) - .subscribe(); - - return () => { - supabase.removeChannel(channel); - }; - }, [supabase, router]); +export default function Posts({ popularPosts, isPopularSelected }: Props) { /* eslint-disable max-len */ return ( <> -
-
    -
  • { - setLatestSelected(false) - setFeedSelected(true) - }} - > - Feed -
  • -
  • { - setLatestSelected(true) - setFeedSelected(false) - }} - > - Latest -
  • -
  • { - setLatestSelected(false) - setFeedSelected(false) - }} - > - Popular -
  • -
-
- Loading posts...

}> -
- {feedSelected && !latestSelected && } - {!feedSelected && latestSelected && } - {!feedSelected && !latestSelected && } -
-
+
+ Loading feed...

}> + +
+
+
+ Loading posts...

}> + +
+
); } \ No newline at end of file diff --git a/src/components/HomePage/HomePageRenderer.tsx b/src/components/HomePage/HomePageRenderer.tsx new file mode 100644 index 0000000..3e033f2 --- /dev/null +++ b/src/components/HomePage/HomePageRenderer.tsx @@ -0,0 +1,53 @@ +"use client"; + +import { useState } from "react"; + +import SideBar from "@/components/Banners/SideBar"; +import Posts from "@/components/HomePage/DisplayPost"; + +import "@/styles/HomePage/homepage.css" +import "@/styles/HomePage/postStyle.css" + +interface HomePageProps { + popularPosts: postWithAuthor[]; + loggedInUsername: string; +} + +export default function HomePage({ popularPosts, loggedInUsername }: HomePageProps) { // eslint-disable-line max-len + + const [isPopularSelected, setIsPopularSelected] = useState(false); + + const displayPopular = (isPopularSelected: boolean) => { + setIsPopularSelected(isPopularSelected) + } + + /* eslint-disable */ + return ( +
+ + +
+ ) + /* eslint-enable */ +} + +interface Props { + popularPostsList: postWithAuthor[]; + displayPopularIfSelected: boolean +} + +function RenderedContent({ popularPostsList, displayPopularIfSelected }: Props) { // eslint-disable-line + return ( +
+ +
+ ); +} diff --git a/src/components/HomePage/OrganicFeedGenerator.tsx b/src/components/HomePage/OrganicFeedGenerator.tsx index 926863e..204fa1d 100644 --- a/src/components/HomePage/OrganicFeedGenerator.tsx +++ b/src/components/HomePage/OrganicFeedGenerator.tsx @@ -16,9 +16,9 @@ export default async function UserOrganicFeed() { const loggedInUserId = user?.id; const { data } = await supabase - .from("profiles") - .select("following") - .eq("id", String(loggedInUserId)); + .from("profiles") + .select("following") + .eq("id", String(loggedInUserId)); // the current logged in user's following data // this works mainly because the json returned from the database is an object @@ -27,19 +27,19 @@ export default async function UserOrganicFeed() { const followingUsersPosts: postWithAuthor[] = []; // TypeScript... don't ask - if (loggedInUsersFollowingData) { + if (loggedInUsersFollowingData) { for (let i = 0; i < loggedInUsersFollowingData.length; i++) { const { data } = await supabase - .from("Blogs") - .select("*, author: profiles(*)") - .eq("author", String(loggedInUsersFollowingData[i])); - + .from("Blogs") + .select("*, author: profiles(*)") + .eq("author", String(loggedInUsersFollowingData[i])); + followingUsersPosts.push(...data ?? []); } - + // this helps sort the array in descending order with latest posts on top /* eslint-disable max-len */ - followingUsersPosts.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()); + followingUsersPosts.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()); // TODO: Display only top 10 posts for preventing latency and add more on reaching end } @@ -48,7 +48,7 @@ export default async function UserOrganicFeed() { return ( <> Building Feed Please wait

}> - {loggedInUsersFollowingData && } + {loggedInUsersFollowingData ? :

You are not following no one

}
) diff --git a/src/components/HomePage/PostDisplayRenderer.tsx b/src/components/HomePage/PostDisplayRenderer.tsx index 88a2f93..c105448 100644 --- a/src/components/HomePage/PostDisplayRenderer.tsx +++ b/src/components/HomePage/PostDisplayRenderer.tsx @@ -1,4 +1,5 @@ import Link from "next/link"; +import Image from "next/image"; import Formatter from "../Post/MarkupFormatter"; @@ -18,12 +19,18 @@ export default function PostDisplayFunction({ posts }: Props) { <> {posts.map((post) => (
- +
-

- {post.author?.username} +

+ user avatar + {post.author?.username}

-

+

{post.title}

diff --git a/src/components/NewPost.tsx b/src/components/NewPost.tsx index 86afa09..0c04728 100644 --- a/src/components/NewPost.tsx +++ b/src/components/NewPost.tsx @@ -5,7 +5,7 @@ import { useState } from "react"; import { useRouter } from "next/navigation"; // import stylesheet -import "@/styles/newPostPage.css"; +import "@/styles/NewPostPage/newPostPage.css"; // Tell's vercel that this is a dynamic function export const dynamic = "force-dynamic"; @@ -37,10 +37,10 @@ export default function NewPost() { // need to get the username of author for `author` column const { data: author } = await supabase - .from("profiles") - .select("username") - .eq("id", String(user?.id)) - .single(); + .from("profiles") + .select("username") + .eq("id", String(user?.id)) + .single(); // If user exists, then only add post to the Blogs table if (user) { @@ -50,7 +50,6 @@ export default function NewPost() { user_id: user.id, author: author?.username, likes: 0, - // author: }); if (status === 201) { diff --git a/src/components/Post/CommentBar.tsx b/src/components/Post/CommentBar.tsx index 4223344..76d00eb 100644 --- a/src/components/Post/CommentBar.tsx +++ b/src/components/Post/CommentBar.tsx @@ -68,19 +68,20 @@ export default function Comment({ postId, userId }: CommentInfo) { /* eslint-disable max-len */ return ( -

- handleComment(event)}> + handleComment(event)} + > setCommentData(e.target.value)} value={commentData} /> - -
); } diff --git a/src/components/Post/CommentSection.tsx b/src/components/Post/CommentSection.tsx index 5e8dd5e..ac3e6df 100644 --- a/src/components/Post/CommentSection.tsx +++ b/src/components/Post/CommentSection.tsx @@ -1,14 +1,12 @@ -// Importing styleSheet -import "@/styles/commentSection.css"; -// Importing necessary libraries import { createServerComponentClient } from "@supabase/auth-helpers-nextjs"; import { cookies } from "next/headers"; import Link from "next/link"; import Image from "next/image"; -// Importing CommentBar component -import Comment from "./CommentBar"; +import Comment from "@/components/Post/CommentBar"; + +import "@/styles/PostPage/commentSection.css"; // eslint-disable-line // Tell's vercel that this is a dynamic function export const dynamic = "force-dynamic"; @@ -43,7 +41,7 @@ export default async function CommentSection({ post }: { post: string }) { /* eslint-disable max-len */ return ( -
+ <>
{userId ? ( @@ -51,7 +49,7 @@ export default async function CommentSection({ post }: { post: string }) {

Ops! Refresh

)}
-
+
{comments?.map((comment) => (
@@ -63,7 +61,7 @@ export default async function CommentSection({ post }: { post: string }) { className="border-solid border-2 border-gray-400 rounded-full" />
-
+

{comment.profiles?.username} @@ -74,6 +72,6 @@ export default async function CommentSection({ post }: { post: string }) {

))}
-
+ ); } diff --git a/src/components/Post/LikeBtn.tsx b/src/components/Post/LikeBtn.tsx index c468b34..ef32b19 100644 --- a/src/components/Post/LikeBtn.tsx +++ b/src/components/Post/LikeBtn.tsx @@ -95,8 +95,8 @@ export default function Like({ post }: { post: postWithAuthor }) {