chore: auto-commit uncommitted changes
This commit is contained in:
parent
bfade7a86f
commit
52edadab72
7
.env
7
.env
|
|
@ -4,3 +4,10 @@ STORE_PATH=/home/johan/dev/dealspace/data/store
|
|||
PORT=9300
|
||||
ENV=production
|
||||
BACKDOOR_CODE=220402
|
||||
SMTP_HOST=smtp.protonmail.ch
|
||||
SMTP_PORT=587
|
||||
SMTP_USER=no-reply@inou.com
|
||||
SMTP_PASS=1H2RGGL7LP8JHSQU
|
||||
SMTP_FROM=no-reply@inou.com
|
||||
SMTP_FROM_NAME=Dealspace via inou
|
||||
OPENROUTER_API_KEY=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
|
||||
|
|
|
|||
177
api/handlers.go
177
api/handlers.go
|
|
@ -3052,3 +3052,180 @@ func (h *Handlers) SetTestRole(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
JSONResponse(w, http.StatusOK, map[string]any{"ok": true, "test_role": req.Role})
|
||||
}
|
||||
|
||||
// AddOrgToDeal handles POST /api/projects/{projectID}/orgs/add — creates org (if new) + deal_org + members in one shot.
|
||||
func (h *Handlers) AddOrgToDeal(w http.ResponseWriter, r *http.Request) {
|
||||
actorID := UserIDFromContext(r.Context())
|
||||
projectID := chi.URLParam(r, "projectID")
|
||||
|
||||
if err := lib.CheckAccessWrite(h.DB, actorID, projectID, ""); err != nil {
|
||||
ErrorResponse(w, http.StatusForbidden, "access_denied", "Access denied")
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
// Org info
|
||||
Name string `json:"name"`
|
||||
Domains []string `json:"domains"`
|
||||
Role string `json:"role"` // seller | buyer | ib | advisor
|
||||
Website string `json:"website"`
|
||||
Description string `json:"description"`
|
||||
Industry string `json:"industry"`
|
||||
Phone string `json:"phone"`
|
||||
Fax string `json:"fax"`
|
||||
Address string `json:"address"`
|
||||
City string `json:"city"`
|
||||
State string `json:"state"`
|
||||
Country string `json:"country"`
|
||||
Founded string `json:"founded"`
|
||||
LinkedIn string `json:"linkedin"`
|
||||
// Selected members
|
||||
Members []lib.DealOrgMember `json:"members"`
|
||||
// Deal org settings
|
||||
DomainLock bool `json:"domain_lock"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
ErrorResponse(w, http.StatusBadRequest, "invalid_json", "Invalid request body")
|
||||
return
|
||||
}
|
||||
|
||||
if req.Name == "" {
|
||||
ErrorResponse(w, http.StatusBadRequest, "missing_fields", "Organization name required")
|
||||
return
|
||||
}
|
||||
if len(req.Domains) == 0 {
|
||||
ErrorResponse(w, http.StatusBadRequest, "missing_fields", "At least one domain required")
|
||||
return
|
||||
}
|
||||
validRoles := map[string]bool{"seller": true, "buyer": true, "ib": true, "advisor": true}
|
||||
if req.Role == "" || !validRoles[req.Role] {
|
||||
ErrorResponse(w, http.StatusBadRequest, "invalid_role", "Role must be one of: seller, buyer, ib, advisor")
|
||||
return
|
||||
}
|
||||
|
||||
for i := range req.Domains {
|
||||
req.Domains[i] = strings.ToLower(strings.TrimSpace(req.Domains[i]))
|
||||
}
|
||||
|
||||
now := time.Now().UnixMilli()
|
||||
|
||||
// Step 1: Create organization entry
|
||||
orgID := uuid.New().String()
|
||||
orgData := lib.OrgData{
|
||||
Name: req.Name,
|
||||
Domains: req.Domains,
|
||||
Role: req.Role,
|
||||
Website: req.Website,
|
||||
Description: req.Description,
|
||||
Industry: req.Industry,
|
||||
Phone: req.Phone,
|
||||
Fax: req.Fax,
|
||||
Address: req.Address,
|
||||
City: req.City,
|
||||
State: req.State,
|
||||
Country: req.Country,
|
||||
Founded: req.Founded,
|
||||
LinkedIn: req.LinkedIn,
|
||||
}
|
||||
orgDataJSON, _ := json.Marshal(orgData)
|
||||
|
||||
orgKey, err := lib.DeriveProjectKey(h.Cfg.MasterKey, orgID)
|
||||
if err != nil {
|
||||
ErrorResponse(w, http.StatusInternalServerError, "internal", "Key derivation failed")
|
||||
return
|
||||
}
|
||||
orgSummary, _ := lib.Pack(orgKey, req.Name)
|
||||
orgDataPacked, _ := lib.Pack(orgKey, string(orgDataJSON))
|
||||
|
||||
_, dbErr := h.DB.Conn.Exec(
|
||||
`INSERT INTO entries (entry_id, project_id, parent_id, type, depth, sort_order,
|
||||
search_key, search_key2, summary, data, stage,
|
||||
assignee_id, return_to_id, origin_id,
|
||||
version, deleted_at, deleted_by, key_version,
|
||||
created_at, updated_at, created_by)
|
||||
VALUES (?,?,?,?,?,?, ?,?,?,?,?, ?,?,?, ?,?,?,?, ?,?,?)`,
|
||||
orgID, orgID, "", lib.TypeOrganization, 0, 0,
|
||||
nil, nil, orgSummary, orgDataPacked, lib.StageDataroom,
|
||||
"", "", "",
|
||||
1, nil, nil, 1,
|
||||
now, now, actorID,
|
||||
)
|
||||
if dbErr != nil {
|
||||
ErrorResponse(w, http.StatusInternalServerError, "internal", "Failed to create organization")
|
||||
return
|
||||
}
|
||||
|
||||
// Step 2: Create deal_org entry linking org to project
|
||||
dealOrgID := uuid.New().String()
|
||||
dealOrgData := lib.DealOrgData{
|
||||
OrgID: orgID,
|
||||
Role: req.Role,
|
||||
DomainLock: req.DomainLock,
|
||||
Members: req.Members,
|
||||
}
|
||||
dealOrgJSON, _ := json.Marshal(dealOrgData)
|
||||
|
||||
projKey, err := lib.DeriveProjectKey(h.Cfg.MasterKey, projectID)
|
||||
if err != nil {
|
||||
ErrorResponse(w, http.StatusInternalServerError, "internal", "Key derivation failed")
|
||||
return
|
||||
}
|
||||
dealSummary, _ := lib.Pack(projKey, req.Name)
|
||||
dealDataPacked, _ := lib.Pack(projKey, string(dealOrgJSON))
|
||||
|
||||
_, dbErr = h.DB.Conn.Exec(
|
||||
`INSERT INTO entries (entry_id, project_id, parent_id, type, depth, sort_order,
|
||||
search_key, search_key2, summary, data, stage,
|
||||
assignee_id, return_to_id, origin_id,
|
||||
version, deleted_at, deleted_by, key_version,
|
||||
created_at, updated_at, created_by)
|
||||
VALUES (?,?,?,?,?,?, ?,?,?,?,?, ?,?,?, ?,?,?,?, ?,?,?)`,
|
||||
dealOrgID, projectID, projectID, lib.TypeDealOrg, 1, 0,
|
||||
nil, nil, dealSummary, dealDataPacked, lib.StagePreDataroom,
|
||||
"", "", "",
|
||||
1, nil, nil, 1,
|
||||
now, now, actorID,
|
||||
)
|
||||
if dbErr != nil {
|
||||
ErrorResponse(w, http.StatusInternalServerError, "internal", "Failed to add organization to deal")
|
||||
return
|
||||
}
|
||||
|
||||
JSONResponse(w, http.StatusCreated, map[string]any{
|
||||
"org_id": orgID,
|
||||
"deal_org_id": dealOrgID,
|
||||
"name": req.Name,
|
||||
"role": req.Role,
|
||||
"members": len(req.Members),
|
||||
})
|
||||
}
|
||||
|
||||
// ScrapeOrg handles POST /api/scrape/org — takes an email, scrapes the domain for org + people data.
|
||||
func (h *Handlers) ScrapeOrg(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
Email string `json:"email"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
ErrorResponse(w, http.StatusBadRequest, "invalid_json", "Invalid request body")
|
||||
return
|
||||
}
|
||||
req.Email = strings.TrimSpace(strings.ToLower(req.Email))
|
||||
if req.Email == "" || !strings.Contains(req.Email, "@") {
|
||||
ErrorResponse(w, http.StatusBadRequest, "missing_fields", "Valid email required")
|
||||
return
|
||||
}
|
||||
|
||||
if h.Cfg.OpenRouterKey == "" {
|
||||
ErrorResponse(w, http.StatusServiceUnavailable, "not_configured", "LLM not configured")
|
||||
return
|
||||
}
|
||||
|
||||
result, err := lib.ScrapeOrgByEmail(h.Cfg.OpenRouterKey, req.Email)
|
||||
if err != nil {
|
||||
log.Printf("scrape org error for %s: %v", req.Email, err)
|
||||
ErrorResponse(w, http.StatusUnprocessableEntity, "scrape_failed", "Could not scrape organization website")
|
||||
return
|
||||
}
|
||||
|
||||
JSONResponse(w, http.StatusOK, result)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -110,6 +110,10 @@ func NewRouter(db *lib.DB, cfg *lib.Config, store lib.ObjectStore, websiteFS fs.
|
|||
r.Get("/projects/{projectID}/orgs", h.ListDealOrgs)
|
||||
r.Post("/projects/{projectID}/orgs", h.CreateDealOrg)
|
||||
r.Delete("/projects/{projectID}/orgs/{dealOrgID}", h.DeleteDealOrg)
|
||||
r.Post("/projects/{projectID}/orgs/add", h.AddOrgToDeal)
|
||||
|
||||
// Scrape (LLM-powered org lookup)
|
||||
r.Post("/scrape/org", h.ScrapeOrg)
|
||||
|
||||
r.Get("/admin/users", h.AdminListUsers)
|
||||
r.Get("/admin/projects", h.AdminListProjects)
|
||||
|
|
|
|||
|
|
@ -111,17 +111,26 @@ func loadConfig() (*lib.Config, error) {
|
|||
backdoorCode := os.Getenv("BACKDOOR_CODE")
|
||||
|
||||
cfg := &lib.Config{
|
||||
MasterKey: masterKey,
|
||||
DBPath: dbPath,
|
||||
StorePath: storePath,
|
||||
Port: port,
|
||||
Env: env,
|
||||
JWTSecret: jwtSecret,
|
||||
BackdoorCode: backdoorCode,
|
||||
MasterKey: masterKey,
|
||||
DBPath: dbPath,
|
||||
StorePath: storePath,
|
||||
Port: port,
|
||||
Env: env,
|
||||
JWTSecret: jwtSecret,
|
||||
BackdoorCode: backdoorCode,
|
||||
OpenRouterKey: os.Getenv("OPENROUTER_API_KEY"),
|
||||
}
|
||||
|
||||
// Initialize mailer
|
||||
cfg.Mailer = lib.NewMailer(cfg)
|
||||
if cfg.Mailer.Enabled() {
|
||||
emailDir := findEmailTemplates()
|
||||
if emailDir != "" {
|
||||
if err := cfg.Mailer.LoadTemplates(emailDir); err != nil {
|
||||
return nil, fmt.Errorf("load email templates: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
|
@ -167,6 +176,20 @@ func splitLines(s string) []string {
|
|||
return lines
|
||||
}
|
||||
|
||||
func findEmailTemplates() string {
|
||||
candidates := []string{
|
||||
"portal/emails",
|
||||
filepath.Join(filepath.Dir(os.Args[0]), "portal/emails"),
|
||||
"/opt/dealspace/portal/emails",
|
||||
}
|
||||
for _, p := range candidates {
|
||||
if info, err := os.Stat(p); err == nil && info.IsDir() {
|
||||
return p
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func findMigration() string {
|
||||
candidates := []string{
|
||||
"migrations",
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1,70 @@
|
|||
package lib
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// CallOpenRouter sends a request to OpenRouter (OpenAI-compatible API).
|
||||
func CallOpenRouter(apiKey, model string, messages []map[string]interface{}, maxTokens int) (string, error) {
|
||||
if apiKey == "" {
|
||||
return "", fmt.Errorf("OpenRouter API key not configured")
|
||||
}
|
||||
|
||||
reqBody := map[string]interface{}{
|
||||
"model": model,
|
||||
"messages": messages,
|
||||
"max_tokens": maxTokens,
|
||||
"temperature": 0.1,
|
||||
}
|
||||
|
||||
jsonBody, err := json.Marshal(reqBody)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("marshal request: %w", err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", "https://openrouter.ai/api/v1/chat/completions", bytes.NewReader(jsonBody))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("create request: %w", err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer "+apiKey)
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("API request: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("read response: %w", err)
|
||||
}
|
||||
if resp.StatusCode != 200 {
|
||||
return "", fmt.Errorf("OpenRouter API error %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
var oaiResp struct {
|
||||
Choices []struct {
|
||||
Message struct {
|
||||
Content string `json:"content"`
|
||||
} `json:"message"`
|
||||
} `json:"choices"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &oaiResp); err != nil {
|
||||
return "", fmt.Errorf("parse response: %w", err)
|
||||
}
|
||||
if len(oaiResp.Choices) == 0 {
|
||||
return "", fmt.Errorf("empty response from OpenRouter")
|
||||
}
|
||||
|
||||
text := strings.TrimSpace(oaiResp.Choices[0].Message.Content)
|
||||
text = strings.TrimPrefix(text, "```json")
|
||||
text = strings.TrimPrefix(text, "```")
|
||||
text = strings.TrimSuffix(text, "```")
|
||||
return strings.TrimSpace(text), nil
|
||||
}
|
||||
|
|
@ -2,8 +2,10 @@ package lib
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net"
|
||||
"net/smtp"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
|
@ -18,6 +20,7 @@ type Mailer struct {
|
|||
User string
|
||||
Pass string
|
||||
From string
|
||||
FromName string
|
||||
templates *template.Template
|
||||
enabled bool
|
||||
}
|
||||
|
|
@ -43,13 +46,19 @@ func NewMailer(cfg *Config) *Mailer {
|
|||
from = "noreply@muskepo.com"
|
||||
}
|
||||
|
||||
fromName := os.Getenv("SMTP_FROM_NAME")
|
||||
if fromName == "" {
|
||||
fromName = "Dealspace"
|
||||
}
|
||||
|
||||
m := &Mailer{
|
||||
Host: host,
|
||||
Port: port,
|
||||
User: os.Getenv("SMTP_USER"),
|
||||
Pass: os.Getenv("SMTP_PASS"),
|
||||
From: from,
|
||||
enabled: true,
|
||||
Host: host,
|
||||
Port: port,
|
||||
User: os.Getenv("SMTP_USER"),
|
||||
Pass: os.Getenv("SMTP_PASS"),
|
||||
From: from,
|
||||
FromName: fromName,
|
||||
enabled: true,
|
||||
}
|
||||
|
||||
return m
|
||||
|
|
@ -91,29 +100,50 @@ func (m *Mailer) Enabled() bool {
|
|||
return m.enabled
|
||||
}
|
||||
|
||||
// Send sends an email with the given HTML body.
|
||||
// Send sends an email with the given HTML body using explicit STARTTLS.
|
||||
func (m *Mailer) Send(to, subject, htmlBody string) error {
|
||||
if !m.enabled {
|
||||
return nil // no-op
|
||||
}
|
||||
|
||||
// Build email message
|
||||
msg := m.buildMessage(to, subject, htmlBody)
|
||||
|
||||
// Connect and send
|
||||
addr := fmt.Sprintf("%s:%d", m.Host, m.Port)
|
||||
|
||||
var auth smtp.Auth
|
||||
if m.User != "" && m.Pass != "" {
|
||||
auth = smtp.PlainAuth("", m.User, m.Pass, m.Host)
|
||||
}
|
||||
|
||||
err := smtp.SendMail(addr, auth, m.From, []string{to}, msg)
|
||||
conn, err := net.Dial("tcp", addr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("send mail to %s: %w", to, err)
|
||||
return fmt.Errorf("dial %s: %w", addr, err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
client, err := smtp.NewClient(conn, m.Host)
|
||||
if err != nil {
|
||||
return fmt.Errorf("smtp client: %w", err)
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
if err = client.StartTLS(&tls.Config{ServerName: m.Host}); err != nil {
|
||||
return fmt.Errorf("starttls: %w", err)
|
||||
}
|
||||
if m.User != "" && m.Pass != "" {
|
||||
if err = client.Auth(smtp.PlainAuth("", m.User, m.Pass, m.Host)); err != nil {
|
||||
return fmt.Errorf("auth: %w", err)
|
||||
}
|
||||
}
|
||||
if err = client.Mail(m.From); err != nil {
|
||||
return fmt.Errorf("mail from: %w", err)
|
||||
}
|
||||
if err = client.Rcpt(to); err != nil {
|
||||
return fmt.Errorf("rcpt to %s: %w", to, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
w, err := client.Data()
|
||||
if err != nil {
|
||||
return fmt.Errorf("data: %w", err)
|
||||
}
|
||||
if _, err = w.Write(msg); err != nil {
|
||||
return fmt.Errorf("write: %w", err)
|
||||
}
|
||||
return w.Close()
|
||||
}
|
||||
|
||||
// SendTemplate renders a template and sends it as an email.
|
||||
|
|
@ -157,7 +187,7 @@ func (m *Mailer) buildMessage(to, subject, htmlBody string) []byte {
|
|||
// Headers
|
||||
buf.WriteString("MIME-Version: 1.0\r\n")
|
||||
buf.WriteString("Content-Type: text/html; charset=\"UTF-8\"\r\n")
|
||||
buf.WriteString(fmt.Sprintf("From: Dealspace <%s>\r\n", m.From))
|
||||
buf.WriteString(fmt.Sprintf("From: %s <%s>\r\n", m.FromName, m.From))
|
||||
buf.WriteString(fmt.Sprintf("To: %s\r\n", to))
|
||||
buf.WriteString(fmt.Sprintf("Subject: %s\r\n", m.encodeSubject(subject)))
|
||||
buf.WriteString("\r\n")
|
||||
|
|
|
|||
|
|
@ -0,0 +1,250 @@
|
|||
package lib
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ScrapedOrg is the structured result from scraping an organization's website.
|
||||
type ScrapedOrg struct {
|
||||
Name string `json:"name"`
|
||||
Domain string `json:"domain"`
|
||||
Logo string `json:"logo,omitempty"` // URL to company logo
|
||||
Description string `json:"description,omitempty"`
|
||||
Industry string `json:"industry,omitempty"`
|
||||
Website string `json:"website"`
|
||||
Phone string `json:"phone,omitempty"`
|
||||
Fax string `json:"fax,omitempty"`
|
||||
Address string `json:"address,omitempty"`
|
||||
City string `json:"city,omitempty"`
|
||||
State string `json:"state,omitempty"`
|
||||
Country string `json:"country,omitempty"`
|
||||
Founded string `json:"founded,omitempty"`
|
||||
LinkedIn string `json:"linkedin,omitempty"`
|
||||
|
||||
People []ScrapedPerson `json:"people,omitempty"`
|
||||
}
|
||||
|
||||
// ScrapedPerson is a person found on the organization's website.
|
||||
type ScrapedPerson struct {
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email,omitempty"`
|
||||
Title string `json:"title,omitempty"`
|
||||
Phone string `json:"phone,omitempty"`
|
||||
Photo string `json:"photo,omitempty"` // URL to headshot
|
||||
Bio string `json:"bio,omitempty"`
|
||||
LinkedIn string `json:"linkedin,omitempty"`
|
||||
}
|
||||
|
||||
const scrapeModel = "google/gemini-2.0-flash-001"
|
||||
|
||||
// ScrapeOrgByEmail takes an email address, extracts the domain,
|
||||
// fetches the website, and uses an LLM to extract org + people data.
|
||||
func ScrapeOrgByEmail(apiKey, email string) (*ScrapedOrg, error) {
|
||||
parts := strings.SplitN(email, "@", 2)
|
||||
if len(parts) != 2 {
|
||||
return nil, fmt.Errorf("invalid email: %s", email)
|
||||
}
|
||||
domain := parts[1]
|
||||
return ScrapeOrg(apiKey, domain)
|
||||
}
|
||||
|
||||
// ScrapeOrg fetches a domain's website and extracts structured org + people data.
|
||||
// Two-pass approach:
|
||||
// 1. Fetch homepage → ask LLM which pages have team/about/contact info
|
||||
// 2. Fetch those pages → ask LLM to extract structured data
|
||||
func ScrapeOrg(apiKey, domain string) (*ScrapedOrg, error) {
|
||||
// Pass 1: fetch homepage
|
||||
base := "https://" + domain
|
||||
homepage := fetchPage(base)
|
||||
if homepage == "" {
|
||||
return nil, fmt.Errorf("could not fetch %s", base)
|
||||
}
|
||||
|
||||
// Ask LLM to find relevant pages
|
||||
discoverPrompt := fmt.Sprintf(`You are analyzing the HTML of %s to find pages that contain:
|
||||
1. Team / leadership / people / staff pages (with bios, headshots, names)
|
||||
2. About / company info pages
|
||||
3. Contact / office address pages
|
||||
|
||||
Look at the navigation, footer, and links in the HTML. Return a JSON array of up to 10 absolute URLs that are most likely to contain team members and company info. Only include URLs on the same domain (%s). Do not include the homepage itself.
|
||||
|
||||
Return ONLY a JSON array of strings, no markdown:
|
||||
["https://%s/about", "https://%s/team", ...]
|
||||
|
||||
If you cannot find any relevant links, return an empty array: []
|
||||
|
||||
HTML:
|
||||
%s`, domain, domain, domain, domain, homepage)
|
||||
|
||||
discoverMessages := []map[string]interface{}{
|
||||
{"role": "user", "content": discoverPrompt},
|
||||
}
|
||||
|
||||
linksRaw, err := CallOpenRouter(apiKey, scrapeModel, discoverMessages, 1024)
|
||||
if err != nil {
|
||||
log.Printf("scrape discover error for %s: %v", domain, err)
|
||||
linksRaw = "[]"
|
||||
}
|
||||
|
||||
var links []string
|
||||
if err := json.Unmarshal([]byte(linksRaw), &links); err != nil {
|
||||
log.Printf("scrape discover parse error for %s: %v (raw: %.200s)", domain, err, linksRaw)
|
||||
links = nil
|
||||
}
|
||||
|
||||
// Fetch discovered pages in parallel
|
||||
var allHTML strings.Builder
|
||||
allHTML.WriteString(fmt.Sprintf("\n<!-- PAGE: %s -->\n", base))
|
||||
allHTML.WriteString(homepage)
|
||||
|
||||
if len(links) > 0 {
|
||||
extra := fetchPages(links)
|
||||
allHTML.WriteString(extra)
|
||||
}
|
||||
|
||||
html := allHTML.String()
|
||||
|
||||
// Pass 2: extract structured data
|
||||
prompt := fmt.Sprintf(`Extract structured data from this company website. Domain: %s
|
||||
|
||||
RULES:
|
||||
- Extract EVERY person mentioned — do not skip anyone
|
||||
- Every person MUST have a "title" (job title / role). Look at section headings, CSS classes, surrounding text to determine titles. Common patterns: "Co-Founder", "Partner", "Managing Director", "Principal", "Investment Professional", "Operating Partner", "Operations Manager", "Finance & Operations", "Analyst", "Associate". If a person is under a heading like "Investment Professionals", their title is "Investment Professional". Never use generic "Team Member".
|
||||
- Photo/logo URLs must be fully qualified (https://...)
|
||||
- Logo: find the company logo image — look for img tags in the header, navbar, or footer with "logo" in the src/alt/class. Return the full absolute URL.
|
||||
- Address: put ONLY the street address in "address" (e.g. "2151 Central Avenue"). Put city, state, country in their own fields. Do NOT combine them.
|
||||
- If you can infer emails from a pattern (e.g. firstname@%s), include them
|
||||
- Bio: 1-2 sentences about their professional background, not personal hobbies
|
||||
- Return at most 25 people. Prioritize leadership, partners, principals, and senior staff over junior employees, interns, or support staff
|
||||
|
||||
Return a single JSON object:
|
||||
{
|
||||
"name": "Company Name",
|
||||
"domain": "%s",
|
||||
"logo": "https://full-url-to-logo.png",
|
||||
"description": "1-2 sentence description",
|
||||
"industry": "sector",
|
||||
"website": "https://%s",
|
||||
"phone": "",
|
||||
"fax": "",
|
||||
"address": "street address only",
|
||||
"city": "",
|
||||
"state": "",
|
||||
"country": "",
|
||||
"founded": "year",
|
||||
"linkedin": "url",
|
||||
"people": [
|
||||
{
|
||||
"name": "Full Name",
|
||||
"email": "email@domain",
|
||||
"title": "Job Title",
|
||||
"phone": "direct phone",
|
||||
"photo": "https://full-url-to-headshot.jpg",
|
||||
"bio": "1-2 sentences",
|
||||
"linkedin": "url"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Return ONLY valid JSON — no markdown, no explanation.
|
||||
|
||||
HTML:
|
||||
%s`, domain, domain, domain, domain, html)
|
||||
|
||||
messages := []map[string]interface{}{
|
||||
{"role": "user", "content": prompt},
|
||||
}
|
||||
|
||||
raw, err := CallOpenRouter(apiKey, scrapeModel, messages, 8192)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("llm scrape: %w", err)
|
||||
}
|
||||
|
||||
var result ScrapedOrg
|
||||
if err := json.Unmarshal([]byte(raw), &result); err != nil {
|
||||
return nil, fmt.Errorf("parse llm response: %w (raw: %.500s)", err, raw)
|
||||
}
|
||||
|
||||
result.Domain = domain
|
||||
if result.Website == "" {
|
||||
result.Website = "https://" + domain
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// fetchPage fetches a single URL and returns its HTML body (or "" on error).
|
||||
func fetchPage(url string) string {
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
req.Header.Set("User-Agent", "Mozilla/5.0 (compatible; Dealspace/1.0)")
|
||||
resp, err := client.Do(req)
|
||||
if err != nil || resp.StatusCode != 200 {
|
||||
if resp != nil {
|
||||
resp.Body.Close()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, _ := io.ReadAll(io.LimitReader(resp.Body, 500*1024))
|
||||
return string(body)
|
||||
}
|
||||
|
||||
// fetchPages fetches multiple URLs concurrently and concatenates their raw HTML.
|
||||
// Skips pages that return errors or non-200 status.
|
||||
func fetchPages(urls []string) string {
|
||||
type result struct {
|
||||
idx int
|
||||
url string
|
||||
body string
|
||||
}
|
||||
|
||||
ch := make(chan result, len(urls))
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
|
||||
for i, u := range urls {
|
||||
go func(idx int, url string) {
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
ch <- result{idx, url, ""}
|
||||
return
|
||||
}
|
||||
req.Header.Set("User-Agent", "Mozilla/5.0 (compatible; Dealspace/1.0)")
|
||||
resp, err := client.Do(req)
|
||||
if err != nil || resp.StatusCode != 200 {
|
||||
if resp != nil {
|
||||
resp.Body.Close()
|
||||
}
|
||||
ch <- result{idx, url, ""}
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, _ := io.ReadAll(io.LimitReader(resp.Body, 500*1024))
|
||||
ch <- result{idx, url, string(body)}
|
||||
}(i, u)
|
||||
}
|
||||
|
||||
results := make([]result, len(urls))
|
||||
for range urls {
|
||||
r := <-ch
|
||||
results[r.idx] = r
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
for _, r := range results {
|
||||
if r.body != "" {
|
||||
sb.WriteString(fmt.Sprintf("\n<!-- PAGE: %s -->\n", r.url))
|
||||
sb.WriteString(r.body)
|
||||
}
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
37
lib/types.go
37
lib/types.go
|
|
@ -66,8 +66,18 @@ type OrgData struct {
|
|||
Name string `json:"name"`
|
||||
Domains []string `json:"domains"` // required, e.g. ["kaseya.com","datto.com"]
|
||||
Role string `json:"role"` // seller | buyer | ib | advisor
|
||||
Logo string `json:"logo,omitempty"`
|
||||
Website string `json:"website,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Industry string `json:"industry,omitempty"`
|
||||
Phone string `json:"phone,omitempty"`
|
||||
Fax string `json:"fax,omitempty"`
|
||||
Address string `json:"address,omitempty"`
|
||||
City string `json:"city,omitempty"`
|
||||
State string `json:"state,omitempty"`
|
||||
Country string `json:"country,omitempty"`
|
||||
Founded string `json:"founded,omitempty"`
|
||||
LinkedIn string `json:"linkedin,omitempty"`
|
||||
ContactName string `json:"contact_name,omitempty"`
|
||||
ContactEmail string `json:"contact_email,omitempty"`
|
||||
}
|
||||
|
|
@ -93,9 +103,13 @@ type DealOrgPerms struct {
|
|||
|
||||
// DealOrgMember is a person associated with a deal org.
|
||||
type DealOrgMember struct {
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
Title string `json:"title,omitempty"`
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
Title string `json:"title,omitempty"`
|
||||
Phone string `json:"phone,omitempty"`
|
||||
Photo string `json:"photo,omitempty"`
|
||||
Bio string `json:"bio,omitempty"`
|
||||
LinkedIn string `json:"linkedin,omitempty"`
|
||||
}
|
||||
|
||||
// User represents an account.
|
||||
|
|
@ -242,14 +256,15 @@ type Challenge struct {
|
|||
|
||||
// Config holds application configuration.
|
||||
type Config struct {
|
||||
MasterKey []byte
|
||||
DBPath string
|
||||
StorePath string
|
||||
Port string
|
||||
Env string // "development" | "production"
|
||||
JWTSecret []byte
|
||||
Mailer *Mailer
|
||||
BackdoorCode string // OTP backdoor for dev/testing
|
||||
MasterKey []byte
|
||||
DBPath string
|
||||
StorePath string
|
||||
Port string
|
||||
Env string // "development" | "production"
|
||||
JWTSecret []byte
|
||||
Mailer *Mailer
|
||||
BackdoorCode string // OTP backdoor for dev/testing
|
||||
OpenRouterKey string // OpenRouter API key for LLM features
|
||||
}
|
||||
|
||||
// RequestData is the JSON structure packed into a request entry's Data field.
|
||||
|
|
|
|||
|
|
@ -15,14 +15,14 @@
|
|||
<a href="/app/tasks" class="text-xl font-bold text-white tracking-tight"><span class="text-[#c9a84c]">Deal</span>space</a>
|
||||
<div class="flex items-center gap-4">
|
||||
<span class="text-xs px-2 py-0.5 bg-[#c9a84c]/20 text-[#c9a84c] rounded-full font-medium">Super Admin</span>
|
||||
<span id="userName" class="text-sm text-[#94a3b8]"></span>
|
||||
<button onclick="logout()" class="text-sm text-[#94a3b8] hover:text-white transition">Logout</button>
|
||||
<span id="userName" class="text-sm text-[#b0bec5]"></span>
|
||||
<button onclick="logout()" class="text-sm text-[#b0bec5] hover:text-white transition">Logout</button>
|
||||
</div>
|
||||
</header>
|
||||
<div class="flex">
|
||||
<nav class="w-56 bg-[#0d1f3c] border-r border-white/[0.08] min-h-[calc(100vh-49px)] sticky top-[49px] shrink-0">
|
||||
<div class="p-3 space-y-0.5">
|
||||
<a href="/app/tasks" class="sidebar-link flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium text-[#94a3b8] transition">
|
||||
<a href="/app/tasks" class="sidebar-link flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium text-[#b0bec5] transition">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"/></svg>
|
||||
App Home</a>
|
||||
<a href="/admin" class="sidebar-link active flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium text-white transition">
|
||||
|
|
@ -32,23 +32,23 @@
|
|||
</nav>
|
||||
<main class="flex-1 p-8 max-w-6xl">
|
||||
<h1 class="text-2xl font-bold text-white mb-2">Admin Dashboard</h1>
|
||||
<p class="text-[#94a3b8] text-sm mb-8">Platform overview and management.</p>
|
||||
<p class="text-[#b0bec5] text-sm mb-8">Platform overview and management.</p>
|
||||
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8">
|
||||
<div class="bg-[#0d1f3c] border border-white/[0.08] rounded-xl p-5">
|
||||
<div class="text-[#94a3b8] text-xs uppercase tracking-wider mb-1">Users</div>
|
||||
<div class="text-[#b0bec5] text-xs uppercase tracking-wider mb-1">Users</div>
|
||||
<div id="statUsers" class="text-3xl font-bold text-white">—</div>
|
||||
</div>
|
||||
<div class="bg-[#0d1f3c] border border-white/[0.08] rounded-xl p-5">
|
||||
<div class="text-[#94a3b8] text-xs uppercase tracking-wider mb-1">Projects</div>
|
||||
<div class="text-[#b0bec5] text-xs uppercase tracking-wider mb-1">Projects</div>
|
||||
<div id="statProjects" class="text-3xl font-bold text-white">—</div>
|
||||
</div>
|
||||
<div class="bg-[#0d1f3c] border border-white/[0.08] rounded-xl p-5">
|
||||
<div class="text-[#94a3b8] text-xs uppercase tracking-wider mb-1">Organizations</div>
|
||||
<div class="text-[#b0bec5] text-xs uppercase tracking-wider mb-1">Organizations</div>
|
||||
<div id="statOrgs" class="text-3xl font-bold text-white">—</div>
|
||||
</div>
|
||||
<div class="bg-[#0d1f3c] border border-white/[0.08] rounded-xl p-5">
|
||||
<div class="text-[#94a3b8] text-xs uppercase tracking-wider mb-1">Active Sessions</div>
|
||||
<div class="text-[#b0bec5] text-xs uppercase tracking-wider mb-1">Active Sessions</div>
|
||||
<div id="statSessions" class="text-3xl font-bold text-white">—</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -57,7 +57,7 @@
|
|||
<div class="flex items-center justify-between mb-4">
|
||||
<h2 class="text-sm font-semibold text-white">All Users</h2>
|
||||
</div>
|
||||
<div id="userList" class="space-y-2"><div class="text-[#94a3b8] text-sm">Loading...</div></div>
|
||||
<div id="userList" class="space-y-2"><div class="text-[#b0bec5] text-sm">Loading...</div></div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
|
@ -95,13 +95,13 @@
|
|||
<div class="w-8 h-8 rounded-full bg-[#c9a84c]/20 flex items-center justify-center text-[#c9a84c] text-sm font-semibold">${(u.name||u.email||'?')[0].toUpperCase()}</div>
|
||||
<div class="flex-1">
|
||||
<div class="text-white text-sm font-medium">${escHtml(u.name || u.email)}</div>
|
||||
${u.name ? `<div class="text-[#475569] text-xs">${escHtml(u.email)}</div>` : ''}
|
||||
${u.name ? `<div class="text-[#8899a6] text-xs">${escHtml(u.email)}</div>` : ''}
|
||||
</div>
|
||||
${u.is_super_admin ? '<span class="text-xs px-2 py-0.5 bg-[#c9a84c]/20 text-[#c9a84c] rounded-full">super admin</span>' : ''}
|
||||
<span class="text-xs text-[#475569]">${new Date(u.created_at).toLocaleDateString()}</span>
|
||||
<span class="text-xs text-[#8899a6]">${new Date(u.created_at).toLocaleDateString()}</span>
|
||||
</div>`).join('');
|
||||
} else {
|
||||
document.getElementById('userList').innerHTML = '<div class="text-[#94a3b8] text-sm">No users found.</div>';
|
||||
document.getElementById('userList').innerHTML = '<div class="text-[#b0bec5] text-sm">No users found.</div>';
|
||||
}
|
||||
} catch(e) {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,24 +15,24 @@
|
|||
<header class="bg-[#0d1f3c] border-b border-white/[0.08] px-6 py-3 flex items-center justify-between sticky top-0 z-50">
|
||||
<a href="/app/tasks" class="text-xl font-bold text-white tracking-tight"><span class="text-[#c9a84c]">Deal</span>space</a>
|
||||
<div class="flex items-center gap-4">
|
||||
<span id="userName" class="text-sm text-[#94a3b8]"></span>
|
||||
<button onclick="logout()" class="text-sm text-[#94a3b8] hover:text-white transition">Logout</button>
|
||||
<span id="userName" class="text-sm text-[#b0bec5]"></span>
|
||||
<button onclick="logout()" class="text-sm text-[#b0bec5] hover:text-white transition">Logout</button>
|
||||
</div>
|
||||
</header>
|
||||
<div class="flex">
|
||||
<nav class="w-56 bg-[#0d1f3c] border-r border-white/[0.08] min-h-[calc(100vh-49px)] sticky top-[49px] shrink-0">
|
||||
<div class="p-3 space-y-0.5">
|
||||
<a href="/app/tasks" class="sidebar-link flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium text-[#94a3b8] transition">
|
||||
<a href="/app/tasks" class="sidebar-link flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium text-[#b0bec5] transition">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"/></svg>
|
||||
My Tasks</a>
|
||||
<a href="/app/projects" class="sidebar-link flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium text-[#94a3b8] transition">
|
||||
<a href="/app/projects" class="sidebar-link flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium text-[#b0bec5] transition">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"/></svg>
|
||||
Projects</a>
|
||||
<a href="/app/orgs" class="sidebar-link active flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium text-white transition">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"/></svg>
|
||||
Organizations</a>
|
||||
<div id="adminLinks" class="hidden"><div class="border-t border-white/[0.08] my-3"></div>
|
||||
<a href="/admin" class="sidebar-link flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium text-[#94a3b8] transition">
|
||||
<a href="/admin" class="sidebar-link flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium text-[#b0bec5] transition">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.066 2.573c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.573 1.066c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.066-2.573c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/></svg>
|
||||
Admin</a></div>
|
||||
</div>
|
||||
|
|
@ -41,17 +41,17 @@
|
|||
<div class="flex items-center justify-between mb-8">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-white mb-1">Organizations</h1>
|
||||
<p class="text-[#94a3b8] text-sm">Company directory — parties eligible to participate in deals.</p>
|
||||
<p class="text-[#b0bec5] text-sm">Company directory — parties eligible to participate in deals.</p>
|
||||
</div>
|
||||
<button id="newOrgBtn" class="hidden px-4 py-2 bg-[#c9a84c] hover:bg-[#b8973f] text-[#0a1628] font-semibold rounded-lg text-sm transition">+ New Organization</button>
|
||||
</div>
|
||||
<div id="orgGrid" class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="text-[#94a3b8] text-sm col-span-2">Loading...</div>
|
||||
<div class="text-[#b0bec5] text-sm col-span-2">Loading...</div>
|
||||
</div>
|
||||
<div id="emptyState" class="hidden text-center py-20">
|
||||
<div class="text-5xl mb-4">🏢</div>
|
||||
<h2 class="text-xl font-semibold text-white mb-2">No organizations yet</h2>
|
||||
<p class="text-[#94a3b8] text-sm">Add buyer, seller, IB, and advisor organizations.</p>
|
||||
<p class="text-[#b0bec5] text-sm">Add buyer, seller, IB, and advisor organizations.</p>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
|
@ -62,17 +62,17 @@
|
|||
<h2 class="text-xl font-semibold text-white mb-6">New Organization</h2>
|
||||
<div id="modalError" class="hidden mb-4 p-3 bg-red-500/10 border border-red-500/20 rounded-lg text-red-400 text-sm"></div>
|
||||
<div class="space-y-4">
|
||||
<div><label class="block text-sm font-medium text-[#94a3b8] mb-1.5">Organization Name</label>
|
||||
<input id="oName" type="text" placeholder="James LLC" class="w-full px-4 py-2.5 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white placeholder-[#475569] focus:outline-none focus:border-[#c9a84c] focus:ring-1 focus:ring-[#c9a84c]"></div>
|
||||
<div><label class="block text-sm font-medium text-[#94a3b8] mb-1.5">Allowed Email Domains <span class="text-red-400">*</span></label>
|
||||
<input id="oDomains" type="text" placeholder="jamesllc.com, kaseya.com" class="w-full px-4 py-2.5 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white placeholder-[#475569] focus:outline-none focus:border-[#c9a84c] focus:ring-1 focus:ring-[#c9a84c]">
|
||||
<p class="text-[#475569] text-xs mt-1">Comma-separated. Only emails from these domains can be invited for this org.</p></div>
|
||||
<div><label class="block text-sm font-medium text-[#94a3b8] mb-1.5">Default Role</label>
|
||||
<div><label class="block text-sm font-medium text-[#b0bec5] mb-1.5">Organization Name</label>
|
||||
<input id="oName" type="text" placeholder="James LLC" class="w-full px-4 py-2.5 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white placeholder-[#8899a6] focus:outline-none focus:border-[#c9a84c] focus:ring-1 focus:ring-[#c9a84c]"></div>
|
||||
<div><label class="block text-sm font-medium text-[#b0bec5] mb-1.5">Allowed Email Domains <span class="text-red-400">*</span></label>
|
||||
<input id="oDomains" type="text" placeholder="jamesllc.com, kaseya.com" class="w-full px-4 py-2.5 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white placeholder-[#8899a6] focus:outline-none focus:border-[#c9a84c] focus:ring-1 focus:ring-[#c9a84c]">
|
||||
<p class="text-[#8899a6] text-xs mt-1">Comma-separated. Only emails from these domains can be invited for this org.</p></div>
|
||||
<div><label class="block text-sm font-medium text-[#b0bec5] mb-1.5">Default Role</label>
|
||||
<select id="oRole" class="w-full px-4 py-2.5 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white focus:outline-none focus:border-[#c9a84c]">
|
||||
<option value="seller">Seller</option><option value="buyer">Buyer</option><option value="ib">IB Advisor</option><option value="advisor">Advisor</option>
|
||||
</select></div>
|
||||
<div><label class="block text-sm font-medium text-[#94a3b8] mb-1.5">Website</label>
|
||||
<input id="oWebsite" type="url" placeholder="https://jamesllc.com" class="w-full px-4 py-2.5 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white placeholder-[#475569] focus:outline-none focus:border-[#c9a84c] focus:ring-1 focus:ring-[#c9a84c]"></div>
|
||||
<div><label class="block text-sm font-medium text-[#b0bec5] mb-1.5">Website</label>
|
||||
<input id="oWebsite" type="url" placeholder="https://jamesllc.com" class="w-full px-4 py-2.5 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white placeholder-[#8899a6] focus:outline-none focus:border-[#c9a84c] focus:ring-1 focus:ring-[#c9a84c]"></div>
|
||||
</div>
|
||||
<div class="flex gap-3 mt-6">
|
||||
<button onclick="closeModal()" class="flex-1 py-2.5 bg-white/[0.05] hover:bg-white/[0.08] text-white rounded-lg text-sm font-medium transition">Cancel</button>
|
||||
|
|
@ -113,7 +113,7 @@
|
|||
<h3 class="text-white font-semibold text-lg leading-tight">${escHtml(d.name || o.summary || 'Untitled')}</h3>
|
||||
<span class="ml-2 shrink-0 px-2 py-0.5 rounded-full text-xs font-medium capitalize ${rc}">${d.role || '?'}</span>
|
||||
</div>
|
||||
<div class="flex gap-1.5 flex-wrap mb-3">${domains.map(dm => `<span class="text-xs font-mono text-[#94a3b8] bg-white/[0.05] px-2 py-0.5 rounded">@${escHtml(dm)}</span>`).join('')}</div>
|
||||
<div class="flex gap-1.5 flex-wrap mb-3">${domains.map(dm => `<span class="text-xs font-mono text-[#b0bec5] bg-white/[0.05] px-2 py-0.5 rounded">@${escHtml(dm)}</span>`).join('')}</div>
|
||||
${d.website ? `<a href="${escHtml(d.website)}" target="_blank" class="text-xs text-[#c9a84c] hover:underline">${escHtml(d.website)}</a>` : ''}
|
||||
</div>`;
|
||||
}).join('');
|
||||
|
|
|
|||
|
|
@ -27,29 +27,29 @@
|
|||
<div class="flex items-center gap-3">
|
||||
<a href="/app/tasks" class="text-xl font-bold text-white tracking-tight"><span class="text-[#c9a84c]">Deal</span>space</a>
|
||||
<span class="text-white/20">/</span>
|
||||
<a href="/app/projects" class="text-sm text-[#94a3b8] hover:text-white transition">Projects</a>
|
||||
<a href="/app/projects" class="text-sm text-[#b0bec5] hover:text-white transition">Projects</a>
|
||||
<span class="text-white/20">/</span>
|
||||
<span id="projectName" class="text-sm text-white font-medium">Loading...</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<span id="userName" class="text-sm text-[#94a3b8]"></span>
|
||||
<button onclick="logout()" class="text-sm text-[#94a3b8] hover:text-white transition">Logout</button>
|
||||
<span id="userName" class="text-sm text-[#b0bec5]"></span>
|
||||
<button onclick="logout()" class="text-sm text-[#b0bec5] hover:text-white transition">Logout</button>
|
||||
</div>
|
||||
</header>
|
||||
<div class="flex">
|
||||
<nav class="w-56 bg-[#0d1f3c] border-r border-white/[0.08] min-h-[calc(100vh-49px)] sticky top-[49px] shrink-0">
|
||||
<div class="p-3 space-y-0.5">
|
||||
<a href="/app/tasks" class="sidebar-link flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium text-[#94a3b8] transition">
|
||||
<a href="/app/tasks" class="sidebar-link flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium text-[#b0bec5] transition">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"/></svg>
|
||||
My Tasks</a>
|
||||
<a href="/app/projects" class="sidebar-link active flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium text-white transition">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"/></svg>
|
||||
Projects</a>
|
||||
<a href="/app/orgs" class="sidebar-link flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium text-[#94a3b8] transition">
|
||||
<a href="/app/orgs" class="sidebar-link flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium text-[#b0bec5] transition">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"/></svg>
|
||||
Organizations</a>
|
||||
<div id="adminLinks" class="hidden"><div class="border-t border-white/[0.08] my-3"></div>
|
||||
<a href="/admin" class="sidebar-link flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium text-[#94a3b8] transition">
|
||||
<a href="/admin" class="sidebar-link flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium text-[#b0bec5] transition">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.066 2.573c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.573 1.066c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.066-2.573c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/></svg>
|
||||
Admin</a></div>
|
||||
</div>
|
||||
|
|
@ -61,7 +61,7 @@
|
|||
<h1 id="projectTitle" class="text-2xl font-bold text-white">Loading...</h1>
|
||||
<span id="projectStatus" class="px-2.5 py-0.5 rounded-full text-xs font-medium"></span>
|
||||
</div>
|
||||
<p id="projectDesc" class="text-[#94a3b8] text-sm"></p>
|
||||
<p id="projectDesc" class="text-[#b0bec5] text-sm"></p>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button id="importBtn" onclick="openImportModal()" class="px-4 py-2 bg-white/[0.05] hover:bg-white/[0.08] text-white rounded-lg text-sm transition flex items-center gap-2">
|
||||
|
|
@ -73,31 +73,31 @@
|
|||
</div>
|
||||
<div class="flex gap-6 border-b border-white/[0.08] mb-6">
|
||||
<button class="tab active pb-3 text-sm font-medium transition" onclick="switchTab('requests', this)">Requests</button>
|
||||
<button class="tab pb-3 text-sm font-medium text-[#94a3b8] transition" onclick="switchTab('orgs', this)">Organizations</button>
|
||||
<button class="tab pb-3 text-sm font-medium text-[#94a3b8] transition" onclick="switchTab('team', this)">Team</button>
|
||||
<button class="tab pb-3 text-sm font-medium text-[#b0bec5] transition" onclick="switchTab('orgs', this)">Parties</button>
|
||||
<button class="tab pb-3 text-sm font-medium text-[#b0bec5] transition" onclick="switchTab('team', this)">Team</button>
|
||||
</div>
|
||||
<div id="tab-requests">
|
||||
<div id="requestList" class="space-y-4"><div class="text-[#94a3b8] text-sm">Loading requests...</div></div>
|
||||
<div id="requestList" class="space-y-4"><div class="text-[#b0bec5] text-sm">Loading requests...</div></div>
|
||||
<div id="requestEmpty" class="hidden text-center py-16">
|
||||
<div class="text-4xl mb-3">📋</div>
|
||||
<h3 class="text-lg font-semibold text-white mb-1">No requests yet</h3>
|
||||
<p class="text-[#94a3b8] text-sm mb-4">Import a diligence checklist or create requests manually.</p>
|
||||
<p class="text-[#b0bec5] text-sm mb-4">Import a diligence checklist or create requests manually.</p>
|
||||
<button onclick="openImportModal()" class="px-4 py-2 bg-[#c9a84c] hover:bg-[#b8973f] text-[#0a1628] font-semibold rounded-lg text-sm transition">Import Checklist</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="tab-orgs" class="hidden">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<p class="text-[#94a3b8] text-sm">Organizations participating in this deal.</p>
|
||||
<button id="addOrgBtn" class="px-3 py-1.5 bg-white/[0.05] hover:bg-white/[0.08] text-white rounded-lg text-sm transition">+ Add Org</button>
|
||||
<p class="text-[#b0bec5] text-sm">Parties involved in this deal.</p>
|
||||
<button id="addOrgBtn" class="px-4 py-2 bg-[#c9a84c] hover:bg-[#b8973f] text-[#0a1628] font-semibold rounded-lg text-sm transition">+ Add Party</button>
|
||||
</div>
|
||||
<div id="orgList" class="space-y-3"><div class="text-[#94a3b8] text-sm">Loading...</div></div>
|
||||
<div id="orgList" class="space-y-3"><div class="text-[#b0bec5] text-sm">Loading...</div></div>
|
||||
</div>
|
||||
<div id="tab-team" class="hidden">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<p class="text-[#94a3b8] text-sm">People with access to this deal.</p>
|
||||
<p class="text-[#b0bec5] text-sm">People with access to this deal.</p>
|
||||
<button id="inviteBtn" class="px-3 py-1.5 bg-[#c9a84c] hover:bg-[#b8973f] text-[#0a1628] font-semibold rounded-lg text-sm transition">+ Invite</button>
|
||||
</div>
|
||||
<div id="teamList" class="space-y-2"><div class="text-[#94a3b8] text-sm">Loading...</div></div>
|
||||
<div id="teamList" class="space-y-2"><div class="text-[#b0bec5] text-sm">Loading...</div></div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
|
@ -106,24 +106,24 @@
|
|||
<h2 class="text-lg font-semibold text-white mb-4">Import Diligence Checklist</h2>
|
||||
<form id="importForm" enctype="multipart/form-data">
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm text-[#94a3b8] mb-1.5">File (CSV or XLSX)</label>
|
||||
<label class="block text-sm text-[#b0bec5] mb-1.5">File (CSV or XLSX)</label>
|
||||
<input type="file" id="importFile" name="file" accept=".csv,.xlsx,.xls" required class="w-full px-3 py-2 bg-white/[0.05] border border-white/[0.08] rounded-lg text-white text-sm focus:outline-none focus:border-[#c9a84c]">
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm text-[#94a3b8] mb-1.5">Import Mode</label>
|
||||
<label class="block text-sm text-[#b0bec5] mb-1.5">Import Mode</label>
|
||||
<select id="importMode" name="mode" class="w-full px-3 py-2 bg-white/[0.05] border border-white/[0.08] rounded-lg text-white text-sm focus:outline-none focus:border-[#c9a84c]">
|
||||
<option value="add">Add to existing requests</option>
|
||||
<option value="replace">Replace all requests</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm text-[#94a3b8] mb-1.5">Section Filter (optional)</label>
|
||||
<input type="text" id="sectionFilter" name="section_filter" placeholder="e.g. Financial" class="w-full px-3 py-2 bg-white/[0.05] border border-white/[0.08] rounded-lg text-white text-sm focus:outline-none focus:border-[#c9a84c] placeholder:text-[#64748b]">
|
||||
<label class="block text-sm text-[#b0bec5] mb-1.5">Section Filter (optional)</label>
|
||||
<input type="text" id="sectionFilter" name="section_filter" placeholder="e.g. Financial" class="w-full px-3 py-2 bg-white/[0.05] border border-white/[0.08] rounded-lg text-white text-sm focus:outline-none focus:border-[#c9a84c] placeholder:text-[#8899a6]">
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input type="checkbox" id="createWorkstreams" name="create_workstreams" class="rounded border-white/[0.2]">
|
||||
<span class="text-sm text-[#94a3b8]">Create workstreams from sections</span>
|
||||
<span class="text-sm text-[#b0bec5]">Create workstreams from sections</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="flex gap-3">
|
||||
|
|
@ -133,6 +133,109 @@
|
|||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Add Org Modal -->
|
||||
<div id="addOrgModal" class="hidden fixed inset-0 bg-black/60 flex items-center justify-center z-50" onclick="if(event.target===this)closeAddOrg()">
|
||||
<div class="bg-[#0d1f3c] rounded-2xl w-full max-w-2xl border border-white/[0.08] max-h-[90vh] overflow-y-auto">
|
||||
<!-- Step 1: Email -->
|
||||
<div id="addOrgStep1" class="p-6">
|
||||
<h2 class="text-lg font-semibold text-white mb-1">Add Party</h2>
|
||||
<p class="text-[#b0bec5] text-sm mb-5">Enter the email address of someone at the organization.</p>
|
||||
<div class="flex gap-3">
|
||||
<input type="email" id="addOrgEmail" placeholder="e.g. michael@andersongroup.com" class="flex-1 px-4 py-2.5 bg-white/[0.05] border border-white/[0.08] rounded-lg text-white text-sm focus:outline-none focus:border-[#c9a84c] placeholder:text-[#8899a6]" onkeydown="if(event.key==='Enter')scrapeOrg()">
|
||||
<button onclick="scrapeOrg()" id="scrapeBtn" class="px-5 py-2.5 bg-[#c9a84c] hover:bg-[#b8973f] text-[#0a1628] font-semibold rounded-lg text-sm transition whitespace-nowrap">Look up</button>
|
||||
</div>
|
||||
<div id="scrapeError" class="hidden mt-3 text-red-400 text-sm"></div>
|
||||
<div id="scrapeLoading" class="hidden mt-4 flex items-center gap-3 text-[#b0bec5] text-sm">
|
||||
<svg class="animate-spin w-4 h-4" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"/></svg>
|
||||
Scanning website...
|
||||
</div>
|
||||
<div class="flex justify-end mt-4">
|
||||
<button onclick="closeAddOrg()" class="px-4 py-2 text-[#b0bec5] hover:text-white text-sm transition">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 2: Org details (prefilled) -->
|
||||
<div id="addOrgStep2" class="hidden p-6">
|
||||
<h2 class="text-lg font-semibold text-white mb-4">Organization Details</h2>
|
||||
<div class="grid grid-cols-2 gap-4 mb-4">
|
||||
<div class="col-span-2">
|
||||
<label class="block text-xs text-[#b0bec5] mb-1">Name *</label>
|
||||
<input type="text" id="orgName" class="w-full px-3 py-2 bg-white/[0.05] border border-white/[0.08] rounded-lg text-white text-sm focus:outline-none focus:border-[#c9a84c]">
|
||||
</div>
|
||||
<div class="col-span-2">
|
||||
<label class="block text-xs text-[#b0bec5] mb-1">Description</label>
|
||||
<textarea id="orgDesc" rows="2" class="w-full px-3 py-2 bg-white/[0.05] border border-white/[0.08] rounded-lg text-white text-sm focus:outline-none focus:border-[#c9a84c] resize-none"></textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs text-[#b0bec5] mb-1">Role in deal *</label>
|
||||
<select id="orgRole" class="w-full px-3 py-2 bg-white/[0.05] border border-white/[0.08] rounded-lg text-white text-sm focus:outline-none focus:border-[#c9a84c]">
|
||||
<option value="">Select...</option>
|
||||
<option value="seller">Seller</option>
|
||||
<option value="buyer">Buyer</option>
|
||||
<option value="ib">Investment Bank</option>
|
||||
<option value="advisor">Advisor</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs text-[#b0bec5] mb-1">Industry</label>
|
||||
<input type="text" id="orgIndustry" class="w-full px-3 py-2 bg-white/[0.05] border border-white/[0.08] rounded-lg text-white text-sm focus:outline-none focus:border-[#c9a84c]">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs text-[#b0bec5] mb-1">Website</label>
|
||||
<input type="text" id="orgWebsite" class="w-full px-3 py-2 bg-white/[0.05] border border-white/[0.08] rounded-lg text-white text-sm focus:outline-none focus:border-[#c9a84c]">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs text-[#b0bec5] mb-1">Phone</label>
|
||||
<input type="text" id="orgPhone" class="w-full px-3 py-2 bg-white/[0.05] border border-white/[0.08] rounded-lg text-white text-sm focus:outline-none focus:border-[#c9a84c]">
|
||||
</div>
|
||||
<div class="col-span-2">
|
||||
<label class="block text-xs text-[#b0bec5] mb-1">Address</label>
|
||||
<input type="text" id="orgAddress" class="w-full px-3 py-2 bg-white/[0.05] border border-white/[0.08] rounded-lg text-white text-sm focus:outline-none focus:border-[#c9a84c]">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs text-[#b0bec5] mb-1">City</label>
|
||||
<input type="text" id="orgCity" class="w-full px-3 py-2 bg-white/[0.05] border border-white/[0.08] rounded-lg text-white text-sm focus:outline-none focus:border-[#c9a84c]">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs text-[#b0bec5] mb-1">State</label>
|
||||
<input type="text" id="orgState" class="w-full px-3 py-2 bg-white/[0.05] border border-white/[0.08] rounded-lg text-white text-sm focus:outline-none focus:border-[#c9a84c]">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs text-[#b0bec5] mb-1">Founded</label>
|
||||
<input type="text" id="orgFounded" class="w-full px-3 py-2 bg-white/[0.05] border border-white/[0.08] rounded-lg text-white text-sm focus:outline-none focus:border-[#c9a84c]">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs text-[#b0bec5] mb-1">LinkedIn</label>
|
||||
<input type="text" id="orgLinkedIn" class="w-full px-3 py-2 bg-white/[0.05] border border-white/[0.08] rounded-lg text-white text-sm focus:outline-none focus:border-[#c9a84c]">
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<button onclick="showAddOrgStep(1)" class="px-4 py-2 text-[#b0bec5] hover:text-white text-sm transition">Back</button>
|
||||
<button onclick="showAddOrgStep(3)" class="px-5 py-2 bg-[#c9a84c] hover:bg-[#b8973f] text-[#0a1628] font-semibold rounded-lg text-sm transition">Next: Select People</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 3: Colleagues -->
|
||||
<div id="addOrgStep3" class="hidden p-6">
|
||||
<h2 class="text-lg font-semibold text-white mb-1">Select Colleagues</h2>
|
||||
<p class="text-[#b0bec5] text-sm mb-4">Choose who to add to this deal. They'll receive an invite email.</p>
|
||||
<div class="flex items-center gap-3 mb-4">
|
||||
<label class="flex items-center gap-2 cursor-pointer text-sm text-[#b0bec5]">
|
||||
<input type="checkbox" id="selectAllPeople" onchange="toggleAllPeople(this.checked)" class="rounded border-white/[0.2] accent-[#c9a84c]">
|
||||
Select all
|
||||
</label>
|
||||
<span id="selectedCount" class="text-xs text-[#8899a6]">0 selected</span>
|
||||
</div>
|
||||
<div id="peopleList" class="space-y-2 mb-4 max-h-[50vh] overflow-y-auto"></div>
|
||||
<div id="noPeople" class="hidden text-[#b0bec5] text-sm py-8 text-center">No team members found on the website.</div>
|
||||
<div class="flex justify-between">
|
||||
<button onclick="showAddOrgStep(2)" class="px-4 py-2 text-[#b0bec5] hover:text-white text-sm transition">Back</button>
|
||||
<button onclick="submitAddOrg()" id="submitOrgBtn" class="px-5 py-2 bg-[#c9a84c] hover:bg-[#b8973f] text-[#0a1628] font-semibold rounded-lg text-sm transition">Add Organization</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const token = localStorage.getItem('ds_token');
|
||||
if (!token) window.location.href = '/app/login';
|
||||
|
|
@ -183,7 +286,7 @@
|
|||
const reqs = sections[sec]; const isExpanded = expandedSections.has(sec);
|
||||
const statusCounts = { open: 0, in_progress: 0, answered: 0, not_applicable: 0 };
|
||||
reqs.forEach(r => { statusCounts[r.status || 'open']++; });
|
||||
return '<div class="border border-white/[0.08] rounded-xl overflow-hidden"><div class="section-header flex items-center justify-between px-5 py-3 bg-[#0d1f3c]" onclick="toggleSection(\'' + escHtml(sec) + '\')"><div class="flex items-center gap-3"><svg class="w-4 h-4 text-[#94a3b8] transition ' + (isExpanded ? 'rotate-90' : '') + '" id="chevron-' + escHtml(sec) + '" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/></svg><span class="text-white font-medium">' + escHtml(sec) + '</span><span class="text-[#94a3b8] text-xs">(' + reqs.length + ' items)</span></div><div class="flex gap-2">' + (statusCounts.open > 0 ? '<span class="px-2 py-0.5 rounded text-xs status-open">' + statusCounts.open + ' open</span>' : '') + (statusCounts.answered > 0 ? '<span class="px-2 py-0.5 rounded text-xs status-answered">' + statusCounts.answered + ' answered</span>' : '') + '</div></div><div id="section-' + escHtml(sec) + '" class="' + (isExpanded ? '' : 'hidden') + '">' + reqs.map(r => '<a href="/app/requests/' + r.entry_id + '" class="req-row flex items-center gap-4 px-5 py-3 border-t border-white/[0.05] transition cursor-pointer"><div class="w-16 shrink-0"><span class="text-xs font-mono text-[#94a3b8]">' + escHtml(r.item_number || '—') + '</span></div><div class="flex-1 min-w-0"><span class="text-white text-sm truncate block">' + escHtml(r.title || 'Untitled') + '</span></div><span class="shrink-0 w-2 h-2 rounded-full ' + (r.priority === 'high' ? 'bg-red-400' : r.priority === 'low' ? 'bg-green-400' : 'bg-yellow-400') + '" title="' + r.priority + ' priority"></span><span class="shrink-0 px-2.5 py-0.5 rounded text-xs font-medium status-' + (r.status || 'open') + '">' + (r.status || 'open').replace('_', ' ') + '</span></a>').join('') + '</div></div>';
|
||||
return '<div class="border border-white/[0.08] rounded-xl overflow-hidden"><div class="section-header flex items-center justify-between px-5 py-3 bg-[#0d1f3c]" onclick="toggleSection(\'' + escHtml(sec) + '\')"><div class="flex items-center gap-3"><svg class="w-4 h-4 text-[#b0bec5] transition ' + (isExpanded ? 'rotate-90' : '') + '" id="chevron-' + escHtml(sec) + '" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/></svg><span class="text-white font-medium">' + escHtml(sec) + '</span><span class="text-[#b0bec5] text-xs">(' + reqs.length + ' items)</span></div><div class="flex gap-2">' + (statusCounts.open > 0 ? '<span class="px-2 py-0.5 rounded text-xs status-open">' + statusCounts.open + ' open</span>' : '') + (statusCounts.answered > 0 ? '<span class="px-2 py-0.5 rounded text-xs status-answered">' + statusCounts.answered + ' answered</span>' : '') + '</div></div><div id="section-' + escHtml(sec) + '" class="' + (isExpanded ? '' : 'hidden') + '">' + reqs.map(r => '<a href="/app/requests/' + r.entry_id + '" class="req-row flex items-center gap-4 px-5 py-3 border-t border-white/[0.05] transition cursor-pointer"><div class="w-16 shrink-0"><span class="text-xs font-mono text-[#b0bec5]">' + escHtml(r.item_number || '—') + '</span></div><div class="flex-1 min-w-0"><span class="text-white text-sm truncate block">' + escHtml(r.title || 'Untitled') + '</span></div><span class="shrink-0 w-2 h-2 rounded-full ' + (r.priority === 'high' ? 'bg-red-400' : r.priority === 'low' ? 'bg-green-400' : 'bg-yellow-400') + '" title="' + r.priority + ' priority"></span><span class="shrink-0 px-2.5 py-0.5 rounded text-xs font-medium status-' + (r.status || 'open') + '">' + (r.status || 'open').replace('_', ' ') + '</span></a>').join('') + '</div></div>';
|
||||
}).join('');
|
||||
} catch(e) { console.error(e); document.getElementById('requestList').innerHTML = '<div class="text-red-400 text-sm">Failed to load requests.</div>'; }
|
||||
}
|
||||
|
|
@ -197,9 +300,9 @@
|
|||
const res = await fetchAPI('/api/projects/' + projectID + '/orgs');
|
||||
const orgs = await res.json();
|
||||
const list = document.getElementById('orgList');
|
||||
if (!orgs || orgs.length === 0) { list.innerHTML = '<div class="text-[#94a3b8] text-sm">No organizations added yet.</div>'; return; }
|
||||
if (!orgs || orgs.length === 0) { list.innerHTML = '<div class="text-[#b0bec5] text-sm">No organizations added yet.</div>'; return; }
|
||||
list.innerHTML = orgs.map(o => { const rc = roleColors[o.role] || 'bg-gray-500/20 text-gray-300'; const domains = o.org_domains || [];
|
||||
return '<div class="flex items-center gap-4 px-5 py-4 rounded-xl bg-[#0d1f3c] border border-white/[0.08]"><div class="flex-1"><div class="flex items-center gap-2 mb-1"><span class="text-white font-medium">' + escHtml(o.org_name || 'Unknown') + '</span><span class="px-2 py-0.5 rounded-full text-xs font-medium capitalize ' + rc + '">' + (o.role || '?') + '</span>' + (o.domain_lock ? '<span class="px-2 py-0.5 rounded-full text-xs bg-white/[0.05] text-[#94a3b8]">🔒 domain locked</span>' : '') + '</div>' + (domains.length > 0 ? '<div class="flex gap-1.5 flex-wrap">' + domains.map(dm => '<span class="text-xs text-[#94a3b8] font-mono bg-white/[0.05] px-2 py-0.5 rounded">@' + dm + '</span>').join('') + '</div>' : '') + '</div></div>';
|
||||
return '<div class="flex items-center gap-4 px-5 py-4 rounded-xl bg-[#0d1f3c] border border-white/[0.08]"><div class="flex-1"><div class="flex items-center gap-2 mb-1"><span class="text-white font-medium">' + escHtml(o.org_name || 'Unknown') + '</span><span class="px-2 py-0.5 rounded-full text-xs font-medium capitalize ' + rc + '">' + (o.role || '?') + '</span>' + (o.domain_lock ? '<span class="px-2 py-0.5 rounded-full text-xs bg-white/[0.05] text-[#b0bec5]">🔒 domain locked</span>' : '') + '</div>' + (domains.length > 0 ? '<div class="flex gap-1.5 flex-wrap">' + domains.map(dm => '<span class="text-xs text-[#b0bec5] font-mono bg-white/[0.05] px-2 py-0.5 rounded">@' + dm + '</span>').join('') + '</div>' : '') + '</div></div>';
|
||||
}).join('');
|
||||
} catch(e) { console.error(e); }
|
||||
}
|
||||
|
|
@ -208,13 +311,13 @@
|
|||
const res = await fetchAPI('/api/projects/' + projectID + '/members');
|
||||
const members = await res.json();
|
||||
const list = document.getElementById('teamList');
|
||||
if (!members || members.length === 0) { list.innerHTML = '<div class="text-[#94a3b8] text-sm">No team members yet.</div>'; return; }
|
||||
list.innerHTML = members.map(m => '<div class="flex items-center gap-4 px-5 py-3 rounded-xl bg-[#0d1f3c] border border-white/[0.08]"><div class="w-8 h-8 rounded-full bg-[#c9a84c]/20 flex items-center justify-center text-[#c9a84c] font-semibold text-sm">' + (m.name||m.email||'?')[0].toUpperCase() + '</div><div class="flex-1"><div class="text-white text-sm font-medium">' + escHtml(m.name || m.email) + '</div>' + (m.name ? '<div class="text-[#94a3b8] text-xs">' + escHtml(m.email) + '</div>' : '') + '</div><span class="text-xs text-[#94a3b8] capitalize">' + (m.role || 'member') + '</span></div>').join('');
|
||||
if (!members || members.length === 0) { list.innerHTML = '<div class="text-[#b0bec5] text-sm">No team members yet.</div>'; return; }
|
||||
list.innerHTML = members.map(m => '<div class="flex items-center gap-4 px-5 py-3 rounded-xl bg-[#0d1f3c] border border-white/[0.08]"><div class="w-8 h-8 rounded-full bg-[#c9a84c]/20 flex items-center justify-center text-[#c9a84c] font-semibold text-sm">' + (m.name||m.email||'?')[0].toUpperCase() + '</div><div class="flex-1"><div class="text-white text-sm font-medium">' + escHtml(m.name || m.email) + '</div>' + (m.name ? '<div class="text-[#b0bec5] text-xs">' + escHtml(m.email) + '</div>' : '') + '</div><span class="text-xs text-[#b0bec5] capitalize">' + (m.role || 'member') + '</span></div>').join('');
|
||||
} catch(e) { console.error(e); }
|
||||
}
|
||||
function switchTab(name, el) {
|
||||
document.querySelectorAll('.tab').forEach(t => { t.classList.remove('active','text-white'); t.classList.add('text-[#94a3b8]'); });
|
||||
el.classList.add('active','text-white'); el.classList.remove('text-[#94a3b8]');
|
||||
document.querySelectorAll('.tab').forEach(t => { t.classList.remove('active','text-white'); t.classList.add('text-[#b0bec5]'); });
|
||||
el.classList.add('active','text-white'); el.classList.remove('text-[#b0bec5]');
|
||||
document.getElementById('tab-requests').classList.toggle('hidden', name !== 'requests');
|
||||
document.getElementById('tab-orgs').classList.toggle('hidden', name !== 'orgs');
|
||||
document.getElementById('tab-team').classList.toggle('hidden', name !== 'team');
|
||||
|
|
@ -244,6 +347,147 @@
|
|||
}).then(r => r.json()).then(d => { if (d.entry_id) window.location.href = '/app/requests/' + d.entry_id; else loadRequests(); });
|
||||
};
|
||||
document.getElementById('importModal').onclick = (e) => { if (e.target.id === 'importModal') closeImportModal(); };
|
||||
|
||||
// ---- Add Org flow ----
|
||||
let scrapedData = null;
|
||||
|
||||
document.getElementById('addOrgBtn').onclick = () => {
|
||||
scrapedData = null;
|
||||
document.getElementById('addOrgEmail').value = '';
|
||||
document.getElementById('scrapeError').classList.add('hidden');
|
||||
document.getElementById('scrapeLoading').classList.add('hidden');
|
||||
showAddOrgStep(1);
|
||||
document.getElementById('addOrgModal').classList.remove('hidden');
|
||||
setTimeout(() => document.getElementById('addOrgEmail').focus(), 100);
|
||||
};
|
||||
|
||||
function closeAddOrg() { document.getElementById('addOrgModal').classList.add('hidden'); }
|
||||
|
||||
function showAddOrgStep(n) {
|
||||
document.getElementById('addOrgStep1').classList.toggle('hidden', n !== 1);
|
||||
document.getElementById('addOrgStep2').classList.toggle('hidden', n !== 2);
|
||||
document.getElementById('addOrgStep3').classList.toggle('hidden', n !== 3);
|
||||
if (n === 3) renderPeople();
|
||||
}
|
||||
|
||||
async function scrapeOrg() {
|
||||
const email = document.getElementById('addOrgEmail').value.trim();
|
||||
if (!email || !email.includes('@')) { document.getElementById('scrapeError').textContent = 'Enter a valid email address.'; document.getElementById('scrapeError').classList.remove('hidden'); return; }
|
||||
document.getElementById('scrapeError').classList.add('hidden');
|
||||
document.getElementById('scrapeLoading').classList.remove('hidden');
|
||||
document.getElementById('scrapeBtn').disabled = true;
|
||||
try {
|
||||
const res = await fetchAPI('/api/scrape/org', { method: 'POST', body: JSON.stringify({ email }) });
|
||||
const data = await res.json();
|
||||
if (!res.ok) { throw new Error(data.error || 'Scrape failed'); }
|
||||
scrapedData = data;
|
||||
// Prefill step 2
|
||||
document.getElementById('orgName').value = data.name || '';
|
||||
document.getElementById('orgDesc').value = data.description || '';
|
||||
document.getElementById('orgIndustry').value = data.industry || '';
|
||||
document.getElementById('orgWebsite').value = data.website || '';
|
||||
document.getElementById('orgPhone').value = data.phone || '';
|
||||
document.getElementById('orgAddress').value = data.address || '';
|
||||
document.getElementById('orgCity').value = data.city || '';
|
||||
document.getElementById('orgState').value = data.state || '';
|
||||
document.getElementById('orgFounded').value = data.founded || '';
|
||||
document.getElementById('orgLinkedIn').value = data.linkedin || '';
|
||||
showAddOrgStep(2);
|
||||
} catch (err) {
|
||||
document.getElementById('scrapeError').textContent = err.message;
|
||||
document.getElementById('scrapeError').classList.remove('hidden');
|
||||
} finally {
|
||||
document.getElementById('scrapeLoading').classList.add('hidden');
|
||||
document.getElementById('scrapeBtn').disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
function renderPeople() {
|
||||
const people = scrapedData?.people || [];
|
||||
const list = document.getElementById('peopleList');
|
||||
const none = document.getElementById('noPeople');
|
||||
if (people.length === 0) { list.innerHTML = ''; none.classList.remove('hidden'); return; }
|
||||
none.classList.add('hidden');
|
||||
list.innerHTML = people.map((p, i) => `
|
||||
<label class="flex items-center gap-3 p-3 rounded-xl bg-white/[0.02] border border-white/[0.06] hover:bg-white/[0.04] cursor-pointer transition">
|
||||
<input type="checkbox" class="person-cb accent-[#c9a84c] rounded" data-idx="${i}" checked onchange="updateSelectedCount()">
|
||||
<div class="w-10 h-10 rounded-full bg-[#0d1f3c] shrink-0 overflow-hidden border border-white/[0.08]">
|
||||
${p.photo ? `<img src="${escHtml(p.photo)}" class="w-full h-full object-cover" onerror="this.style.display='none';this.parentElement.innerHTML='<div class=\\'w-full h-full flex items-center justify-center text-[#c9a84c] font-semibold\\'>${escHtml(p.name[0])}</div>'">` : `<div class="w-full h-full flex items-center justify-center text-[#c9a84c] font-semibold">${escHtml(p.name[0])}</div>`}
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-white text-sm font-medium">${escHtml(p.name)}</span>
|
||||
${p.title ? `<span class="text-xs text-[#b0bec5]">${escHtml(p.title)}</span>` : ''}
|
||||
</div>
|
||||
<div class="flex items-center gap-3 text-xs text-[#8899a6]">
|
||||
${p.email ? `<span>${escHtml(p.email)}</span>` : ''}
|
||||
${p.phone ? `<span>${escHtml(p.phone)}</span>` : ''}
|
||||
</div>
|
||||
${p.bio ? `<div class="text-xs text-[#8899a6] mt-0.5 truncate">${escHtml(p.bio)}</div>` : ''}
|
||||
</div>
|
||||
</label>
|
||||
`).join('');
|
||||
document.getElementById('selectAllPeople').checked = true;
|
||||
updateSelectedCount();
|
||||
}
|
||||
|
||||
function toggleAllPeople(checked) {
|
||||
document.querySelectorAll('.person-cb').forEach(cb => cb.checked = checked);
|
||||
updateSelectedCount();
|
||||
}
|
||||
|
||||
function updateSelectedCount() {
|
||||
const checked = document.querySelectorAll('.person-cb:checked').length;
|
||||
const total = document.querySelectorAll('.person-cb').length;
|
||||
document.getElementById('selectedCount').textContent = checked + ' of ' + total + ' selected';
|
||||
document.getElementById('selectAllPeople').checked = (checked === total);
|
||||
}
|
||||
|
||||
async function submitAddOrg() {
|
||||
const name = document.getElementById('orgName').value.trim();
|
||||
const role = document.getElementById('orgRole').value;
|
||||
if (!name) { alert('Organization name is required.'); return; }
|
||||
if (!role) { alert('Please select a role for this organization.'); return; }
|
||||
|
||||
const domain = scrapedData?.domain || '';
|
||||
const people = scrapedData?.people || [];
|
||||
const selectedMembers = [];
|
||||
document.querySelectorAll('.person-cb:checked').forEach(cb => {
|
||||
const p = people[parseInt(cb.dataset.idx)];
|
||||
if (p) selectedMembers.push({ name: p.name, email: p.email || '', title: p.title || '', phone: p.phone || '', photo: p.photo || '', bio: p.bio || '', linkedin: p.linkedin || '' });
|
||||
});
|
||||
|
||||
const btn = document.getElementById('submitOrgBtn');
|
||||
btn.disabled = true; btn.textContent = 'Adding...';
|
||||
|
||||
try {
|
||||
const res = await fetchAPI('/api/projects/' + projectID + '/orgs/add', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
name,
|
||||
domains: domain ? [domain] : [],
|
||||
role,
|
||||
website: document.getElementById('orgWebsite').value,
|
||||
description: document.getElementById('orgDesc').value,
|
||||
industry: document.getElementById('orgIndustry').value,
|
||||
phone: document.getElementById('orgPhone').value,
|
||||
address: document.getElementById('orgAddress').value,
|
||||
city: document.getElementById('orgCity').value,
|
||||
state: document.getElementById('orgState').value,
|
||||
founded: document.getElementById('orgFounded').value,
|
||||
linkedin: document.getElementById('orgLinkedIn').value,
|
||||
members: selectedMembers,
|
||||
domain_lock: true,
|
||||
})
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(data.error || 'Failed to add organization');
|
||||
closeAddOrg();
|
||||
loadOrgs();
|
||||
} catch (err) { alert(err.message); }
|
||||
finally { btn.disabled = false; btn.textContent = 'Add Organization'; }
|
||||
}
|
||||
|
||||
loadProject(); loadRequests();
|
||||
</script>
|
||||
</body>
|
||||
|
|
|
|||
|
|
@ -15,24 +15,24 @@
|
|||
<header class="bg-[#0d1f3c] border-b border-white/[0.08] px-6 py-3 flex items-center justify-between sticky top-0 z-50">
|
||||
<a href="/app/tasks" class="text-xl font-bold text-white tracking-tight"><span class="text-[#c9a84c]">Deal</span>space</a>
|
||||
<div class="flex items-center gap-4">
|
||||
<span id="userName" class="text-sm text-[#94a3b8]"></span>
|
||||
<button onclick="logout()" class="text-sm text-[#94a3b8] hover:text-white transition">Logout</button>
|
||||
<span id="userName" class="text-sm text-[#b0bec5]"></span>
|
||||
<button onclick="logout()" class="text-sm text-[#b0bec5] hover:text-white transition">Logout</button>
|
||||
</div>
|
||||
</header>
|
||||
<div class="flex">
|
||||
<nav class="w-56 bg-[#0d1f3c] border-r border-white/[0.08] min-h-[calc(100vh-49px)] sticky top-[49px] shrink-0">
|
||||
<div class="p-3 space-y-0.5">
|
||||
<a href="/app/tasks" class="sidebar-link flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium text-[#94a3b8] transition">
|
||||
<a href="/app/tasks" class="sidebar-link flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium text-[#b0bec5] transition">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"/></svg>
|
||||
My Tasks</a>
|
||||
<a href="/app/projects" class="sidebar-link active flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium text-white transition">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"/></svg>
|
||||
Projects</a>
|
||||
<a href="/app/orgs" class="sidebar-link flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium text-[#94a3b8] transition">
|
||||
<a href="/app/orgs" class="sidebar-link flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium text-[#b0bec5] transition">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"/></svg>
|
||||
Organizations</a>
|
||||
<div id="adminLinks" class="hidden"><div class="border-t border-white/[0.08] my-3"></div>
|
||||
<a href="/admin" class="sidebar-link flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium text-[#94a3b8] transition">
|
||||
<a href="/admin" class="sidebar-link flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium text-[#b0bec5] transition">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.066 2.573c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.573 1.066c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.066-2.573c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/></svg>
|
||||
Admin</a></div>
|
||||
</div>
|
||||
|
|
@ -41,17 +41,17 @@
|
|||
<div class="flex items-center justify-between mb-8">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-white mb-1">Projects</h1>
|
||||
<p class="text-[#94a3b8] text-sm">All deals you have access to.</p>
|
||||
<p class="text-[#b0bec5] text-sm">All deals you have access to.</p>
|
||||
</div>
|
||||
<button id="newProjectBtn" class="hidden px-4 py-2 bg-[#c9a84c] hover:bg-[#b8973f] text-[#0a1628] font-semibold rounded-lg text-sm transition">+ New Project</button>
|
||||
</div>
|
||||
<div id="projectGrid" class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
|
||||
<div class="text-[#94a3b8] text-sm col-span-3">Loading projects...</div>
|
||||
<div class="text-[#b0bec5] text-sm col-span-3">Loading projects...</div>
|
||||
</div>
|
||||
<div id="emptyState" class="hidden text-center py-20">
|
||||
<div class="text-5xl mb-4">📁</div>
|
||||
<h2 class="text-xl font-semibold text-white mb-2">No projects yet</h2>
|
||||
<p class="text-[#94a3b8]">You haven't been added to any deals yet.</p>
|
||||
<p class="text-[#b0bec5]">You haven't been added to any deals yet.</p>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
|
@ -62,11 +62,11 @@
|
|||
<h2 class="text-xl font-semibold text-white mb-6">New Project</h2>
|
||||
<div id="modalError" class="hidden mb-4 p-3 bg-red-500/10 border border-red-500/20 rounded-lg text-red-400 text-sm"></div>
|
||||
<div class="space-y-4">
|
||||
<div><label class="block text-sm font-medium text-[#94a3b8] mb-1.5">Deal Name</label>
|
||||
<input id="pName" type="text" placeholder="Project James" class="w-full px-4 py-2.5 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white placeholder-[#475569] focus:outline-none focus:border-[#c9a84c] focus:ring-1 focus:ring-[#c9a84c]"></div>
|
||||
<div><label class="block text-sm font-medium text-[#94a3b8] mb-1.5">Description</label>
|
||||
<textarea id="pDesc" rows="2" placeholder="Brief description of this deal..." class="w-full px-4 py-2.5 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white placeholder-[#475569] focus:outline-none focus:border-[#c9a84c] focus:ring-1 focus:ring-[#c9a84c] resize-none"></textarea></div>
|
||||
<div><label class="block text-sm font-medium text-[#94a3b8] mb-1.5">Status</label>
|
||||
<div><label class="block text-sm font-medium text-[#b0bec5] mb-1.5">Deal Name</label>
|
||||
<input id="pName" type="text" placeholder="Project James" class="w-full px-4 py-2.5 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white placeholder-[#8899a6] focus:outline-none focus:border-[#c9a84c] focus:ring-1 focus:ring-[#c9a84c]"></div>
|
||||
<div><label class="block text-sm font-medium text-[#b0bec5] mb-1.5">Description</label>
|
||||
<textarea id="pDesc" rows="2" placeholder="Brief description of this deal..." class="w-full px-4 py-2.5 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white placeholder-[#8899a6] focus:outline-none focus:border-[#c9a84c] focus:ring-1 focus:ring-[#c9a84c] resize-none"></textarea></div>
|
||||
<div><label class="block text-sm font-medium text-[#b0bec5] mb-1.5">Status</label>
|
||||
<select id="pStatus" class="w-full px-4 py-2.5 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white focus:outline-none focus:border-[#c9a84c]">
|
||||
<option value="active">Active</option><option value="draft">Draft</option><option value="closed">Closed</option>
|
||||
</select></div>
|
||||
|
|
@ -116,8 +116,8 @@
|
|||
<h3 class="text-white font-semibold text-lg leading-tight">${escHtml(d.name || p.summary || 'Untitled')}</h3>
|
||||
<span class="ml-2 shrink-0 px-2 py-0.5 rounded-full text-xs font-medium ${sc} capitalize">${status}</span>
|
||||
</div>
|
||||
${d.description ? `<p class="text-[#94a3b8] text-sm mb-4 line-clamp-2">${escHtml(d.description)}</p>` : '<div class="mb-4"></div>'}
|
||||
<div class="flex items-center gap-2 text-xs text-[#475569]">
|
||||
${d.description ? `<p class="text-[#b0bec5] text-sm mb-4 line-clamp-2">${escHtml(d.description)}</p>` : '<div class="mb-4"></div>'}
|
||||
<div class="flex items-center gap-2 text-xs text-[#8899a6]">
|
||||
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/></svg>
|
||||
${new Date(p.created_at).toLocaleDateString('en-US', {month:'short',day:'numeric',year:'numeric'})}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -15,28 +15,28 @@
|
|||
<div class="flex items-center gap-3">
|
||||
<a href="/app/tasks" class="text-xl font-bold text-white tracking-tight"><span class="text-[#c9a84c]">Deal</span>space</a>
|
||||
<span class="text-white/20">/</span>
|
||||
<a href="/app/projects" class="text-sm text-[#94a3b8] hover:text-white transition">Projects</a>
|
||||
<a href="/app/projects" class="text-sm text-[#b0bec5] hover:text-white transition">Projects</a>
|
||||
<span class="text-white/20">/</span>
|
||||
<a id="backToProject" href="/app/projects" class="text-sm text-[#94a3b8] hover:text-white transition">Project</a>
|
||||
<a id="backToProject" href="/app/projects" class="text-sm text-[#b0bec5] hover:text-white transition">Project</a>
|
||||
<span class="text-white/20">/</span>
|
||||
<span id="reqRef" class="text-sm text-white font-medium">Request</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<span id="userName" class="text-sm text-[#94a3b8]"></span>
|
||||
<button onclick="logout()" class="text-sm text-[#94a3b8] hover:text-white transition">Logout</button>
|
||||
<span id="userName" class="text-sm text-[#b0bec5]"></span>
|
||||
<button onclick="logout()" class="text-sm text-[#b0bec5] hover:text-white transition">Logout</button>
|
||||
</div>
|
||||
</header>
|
||||
<div class="flex">
|
||||
<nav class="w-56 bg-[#0d1f3c] border-r border-white/[0.08] min-h-[calc(100vh-49px)] sticky top-[49px] shrink-0">
|
||||
<div class="p-3 space-y-0.5">
|
||||
<a href="/app/tasks" class="sidebar-link flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium text-[#94a3b8] transition">
|
||||
<a href="/app/tasks" class="sidebar-link flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium text-[#b0bec5] transition">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"/></svg>
|
||||
My Tasks</a>
|
||||
<a href="/app/projects" class="sidebar-link active flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium text-white transition">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"/></svg>
|
||||
Projects</a>
|
||||
<div id="adminLinks" class="hidden"><div class="border-t border-white/[0.08] my-3"></div>
|
||||
<a href="/admin" class="sidebar-link flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium text-[#94a3b8] transition">
|
||||
<a href="/admin" class="sidebar-link flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium text-[#b0bec5] transition">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.066 2.573c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.573 1.066c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.066-2.573c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/></svg>
|
||||
Admin</a></div>
|
||||
</div>
|
||||
|
|
@ -47,11 +47,11 @@
|
|||
<div class="flex items-start gap-4 mb-3">
|
||||
<div class="flex-1">
|
||||
<h1 id="reqTitle" class="text-2xl font-bold text-white mb-2">Loading...</h1>
|
||||
<p id="reqDesc" class="text-[#94a3b8] text-sm"></p>
|
||||
<p id="reqDesc" class="text-[#b0bec5] text-sm"></p>
|
||||
</div>
|
||||
<span id="reqStatus" class="shrink-0 px-3 py-1 rounded-full text-sm font-medium"></span>
|
||||
</div>
|
||||
<div class="flex gap-3 flex-wrap text-xs text-[#475569]">
|
||||
<div class="flex gap-3 flex-wrap text-xs text-[#8899a6]">
|
||||
<span id="reqDue"></span>
|
||||
<span id="reqAssignee"></span>
|
||||
</div>
|
||||
|
|
@ -59,23 +59,23 @@
|
|||
|
||||
<!-- Answer / Upload -->
|
||||
<div class="bg-[#0d1f3c] border border-white/[0.08] rounded-xl p-6 mb-6">
|
||||
<h2 class="text-sm font-semibold text-[#94a3b8] uppercase tracking-wider mb-4">Response</h2>
|
||||
<h2 class="text-sm font-semibold text-[#b0bec5] uppercase tracking-wider mb-4">Response</h2>
|
||||
<div id="answers" class="space-y-4 mb-6"></div>
|
||||
<div id="uploadArea" class="border-2 border-dashed border-white/[0.08] rounded-xl p-8 text-center hover:border-[#c9a84c]/40 transition cursor-pointer" onclick="document.getElementById('fileInput').click()">
|
||||
<div class="text-3xl mb-2">📎</div>
|
||||
<p class="text-[#94a3b8] text-sm">Drop files here or click to upload</p>
|
||||
<p class="text-[#475569] text-xs mt-1">PDF, DOCX, XLSX, images</p>
|
||||
<p class="text-[#b0bec5] text-sm">Drop files here or click to upload</p>
|
||||
<p class="text-[#8899a6] text-xs mt-1">PDF, DOCX, XLSX, images</p>
|
||||
<input id="fileInput" type="file" multiple class="hidden" onchange="uploadFiles(this.files)">
|
||||
</div>
|
||||
<div id="uploadStatus" class="mt-3 text-sm text-[#94a3b8]"></div>
|
||||
<div id="uploadStatus" class="mt-3 text-sm text-[#b0bec5]"></div>
|
||||
</div>
|
||||
|
||||
<!-- Activity / Comments -->
|
||||
<div class="bg-[#0d1f3c] border border-white/[0.08] rounded-xl p-6">
|
||||
<h2 class="text-sm font-semibold text-[#94a3b8] uppercase tracking-wider mb-4">Comments</h2>
|
||||
<h2 class="text-sm font-semibold text-[#b0bec5] uppercase tracking-wider mb-4">Comments</h2>
|
||||
<div id="comments" class="space-y-3 mb-4"></div>
|
||||
<div class="flex gap-3">
|
||||
<textarea id="commentText" rows="2" placeholder="Add a comment..." class="flex-1 px-4 py-2.5 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white placeholder-[#475569] focus:outline-none focus:border-[#c9a84c] focus:ring-1 focus:ring-[#c9a84c] text-sm resize-none"></textarea>
|
||||
<textarea id="commentText" rows="2" placeholder="Add a comment..." class="flex-1 px-4 py-2.5 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white placeholder-[#8899a6] focus:outline-none focus:border-[#c9a84c] focus:ring-1 focus:ring-[#c9a84c] text-sm resize-none"></textarea>
|
||||
<button onclick="postComment()" class="px-4 py-2 bg-[#c9a84c] hover:bg-[#b8973f] text-[#0a1628] font-semibold rounded-lg text-sm transition self-end">Post</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -127,7 +127,7 @@
|
|||
const res = await fetchAPI('/api/entries/' + reqID + '/children?type=answer');
|
||||
const items = await res.json();
|
||||
const el = document.getElementById('answers');
|
||||
if (!items || items.length === 0) { el.innerHTML = '<p class="text-[#475569] text-sm">No documents uploaded yet.</p>'; return; }
|
||||
if (!items || items.length === 0) { el.innerHTML = '<p class="text-[#8899a6] text-sm">No documents uploaded yet.</p>'; return; }
|
||||
el.innerHTML = items.map(a => {
|
||||
const d = parseData(a.data_text);
|
||||
const name = d.filename || d.name || a.summary || 'Document';
|
||||
|
|
@ -135,7 +135,7 @@
|
|||
<span class="text-2xl">${name.endsWith('.pdf') ? '📄' : name.match(/\.(jpg|jpeg|png|gif)$/i) ? '🖼️' : '📎'}</span>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="text-white text-sm font-medium truncate">${escHtml(name)}</div>
|
||||
<div class="text-[#475569] text-xs">${new Date(a.created_at).toLocaleString()}</div>
|
||||
<div class="text-[#8899a6] text-xs">${new Date(a.created_at).toLocaleString()}</div>
|
||||
</div>
|
||||
<a href="/api/entries/${a.entry_id}/download" class="text-[#c9a84c] text-sm hover:underline">Download</a>
|
||||
</div>`;
|
||||
|
|
@ -164,7 +164,7 @@
|
|||
const res = await fetchAPI('/api/entries/' + reqID + '/children?type=comment');
|
||||
const items = await res.json();
|
||||
const el = document.getElementById('comments');
|
||||
if (!items || items.length === 0) { el.innerHTML = '<p class="text-[#475569] text-sm">No comments yet.</p>'; return; }
|
||||
if (!items || items.length === 0) { el.innerHTML = '<p class="text-[#8899a6] text-sm">No comments yet.</p>'; return; }
|
||||
el.innerHTML = items.map(c => {
|
||||
const d = parseData(c.data_text);
|
||||
return `<div class="flex gap-3">
|
||||
|
|
@ -172,9 +172,9 @@
|
|||
<div>
|
||||
<div class="flex items-baseline gap-2 mb-1">
|
||||
<span class="text-white text-sm font-medium">${escHtml(d.author||'Unknown')}</span>
|
||||
<span class="text-[#475569] text-xs">${new Date(c.created_at).toLocaleString()}</span>
|
||||
<span class="text-[#8899a6] text-xs">${new Date(c.created_at).toLocaleString()}</span>
|
||||
</div>
|
||||
<p class="text-[#94a3b8] text-sm">${escHtml(d.text||'')}</p>
|
||||
<p class="text-[#b0bec5] text-sm">${escHtml(d.text||'')}</p>
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
|
|
|
|||
|
|
@ -30,8 +30,8 @@
|
|||
</select>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<span id="userName" class="text-sm text-[#94a3b8]"></span>
|
||||
<button onclick="logout()" class="text-sm text-[#94a3b8] hover:text-white transition">Logout</button>
|
||||
<span id="userName" class="text-sm text-[#b0bec5]"></span>
|
||||
<button onclick="logout()" class="text-sm text-[#b0bec5] hover:text-white transition">Logout</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
|
@ -40,16 +40,16 @@
|
|||
<nav class="w-56 bg-[#0d1f3c] border-r border-white/[0.08] min-h-[calc(100vh-49px)] sticky top-[49px] shrink-0">
|
||||
<div class="p-3 space-y-0.5">
|
||||
<a href="/app/tasks" class="sidebar-link active flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium text-white transition">
|
||||
<svg class="w-4 h-4 text-[#94a3b8]" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"/></svg>
|
||||
<svg class="w-4 h-4 text-[#b0bec5]" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"/></svg>
|
||||
My Tasks
|
||||
</a>
|
||||
<a href="/app/projects" class="sidebar-link flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium text-[#94a3b8] transition">
|
||||
<a href="/app/projects" class="sidebar-link flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium text-[#b0bec5] transition">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"/></svg>
|
||||
Projects
|
||||
</a>
|
||||
<div id="adminLinks" class="hidden">
|
||||
<div class="border-t border-white/[0.08] my-3"></div>
|
||||
<a href="/admin" class="sidebar-link flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium text-[#94a3b8] transition">
|
||||
<a href="/admin" class="sidebar-link flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium text-[#b0bec5] transition">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.066 2.573c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.573 1.066c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.066-2.573c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/></svg>
|
||||
Admin
|
||||
</a>
|
||||
|
|
@ -62,19 +62,19 @@
|
|||
<!-- Greeting -->
|
||||
<div class="mb-8">
|
||||
<h1 id="greeting" class="text-2xl font-bold text-white mb-1"></h1>
|
||||
<p class="text-[#94a3b8] text-sm">Here are your pending tasks.</p>
|
||||
<p class="text-[#b0bec5] text-sm">Here are your pending tasks.</p>
|
||||
</div>
|
||||
|
||||
<!-- Task List -->
|
||||
<div id="taskList" class="space-y-3">
|
||||
<div class="text-[#94a3b8] text-sm">Loading tasks...</div>
|
||||
<div class="text-[#b0bec5] text-sm">Loading tasks...</div>
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div id="emptyState" class="hidden text-center py-20">
|
||||
<div class="text-5xl mb-4">🎉</div>
|
||||
<h2 class="text-xl font-semibold text-white mb-2">You're all caught up</h2>
|
||||
<p class="text-[#94a3b8]">No tasks need your attention right now.</p>
|
||||
<p class="text-[#b0bec5]">No tasks need your attention right now.</p>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
|
@ -136,12 +136,12 @@
|
|||
<a href="/app/requests/${t.entry_id}" class="task-card block bg-[#0d1f3c] border border-white/[0.08] rounded-xl p-5 transition cursor-pointer">
|
||||
<div class="flex items-center gap-3 mb-2">
|
||||
<span class="w-2.5 h-2.5 rounded-full priority-${priority} shrink-0"></span>
|
||||
${ref ? `<span class="text-xs font-mono text-[#94a3b8]">${ref}</span>` : ''}
|
||||
${ref ? `<span class="text-xs font-mono text-[#b0bec5]">${ref}</span>` : ''}
|
||||
<span class="text-white font-medium flex-1">${escapeHtml(title)}</span>
|
||||
${due ? `<span class="text-xs text-[#94a3b8]">Due: ${due}</span>` : ''}
|
||||
${due ? `<span class="text-xs text-[#b0bec5]">Due: ${due}</span>` : ''}
|
||||
</div>
|
||||
<div class="flex items-center gap-3 text-xs text-[#475569]">
|
||||
<span class="capitalize px-2 py-0.5 rounded-full bg-white/[0.05] text-[#94a3b8]">${status}</span>
|
||||
<div class="flex items-center gap-3 text-xs text-[#8899a6]">
|
||||
<span class="capitalize px-2 py-0.5 rounded-full bg-white/[0.05] text-[#b0bec5]">${status}</span>
|
||||
<span>${t.type || 'request'}</span>
|
||||
</div>
|
||||
</a>
|
||||
|
|
|
|||
|
|
@ -20,22 +20,22 @@
|
|||
<h1 class="text-3xl font-bold text-white tracking-tight">
|
||||
<span class="text-[#c9a84c]">Deal</span>space
|
||||
</h1>
|
||||
<p class="text-[#94a3b8] mt-2 text-sm">Secure M&A deal management</p>
|
||||
<p class="text-[#b0bec5] mt-2 text-sm">Secure M&A deal management</p>
|
||||
</div>
|
||||
|
||||
<!-- Step 1: Email -->
|
||||
<div id="step-email" class="bg-[#0d1f3c] border border-white/[0.08] rounded-xl p-8">
|
||||
<h2 class="text-xl font-semibold text-white mb-2">Sign in</h2>
|
||||
<p class="text-[#94a3b8] text-sm mb-6">Enter your email to receive a login code.</p>
|
||||
<p class="text-[#b0bec5] text-sm mb-6">Enter your email to receive a login code.</p>
|
||||
|
||||
<div id="error-email" class="hidden mb-4 p-3 bg-red-500/10 border border-red-500/20 rounded-lg text-red-400 text-sm"></div>
|
||||
|
||||
<form id="emailForm" class="space-y-5">
|
||||
<div>
|
||||
<label for="email" class="block text-sm font-medium text-[#94a3b8] mb-1.5">Email</label>
|
||||
<label for="email" class="block text-sm font-medium text-[#b0bec5] mb-1.5">Email</label>
|
||||
<input type="email" id="email" name="email" required autocomplete="email" autofocus
|
||||
placeholder="you@company.com"
|
||||
class="w-full px-4 py-2.5 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white placeholder-[#475569] focus:outline-none focus:border-[#c9a84c] focus:ring-1 focus:ring-[#c9a84c] transition">
|
||||
class="w-full px-4 py-2.5 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white placeholder-[#8899a6] focus:outline-none focus:border-[#c9a84c] focus:ring-1 focus:ring-[#c9a84c] transition">
|
||||
</div>
|
||||
<button type="submit" id="emailBtn"
|
||||
class="w-full py-2.5 bg-[#c9a84c] hover:bg-[#b8973f] text-[#0a1628] font-semibold rounded-lg transition disabled:opacity-50">
|
||||
|
|
@ -47,7 +47,7 @@
|
|||
<!-- Step 2: OTP Code -->
|
||||
<div id="step-code" class="hidden bg-[#0d1f3c] border border-white/[0.08] rounded-xl p-8">
|
||||
<h2 class="text-xl font-semibold text-white mb-2">Enter your code</h2>
|
||||
<p class="text-[#94a3b8] text-sm mb-6">
|
||||
<p class="text-[#b0bec5] text-sm mb-6">
|
||||
We sent a 6-digit code to <span id="sent-email" class="text-white font-medium"></span>
|
||||
</p>
|
||||
|
||||
|
|
@ -55,11 +55,11 @@
|
|||
|
||||
<form id="codeForm" class="space-y-5">
|
||||
<div>
|
||||
<label for="code" class="block text-sm font-medium text-[#94a3b8] mb-1.5">Login code</label>
|
||||
<label for="code" class="block text-sm font-medium text-[#b0bec5] mb-1.5">Login code</label>
|
||||
<input type="text" id="code" name="code" required autocomplete="one-time-code"
|
||||
maxlength="6" inputmode="numeric" pattern="[0-9]*"
|
||||
placeholder="000000"
|
||||
class="code-input w-full px-4 py-3 bg-[#0a1628] border border-white/[0.08] rounded-lg text-[#c9a84c] placeholder-[#475569] focus:outline-none focus:border-[#c9a84c] focus:ring-1 focus:ring-[#c9a84c] transition">
|
||||
class="code-input w-full px-4 py-3 bg-[#0a1628] border border-white/[0.08] rounded-lg text-[#c9a84c] placeholder-[#8899a6] focus:outline-none focus:border-[#c9a84c] focus:ring-1 focus:ring-[#c9a84c] transition">
|
||||
</div>
|
||||
<button type="submit" id="codeBtn"
|
||||
class="w-full py-2.5 bg-[#c9a84c] hover:bg-[#b8973f] text-[#0a1628] font-semibold rounded-lg transition disabled:opacity-50">
|
||||
|
|
@ -68,7 +68,7 @@
|
|||
</form>
|
||||
|
||||
<div class="mt-4 flex items-center justify-between">
|
||||
<button id="backBtn" class="text-[#94a3b8] text-sm hover:text-white transition">
|
||||
<button id="backBtn" class="text-[#b0bec5] text-sm hover:text-white transition">
|
||||
← Use a different email
|
||||
</button>
|
||||
<button id="resendBtn" class="text-[#c9a84c] text-sm hover:text-[#b8973f] transition">
|
||||
|
|
@ -77,11 +77,11 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-center text-[#475569] text-xs mt-6">
|
||||
<p class="text-center text-[#8899a6] text-xs mt-6">
|
||||
Don’t have an account? Dealspace is invite-only.<br>
|
||||
<a href="/#demo" class="text-[#c9a84c] hover:underline">Request access on muskepo.com</a>
|
||||
</p>
|
||||
<p class="text-center text-[#475569] text-xs mt-3">© 2026 Muskepo B.V. — Amsterdam</p>
|
||||
<p class="text-center text-[#8899a6] text-xs mt-3">© 2026 Muskepo B.V. — Amsterdam</p>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
|
|
|
|||
|
|
@ -19,33 +19,33 @@
|
|||
<h1 class="text-3xl font-bold text-white tracking-tight">
|
||||
<span class="text-[#c9a84c]">Deal</span>space
|
||||
</h1>
|
||||
<p class="text-[#94a3b8] mt-2 text-sm">First-time setup</p>
|
||||
<p class="text-[#b0bec5] mt-2 text-sm">First-time setup</p>
|
||||
</div>
|
||||
|
||||
<!-- Setup Card -->
|
||||
<div class="bg-[#0d1f3c] border border-white/[0.08] rounded-xl p-8">
|
||||
<h2 class="text-xl font-semibold text-white mb-2">Create admin account</h2>
|
||||
<p class="text-[#94a3b8] text-sm mb-6">This will be the first administrator account for your Dealspace instance.</p>
|
||||
<p class="text-[#b0bec5] text-sm mb-6">This will be the first administrator account for your Dealspace instance.</p>
|
||||
|
||||
<div id="error" class="hidden mb-4 p-3 bg-red-500/10 border border-red-500/20 rounded-lg text-red-400 text-sm"></div>
|
||||
<div id="success" class="hidden mb-4 p-3 bg-green-500/10 border border-green-500/20 rounded-lg text-green-400 text-sm"></div>
|
||||
|
||||
<form id="setupForm" class="space-y-5">
|
||||
<div>
|
||||
<label for="name" class="block text-sm font-medium text-[#94a3b8] mb-1.5">Full name</label>
|
||||
<label for="name" class="block text-sm font-medium text-[#b0bec5] mb-1.5">Full name</label>
|
||||
<input type="text" id="name" name="name" required autofocus
|
||||
class="w-full px-4 py-2.5 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white placeholder-[#475569] focus:outline-none focus:border-[#c9a84c] focus:ring-1 focus:ring-[#c9a84c] transition">
|
||||
class="w-full px-4 py-2.5 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white placeholder-[#8899a6] focus:outline-none focus:border-[#c9a84c] focus:ring-1 focus:ring-[#c9a84c] transition">
|
||||
</div>
|
||||
<div>
|
||||
<label for="email" class="block text-sm font-medium text-[#94a3b8] mb-1.5">Email</label>
|
||||
<label for="email" class="block text-sm font-medium text-[#b0bec5] mb-1.5">Email</label>
|
||||
<input type="email" id="email" name="email" required autocomplete="email"
|
||||
class="w-full px-4 py-2.5 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white placeholder-[#475569] focus:outline-none focus:border-[#c9a84c] focus:ring-1 focus:ring-[#c9a84c] transition">
|
||||
class="w-full px-4 py-2.5 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white placeholder-[#8899a6] focus:outline-none focus:border-[#c9a84c] focus:ring-1 focus:ring-[#c9a84c] transition">
|
||||
</div>
|
||||
<div>
|
||||
<label for="password" class="block text-sm font-medium text-[#94a3b8] mb-1.5">Password</label>
|
||||
<label for="password" class="block text-sm font-medium text-[#b0bec5] mb-1.5">Password</label>
|
||||
<input type="password" id="password" name="password" required minlength="8" autocomplete="new-password"
|
||||
class="w-full px-4 py-2.5 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white placeholder-[#475569] focus:outline-none focus:border-[#c9a84c] focus:ring-1 focus:ring-[#c9a84c] transition">
|
||||
<p class="text-xs text-[#475569] mt-1">Minimum 8 characters</p>
|
||||
class="w-full px-4 py-2.5 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white placeholder-[#8899a6] focus:outline-none focus:border-[#c9a84c] focus:ring-1 focus:ring-[#c9a84c] transition">
|
||||
<p class="text-xs text-[#8899a6] mt-1">Minimum 8 characters</p>
|
||||
</div>
|
||||
<button type="submit" id="submitBtn"
|
||||
class="w-full py-2.5 bg-[#c9a84c] hover:bg-[#b8973f] text-[#0a1628] font-semibold rounded-lg transition disabled:opacity-50">
|
||||
|
|
@ -54,7 +54,7 @@
|
|||
</form>
|
||||
</div>
|
||||
|
||||
<p class="text-center text-[#475569] text-xs mt-8">© 2026 Muskepo B.V. — Amsterdam</p>
|
||||
<p class="text-center text-[#8899a6] text-xs mt-8">© 2026 Muskepo B.V. — Amsterdam</p>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
|
|
|
|||
|
|
@ -13,8 +13,8 @@ html[data-theme="midnight"] {
|
|||
--ds-bg: #0a1628;
|
||||
--ds-sf: #0d1f3c;
|
||||
--ds-tx: #fff;
|
||||
--ds-tx2: #94a3b8;
|
||||
--ds-tx3: #475569;
|
||||
--ds-tx2: #b0bec5;
|
||||
--ds-tx3: #8899a6;
|
||||
--ds-ac: #c9a84c;
|
||||
--ds-ac2: #b8973f;
|
||||
--ds-act: #0a1628;
|
||||
|
|
@ -29,8 +29,8 @@ html[data-theme="light"] {
|
|||
--ds-bg: #f0f2f5;
|
||||
--ds-sf: #fff;
|
||||
--ds-tx: #1a202c;
|
||||
--ds-tx2: #64748b;
|
||||
--ds-tx3: #94a3b8;
|
||||
--ds-tx2: #8899a6;
|
||||
--ds-tx3: #b0bec5;
|
||||
--ds-ac: #2563eb;
|
||||
--ds-ac2: #1d4ed8;
|
||||
--ds-act: #fff;
|
||||
|
|
@ -45,8 +45,8 @@ html[data-theme="slate"] {
|
|||
--ds-bg: #1e293b;
|
||||
--ds-sf: #334155;
|
||||
--ds-tx: #f1f5f9;
|
||||
--ds-tx2: #94a3b8;
|
||||
--ds-tx3: #64748b;
|
||||
--ds-tx2: #b0bec5;
|
||||
--ds-tx3: #8899a6;
|
||||
--ds-ac: #14b8a6;
|
||||
--ds-ac2: #0d9488;
|
||||
--ds-act: #0f172a;
|
||||
|
|
@ -61,8 +61,8 @@ html[data-theme="compact"] {
|
|||
--ds-bg: #0a1628;
|
||||
--ds-sf: #0d1f3c;
|
||||
--ds-tx: #fff;
|
||||
--ds-tx2: #94a3b8;
|
||||
--ds-tx3: #475569;
|
||||
--ds-tx2: #b0bec5;
|
||||
--ds-tx3: #8899a6;
|
||||
--ds-ac: #c9a84c;
|
||||
--ds-ac2: #b8973f;
|
||||
--ds-act: #0a1628;
|
||||
|
|
@ -145,8 +145,8 @@ nav { background: var(--ds-sf) !important; border-color: var(--ds-bd) !important
|
|||
[class*="bg-[#0d1f3c]"] { background-color: var(--ds-sf) !important; }
|
||||
[class*="bg-[#0a1628]"] { background-color: var(--ds-bg) !important; }
|
||||
[class*="text-[#c9a84c]"] { color: var(--ds-ac) !important; }
|
||||
[class*="text-[#94a3b8]"] { color: var(--ds-tx2) !important; }
|
||||
[class*="text-[#475569]"] { color: var(--ds-tx3) !important; }
|
||||
[class*="text-[#b0bec5]"] { color: var(--ds-tx2) !important; }
|
||||
[class*="text-[#8899a6]"] { color: var(--ds-tx3) !important; }
|
||||
[class*="text-[#0a1628]"] { color: var(--ds-act) !important; }
|
||||
[class*="bg-[#c9a84c]"] { background-color: var(--ds-ac) !important; }
|
||||
[class*="hover:bg-[#b8973f]"]:hover { background-color: var(--ds-ac2) !important; }
|
||||
|
|
@ -155,6 +155,7 @@ nav { background: var(--ds-sf) !important; border-color: var(--ds-bd) !important
|
|||
[class*="hover:bg-white/"]:hover { background-color: var(--ds-hv) !important; }
|
||||
input, textarea, select { background-color: var(--ds-inp) !important; color: var(--ds-tx) !important; border-color: var(--ds-bd) !important; }
|
||||
input:focus, textarea:focus, select:focus { border-color: var(--ds-ac) !important; }
|
||||
input.field-error, textarea.field-error, select.field-error { border-color: #ef4444 !important; }
|
||||
[class*="bg-black/"] { background-color: rgba(0,0,0,.6) !important; }
|
||||
|
||||
/* ===== SIDEBAR ===== */
|
||||
|
|
@ -206,7 +207,7 @@ input:focus, textarea:focus, select:focus { border-color: var(--ds-ac) !importan
|
|||
text-align: left;
|
||||
padding: 6px 14px;
|
||||
font-size: 13px;
|
||||
color: var(--ds-tx2, #94a3b8);
|
||||
color: var(--ds-tx2, #b0bec5);
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
|
|
@ -235,7 +236,7 @@ input:focus, textarea:focus, select:focus { border-color: var(--ds-ac) !importan
|
|||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: var(--ds-tx3, #475569);
|
||||
color: var(--ds-tx3, #8899a6);
|
||||
border-bottom: 1px solid var(--ds-bd, rgba(255,255,255,.08));
|
||||
user-select: none;
|
||||
white-space: nowrap;
|
||||
|
|
@ -251,7 +252,7 @@ input:focus, textarea:focus, select:focus { border-color: var(--ds-ac) !importan
|
|||
#reqTree tbody tr.row-list { background: var(--ds-sf, #0d1f3c); }
|
||||
#reqTree tbody tr.row-list:hover { background: color-mix(in srgb, var(--ds-sf, #0d1f3c) 85%, var(--ds-tx, white)); }
|
||||
#reqTree tbody tr.row-section { background: rgba(255,255,255,.02); }
|
||||
#reqTree .row-num { font-variant-numeric: tabular-nums; font-size: 12px; color: var(--ds-tx3, #475569); }
|
||||
#reqTree .row-num { font-variant-numeric: tabular-nums; font-size: 12px; color: var(--ds-tx3, #8899a6); }
|
||||
|
||||
/* ===== DRAG & DROP ===== */
|
||||
#reqTree .drag-handle { cursor: grab; opacity: 0.3; transition: opacity .15s; }
|
||||
|
|
@ -263,7 +264,7 @@ input:focus, textarea:focus, select:focus { border-color: var(--ds-ac) !importan
|
|||
width: 16px;
|
||||
text-align: center;
|
||||
font-size: 10px;
|
||||
color: var(--ds-tx3, #475569);
|
||||
color: var(--ds-tx3, #8899a6);
|
||||
transition: transform .15s;
|
||||
}
|
||||
#reqTree .collapse-btn.collapsed { transform: rotate(-90deg); }
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@
|
|||
#aria-close {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #94a3b8;
|
||||
color: #b0bec5;
|
||||
font-size: 20px;
|
||||
cursor: pointer;
|
||||
padding: 0 4px;
|
||||
|
|
@ -167,7 +167,7 @@
|
|||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: #94a3b8;
|
||||
background: #b0bec5;
|
||||
animation: aria-bounce 1.2s infinite;
|
||||
}
|
||||
.aria-typing span:nth-child(2) { animation-delay: 0.2s; }
|
||||
|
|
@ -202,7 +202,7 @@
|
|||
}
|
||||
|
||||
#aria-input:focus { border-color: rgba(201,168,76,0.5); }
|
||||
#aria-input::placeholder { color: #475569; }
|
||||
#aria-input::placeholder { color: #8899a6; }
|
||||
|
||||
#aria-send {
|
||||
width: 36px;
|
||||
|
|
|
|||
|
|
@ -5,23 +5,23 @@
|
|||
{{define "content"}}
|
||||
<div class="p-8 max-w-6xl">
|
||||
<h1 class="text-2xl font-bold text-white mb-2">Admin Dashboard</h1>
|
||||
<p class="text-[#94a3b8] text-sm mb-8">Platform overview and management.</p>
|
||||
<p class="text-[#b0bec5] text-sm mb-8">Platform overview and management.</p>
|
||||
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8">
|
||||
<div class="bg-[#0d1f3c] border border-white/[0.08] rounded-xl p-5">
|
||||
<div class="text-[#94a3b8] text-xs uppercase tracking-wider mb-1">Users</div>
|
||||
<div class="text-[#b0bec5] text-xs uppercase tracking-wider mb-1">Users</div>
|
||||
<div id="statUsers" class="text-3xl font-bold text-white">—</div>
|
||||
</div>
|
||||
<div class="bg-[#0d1f3c] border border-white/[0.08] rounded-xl p-5">
|
||||
<div class="text-[#94a3b8] text-xs uppercase tracking-wider mb-1">Projects</div>
|
||||
<div class="text-[#b0bec5] text-xs uppercase tracking-wider mb-1">Projects</div>
|
||||
<div id="statProjects" class="text-3xl font-bold text-white">—</div>
|
||||
</div>
|
||||
<div class="bg-[#0d1f3c] border border-white/[0.08] rounded-xl p-5">
|
||||
<div class="text-[#94a3b8] text-xs uppercase tracking-wider mb-1">Organizations</div>
|
||||
<div class="text-[#b0bec5] text-xs uppercase tracking-wider mb-1">Organizations</div>
|
||||
<div id="statOrgs" class="text-3xl font-bold text-white">—</div>
|
||||
</div>
|
||||
<div class="bg-[#0d1f3c] border border-white/[0.08] rounded-xl p-5">
|
||||
<div class="text-[#94a3b8] text-xs uppercase tracking-wider mb-1">Active Sessions</div>
|
||||
<div class="text-[#b0bec5] text-xs uppercase tracking-wider mb-1">Active Sessions</div>
|
||||
<div id="statSessions" class="text-3xl font-bold text-white">—</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -30,7 +30,7 @@
|
|||
<div class="flex items-center justify-between mb-4">
|
||||
<h2 class="text-sm font-semibold text-white">All Users</h2>
|
||||
</div>
|
||||
<div id="userList" class="space-y-2"><div class="text-[#94a3b8] text-sm">Loading...</div></div>
|
||||
<div id="userList" class="space-y-2"><div class="text-[#b0bec5] text-sm">Loading...</div></div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
|
@ -58,13 +58,13 @@
|
|||
<div class="w-8 h-8 rounded-full bg-[#c9a84c]/20 flex items-center justify-center text-[#c9a84c] text-sm font-semibold">${(u.name||u.email||'?')[0].toUpperCase()}</div>
|
||||
<div class="flex-1">
|
||||
<div class="text-white text-sm font-medium">${escHtml(u.name || u.email)}</div>
|
||||
${u.name ? `<div class="text-[#475569] text-xs">${escHtml(u.email)}</div>` : ''}
|
||||
${u.name ? `<div class="text-[#8899a6] text-xs">${escHtml(u.email)}</div>` : ''}
|
||||
</div>
|
||||
${u.is_super_admin ? '<span class="text-xs px-2 py-0.5 bg-[#c9a84c]/20 text-[#c9a84c] rounded-full">super admin</span>' : ''}
|
||||
<span class="text-xs text-[#475569]">${new Date(u.created_at).toLocaleDateString()}</span>
|
||||
<span class="text-xs text-[#8899a6]">${new Date(u.created_at).toLocaleDateString()}</span>
|
||||
</div>`).join('');
|
||||
} else {
|
||||
document.getElementById('userList').innerHTML = '<div class="text-[#94a3b8] text-sm">No users found.</div>';
|
||||
document.getElementById('userList').innerHTML = '<div class="text-[#b0bec5] text-sm">No users found.</div>';
|
||||
}
|
||||
} catch(e) {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,16 +3,16 @@
|
|||
<div class="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-white mb-1">Organizations</h1>
|
||||
<p class="text-[#94a3b8] text-sm">Company directory — parties eligible to participate in deals.</p>
|
||||
<p class="text-[#b0bec5] text-sm">Company directory — parties eligible to participate in deals.</p>
|
||||
</div>
|
||||
<button id="newOrgBtn" class="hidden px-4 py-2 bg-[#c9a84c] hover:bg-[#b8973f] text-[#0a1628] font-semibold rounded-lg text-sm transition">+ New Organization</button>
|
||||
</div>
|
||||
<div id="orgGrid" class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="text-[#94a3b8] text-sm col-span-2">Loading...</div>
|
||||
<div class="text-[#b0bec5] text-sm col-span-2">Loading...</div>
|
||||
</div>
|
||||
<div id="emptyState" class="hidden text-center py-20">
|
||||
<h2 class="text-xl font-semibold text-white mb-2">No organizations yet</h2>
|
||||
<p class="text-[#94a3b8] text-sm">Add buyer, seller, IB, and advisor organizations.</p>
|
||||
<p class="text-[#b0bec5] text-sm">Add buyer, seller, IB, and advisor organizations.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -22,17 +22,17 @@
|
|||
<h2 class="text-xl font-semibold text-white mb-6">New Organization</h2>
|
||||
<div id="modalError" class="hidden mb-4 p-3 bg-red-500/10 border border-red-500/20 rounded-lg text-red-400 text-sm"></div>
|
||||
<div class="space-y-4">
|
||||
<div><label class="block text-sm font-medium text-[#94a3b8] mb-1.5">Name</label>
|
||||
<input id="oName" type="text" placeholder="Blackstone Group" class="w-full px-4 py-2.5 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white placeholder-[#475569] focus:outline-none focus:border-[#c9a84c]"></div>
|
||||
<div><label class="block text-sm font-medium text-[#94a3b8] mb-1.5">Email Domains <span class="text-red-400">*</span></label>
|
||||
<input id="oDomains" type="text" placeholder="blackstone.com" class="w-full px-4 py-2.5 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white placeholder-[#475569] focus:outline-none focus:border-[#c9a84c]">
|
||||
<p class="text-[#475569] text-xs mt-1">Comma-separated. Used to validate invite emails.</p></div>
|
||||
<div><label class="block text-sm font-medium text-[#94a3b8] mb-1.5">Role</label>
|
||||
<div><label class="block text-sm font-medium text-[#b0bec5] mb-1.5">Name</label>
|
||||
<input id="oName" type="text" placeholder="Blackstone Group" class="w-full px-4 py-2.5 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white placeholder-[#8899a6] focus:outline-none focus:border-[#c9a84c]"></div>
|
||||
<div><label class="block text-sm font-medium text-[#b0bec5] mb-1.5">Email Domains <span class="text-red-400">*</span></label>
|
||||
<input id="oDomains" type="text" placeholder="blackstone.com" class="w-full px-4 py-2.5 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white placeholder-[#8899a6] focus:outline-none focus:border-[#c9a84c]">
|
||||
<p class="text-[#8899a6] text-xs mt-1">Comma-separated. Used to validate invite emails.</p></div>
|
||||
<div><label class="block text-sm font-medium text-[#b0bec5] mb-1.5">Role</label>
|
||||
<select id="oRole" class="w-full px-4 py-2.5 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white focus:outline-none focus:border-[#c9a84c]">
|
||||
<option value="seller">Seller</option><option value="buyer">Buyer</option><option value="ib">IB Advisor</option><option value="advisor">Advisor</option>
|
||||
</select></div>
|
||||
<div><label class="block text-sm font-medium text-[#94a3b8] mb-1.5">Website</label>
|
||||
<input id="oWebsite" type="text" placeholder="blackstone.com" class="w-full px-4 py-2.5 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white placeholder-[#475569] focus:outline-none focus:border-[#c9a84c]"></div>
|
||||
<div><label class="block text-sm font-medium text-[#b0bec5] mb-1.5">Website</label>
|
||||
<input id="oWebsite" type="text" placeholder="blackstone.com" class="w-full px-4 py-2.5 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white placeholder-[#8899a6] focus:outline-none focus:border-[#c9a84c]"></div>
|
||||
</div>
|
||||
<div class="flex gap-3 mt-6">
|
||||
<button onclick="closeModal()" class="flex-1 py-2.5 bg-white/[0.05] hover:bg-white/[0.08] text-white rounded-lg text-sm font-medium transition">Cancel</button>
|
||||
|
|
@ -46,23 +46,23 @@
|
|||
<div class="bg-[#0d1f3c] border border-white/[0.08] rounded-xl p-8 w-full max-w-md">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h2 class="text-xl font-semibold text-white">Edit Organization</h2>
|
||||
<button onclick="closeEditModal()" class="text-[#94a3b8] hover:text-white text-2xl leading-none">×</button>
|
||||
<button onclick="closeEditModal()" class="text-[#b0bec5] hover:text-white text-2xl leading-none">×</button>
|
||||
</div>
|
||||
<div id="editModalError" class="hidden mb-4 p-3 bg-red-500/10 border border-red-500/20 rounded-lg text-red-400 text-sm"></div>
|
||||
<div class="space-y-4">
|
||||
<div><label class="block text-sm font-medium text-[#94a3b8] mb-1.5">Name</label>
|
||||
<div><label class="block text-sm font-medium text-[#b0bec5] mb-1.5">Name</label>
|
||||
<input id="eName" type="text" class="w-full px-4 py-2.5 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white focus:outline-none focus:border-[#c9a84c]"></div>
|
||||
<div><label class="block text-sm font-medium text-[#94a3b8] mb-1.5">Email Domains</label>
|
||||
<input id="eDomains" type="text" placeholder="blackstone.com, pe.blackstone.com" class="w-full px-4 py-2.5 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white placeholder-[#475569] focus:outline-none focus:border-[#c9a84c]">
|
||||
<p class="text-[#475569] text-xs mt-1">Comma-separated.</p></div>
|
||||
<div><label class="block text-sm font-medium text-[#94a3b8] mb-1.5">Role</label>
|
||||
<div><label class="block text-sm font-medium text-[#b0bec5] mb-1.5">Email Domains</label>
|
||||
<input id="eDomains" type="text" placeholder="blackstone.com, pe.blackstone.com" class="w-full px-4 py-2.5 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white placeholder-[#8899a6] focus:outline-none focus:border-[#c9a84c]">
|
||||
<p class="text-[#8899a6] text-xs mt-1">Comma-separated.</p></div>
|
||||
<div><label class="block text-sm font-medium text-[#b0bec5] mb-1.5">Role</label>
|
||||
<select id="eRole" class="w-full px-4 py-2.5 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white focus:outline-none focus:border-[#c9a84c]">
|
||||
<option value="seller">Seller</option><option value="buyer">Buyer</option><option value="ib">IB Advisor</option><option value="advisor">Advisor</option>
|
||||
</select></div>
|
||||
<div><label class="block text-sm font-medium text-[#94a3b8] mb-1.5">Website</label>
|
||||
<input id="eWebsite" type="text" placeholder="blackstone.com" class="w-full px-4 py-2.5 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white placeholder-[#475569] focus:outline-none focus:border-[#c9a84c]"></div>
|
||||
<div><label class="block text-sm font-medium text-[#94a3b8] mb-1.5">Description</label>
|
||||
<textarea id="eDesc" rows="2" placeholder="Optional notes..." class="w-full px-4 py-2.5 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white placeholder-[#475569] focus:outline-none focus:border-[#c9a84c] resize-none"></textarea></div>
|
||||
<div><label class="block text-sm font-medium text-[#b0bec5] mb-1.5">Website</label>
|
||||
<input id="eWebsite" type="text" placeholder="blackstone.com" class="w-full px-4 py-2.5 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white placeholder-[#8899a6] focus:outline-none focus:border-[#c9a84c]"></div>
|
||||
<div><label class="block text-sm font-medium text-[#b0bec5] mb-1.5">Description</label>
|
||||
<textarea id="eDesc" rows="2" placeholder="Optional notes..." class="w-full px-4 py-2.5 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white placeholder-[#8899a6] focus:outline-none focus:border-[#c9a84c] resize-none"></textarea></div>
|
||||
</div>
|
||||
<div class="flex gap-3 mt-6">
|
||||
<button onclick="closeEditModal()" class="flex-1 py-2.5 bg-white/[0.05] hover:bg-white/[0.08] text-white rounded-lg text-sm font-medium transition">Cancel</button>
|
||||
|
|
@ -108,7 +108,7 @@
|
|||
<h3 class="text-white font-semibold leading-tight">${escHtml(name)}</h3>
|
||||
<span class="ml-2 shrink-0 px-2 py-0.5 rounded-full text-xs font-medium capitalize ${rc}">${role || '—'}</span>
|
||||
</div>
|
||||
<div class="flex gap-1.5 flex-wrap">${domains.map(dm => `<span class="text-xs font-mono text-[#94a3b8] bg-white/[0.05] px-2 py-0.5 rounded">@${escHtml(dm)}</span>`).join('')}</div>
|
||||
<div class="flex gap-1.5 flex-wrap">${domains.map(dm => `<span class="text-xs font-mono text-[#b0bec5] bg-white/[0.05] px-2 py-0.5 rounded">@${escHtml(dm)}</span>`).join('')}</div>
|
||||
${o.website ? `<div class="mt-2 text-xs" style="color:var(--ds-tx3)">${escHtml(o.website)}</div>` : ''}
|
||||
</div>`;
|
||||
}).join('');
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -3,17 +3,17 @@
|
|||
<div class="flex items-center justify-between mb-8">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-white mb-1">Projects</h1>
|
||||
<p class="text-[#94a3b8] text-sm">All deals you have access to.</p>
|
||||
<p class="text-[#b0bec5] text-sm">All deals you have access to.</p>
|
||||
</div>
|
||||
<button id="newProjectBtn" class="hidden px-4 py-2 bg-[#c9a84c] hover:bg-[#b8973f] text-[#0a1628] font-semibold rounded-lg text-sm transition">+ New Project</button>
|
||||
</div>
|
||||
<div id="projectGrid" class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
|
||||
<div class="text-[#94a3b8] text-sm col-span-3">Loading projects...</div>
|
||||
<div class="text-[#b0bec5] text-sm col-span-3">Loading projects...</div>
|
||||
</div>
|
||||
<div id="emptyState" class="hidden text-center py-20">
|
||||
<div class="text-5xl mb-4">📁</div>
|
||||
<h2 class="text-xl font-semibold text-white mb-2">No projects yet</h2>
|
||||
<p class="text-[#94a3b8]">You haven't been added to any deals yet.</p>
|
||||
<p class="text-[#b0bec5]">You haven't been added to any deals yet.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -23,11 +23,11 @@
|
|||
<h2 class="text-xl font-semibold text-white mb-6">New Project</h2>
|
||||
<div id="modalError" class="hidden mb-4 p-3 bg-red-500/10 border border-red-500/20 rounded-lg text-red-400 text-sm"></div>
|
||||
<div class="space-y-4">
|
||||
<div><label class="block text-sm font-medium text-[#94a3b8] mb-1.5">Deal Name</label>
|
||||
<input id="pName" type="text" placeholder="Project James" class="w-full px-4 py-2.5 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white placeholder-[#475569] focus:outline-none focus:border-[#c9a84c] focus:ring-1 focus:ring-[#c9a84c]"></div>
|
||||
<div><label class="block text-sm font-medium text-[#94a3b8] mb-1.5">Description</label>
|
||||
<textarea id="pDesc" rows="2" placeholder="Brief description of this deal..." class="w-full px-4 py-2.5 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white placeholder-[#475569] focus:outline-none focus:border-[#c9a84c] focus:ring-1 focus:ring-[#c9a84c] resize-none"></textarea></div>
|
||||
<div><label class="block text-sm font-medium text-[#94a3b8] mb-1.5">Status</label>
|
||||
<div><label class="block text-sm font-medium text-[#b0bec5] mb-1.5">Deal Name</label>
|
||||
<input id="pName" type="text" placeholder="Project James" class="w-full px-4 py-2.5 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white placeholder-[#8899a6] focus:outline-none focus:border-[#c9a84c] focus:ring-1 focus:ring-[#c9a84c]"></div>
|
||||
<div><label class="block text-sm font-medium text-[#b0bec5] mb-1.5">Description</label>
|
||||
<textarea id="pDesc" rows="2" placeholder="Brief description of this deal..." class="w-full px-4 py-2.5 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white placeholder-[#8899a6] focus:outline-none focus:border-[#c9a84c] focus:ring-1 focus:ring-[#c9a84c] resize-none"></textarea></div>
|
||||
<div><label class="block text-sm font-medium text-[#b0bec5] mb-1.5">Status</label>
|
||||
<select id="pStatus" class="w-full px-4 py-2.5 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white focus:outline-none focus:border-[#c9a84c]">
|
||||
<option value="active">Active</option><option value="draft">Draft</option><option value="closed">Closed</option>
|
||||
</select></div>
|
||||
|
|
@ -68,8 +68,8 @@
|
|||
<h3 class="text-white font-semibold text-lg leading-tight">${escHtml(d.name || p.summary || 'Untitled')}</h3>
|
||||
<span class="ml-2 shrink-0 px-2 py-0.5 rounded-full text-xs font-medium ${sc} capitalize">${status}</span>
|
||||
</div>
|
||||
${d.description ? `<p class="text-[#94a3b8] text-sm mb-4 line-clamp-2">${escHtml(d.description)}</p>` : '<div class="mb-4"></div>'}
|
||||
<div class="flex items-center gap-2 text-xs text-[#475569]">
|
||||
${d.description ? `<p class="text-[#b0bec5] text-sm mb-4 line-clamp-2">${escHtml(d.description)}</p>` : '<div class="mb-4"></div>'}
|
||||
<div class="flex items-center gap-2 text-xs text-[#8899a6]">
|
||||
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/></svg>
|
||||
${new Date(p.created_at).toLocaleDateString('en-US', {month:'short',day:'numeric',year:'numeric'})}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
<div class="flex items-center gap-3">
|
||||
<a href="/app/projects" class="text-2xl font-bold text-white tracking-tight"><span class="text-[#c9a84c]">Deal</span>space</a>
|
||||
<span class="text-white/20">/</span>
|
||||
<a id="backToProject" href="/app/projects" class="text-sm text-[#94a3b8] hover:text-white transition">Project</a>
|
||||
<a id="backToProject" href="/app/projects" class="text-sm text-[#b0bec5] hover:text-white transition">Project</a>
|
||||
<span class="text-white/20">/</span>
|
||||
<span id="reqRef" class="text-sm text-white font-medium">—</span>
|
||||
</div>
|
||||
|
|
@ -282,14 +282,14 @@
|
|||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-1">
|
||||
<span class="text-white text-sm font-medium truncate" id="fname-${a.entry_id}">${escHtml(name)}</span>
|
||||
<button onclick="startRenameFile('${a.entry_id}')" class="shrink-0 p-0.5 rounded hover:bg-white/[0.08] text-[#94a3b8] hover:text-white transition" title="Rename">
|
||||
<button onclick="startRenameFile('${a.entry_id}')" class="shrink-0 p-0.5 rounded hover:bg-white/[0.08] text-[#b0bec5] hover:text-white transition" title="Rename">
|
||||
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="text-[#475569] text-xs">${a.created_at ? new Date(a.created_at * 1000).toLocaleString() : ''}</div>
|
||||
<div class="text-[#8899a6] text-xs">${a.created_at ? new Date(a.created_at * 1000).toLocaleString() : ''}</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 shrink-0">
|
||||
${objectID && previewUrl ? `<a href="${previewUrl}" target="_blank" class="px-2 py-1 rounded text-xs font-medium bg-white/[0.06] text-[#94a3b8] hover:text-white hover:bg-white/[0.1] transition">${isVideo ? 'Play' : (isBuyerRole() ? 'View' : 'Preview')}</a>` : ''}
|
||||
${objectID && previewUrl ? `<a href="${previewUrl}" target="_blank" class="px-2 py-1 rounded text-xs font-medium bg-white/[0.06] text-[#b0bec5] hover:text-white hover:bg-white/[0.1] transition">${isVideo ? 'Play' : (isBuyerRole() ? 'View' : 'Preview')}</a>` : ''}
|
||||
${objectID && !isBuyerRole() ? `<a href="${downloadUrl}" class="text-sm hover:underline" style="color:var(--ds-ac)">Download</a>` : ''}
|
||||
</div>
|
||||
</div>`;
|
||||
|
|
@ -397,7 +397,7 @@
|
|||
container.innerHTML = tabs.map(t =>
|
||||
`<button onclick="switchChannel(${JSON.stringify(t.key)})"
|
||||
class="px-4 py-3 text-sm font-medium whitespace-nowrap border-b-2 -mb-px transition
|
||||
${activeChannel === t.key ? 'border-[#c9a84c] text-white' : 'border-transparent text-[#94a3b8] hover:text-white'}">
|
||||
${activeChannel === t.key ? 'border-[#c9a84c] text-white' : 'border-transparent text-[#b0bec5] hover:text-white'}">
|
||||
${t.label}
|
||||
</button>`
|
||||
).join('');
|
||||
|
|
@ -418,7 +418,7 @@
|
|||
const top = ch.filter(c => !parseData(c.data_text).parent_comment_id);
|
||||
const nested = ch.filter(c => !!parseData(c.data_text).parent_comment_id);
|
||||
if (top.length === 0) {
|
||||
el.innerHTML = '<p class="text-[#475569] text-sm">No messages yet.</p>';
|
||||
el.innerHTML = '<p class="text-[#8899a6] text-sm">No messages yet.</p>';
|
||||
return;
|
||||
}
|
||||
el.innerHTML = top.map(c => {
|
||||
|
|
@ -432,17 +432,17 @@
|
|||
function renderOneComment(c, d, isReply) {
|
||||
const av = (d.author || '?')[0].toUpperCase();
|
||||
const ts = c.created_at ? new Date(c.created_at * 1000).toLocaleString('en-US', {month:'short',day:'numeric',hour:'numeric',minute:'2-digit'}) : '';
|
||||
const replyBtn = isReply ? '' : `<button onclick="startReply('${c.entry_id}')" class="text-xs text-[#475569] hover:text-[#94a3b8] ml-2 transition">Reply</button>`;
|
||||
const replyBtn = isReply ? '' : `<button onclick="startReply('${c.entry_id}')" class="text-xs text-[#8899a6] hover:text-[#b0bec5] ml-2 transition">Reply</button>`;
|
||||
const indent = isReply ? 'ml-10 pl-4 border-l-2 border-[#c9a84c]/30' : '';
|
||||
return `<div class="flex gap-3 ${indent}" data-comment-id="${c.entry_id}">
|
||||
<div class="w-7 h-7 rounded-full bg-[#c9a84c]/20 flex items-center justify-center text-[#c9a84c] text-xs font-semibold shrink-0 mt-0.5">${av}</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-baseline gap-2 mb-1">
|
||||
<span class="text-white text-sm font-medium">${escHtml(d.author || 'Unknown')}</span>
|
||||
<span class="text-[#475569] text-xs">${ts}</span>
|
||||
<span class="text-[#8899a6] text-xs">${ts}</span>
|
||||
${replyBtn}
|
||||
</div>
|
||||
<p class="text-[#94a3b8] text-sm leading-relaxed">${escHtml(d.text || '')}</p>
|
||||
<p class="text-[#b0bec5] text-sm leading-relaxed">${escHtml(d.text || '')}</p>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
|
@ -456,10 +456,10 @@
|
|||
form.id = 'replyForm';
|
||||
form.className = 'ml-10 pl-4 border-l-2 border-[#c9a84c]/30 mt-2';
|
||||
form.innerHTML = `<div class="flex gap-2 items-end">
|
||||
<textarea id="replyText" rows="2" placeholder="Reply..." class="flex-1 px-3 py-2 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white placeholder-[#475569] focus:outline-none focus:border-[#c9a84c] text-sm resize-none"></textarea>
|
||||
<textarea id="replyText" rows="2" placeholder="Reply..." class="flex-1 px-3 py-2 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white placeholder-[#8899a6] focus:outline-none focus:border-[#c9a84c] text-sm resize-none"></textarea>
|
||||
<div class="flex flex-col gap-1">
|
||||
<button onclick="postComment('${commentId}')" class="px-3 py-1.5 bg-[#c9a84c] hover:bg-[#b8973f] text-[#0a1628] font-semibold rounded-lg text-xs transition">Reply</button>
|
||||
<button onclick="cancelReply()" class="px-3 py-1.5 bg-white/[0.05] hover:bg-white/[0.08] text-[#94a3b8] rounded-lg text-xs transition">Cancel</button>
|
||||
<button onclick="cancelReply()" class="px-3 py-1.5 bg-white/[0.05] hover:bg-white/[0.08] text-[#b0bec5] rounded-lg text-xs transition">Cancel</button>
|
||||
</div>
|
||||
</div>`;
|
||||
el.insertAdjacentElement('afterend', form);
|
||||
|
|
|
|||
|
|
@ -14,19 +14,19 @@
|
|||
<!-- Greeting -->
|
||||
<div class="mb-8">
|
||||
<h1 id="greeting" class="text-2xl font-bold text-white mb-1"></h1>
|
||||
<p class="text-[#94a3b8] text-sm">Here are your pending tasks.</p>
|
||||
<p class="text-[#b0bec5] text-sm">Here are your pending tasks.</p>
|
||||
</div>
|
||||
|
||||
<!-- Task List -->
|
||||
<div id="taskList" class="space-y-3">
|
||||
<div class="text-[#94a3b8] text-sm">Loading tasks...</div>
|
||||
<div class="text-[#b0bec5] text-sm">Loading tasks...</div>
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div id="emptyState" class="hidden text-center py-20">
|
||||
<div class="text-5xl mb-4">🎉</div>
|
||||
<h2 class="text-xl font-semibold text-white mb-2">You're all caught up</h2>
|
||||
<p class="text-[#94a3b8]">No tasks need your attention right now.</p>
|
||||
<p class="text-[#b0bec5]">No tasks need your attention right now.</p>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
|
@ -64,12 +64,12 @@
|
|||
<a href="/app/requests/${t.entry_id}" class="task-card block bg-[#0d1f3c] border border-white/[0.08] rounded-xl p-5 transition cursor-pointer">
|
||||
<div class="flex items-center gap-3 mb-2">
|
||||
<span class="w-2.5 h-2.5 rounded-full priority-${priority} shrink-0"></span>
|
||||
${ref ? `<span class="text-xs font-mono text-[#94a3b8]">${ref}</span>` : ''}
|
||||
${ref ? `<span class="text-xs font-mono text-[#b0bec5]">${ref}</span>` : ''}
|
||||
<span class="text-white font-medium flex-1">${escHtml(title)}</span>
|
||||
${due ? `<span class="text-xs text-[#94a3b8]">Due: ${due}</span>` : ''}
|
||||
${due ? `<span class="text-xs text-[#b0bec5]">Due: ${due}</span>` : ''}
|
||||
</div>
|
||||
<div class="flex items-center gap-3 text-xs text-[#475569]">
|
||||
<span class="capitalize px-2 py-0.5 rounded-full bg-white/[0.05] text-[#94a3b8]">${status}</span>
|
||||
<div class="flex items-center gap-3 text-xs text-[#8899a6]">
|
||||
<span class="capitalize px-2 py-0.5 rounded-full bg-white/[0.05] text-[#b0bec5]">${status}</span>
|
||||
<span>${t.type || 'request'}</span>
|
||||
</div>
|
||||
</a>
|
||||
|
|
|
|||
|
|
@ -5,68 +5,57 @@
|
|||
<h1 class="text-3xl font-bold text-white tracking-tight">
|
||||
<span class="text-[#c9a84c]">Deal</span>space
|
||||
</h1>
|
||||
<p class="text-[#94a3b8] mt-2 text-sm">Secure M&A deal management</p>
|
||||
<p class="text-[#b0bec5] mt-2 text-sm">Secure M&A deal management</p>
|
||||
</div>
|
||||
|
||||
<!-- Step 1: Email -->
|
||||
<div id="step-email" class="bg-[#0d1f3c] border border-white/[0.08] rounded-xl p-8">
|
||||
<div class="bg-[#0d1f3c] border border-white/[0.08] rounded-xl p-8">
|
||||
<h2 class="text-xl font-semibold text-white mb-2">Sign in</h2>
|
||||
<p class="text-[#94a3b8] text-sm mb-6">Enter your email to receive a login code.</p>
|
||||
<p id="login-hint" class="text-[#b0bec5] text-sm mb-6">Enter your email to receive a login code.</p>
|
||||
|
||||
<div id="error-email" class="hidden mb-4 p-3 bg-red-500/10 border border-red-500/20 rounded-lg text-red-400 text-sm"></div>
|
||||
<div id="error-code" class="hidden mb-4 p-3 bg-red-500/10 border border-red-500/20 rounded-lg text-red-400 text-sm"></div>
|
||||
|
||||
<form id="emailForm" class="space-y-5">
|
||||
<div class="space-y-5">
|
||||
<!-- Email -->
|
||||
<div>
|
||||
<label for="email" class="block text-sm font-medium text-[#94a3b8] mb-1.5">Email</label>
|
||||
<label for="email" class="block text-sm font-medium text-[#b0bec5] mb-1.5">Email</label>
|
||||
<input type="email" id="email" name="email" required autocomplete="email" autofocus
|
||||
placeholder="you@company.com"
|
||||
class="w-full px-4 py-2.5 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white placeholder-[#475569] focus:outline-none focus:border-[#c9a84c] focus:ring-1 focus:ring-[#c9a84c] transition">
|
||||
class="w-full px-4 py-2.5 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white placeholder-[#8899a6] focus:outline-none focus:border-[#c9a84c] focus:ring-1 focus:ring-[#c9a84c] transition">
|
||||
</div>
|
||||
<button type="submit" id="emailBtn"
|
||||
|
||||
<!-- Code (always visible) -->
|
||||
<div>
|
||||
<label for="code" class="block text-sm font-medium text-[#b0bec5] mb-1.5">Login code</label>
|
||||
<input type="text" id="code" name="code" autocomplete="one-time-code"
|
||||
maxlength="6" inputmode="numeric" pattern="[0-9]*"
|
||||
placeholder="000000"
|
||||
class="code-input w-full px-4 py-3 bg-[#0a1628] border border-white/[0.08] rounded-lg text-[#c9a84c] placeholder-[#8899a6] focus:outline-none focus:border-[#c9a84c] focus:ring-1 focus:ring-[#c9a84c] transition">
|
||||
<p id="code-status" class="text-[#8899a6] text-xs mt-1.5"></p>
|
||||
</div>
|
||||
|
||||
<button type="button" id="emailBtn" onclick="sendCode()"
|
||||
class="w-full py-2.5 bg-[#c9a84c] hover:bg-[#b8973f] text-[#0a1628] font-semibold rounded-lg transition disabled:opacity-50">
|
||||
Send login code
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Step 2: OTP Code -->
|
||||
<div id="step-code" style="display:none" class="bg-[#0d1f3c] border border-white/[0.08] rounded-xl p-8">
|
||||
<h2 class="text-xl font-semibold text-white mb-2">Enter your code</h2>
|
||||
<p class="text-[#94a3b8] text-sm mb-6">
|
||||
We sent a 6-digit code to <span id="sent-email" class="text-white font-medium"></span>
|
||||
</p>
|
||||
|
||||
<div id="error-code" class="hidden mb-4 p-3 bg-red-500/10 border border-red-500/20 rounded-lg text-red-400 text-sm"></div>
|
||||
|
||||
<form id="codeForm" class="space-y-5">
|
||||
<div>
|
||||
<label for="code" class="block text-sm font-medium text-[#94a3b8] mb-1.5">Login code</label>
|
||||
<input type="text" id="code" name="code" required autocomplete="one-time-code"
|
||||
maxlength="6" inputmode="numeric" pattern="[0-9]*"
|
||||
placeholder="000000"
|
||||
class="code-input w-full px-4 py-3 bg-[#0a1628] border border-white/[0.08] rounded-lg text-[#c9a84c] placeholder-[#475569] focus:outline-none focus:border-[#c9a84c] focus:ring-1 focus:ring-[#c9a84c] transition">
|
||||
</div>
|
||||
<button type="submit" id="codeBtn"
|
||||
<button type="button" id="codeBtn" onclick="verifyCode()" style="display:none"
|
||||
class="w-full py-2.5 bg-[#c9a84c] hover:bg-[#b8973f] text-[#0a1628] font-semibold rounded-lg transition disabled:opacity-50">
|
||||
Verify & sign in
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="mt-4 flex items-center justify-between">
|
||||
<button id="backBtn" class="text-[#94a3b8] text-sm hover:text-white transition">
|
||||
← Use a different email
|
||||
</button>
|
||||
<button id="resendBtn" class="text-[#c9a84c] text-sm hover:text-[#b8973f] transition">
|
||||
Resend code
|
||||
</button>
|
||||
<div id="resendRow" style="display:none" class="flex items-center justify-center">
|
||||
<button id="resendBtn" onclick="resendCode()" class="text-[#c9a84c] text-sm hover:text-[#b8973f] transition">
|
||||
Resend code
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-center text-[#475569] text-xs mt-6">
|
||||
<p class="text-center text-[#8899a6] text-xs mt-6">
|
||||
Don’t have an account? Dealspace is invite-only.<br>
|
||||
<a href="/#demo" class="text-[#c9a84c] hover:underline">Request access on muskepo.com</a>
|
||||
</p>
|
||||
<p class="text-center text-[#475569] text-xs mt-3">© 2026 Muskepo B.V. — Amsterdam</p>
|
||||
<p class="text-center text-[#8899a6] text-xs mt-3">© 2026 Muskepo B.V. — Amsterdam</p>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
|
|
@ -84,17 +73,27 @@
|
|||
}
|
||||
|
||||
let currentEmail = '';
|
||||
let codeSent = false;
|
||||
|
||||
// Step 1: Send challenge
|
||||
document.getElementById('emailForm').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
// Enter on email field sends code, Enter on code field verifies
|
||||
document.getElementById('email').addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter') { e.preventDefault(); sendCode(); }
|
||||
});
|
||||
document.getElementById('code').addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter') { e.preventDefault(); verifyCode(); }
|
||||
});
|
||||
|
||||
async function sendCode() {
|
||||
const btn = document.getElementById('emailBtn');
|
||||
const errorEl = document.getElementById('error-email');
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Sending code...';
|
||||
errorEl.classList.add('hidden');
|
||||
|
||||
const statusEl = document.getElementById('code-status');
|
||||
currentEmail = document.getElementById('email').value.trim().toLowerCase();
|
||||
if (!currentEmail) return;
|
||||
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Sending...';
|
||||
errorEl.classList.add('hidden');
|
||||
statusEl.textContent = 'Sending code...';
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/auth/challenge', {
|
||||
|
|
@ -102,29 +101,31 @@
|
|||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email: currentEmail }),
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(data.error || 'Failed to send code');
|
||||
|
||||
// Show code step
|
||||
document.getElementById('sent-email').textContent = currentEmail;
|
||||
document.getElementById('step-email').style.display='none';
|
||||
document.getElementById('step-code').style.display='block';
|
||||
codeSent = true;
|
||||
statusEl.textContent = 'Code sent to ' + currentEmail;
|
||||
document.getElementById('emailBtn').style.display = 'none';
|
||||
document.getElementById('codeBtn').style.display = 'block';
|
||||
document.getElementById('resendRow').style.display = 'flex';
|
||||
document.getElementById('code').focus();
|
||||
} catch (err) {
|
||||
errorEl.textContent = err.message;
|
||||
errorEl.classList.remove('hidden');
|
||||
statusEl.textContent = '';
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Send login code';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Step 2: Verify code
|
||||
document.getElementById('codeForm').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
async function verifyCode() {
|
||||
const btn = document.getElementById('codeBtn');
|
||||
const errorEl = document.getElementById('error-code');
|
||||
const codeVal = document.getElementById('code').value.trim();
|
||||
if (!codeVal || !currentEmail) return;
|
||||
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Verifying...';
|
||||
errorEl.classList.add('hidden');
|
||||
|
|
@ -133,22 +134,14 @@
|
|||
const res = await fetch('/api/auth/verify', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
email: currentEmail,
|
||||
code: document.getElementById('code').value.trim(),
|
||||
}),
|
||||
body: JSON.stringify({ email: currentEmail, code: codeVal }),
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(data.error || 'Invalid or expired code');
|
||||
|
||||
localStorage.setItem('ds_token', data.token);
|
||||
localStorage.setItem('ds_user', JSON.stringify(data.user));
|
||||
|
||||
// Set cookie for server-side auth (OAuth consent flow)
|
||||
document.cookie = 'ds_token=' + data.token + '; path=/; SameSite=Lax; max-age=3600';
|
||||
|
||||
// Redirect to next URL or default to tasks
|
||||
window.location.href = nextURL;
|
||||
} catch (err) {
|
||||
errorEl.textContent = err.message;
|
||||
|
|
@ -156,46 +149,32 @@
|
|||
btn.disabled = false;
|
||||
btn.textContent = 'Verify & sign in';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Back button
|
||||
document.getElementById('backBtn').addEventListener('click', () => {
|
||||
document.getElementById('step-code').style.display='none';
|
||||
document.getElementById('step-email').style.display='block';
|
||||
document.getElementById('code').value = '';
|
||||
document.getElementById('error-code').classList.add('hidden');
|
||||
document.getElementById('email').focus();
|
||||
});
|
||||
|
||||
// Resend button
|
||||
document.getElementById('resendBtn').addEventListener('click', async () => {
|
||||
async function resendCode() {
|
||||
const btn = document.getElementById('resendBtn');
|
||||
const statusEl = document.getElementById('code-status');
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Sending...';
|
||||
|
||||
try {
|
||||
await fetch('/api/auth/challenge', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email: currentEmail }),
|
||||
});
|
||||
statusEl.textContent = 'New code sent to ' + currentEmail;
|
||||
btn.textContent = 'Code sent!';
|
||||
setTimeout(() => {
|
||||
btn.textContent = 'Resend code';
|
||||
btn.disabled = false;
|
||||
}, 3000);
|
||||
setTimeout(() => { btn.textContent = 'Resend code'; btn.disabled = false; }, 3000);
|
||||
} catch {
|
||||
btn.textContent = 'Resend code';
|
||||
btn.disabled = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Auto-submit when 6 digits entered
|
||||
document.getElementById('code').addEventListener('input', (e) => {
|
||||
e.target.value = e.target.value.replace(/\D/g, '').slice(0, 6);
|
||||
if (e.target.value.length === 6) {
|
||||
document.getElementById('codeForm').dispatchEvent(new Event('submit'));
|
||||
}
|
||||
if (e.target.value.length === 6 && codeSent) verifyCode();
|
||||
});
|
||||
</script>
|
||||
{{end}}
|
||||
|
|
|
|||
|
|
@ -5,33 +5,33 @@
|
|||
<h1 class="text-3xl font-bold text-white tracking-tight">
|
||||
<span class="text-[#c9a84c]">Deal</span>space
|
||||
</h1>
|
||||
<p class="text-[#94a3b8] mt-2 text-sm">First-time setup</p>
|
||||
<p class="text-[#b0bec5] mt-2 text-sm">First-time setup</p>
|
||||
</div>
|
||||
|
||||
<!-- Setup Card -->
|
||||
<div class="bg-[#0d1f3c] border border-white/[0.08] rounded-xl p-8">
|
||||
<h2 class="text-xl font-semibold text-white mb-2">Create admin account</h2>
|
||||
<p class="text-[#94a3b8] text-sm mb-6">This will be the first administrator account for your Dealspace instance.</p>
|
||||
<p class="text-[#b0bec5] text-sm mb-6">This will be the first administrator account for your Dealspace instance.</p>
|
||||
|
||||
<div id="error" class="hidden mb-4 p-3 bg-red-500/10 border border-red-500/20 rounded-lg text-red-400 text-sm"></div>
|
||||
<div id="success" class="hidden mb-4 p-3 bg-green-500/10 border border-green-500/20 rounded-lg text-green-400 text-sm"></div>
|
||||
|
||||
<form id="setupForm" class="space-y-5">
|
||||
<div>
|
||||
<label for="name" class="block text-sm font-medium text-[#94a3b8] mb-1.5">Full name</label>
|
||||
<label for="name" class="block text-sm font-medium text-[#b0bec5] mb-1.5">Full name</label>
|
||||
<input type="text" id="name" name="name" required autofocus
|
||||
class="w-full px-4 py-2.5 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white placeholder-[#475569] focus:outline-none focus:border-[#c9a84c] focus:ring-1 focus:ring-[#c9a84c] transition">
|
||||
class="w-full px-4 py-2.5 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white placeholder-[#8899a6] focus:outline-none focus:border-[#c9a84c] focus:ring-1 focus:ring-[#c9a84c] transition">
|
||||
</div>
|
||||
<div>
|
||||
<label for="email" class="block text-sm font-medium text-[#94a3b8] mb-1.5">Email</label>
|
||||
<label for="email" class="block text-sm font-medium text-[#b0bec5] mb-1.5">Email</label>
|
||||
<input type="email" id="email" name="email" required autocomplete="email"
|
||||
class="w-full px-4 py-2.5 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white placeholder-[#475569] focus:outline-none focus:border-[#c9a84c] focus:ring-1 focus:ring-[#c9a84c] transition">
|
||||
class="w-full px-4 py-2.5 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white placeholder-[#8899a6] focus:outline-none focus:border-[#c9a84c] focus:ring-1 focus:ring-[#c9a84c] transition">
|
||||
</div>
|
||||
<div>
|
||||
<label for="password" class="block text-sm font-medium text-[#94a3b8] mb-1.5">Password</label>
|
||||
<label for="password" class="block text-sm font-medium text-[#b0bec5] mb-1.5">Password</label>
|
||||
<input type="password" id="password" name="password" required minlength="8" autocomplete="new-password"
|
||||
class="w-full px-4 py-2.5 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white placeholder-[#475569] focus:outline-none focus:border-[#c9a84c] focus:ring-1 focus:ring-[#c9a84c] transition">
|
||||
<p class="text-xs text-[#475569] mt-1">Minimum 8 characters</p>
|
||||
class="w-full px-4 py-2.5 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white placeholder-[#8899a6] focus:outline-none focus:border-[#c9a84c] focus:ring-1 focus:ring-[#c9a84c] transition">
|
||||
<p class="text-xs text-[#8899a6] mt-1">Minimum 8 characters</p>
|
||||
</div>
|
||||
<button type="submit" id="submitBtn"
|
||||
class="w-full py-2.5 bg-[#c9a84c] hover:bg-[#b8973f] text-[#0a1628] font-semibold rounded-lg transition disabled:opacity-50">
|
||||
|
|
@ -40,7 +40,7 @@
|
|||
</form>
|
||||
</div>
|
||||
|
||||
<p class="text-center text-[#475569] text-xs mt-8">© 2026 Muskepo B.V. — Amsterdam</p>
|
||||
<p class="text-center text-[#8899a6] text-xs mt-8">© 2026 Muskepo B.V. — Amsterdam</p>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
|
|
|
|||
|
|
@ -21,8 +21,8 @@
|
|||
<div id="testRoleBanner" style="display:none;position:fixed;bottom:0;left:0;right:0;background:#b45309;color:#fff;text-align:center;font-size:12px;padding:3px 0;z-index:999;pointer-events:none"></div>
|
||||
<div class="flex items-center gap-4">
|
||||
{{block "header-right-extra" .}}{{end}}
|
||||
<span id="userName" class="text-sm text-[#94a3b8]"></span>
|
||||
<button onclick="logout()" class="text-sm text-[#94a3b8] hover:text-white transition">Logout</button>
|
||||
<span id="userName" class="text-sm text-[#b0bec5]"></span>
|
||||
<button onclick="logout()" class="text-sm text-[#b0bec5] hover:text-white transition">Logout</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
|
@ -30,13 +30,13 @@
|
|||
<!-- Sidebar -->
|
||||
<nav class="w-60 bg-[#0d1f3c] border-r border-white/[0.08] min-h-[calc(100vh-57px)] sticky top-[57px] shrink-0 flex flex-col justify-between">
|
||||
<div class="p-3 space-y-0.5">
|
||||
<a href="/app/tasks" class="sidebar-link {{if eq .ActiveNav "tasks"}}active{{end}} flex items-center gap-3 px-3 py-2.5 rounded-lg text-base font-medium text-[#94a3b8] transition">
|
||||
<a href="/app/tasks" class="sidebar-link {{if eq .ActiveNav "tasks"}}active{{end}} flex items-center gap-3 px-3 py-2.5 rounded-lg text-base font-medium text-[#b0bec5] transition">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"/></svg>
|
||||
My Tasks</a>
|
||||
<a href="/app/projects" class="sidebar-link {{if eq .ActiveNav "projects"}}active{{end}} flex items-center gap-3 px-3 py-2.5 rounded-lg text-base font-medium text-[#94a3b8] transition">
|
||||
<a href="/app/projects" class="sidebar-link {{if eq .ActiveNav "projects"}}active{{end}} flex items-center gap-3 px-3 py-2.5 rounded-lg text-base font-medium text-[#b0bec5] transition">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"/></svg>
|
||||
Projects</a>
|
||||
<a href="/app/orgs" class="sidebar-link {{if eq .ActiveNav "orgs"}}active{{end}} flex items-center gap-3 px-3 py-2.5 rounded-lg text-base font-medium text-[#94a3b8] transition">
|
||||
<a href="/app/orgs" class="sidebar-link {{if eq .ActiveNav "orgs"}}active{{end}} flex items-center gap-3 px-3 py-2.5 rounded-lg text-base font-medium text-[#b0bec5] transition">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"/></svg>
|
||||
Organizations</a>
|
||||
</div>
|
||||
|
|
@ -55,7 +55,7 @@
|
|||
<option value="advisor">Advisor</option>
|
||||
</select>
|
||||
</div>
|
||||
<a href="/admin" class="sidebar-link {{if eq .ActiveNav "admin"}}active{{end}} flex items-center gap-3 px-3 py-2.5 rounded-lg text-base font-medium text-[#94a3b8] transition">
|
||||
<a href="/admin" class="sidebar-link {{if eq .ActiveNav "admin"}}active{{end}} flex items-center gap-3 px-3 py-2.5 rounded-lg text-base font-medium text-[#b0bec5] transition">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.066 2.573c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.573 1.066c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.066-2.573c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/></svg>
|
||||
Admin</a>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in New Issue