Compare commits

..

No commits in common. "73ee4774711af982376297cc2c71aa4432d2091c" and "719f83a444d35d71ba18d02e503ff2865a854edb" have entirely different histories.

42 changed files with 7889 additions and 1 deletions

1
.env.example Normal file
View File

@ -0,0 +1 @@
DATABASE_URL=""

13
.eslintignore Normal file
View File

@ -0,0 +1,13 @@
.DS_Store
node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example
# Ignore files for PNPM, NPM and YARN
pnpm-lock.yaml
package-lock.json
yarn.lock

15
.eslintrc.cjs Normal file
View File

@ -0,0 +1,15 @@
module.exports = {
root: true,
extends: ['eslint:recommended', 'prettier'],
plugins: ['svelte3'],
overrides: [{ files: ['*.svelte'], processor: 'svelte3/svelte3' }],
parserOptions: {
sourceType: 'module',
ecmaVersion: 2020
},
env: {
browser: true,
es2017: true,
node: true
}
};

10
.gitignore vendored Normal file
View File

@ -0,0 +1,10 @@
.DS_Store
node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example
vite.config.js.timestamp-*
vite.config.ts.timestamp-*

1
.npmrc Normal file
View File

@ -0,0 +1 @@
engine-strict=true

13
.prettierignore Normal file
View File

@ -0,0 +1,13 @@
.DS_Store
node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example
# Ignore files for PNPM, NPM and YARN
pnpm-lock.yaml
package-lock.json
yarn.lock

9
.prettierrc Normal file
View File

@ -0,0 +1,9 @@
{
"useTabs": true,
"singleQuote": true,
"trailingComma": "none",
"printWidth": 100,
"plugins": ["prettier-plugin-svelte"],
"pluginSearchDirs": ["."],
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
}

View File

@ -1,2 +1,38 @@
# website
# create-svelte
Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/master/packages/create-svelte).
## Creating a project
If you're seeing this, you've probably already done this step. Congrats!
```bash
# create a new project in the current directory
npm create svelte@latest
# create a new project in my-app
npm create svelte@latest my-app
```
## Developing
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
```bash
npm run dev
# or start the server and open the app in a new browser tab
npm run dev -- --open
```
## Building
To create a production version of your app:
```bash
npm run build
```
You can preview the production build with `npm run preview`.
> To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment.

17
jsconfig.json Normal file
View File

@ -0,0 +1,17 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true
}
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias and https://kit.svelte.dev/docs/configuration#files
//
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
// from the referenced tsconfig.json - TypeScript does not merge them in
}

6637
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

41
package.json Normal file
View File

@ -0,0 +1,41 @@
{
"name": "ai-outreach",
"version": "0.0.1",
"private": true,
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json --watch",
"test": "playwright test",
"test:unit": "vitest",
"lint": "prettier --plugin-search-dir . --check . && eslint .",
"format": "prettier --plugin-search-dir . --write ."
},
"devDependencies": {
"@playwright/test": "^1.28.1",
"@sveltejs/adapter-auto": "^2.0.0",
"@sveltejs/kit": "^1.5.0",
"eslint": "^8.28.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-svelte3": "^4.0.0",
"fb-sdk-v15": "^1.1.0",
"i": "^0.3.7",
"lodash": "^4.17.21",
"npm": "^9.6.3",
"prettier": "^2.8.0",
"prettier-plugin-svelte": "^2.8.1",
"prisma": "^4.12.0",
"puppeteer": "^19.8.3",
"svelte": "^3.54.0",
"svelte-check": "^3.0.1",
"typescript": "^5.0.0",
"vite": "^4.2.0",
"vitest": "^0.25.3"
},
"type": "module",
"dependencies": {
"@prisma/client": "^4.12.0"
}
}

10
playwright.config.js Normal file
View File

@ -0,0 +1,10 @@
/** @type {import('@playwright/test').PlaywrightTestConfig} */
const config = {
webServer: {
command: 'npm run build && npm run preview',
port: 4173
},
testDir: 'tests'
};
export default config;

46
prisma/schema.prisma Normal file
View File

@ -0,0 +1,46 @@
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model Prospect {
id String @id @default(uuid())
name String
email String @unique
media SocialMedia
mediaId String
autopilot Boolean @default(true)
account Account @relation(fields: [accountId], references: [id])
accountId String
}
model templateMessage {
id String @id @default(uuid())
content String
account Account @relation(fields: [accountId], references: [id])
accountId String
}
model Account {
id String @id @default(uuid())
username String @unique
email String @unique
hashed_password String
prospects Prospect[]
templateMessages templateMessage[]
}
enum SocialMedia {
LINKEDIN
TWITTER
INSTAGRAM
FACEBOOK
WHATSAPP
}

12
src/app.d.ts vendored Normal file
View File

@ -0,0 +1,12 @@
// See https://kit.svelte.dev/docs/types#app
// for information about these interfaces
declare global {
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface Platform {}
}
}
export {};

68
src/app.html Normal file
View File

@ -0,0 +1,68 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width" />
<style>
:root {
--color_text_default: rgba(71, 84, 103, 1);
--color_text_default_rgb: 71, 84, 103;
--color_alert_default: rgba(250, 181, 21, 1);
--color_alert_default_rgb: 250, 181, 21;
--color_primary_default: rgba(97, 114, 243, 1);
--color_primary_default_rgb: 97, 114, 243;
--color_success_default: rgba(23, 219, 78, 1);
--color_success_default_rgb: 23, 219, 78;
--color_surface_default: rgba(255, 255, 255, 1);
--color_surface_default_rgb: 255, 255, 255;
--color_background_default: rgba(255, 255, 255, 0);
--color_background_default_rgb: 255, 255, 255;
--color_destructive_default: rgba(255, 0, 0, 1);
--color_destructive_default_rgb: 255, 0, 0;
--color_primary_contrast_default: rgba(56, 73, 128, 1);
--color_primary_contrast_default_rgb: 56, 73, 128;
}
* {
box-sizing: border-box;
}
body {
font-size: 1rem;
margin: 0;
padding: 0;
font-family: Helvetica, Arial, sans-serif;
}
</style>
<script async>
window.fbAsyncInit = function() {
FB.init({
appId : '206087212116799',
cookie : true,
xfbml : true,
version : 'v16.0'
});
FB.AppEvents.logPageView();
};
(function(d, s, id){
var js, fjs = d.getElementsByTagName(s)[0];
if (d.getElementById(id)) {return;}
js = d.createElement(s); js.id = id;
js.src = "https://connect.facebook.net/en_US/sdk.js";
fjs.parentNode.insertBefore(js, fjs);
}(document, 'script', 'facebook-jssdk'));
</script>
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

View File

@ -0,0 +1,84 @@
<script>
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
/**
* @type {boolean}
*/
export let checked;
const handleCheck = () => {
dispatch('toggle', {
checked
});
};
</script>
<label class="switch">
<div class="switch-container">
<input type="checkbox" bind:checked on:change={handleCheck} />
<span class="slider round" />
</div>
</label>
<style>
.switch {
position: relative;
display: inline-block;
width: 60px;
height: 34px;
}
.switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
-webkit-transition: 0.4s;
transition: 0.4s;
}
.slider:before {
position: absolute;
content: '';
height: 26px;
width: 26px;
left: 4px;
bottom: 4px;
background-color: white;
-webkit-transition: 0.4s;
transition: 0.4s;
}
input:checked + .slider {
background-color: #2196f3;
}
input:focus + .slider {
box-shadow: 0 0 1px #2196f3;
}
input:checked + .slider:before {
-webkit-transform: translateX(26px);
-ms-transform: translateX(26px);
transform: translateX(26px);
}
/* Rounded sliders */
.slider.round {
border-radius: 34px;
}
.slider.round:before {
border-radius: 50%;
}
</style>

7
src/index.test.js Normal file
View File

@ -0,0 +1,7 @@
import { describe, it, expect } from 'vitest';
describe('sum test', () => {
it('adds 1 + 2 to equal 3', () => {
expect(1 + 2).toBe(3);
});
});

11
src/lib/server/prisma.js Normal file
View File

@ -0,0 +1,11 @@
import { PrismaClient } from "@prisma/client"
global.prisma;
const prisma = global.prisma || new PrismaClient()
if (process.env.NODE_ENV === "development") {
global.prisma = prisma
}
export { prisma }

2
src/routes/+page.svelte Normal file
View File

@ -0,0 +1,2 @@
<h1>Welcome to SvelteKit</h1>
<p>Visit <a href="https://kit.svelte.dev">kit.svelte.dev</a> to read the documentation</p>

View File

@ -0,0 +1,178 @@
<script>
import { currentPage } from './stores.js';
import { browser } from '$app/environment';
if (browser) {
let path = window.location.pathname.split('/');
if (path[2]) {
currentPage.set(path[2]);
}
}
/**
*
* @param {string} page
*/
function navigate(page) {
currentPage.set(page);
}
</script>
<div class="container">
<div class="sidebar">
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-missing-attribute -->
<div class="nav-wrapper">
<div
on:click={() => navigate('messages')}
class="nav-button"
class:selected={$currentPage === 'messages'}
>
<div class="nav-icon">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="rgba(152, 162, 179, 1)"
aria-hidden="true"
>
<path
fill-rule="evenodd"
d="M10 3c-4.31 0-8 3.033-8 7 0 2.024.978 3.825 2.499 5.085a3.478 3.478 0 01-.522 1.756.75.75 0 00.584 1.143 5.976 5.976 0 003.936-1.108c.487.082.99.124 1.503.124 4.31 0 8-3.033 8-7s-3.69-7-8-7zm0 8a1 1 0 100-2 1 1 0 000 2zm-2-1a1 1 0 11-2 0 1 1 0 012 0zm5 1a1 1 0 100-2 1 1 0 000 2z"
clip-rule="evenodd"
/></svg
>
</div>
</div>
<div
on:click={() => navigate('prospects')}
class="nav-button"
class:selected={$currentPage === 'prospects'}
>
<div class="nav-icon">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="rgba(152, 162, 179, 1)"
aria-hidden="true"
>
<path
d="M7 8a3 3 0 100-6 3 3 0 000 6zM14.5 9a2.5 2.5 0 100-5 2.5 2.5 0 000 5zM1.615 16.428a1.224 1.224 0 01-.569-1.175 6.002 6.002 0 0111.908 0c.058.467-.172.92-.57 1.174A9.953 9.953 0 017 18a9.953 9.953 0 01-5.385-1.572zM14.5 16h-.106c.07-.297.088-.611.048-.933a7.47 7.47 0 00-1.588-3.755 4.502 4.502 0 015.874 2.636.818.818 0 01-.36.98A7.465 7.465 0 0114.5 16z"
/></svg
>
</div>
</div>
<div
on:click={() => navigate('settings')}
class="nav-button"
class:selected={$currentPage === 'settings'}
>
<div class="nav-icon">
<svg
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M11 3H13C13.5523 3 14 3.44772 14 4V4.56879C14 4.99659 14.2871 5.36825 14.6822 5.53228C15.0775 5.69638 15.5377 5.63384 15.8403 5.33123L16.2426 4.92891C16.6331 4.53838 17.2663 4.53838 17.6568 4.92891L19.071 6.34312C19.4616 6.73365 19.4615 7.36681 19.071 7.75734L18.6688 8.1596C18.3661 8.46223 18.3036 8.92247 18.4677 9.31774C18.6317 9.71287 19.0034 10 19.4313 10L20 10C20.5523 10 21 10.4477 21 11V13C21 13.5523 20.5523 14 20 14H19.4312C19.0034 14 18.6318 14.2871 18.4677 14.6822C18.3036 15.0775 18.3661 15.5377 18.6688 15.8403L19.071 16.2426C19.4616 16.6331 19.4616 17.2663 19.071 17.6568L17.6568 19.071C17.2663 19.4616 16.6331 19.4616 16.2426 19.071L15.8403 18.6688C15.5377 18.3661 15.0775 18.3036 14.6822 18.4677C14.2871 18.6318 14 19.0034 14 19.4312V20C14 20.5523 13.5523 21 13 21H11C10.4477 21 10 20.5523 10 20V19.4313C10 19.0034 9.71287 18.6317 9.31774 18.4677C8.92247 18.3036 8.46223 18.3661 8.1596 18.6688L7.75732 19.071C7.36679 19.4616 6.73363 19.4616 6.34311 19.071L4.92889 17.6568C4.53837 17.2663 4.53837 16.6331 4.92889 16.2426L5.33123 15.8403C5.63384 15.5377 5.69638 15.0775 5.53228 14.6822C5.36825 14.2871 4.99659 14 4.56879 14H4C3.44772 14 3 13.5523 3 13V11C3 10.4477 3.44772 10 4 10L4.56877 10C4.99658 10 5.36825 9.71288 5.53229 9.31776C5.6964 8.9225 5.63386 8.46229 5.33123 8.15966L4.92891 7.75734C4.53838 7.36681 4.53838 6.73365 4.92891 6.34313L6.34312 4.92891C6.73365 4.53839 7.36681 4.53839 7.75734 4.92891L8.15966 5.33123C8.46228 5.63386 8.9225 5.6964 9.31776 5.53229C9.71288 5.36825 10 4.99658 10 4.56876V4C10 3.44772 10.4477 3 11 3Z"
stroke="#000000"
stroke-width="1.5"
/>
<path
d="M14 12C14 13.1046 13.1046 14 12 14C10.8954 14 10 13.1046 10 12C10 10.8954 10.8954 10 12 10C13.1046 10 14 10.8954 14 12Z"
stroke="#000000"
stroke-width="1.5"
/>
</svg>
</div>
</div>
</div>
<div class="nav-button">
<div class="nav-icon">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="rgba(152, 162, 179, 1)"
aria-hidden="true"
>
<path
fill-rule="evenodd"
d="M3 4.25A2.25 2.25 0 015.25 2h5.5A2.25 2.25 0 0113 4.25v2a.75.75 0 01-1.5 0v-2a.75.75 0 00-.75-.75h-5.5a.75.75 0 00-.75.75v11.5c0 .414.336.75.75.75h5.5a.75.75 0 00.75-.75v-2a.75.75 0 011.5 0v2A2.25 2.25 0 0110.75 18h-5.5A2.25 2.25 0 013 15.75V4.25z"
clip-rule="evenodd"
/>
<path
fill-rule="evenodd"
d="M6 10a.75.75 0 01.75-.75h9.546l-1.048-.943a.75.75 0 111.004-1.114l2.5 2.25a.75.75 0 010 1.114l-2.5 2.25a.75.75 0 11-1.004-1.114l1.048-.943H6.75A.75.75 0 016 10z"
clip-rule="evenodd"
/></svg
>
</div>
</div>
</div>
<div style="width:100%">
<slot />
</div>
</div>
<style>
.container {
display: flex;
flex-direction: row;
height: 100vh;
}
.sidebar {
display: flex;
width: 70px;
background-color: rgb(252, 252, 253);
align-self: flex-start;
height: 100%;
box-sizing: border-box;
flex-direction: column;
}
.nav-wrapper {
display: flex;
flex-direction: column;
height: 100%;
}
.nav-icon {
place-self: center;
min-width: 20px;
max-width: 20px;
min-height: 20px;
max-height: 20px;
width: 20px;
height: 20px;
margin: 0px;
}
.nav-button {
align-self: center;
min-width: 0px;
order: 5;
min-height: 0px;
height: max-content;
width: auto;
margin: 10px;
overflow: visible;
background-color: rgb(249, 250, 251);
border-radius: 10px;
padding: 10px;
cursor: pointer;
}
.nav-button.selected {
background-color: rgb(242, 244, 247);
}
.profile-icon {
display: flex;
flex-direction: column;
width: 100%;
/* align-self: flex-end; */
}
</style>

View File

@ -0,0 +1,7 @@
import { prisma } from '$lib/server/prisma'
export async function load() {
return {
prospects: await prisma.prospect.findMany()
}
}

View File

@ -0,0 +1,47 @@
<script context="module">
import { setContext } from 'svelte';
export async function load() {
FB.api('/me', { locale: 'en_us', fields: 'id,first_name,last_name,email' }, (res) => {
setContext(res.authResponse);
});
}
</script>
<script>
// @ts-nocheck
import { currentPage } from './stores.js';
import Prospects from './pages/prospects.svelte';
import Messages from './pages/messages.svelte';
import Settings from './pages/settings.svelte';
const pages = {
messages: Messages,
prospects: Prospects,
settings: Settings
};
export let data;
</script>
<div class="center">
<div class="panel">
<svelte:component this={pages[$currentPage]} {data} />
</div>
</div>
<style>
.center {
background-color: #f2f4f7;
height: 100%;
width: 100%;
padding: 4rem;
}
.panel {
height: 100%;
background-color: rgb(252, 252, 253);
box-shadow: rgb(208, 213, 221) 0px 2px 16px -5px;
border-radius: 20px;
overflow: hidden;
}
</style>

View File

@ -0,0 +1,285 @@
<script>
/**
* Represents a contact with an array of messages.
* @typedef {Object} Contact
* @property {string} name - The name of the contact.
* @property {Message[]} messages - An array of messages sent to/from the contact.
*/
/**
* Represents a message sent to/from a contact.
* @typedef {Object} Message
* @property {string} sender - The sender of the message.
* @property {string} text - The text of the message.
* @property {Date} timestamp - The timestamp of the message.
*/
import { onMount } from 'svelte';
import { afterUpdate } from 'svelte';
import Switch from '@components/switch.svelte';
export let data;
let contacts = data.prospects;
let selectedContact = contacts[0];
let messageInput = '';
/**
* @type {Message[]}
*/
let messages = [];
/**
* @param {string} name
*/
function addContact(name) {
contacts.push({ name, messages: [] });
}
function addMessage() {}
function sendMessage() {
if (messageInput.trim() !== '') {
}
}
onMount(() => {
messages = selectedContact.messages;
});
afterUpdate(() => {
messages = selectedContact.messages;
});
</script>
<div class="container">
<div class="contacts">
{#each contacts as contact}
<div
class="contact {selectedContact === contact ? 'selected' : ''}"
on:click={() => (selectedContact = contact)}
>
{contact.name}
</div>
{/each}
</div>
<div class="chat-container">
<div class="header">
<p class="contact-name">{selectedContact.name}</p>
<div style="display:flex;flex-direction:row;align-items:center;">
AI responses:
<Switch checked={selectedContact.autopilot} />
</div>
</div>
<div class="messages">
<!-- {#each messages as message}
<div class="message-container {message.sender === 'me' ? 'sent' : 'received'}">
<div class="message">
<div class="message-content">
<div class="message-text">{message.text}</div>
<div class="message-timestamp">
{new Date(message.timestamp).toLocaleTimeString([], {
hour12: false,
hour: '2-digit',
minute: '2-digit'
})}
</div>
</div>
</div>
</div>
{/each} -->
</div>
<div class="input-container">
<div class="input">
<input type="text" bind:value={messageInput} />
<button on:click={sendMessage}>Send</button>
</div>
</div>
</div>
</div>
<style>
.container {
display: flex;
height: 100%;
}
.chat-container {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
}
.contacts {
width: 20%;
padding: 1rem;
overflow-y: auto;
}
.contact {
padding: 0.5rem;
cursor: pointer;
}
.contact.selected {
background-color: #e6e6e6;
}
.message-container {
display: flex;
position: relative;
align-content: stretch;
justify-content: flex-end;
flex-direction: column;
flex-wrap: nowrap;
box-sizing: content-box;
min-height: 50px;
border-top: 0px none rgb(150, 150, 150);
border-left: 0px none rgb(150, 150, 150);
margin-top: 0px;
}
.messages {
display: flex;
flex-direction: column;
padding: 1rem 2rem;
overflow-y: auto;
height: 100%;
}
.message {
display: flex;
align-self: flex-end;
align-content: stretch;
min-width: 0px;
order: 3;
min-height: 0px;
height: max-content;
flex-grow: 0;
flex-shrink: 0;
width: auto;
margin: 0px;
justify-content: flex-end;
gap: 8px;
overflow: visible;
border-radius: 0px;
padding: 8px;
}
.message-content {
display: flex;
align-content: stretch;
align-self: flex-start;
min-width: 70px;
order: 2;
min-height: 0px;
width: max-content;
flex-grow: 0;
height: max-content;
margin: 0px;
justify-content: space-between;
gap: 8px;
overflow: visible;
border-radius: 20px 20px 0px;
padding: 10px 10px 10px 20px;
color: rgb(254, 255, 255);
background-color: var(--color_primary_default);
}
.message-container.received .message {
align-self: flex-start !important;
}
.message-container.received .message .message-content {
background-color: rgb(242, 244, 247);
border-radius: 20px 20px 20px 0px;
color: var(--color_text_default);
}
.message-container.received .message .message-content .message-timestamp {
color: rgba(var(--color_text_default_rgb), 0.75);
}
.message-text {
position: relative;
align-self: flex-start;
min-width: 0px;
max-width: 500px;
order: 1;
min-height: 0px;
width: max-content;
flex-grow: 0;
height: max-content;
margin: 0px;
white-space: pre-wrap;
overflow: visible;
word-break: break-word;
font-size: 15px;
font-weight: 400;
line-height: 1.4;
border-radius: 0px;
}
.message-timestamp {
align-self: flex-end;
min-width: 0px;
order: 2;
min-height: 0px;
width: max-content;
flex-grow: 0;
height: max-content;
margin: 0px;
white-space: pre-wrap;
overflow: visible;
word-break: break-word;
font-size: 10px;
font-weight: 400;
color: rgba(255, 255, 255, 0.75);
text-align: right;
line-height: 1.5;
border-radius: 0px;
}
.input-container {
display: flex;
flex-direction: column;
justify-content: flex-end;
width: 100%;
}
.header {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
padding: 1rem;
font-weight: bold;
}
.input {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem;
}
input {
flex: 1;
margin-right: 1rem;
}
button {
background-color: #007bff;
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 0.5rem;
cursor: pointer;
}
</style>

View File

@ -0,0 +1,217 @@
<script>
/**
* @typedef {Object} Prospect
* @property {number} id
* @property {string} name
* @property {string} email
* @property {string} media
* @property {string} mediaId
* @property {boolean} autopilot
* @property {string} accountId
*/
import Switch from '@components/switch.svelte';
export let data;
/**
* @type {Prospect[]}
*/
let prospects = data.prospects;
/**
* @param {number} id
*/
function deleteProspect(id) {
prospects = prospects.filter((prospect) => prospect.id !== id);
}
/**
* @param {number} id
* @param {boolean} value
*/
function setAiResponses(id, value) {
prospects = prospects.map((prospect) => {
if (prospect.id === id) {
prospect.autopilot = value;
}
return prospect;
});
}
</script>
<div>
<div class="table-header">
<div class="table-column-title stretch">Name</div>
<div class="table-column-title stretch">Bot Activation</div>
<div class="table-column-title" style="min-width: 147px">
<div class="table-column-title stretch">Archive</div>
<div class="table-column-title stretch">Block</div>
<div class="table-column-title stretch">Delete</div>
</div>
</div>
{#each prospects as prospect}
<div class="prospect" data-key={prospect.id}>
<div class="prospect-info stretch">
<img
class="prospect-icon"
src={`/socials/${prospect.media.toLocaleLowerCase()}-icon.png`}
alt={prospect.name}
/>
<span class="prospect-name">{prospect.name}</span>
</div>
<div class="prospect-bot-toggle stretch">
<Switch
checked={prospect.autopilot}
on:toggle={(event) => setAiResponses(prospect.id, event.detail.checked)}
/>
</div>
<div class="prospect-actions">
<button class="prospect-action" on:click={() => console.log()}>
<div class="prospect-action-icon">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path d="M2 3a1 1 0 00-1 1v1a1 1 0 001 1h16a1 1 0 001-1V4a1 1 0 00-1-1H2z" />
<path
fill-rule="evenodd"
d="M2 7.5h16l-.811 7.71a2 2 0 01-1.99 1.79H4.802a2 2 0 01-1.99-1.79L2 7.5zM7 11a1 1 0 011-1h4a1 1 0 110 2H8a1 1 0 01-1-1z"
clip-rule="evenodd"
/></svg
>
</div>
</button>
<button class="prospect-action" on:click={() => console.log()}>
<div class="prospect-action-icon">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fill-rule="evenodd"
d="M10 1a4.5 4.5 0 00-4.5 4.5V9H5a2 2 0 00-2 2v6a2 2 0 002 2h10a2 2 0 002-2v-6a2 2 0 00-2-2h-.5V5.5A4.5 4.5 0 0010 1zm3 8V5.5a3 3 0 10-6 0V9h6z"
clip-rule="evenodd"
/></svg
>
</div>
</button>
<button class="prospect-action" on:click={() => deleteProspect(prospect.id)}>
<div class="prospect-action-icon">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fill-rule="evenodd"
d="M8.75 1A2.75 2.75 0 006 3.75v.443c-.795.077-1.584.176-2.365.298a.75.75 0 10.23 1.482l.149-.022.841 10.518A2.75 2.75 0 007.596 19h4.807a2.75 2.75 0 002.742-2.53l.841-10.52.149.023a.75.75 0 00.23-1.482A41.03 41.03 0 0014 4.193V3.75A2.75 2.75 0 0011.25 1h-2.5zM10 4c.84 0 1.673.025 2.5.075V3.75c0-.69-.56-1.25-1.25-1.25h-2.5c-.69 0-1.25.56-1.25 1.25v.325C8.327 4.025 9.16 4 10 4zM8.58 7.72a.75.75 0 00-1.5.06l.3 7.5a.75.75 0 101.5-.06l-.3-7.5zm4.34.06a.75.75 0 10-1.5-.06l-.3 7.5a.75.75 0 101.5.06l.3-7.5z"
clip-rule="evenodd"
/></svg
>
</div>
</button>
</div>
</div>
{/each}
</div>
<style>
.table-header {
display: flex;
flex-direction: row;
align-content: stretch;
background-color: rgb(249, 250, 251);
height: min-content;
padding: 10px;
}
.table-column-title {
display: flex;
flex-direction: row;
gap: 10px;
font-size: 14px;
align-content: space-between;
}
.table-column-title.stretch {
width: 100%;
}
.prospect {
display: flex;
flex-direction: row;
align-content: stretch;
justify-content: space-between;
padding: 8px;
min-height: 56px;
border-bottom: 1px solid #ccc;
}
.prospect div.stretch {
width: 100%;
}
.prospect-actions,
.prospect-info {
display: flex;
align-items: center;
}
.prospect-icon {
width: 25px;
margin-right: 1rem;
}
.prospect-name {
font-weight: bold;
margin-right: 1rem;
}
.prospect-actions {
min-width: 148px !important;
justify-content: space-between;
align-content: stretch;
}
.prospect-action {
display: grid;
align-self: center;
width: 30px;
height: 30px;
border-radius: 30px;
background-color: white;
color: rgba(152, 162, 179, 1);
border: none;
margin: 0;
padding: 0;
cursor: pointer;
}
.prospect-action:hover {
color: white;
}
.prospect-action:nth-child(1):hover {
background-color: orange;
}
.prospect-action:nth-child(2):hover {
background-color: gray;
}
.prospect-action:nth-child(3):hover {
background-color: red;
}
.prospect-action-icon {
place-self: center;
width: 20px;
height: 20px;
margin: 0px;
}
</style>

View File

@ -0,0 +1,44 @@
<script>
// @ts-nocheck
import { getContext, onMount, setContext } from 'svelte';
let isLoggedIn = false;
let auth;
onMount(() => {
FB.getLoginStatus(function (response) {
if (response.status === 'connected') {
isLoggedIn = true;
}
});
});
async function loginWithFacebook() {
try {
const response = await new Promise((resolve, reject) => {
FB.login(
function (response) {
if (response.authResponse) {
resolve(response);
} else {
reject(response);
}
},
{ scope: 'email, public_profile, instagram_manage_messages' }
);
});
auth = response.authResponse;
isLoggedIn = true;
console.log(auth);
} catch (error) {
console.log(error);
}
}
setContext('isLoggedIn', isLoggedIn);
</script>
<p>{getContext('auth')}</p>
{isLoggedIn}
<button on:click={loginWithFacebook}>Login with Facebook</button>

View File

@ -0,0 +1,3 @@
import { writable } from 'svelte/store';
export const currentPage = writable("messages");

1
static/archive.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="rgba(152, 162, 179, 1)" aria-hidden="true"> <path d="M2 3a1 1 0 00-1 1v1a1 1 0 001 1h16a1 1 0 001-1V4a1 1 0 00-1-1H2z"></path> <path fill-rule="evenodd" d="M2 7.5h16l-.811 7.71a2 2 0 01-1.99 1.79H4.802a2 2 0 01-1.99-1.79L2 7.5zM7 11a1 1 0 011-1h4a1 1 0 110 2H8a1 1 0 01-1-1z" clip-rule="evenodd"></path></svg>

After

Width:  |  Height:  |  Size: 377 B

BIN
static/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

19
static/home-nav.svg Normal file
View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="-1 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>home [#1393]</title>
<desc>Created with Sketch.</desc>
<defs>
</defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Dribbble-Light-Preview" transform="translate(-341.000000, -720.000000)" fill="#000000">
<g id="icons" transform="translate(56.000000, 160.000000)">
<path d="M299.875,576.21213 C299.875,576.76413 299.399,577.00013 298.8125,577.00013 L297.75,577.00013 C297.1635,577.00013 296.6875,576.76413 296.6875,576.21213 L296.6875,575.21213 C296.6875,574.10713 295.736562,573.00013 294.5625,573.00013 L292.4375,573.00013 C291.263438,573.00013 290.3125,574.10713 290.3125,575.21213 L290.3125,576.21213 C290.3125,576.76413 289.8365,577.00013 289.25,577.00013 L288.1875,577.00013 C287.601,577.00013 287.125,576.76413 287.125,576.21213 L287.125,568.14913 C287.125,568.01613 287.181312,567.88913 287.280125,567.79513 L292.738188,562.65913 C293.153625,562.26813 293.826188,562.26813 294.240563,562.65913 L299.719875,567.81513 C299.818688,567.90913 299.875,568.03613 299.875,568.16813 L299.875,576.21213 Z M302,567.62513 C302,567.36013 301.888438,567.10713 301.690812,566.91913 L294.998125,560.58913 C294.169375,559.80613 292.823188,559.80313 291.99125,560.58313 L285.312375,566.84813 C285.112625,567.03613 285,567.29013 285,567.55613 L285,577.21213 C285,578.31713 285.950938,579.00013 287.125,579.00013 L290.3125,579.00013 C291.486562,579.00013 292.4375,578.31713 292.4375,577.21213 L292.4375,576.21213 C292.4375,575.66013 292.9135,575.21213 293.5,575.21213 C294.0865,575.21213 294.5625,575.66013 294.5625,576.21213 L294.5625,577.21213 C294.5625,578.31713 295.513438,579.00013 296.6875,579.00013 L299.875,579.00013 C301.049062,579.00013 302,578.31713 302,577.21213 L302,567.62513 Z" id="home-[#1393]">
</path>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

1
static/lock.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="rgba(152, 162, 179, 1)" aria-hidden="true"> <path fill-rule="evenodd" d="M10 1a4.5 4.5 0 00-4.5 4.5V9H5a2 2 0 00-2 2v6a2 2 0 002 2h10a2 2 0 002-2v-6a2 2 0 00-2-2h-.5V5.5A4.5 4.5 0 0010 1zm3 8V5.5a3 3 0 10-6 0V9h6z" clip-rule="evenodd"></path></svg>

After

Width:  |  Height:  |  Size: 315 B

1
static/messages-nav.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="rgba(152, 162, 179, 1)" aria-hidden="true"> <path fill-rule="evenodd" d="M10 3c-4.31 0-8 3.033-8 7 0 2.024.978 3.825 2.499 5.085a3.478 3.478 0 01-.522 1.756.75.75 0 00.584 1.143 5.976 5.976 0 003.936-1.108c.487.082.99.124 1.503.124 4.31 0 8-3.033 8-7s-3.69-7-8-7zm0 8a1 1 0 100-2 1 1 0 000 2zm-2-1a1 1 0 11-2 0 1 1 0 012 0zm5 1a1 1 0 100-2 1 1 0 000 2z" clip-rule="evenodd"></path></svg>

After

Width:  |  Height:  |  Size: 454 B

9
static/people-nav.svg Normal file
View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18 7.16C17.94 7.15 17.87 7.15 17.81 7.16C16.43 7.11 15.33 5.98 15.33 4.58C15.33 3.15 16.48 2 17.91 2C19.34 2 20.49 3.16 20.49 4.58C20.48 5.98 19.38 7.11 18 7.16Z" stroke="#292D32" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M16.9699 14.44C18.3399 14.67 19.8499 14.43 20.9099 13.72C22.3199 12.78 22.3199 11.24 20.9099 10.3C19.8399 9.59004 18.3099 9.35003 16.9399 9.59003" stroke="#292D32" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5.96998 7.16C6.02998 7.15 6.09998 7.15 6.15998 7.16C7.53998 7.11 8.63998 5.98 8.63998 4.58C8.63998 3.15 7.48998 2 6.05998 2C4.62998 2 3.47998 3.16 3.47998 4.58C3.48998 5.98 4.58998 7.11 5.96998 7.16Z" stroke="#292D32" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6.99994 14.44C5.62994 14.67 4.11994 14.43 3.05994 13.72C1.64994 12.78 1.64994 11.24 3.05994 10.3C4.12994 9.59004 5.65994 9.35003 7.02994 9.59003" stroke="#292D32" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12 14.63C11.94 14.62 11.87 14.62 11.81 14.63C10.43 14.58 9.32996 13.45 9.32996 12.05C9.32996 10.62 10.48 9.46997 11.91 9.46997C13.34 9.46997 14.49 10.63 14.49 12.05C14.48 13.45 13.38 14.59 12 14.63Z" stroke="#292D32" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9.08997 17.78C7.67997 18.72 7.67997 20.26 9.08997 21.2C10.69 22.27 13.31 22.27 14.91 21.2C16.32 20.26 16.32 18.72 14.91 17.78C13.32 16.72 10.69 16.72 9.08997 17.78Z" stroke="#292D32" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

BIN
static/profile-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

5
static/settings-nav.svg Normal file
View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11 3H13C13.5523 3 14 3.44772 14 4V4.56879C14 4.99659 14.2871 5.36825 14.6822 5.53228C15.0775 5.69638 15.5377 5.63384 15.8403 5.33123L16.2426 4.92891C16.6331 4.53838 17.2663 4.53838 17.6568 4.92891L19.071 6.34312C19.4616 6.73365 19.4615 7.36681 19.071 7.75734L18.6688 8.1596C18.3661 8.46223 18.3036 8.92247 18.4677 9.31774C18.6317 9.71287 19.0034 10 19.4313 10L20 10C20.5523 10 21 10.4477 21 11V13C21 13.5523 20.5523 14 20 14H19.4312C19.0034 14 18.6318 14.2871 18.4677 14.6822C18.3036 15.0775 18.3661 15.5377 18.6688 15.8403L19.071 16.2426C19.4616 16.6331 19.4616 17.2663 19.071 17.6568L17.6568 19.071C17.2663 19.4616 16.6331 19.4616 16.2426 19.071L15.8403 18.6688C15.5377 18.3661 15.0775 18.3036 14.6822 18.4677C14.2871 18.6318 14 19.0034 14 19.4312V20C14 20.5523 13.5523 21 13 21H11C10.4477 21 10 20.5523 10 20V19.4313C10 19.0034 9.71287 18.6317 9.31774 18.4677C8.92247 18.3036 8.46223 18.3661 8.1596 18.6688L7.75732 19.071C7.36679 19.4616 6.73363 19.4616 6.34311 19.071L4.92889 17.6568C4.53837 17.2663 4.53837 16.6331 4.92889 16.2426L5.33123 15.8403C5.63384 15.5377 5.69638 15.0775 5.53228 14.6822C5.36825 14.2871 4.99659 14 4.56879 14H4C3.44772 14 3 13.5523 3 13V11C3 10.4477 3.44772 10 4 10L4.56877 10C4.99658 10 5.36825 9.71288 5.53229 9.31776C5.6964 8.9225 5.63386 8.46229 5.33123 8.15966L4.92891 7.75734C4.53838 7.36681 4.53838 6.73365 4.92891 6.34313L6.34312 4.92891C6.73365 4.53839 7.36681 4.53839 7.75734 4.92891L8.15966 5.33123C8.46228 5.63386 8.9225 5.6964 9.31776 5.53229C9.71288 5.36825 10 4.99658 10 4.56876V4C10 3.44772 10.4477 3 11 3Z" stroke="#000000" stroke-width="1.5"/>
<path d="M14 12C14 13.1046 13.1046 14 12 14C10.8954 14 10 13.1046 10 12C10 10.8954 10.8954 10 12 10C13.1046 10 14 10.8954 14 12Z" stroke="#000000" stroke-width="1.5"/>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

6
static/trash.svg Normal file
View File

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="rgba(152, 162, 179, 1)"
aria-hidden="true">
<path fill-rule="evenodd"
d="M8.75 1A2.75 2.75 0 006 3.75v.443c-.795.077-1.584.176-2.365.298a.75.75 0 10.23 1.482l.149-.022.841 10.518A2.75 2.75 0 007.596 19h4.807a2.75 2.75 0 002.742-2.53l.841-10.52.149.023a.75.75 0 00.23-1.482A41.03 41.03 0 0014 4.193V3.75A2.75 2.75 0 0011.25 1h-2.5zM10 4c.84 0 1.673.025 2.5.075V3.75c0-.69-.56-1.25-1.25-1.25h-2.5c-.69 0-1.25.56-1.25 1.25v.325C8.327 4.025 9.16 4 10 4zM8.58 7.72a.75.75 0 00-1.5.06l.3 7.5a.75.75 0 101.5-.06l-.3-7.5zm4.34.06a.75.75 0 10-1.5-.06l-.3 7.5a.75.75 0 101.5.06l.3-7.5z"
clip-rule="evenodd"></path>
</svg>

After

Width:  |  Height:  |  Size: 694 B

17
svelte.config.js Normal file
View File

@ -0,0 +1,17 @@
import adapter from '@sveltejs/adapter-auto';
/** @type {import('@sveltejs/kit').Config} */
const config = {
kit: {
// adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
// If your environment is not supported or you settled on a specific environment, switch out the adapter.
// See https://kit.svelte.dev/docs/adapters for more information about adapters.
adapter: adapter(),
alias: {
"@components/*": "./src/components/*"
},
}
};
export default config;

6
tests/test.js Normal file
View File

@ -0,0 +1,6 @@
import { expect, test } from '@playwright/test';
test('index page has expected h1', async ({ page }) => {
await page.goto('/');
await expect(page.getByRole('heading', { name: 'Welcome to SvelteKit' })).toBeVisible();
});

9
vite.config.js Normal file
View File

@ -0,0 +1,9 @@
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vitest/config';
export default defineConfig({
plugins: [sveltekit()],
test: {
include: ['src/**/*.{test,spec}.{js,ts}']
}
});