diff --git a/api/tangled/actorprofile.go b/api/tangled/actorprofile.go new file mode 100644 index 0000000..5d01787 --- /dev/null +++ b/api/tangled/actorprofile.go @@ -0,0 +1,31 @@ +// Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT. + +package tangled + +// schema: sh.tangled.actor.profile + +import ( + "github.com/bluesky-social/indigo/lex/util" +) + +const ( + ActorProfileNSID = "sh.tangled.actor.profile" +) + +func init() { + util.RegisterType("sh.tangled.actor.profile", &ActorProfile{}) +} // +// RECORDTYPE: ActorProfile +type ActorProfile struct { + LexiconTypeID string `json:"$type,const=sh.tangled.actor.profile" cborgen:"$type,const=sh.tangled.actor.profile"` + // bluesky: Include link to this account on Bluesky. + Bluesky *bool `json:"bluesky,omitempty" cborgen:"bluesky,omitempty"` + // description: Free-form profile description text. + Description *string `json:"description,omitempty" cborgen:"description,omitempty"` + Links []string `json:"links,omitempty" cborgen:"links,omitempty"` + // location: Free-form location text. + Location *string `json:"location,omitempty" cborgen:"location,omitempty"` + // pinnedRepositories: Any ATURI, it is up to appviews to validate these fields. + PinnedRepositories []string `json:"pinnedRepositories,omitempty" cborgen:"pinnedRepositories,omitempty"` + Stats []string `json:"stats,omitempty" cborgen:"stats,omitempty"` +} diff --git a/api/tangled/cbor_gen.go b/api/tangled/cbor_gen.go index 0b745ba..7d0f687 100644 --- a/api/tangled/cbor_gen.go +++ b/api/tangled/cbor_gen.go @@ -3389,3 +3389,516 @@ func (t *RepoArtifact) UnmarshalCBOR(r io.Reader) (err error) { return nil } +func (t *ActorProfile) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + fieldCount := 7 + + if t.Bluesky == nil { + fieldCount-- + } + + if t.Description == nil { + fieldCount-- + } + + if t.Links == nil { + fieldCount-- + } + + if t.Location == nil { + fieldCount-- + } + + if t.PinnedRepositories == nil { + fieldCount-- + } + + if t.Stats == nil { + fieldCount-- + } + + if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil { + return err + } + + // t.LexiconTypeID (string) (string) + if len("$type") > 1000000 { + return xerrors.Errorf("Value in field \"$type\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("$type"))); err != nil { + return err + } + if _, err := cw.WriteString(string("$type")); err != nil { + return err + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("sh.tangled.actor.profile"))); err != nil { + return err + } + if _, err := cw.WriteString(string("sh.tangled.actor.profile")); err != nil { + return err + } + + // t.Links ([]string) (slice) + if t.Links != nil { + + if len("links") > 1000000 { + return xerrors.Errorf("Value in field \"links\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("links"))); err != nil { + return err + } + if _, err := cw.WriteString(string("links")); err != nil { + return err + } + + if len(t.Links) > 8192 { + return xerrors.Errorf("Slice value in field t.Links was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.Links))); err != nil { + return err + } + for _, v := range t.Links { + if len(v) > 1000000 { + return xerrors.Errorf("Value in field v was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(v))); err != nil { + return err + } + if _, err := cw.WriteString(string(v)); err != nil { + return err + } + + } + } + + // t.Stats ([]string) (slice) + if t.Stats != nil { + + if len("stats") > 1000000 { + return xerrors.Errorf("Value in field \"stats\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("stats"))); err != nil { + return err + } + if _, err := cw.WriteString(string("stats")); err != nil { + return err + } + + if len(t.Stats) > 8192 { + return xerrors.Errorf("Slice value in field t.Stats was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.Stats))); err != nil { + return err + } + for _, v := range t.Stats { + if len(v) > 1000000 { + return xerrors.Errorf("Value in field v was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(v))); err != nil { + return err + } + if _, err := cw.WriteString(string(v)); err != nil { + return err + } + + } + } + + // t.Bluesky (bool) (bool) + if t.Bluesky != nil { + + if len("bluesky") > 1000000 { + return xerrors.Errorf("Value in field \"bluesky\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("bluesky"))); err != nil { + return err + } + if _, err := cw.WriteString(string("bluesky")); err != nil { + return err + } + + if t.Bluesky == nil { + if _, err := cw.Write(cbg.CborNull); err != nil { + return err + } + } else { + if err := cbg.WriteBool(w, *t.Bluesky); err != nil { + return err + } + } + } + + // t.Location (string) (string) + if t.Location != nil { + + if len("location") > 1000000 { + return xerrors.Errorf("Value in field \"location\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("location"))); err != nil { + return err + } + if _, err := cw.WriteString(string("location")); err != nil { + return err + } + + if t.Location == nil { + if _, err := cw.Write(cbg.CborNull); err != nil { + return err + } + } else { + if len(*t.Location) > 1000000 { + return xerrors.Errorf("Value in field t.Location was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.Location))); err != nil { + return err + } + if _, err := cw.WriteString(string(*t.Location)); err != nil { + return err + } + } + } + + // t.Description (string) (string) + if t.Description != nil { + + if len("description") > 1000000 { + return xerrors.Errorf("Value in field \"description\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("description"))); err != nil { + return err + } + if _, err := cw.WriteString(string("description")); err != nil { + return err + } + + if t.Description == nil { + if _, err := cw.Write(cbg.CborNull); err != nil { + return err + } + } else { + if len(*t.Description) > 1000000 { + return xerrors.Errorf("Value in field t.Description was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.Description))); err != nil { + return err + } + if _, err := cw.WriteString(string(*t.Description)); err != nil { + return err + } + } + } + + // t.PinnedRepositories ([]string) (slice) + if t.PinnedRepositories != nil { + + if len("pinnedRepositories") > 1000000 { + return xerrors.Errorf("Value in field \"pinnedRepositories\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("pinnedRepositories"))); err != nil { + return err + } + if _, err := cw.WriteString(string("pinnedRepositories")); err != nil { + return err + } + + if len(t.PinnedRepositories) > 8192 { + return xerrors.Errorf("Slice value in field t.PinnedRepositories was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.PinnedRepositories))); err != nil { + return err + } + for _, v := range t.PinnedRepositories { + if len(v) > 1000000 { + return xerrors.Errorf("Value in field v was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(v))); err != nil { + return err + } + if _, err := cw.WriteString(string(v)); err != nil { + return err + } + + } + } + return nil +} + +func (t *ActorProfile) UnmarshalCBOR(r io.Reader) (err error) { + *t = ActorProfile{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("ActorProfile: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 18) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.LexiconTypeID (string) (string) + case "$type": + + { + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.LexiconTypeID = string(sval) + } + // t.Links ([]string) (slice) + case "links": + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 8192 { + return fmt.Errorf("t.Links: array too large (%d)", extra) + } + + if maj != cbg.MajArray { + return fmt.Errorf("expected cbor array") + } + + if extra > 0 { + t.Links = make([]string, extra) + } + + for i := 0; i < int(extra); i++ { + { + var maj byte + var extra uint64 + var err error + _ = maj + _ = extra + _ = err + + { + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.Links[i] = string(sval) + } + + } + } + // t.Stats ([]string) (slice) + case "stats": + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 8192 { + return fmt.Errorf("t.Stats: array too large (%d)", extra) + } + + if maj != cbg.MajArray { + return fmt.Errorf("expected cbor array") + } + + if extra > 0 { + t.Stats = make([]string, extra) + } + + for i := 0; i < int(extra); i++ { + { + var maj byte + var extra uint64 + var err error + _ = maj + _ = extra + _ = err + + { + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.Stats[i] = string(sval) + } + + } + } + // t.Bluesky (bool) (bool) + case "bluesky": + + { + b, err := cr.ReadByte() + if err != nil { + return err + } + if b != cbg.CborNull[0] { + if err := cr.UnreadByte(); err != nil { + return err + } + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + if maj != cbg.MajOther { + return fmt.Errorf("booleans must be major type 7") + } + + var val bool + switch extra { + case 20: + val = false + case 21: + val = true + default: + return fmt.Errorf("booleans are either major type 7, value 20 or 21 (got %d)", extra) + } + t.Bluesky = &val + } + } + // t.Location (string) (string) + case "location": + + { + b, err := cr.ReadByte() + if err != nil { + return err + } + if b != cbg.CborNull[0] { + if err := cr.UnreadByte(); err != nil { + return err + } + + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.Location = (*string)(&sval) + } + } + // t.Description (string) (string) + case "description": + + { + b, err := cr.ReadByte() + if err != nil { + return err + } + if b != cbg.CborNull[0] { + if err := cr.UnreadByte(); err != nil { + return err + } + + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.Description = (*string)(&sval) + } + } + // t.PinnedRepositories ([]string) (slice) + case "pinnedRepositories": + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 8192 { + return fmt.Errorf("t.PinnedRepositories: array too large (%d)", extra) + } + + if maj != cbg.MajArray { + return fmt.Errorf("expected cbor array") + } + + if extra > 0 { + t.PinnedRepositories = make([]string, extra) + } + + for i := 0; i < int(extra); i++ { + { + var maj byte + var extra uint64 + var err error + _ = maj + _ = extra + _ = err + + { + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.PinnedRepositories[i] = string(sval) + } + + } + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} diff --git a/appview/db/artifact.go b/appview/db/artifact.go index 8e93844..51aae7a 100644 --- a/appview/db/artifact.go +++ b/appview/db/artifact.go @@ -57,22 +57,6 @@ func AddArtifact(e Execer, artifact Artifact) error { return err } -type filter struct { - key string - arg any -} - -func Filter(key string, arg any) filter { - return filter{ - key: key, - arg: arg, - } -} - -func (f filter) Condition() string { - return fmt.Sprintf("%s = ?", f.key) -} - func GetArtifact(e Execer, filters ...filter) ([]Artifact, error) { var artifacts []Artifact diff --git a/appview/db/db.go b/appview/db/db.go index 0b74db7..eb19053 100644 --- a/appview/db/db.go +++ b/appview/db/db.go @@ -3,6 +3,7 @@ package db import ( "context" "database/sql" + "fmt" "log" _ "github.com/mattn/go-sqlite3" @@ -231,6 +232,62 @@ func Make(dbPath string) (*DB, error) { foreign key (repo_at) references repos(at_uri) on delete cascade ); + create table if not exists profile ( + -- id + id integer primary key autoincrement, + did text not null, + + -- data + description text not null, + include_bluesky integer not null default 0, + location text, + + -- constraints + unique(did) + ); + create table if not exists profile_links ( + -- id + id integer primary key autoincrement, + did text not null, + + -- data + link text not null, + + -- constraints + foreign key (did) references profile(did) on delete cascade + ); + create table if not exists profile_stats ( + -- id + id integer primary key autoincrement, + did text not null, + + -- data + kind text not null check (kind in ( + "merged-pull-request-count", + "closed-pull-request-count", + "open-pull-request-count", + "open-issue-count", + "closed-issue-count", + "repository-count" + )), + + -- constraints + foreign key (did) references profile(did) on delete cascade + ); + create table if not exists profile_pinned_repositories ( + -- id + id integer primary key autoincrement, + did text not null, + + -- data + at_uri text not null, + + -- constraints + unique(did, at_uri), + foreign key (did) references profile(did) on delete cascade, + foreign key (at_uri) references repos(at_uri) on delete cascade + ); + create table if not exists migrations ( id integer primary key autoincrement, name text unique @@ -348,3 +405,19 @@ func runMigration(d *sql.DB, name string, migrationFn migrationFn) error { return nil } + +type filter struct { + key string + arg any +} + +func Filter(key string, arg any) filter { + return filter{ + key: key, + arg: arg, + } +} + +func (f filter) Condition() string { + return fmt.Sprintf("%s = ?", f.key) +} diff --git a/appview/db/profile.go b/appview/db/profile.go index 3e9bd5a..a21ba6a 100644 --- a/appview/db/profile.go +++ b/appview/db/profile.go @@ -1,8 +1,13 @@ package db import ( + "database/sql" "fmt" + "log" "time" + + "github.com/bluesky-social/indigo/atproto/syntax" + "tangled.sh/tangled.sh/core/api/tangled" ) type RepoEvent struct { @@ -162,3 +167,283 @@ func MakeProfileTimeline(e Execer, forDid string) (*ProfileTimeline, error) { return &timeline, nil } + +type Profile struct { + // ids + ID int + Did string + + // data + Description string + IncludeBluesky bool + Location string + Links [5]string + Stats [2]VanityStat + PinnedRepos [6]syntax.ATURI +} + +func (p Profile) IsLinksEmpty() bool { + for _, l := range p.Links { + if l != "" { + return false + } + } + return true +} + +func (p Profile) IsStatsEmpty() bool { + for _, s := range p.Stats { + if s.Kind != "" { + return false + } + } + return true +} + +func (p Profile) IsPinnedReposEmpty() bool { + for _, r := range p.PinnedRepos { + if r != "" { + return false + } + } + return true +} + +type VanityStatKind string + +const ( + VanityStatMergedPRCount VanityStatKind = "merged-pull-request-count" + VanityStatClosedPRCount VanityStatKind = "closed-pull-request-count" + VanityStatOpenPRCount VanityStatKind = "open-pull-request-count" + VanityStatOpenIssueCount VanityStatKind = "open-issue-count" + VanityStatClosedIssueCount VanityStatKind = "closed-issue-count" + VanityStatRepositoryCount VanityStatKind = "repository-count" +) + +func (v VanityStatKind) String() string { + switch v { + case VanityStatMergedPRCount: + return "Merged PRs" + case VanityStatClosedPRCount: + return "Closed PRs" + case VanityStatOpenPRCount: + return "Open PRs" + case VanityStatOpenIssueCount: + return "Open Issues" + case VanityStatClosedIssueCount: + return "Closed Issues" + case VanityStatRepositoryCount: + return "Repositories" + } + return "" +} + +type VanityStat struct { + Kind VanityStatKind + Value uint64 +} + +func (p *Profile) ProfileAt() syntax.ATURI { + return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", p.Did, tangled.ActorProfileNSID, "self")) +} + +func UpsertProfile(tx *sql.Tx, profile *Profile) error { + defer tx.Rollback() + + // update links + _, err := tx.Exec(`delete from profile_links where did = ?`, profile.Did) + if err != nil { + return err + } + // update vanity stats + _, err = tx.Exec(`delete from profile_stats where did = ?`, profile.Did) + if err != nil { + return err + } + + // update pinned repos + _, err = tx.Exec(`delete from profile_pinned_repositories where did = ?`, profile.Did) + if err != nil { + return err + } + + includeBskyValue := 0 + if profile.IncludeBluesky { + includeBskyValue = 1 + } + + _, err = tx.Exec( + `insert or replace into profile ( + did, + description, + include_bluesky, + location + ) + values (?, ?, ?, ?)`, + profile.Did, + profile.Description, + includeBskyValue, + profile.Location, + ) + + if err != nil { + log.Println("profile", "err", err) + return err + } + + for _, link := range profile.Links { + if link == "" { + continue + } + + _, err := tx.Exec( + `insert into profile_links (did, link) values (?, ?)`, + profile.Did, + link, + ) + + if err != nil { + log.Println("profile_links", "err", err) + return err + } + } + + for _, v := range profile.Stats { + if v.Kind == "" { + continue + } + + _, err := tx.Exec( + `insert into profile_stats (did, kind) values (?, ?)`, + profile.Did, + v.Kind, + ) + + if err != nil { + log.Println("profile_stats", "err", err) + return err + } + } + + for _, pin := range profile.PinnedRepos { + if pin == "" { + continue + } + + _, err := tx.Exec( + `insert into profile_pinned_repositories (did, at_uri) values (?, ?)`, + profile.Did, + pin, + ) + + if err != nil { + log.Println("profile_pinned_repositories") + return err + } + } + + return tx.Commit() +} + +func GetProfile(e Execer, did string) (*Profile, error) { + var profile Profile + profile.Did = did + + includeBluesky := 0 + err := e.QueryRow( + `select description, include_bluesky, location from profile where did = ?`, + did, + ).Scan(&profile.Description, &includeBluesky, &profile.Location) + if err == sql.ErrNoRows { + profile := Profile{} + profile.Did = did + return &profile, nil + } + + if err != nil { + return nil, err + } + + if includeBluesky != 0 { + profile.IncludeBluesky = true + } + + rows, err := e.Query(`select link from profile_links where did = ?`, did) + if err != nil { + return nil, err + } + defer rows.Close() + i := 0 + for rows.Next() { + if err := rows.Scan(&profile.Links[i]); err != nil { + return nil, err + } + i++ + } + + rows, err = e.Query(`select kind from profile_stats where did = ?`, did) + if err != nil { + return nil, err + } + defer rows.Close() + i = 0 + for rows.Next() { + if err := rows.Scan(&profile.Stats[i].Kind); err != nil { + return nil, err + } + value, err := GetVanityStat(e, profile.Did, profile.Stats[i].Kind) + if err != nil { + return nil, err + } + profile.Stats[i].Value = value + i++ + } + + rows, err = e.Query(`select at_uri from profile_pinned_repositories where did = ?`, did) + if err != nil { + return nil, err + } + defer rows.Close() + i = 0 + for rows.Next() { + if err := rows.Scan(&profile.PinnedRepos[i]); err != nil { + return nil, err + } + i++ + } + + return &profile, nil +} + +func GetVanityStat(e Execer, did string, stat VanityStatKind) (uint64, error) { + query := "" + var args []any + switch stat { + case VanityStatMergedPRCount: + query = `select count(id) from pulls where owner_did = ? and state = ?` + args = append(args, did, PullMerged) + case VanityStatClosedPRCount: + query = `select count(id) from pulls where owner_did = ? and state = ?` + args = append(args, did, PullClosed) + case VanityStatOpenPRCount: + query = `select count(id) from pulls where owner_did = ? and state = ?` + args = append(args, did, PullOpen) + case VanityStatOpenIssueCount: + query = `select count(id) from issues where owner_did = ? and open = 1` + args = append(args, did) + case VanityStatClosedIssueCount: + query = `select count(id) from issues where owner_did = ? and open = 0` + args = append(args, did) + case VanityStatRepositoryCount: + query = `select count(id) from repos where did = ?` + args = append(args, did) + } + + var result uint64 + err := e.QueryRow(query, args...).Scan(&result) + if err != nil { + return 0, err + } + + return result, nil +} diff --git a/appview/db/repos.go b/appview/db/repos.go index 8c20144..9b9329c 100644 --- a/appview/db/repos.go +++ b/appview/db/repos.go @@ -2,9 +2,11 @@ package db import ( "database/sql" + "fmt" "time" "github.com/bluesky-social/indigo/atproto/syntax" + "tangled.sh/tangled.sh/core/api/tangled" ) type Repo struct { @@ -23,6 +25,10 @@ type Repo struct { Source string } +func (r Repo) RepoAt() syntax.ATURI { + return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", r.Did, tangled.RepoNSID, r.Rkey)) +} + func GetAllRepos(e Execer, limit int) ([]Repo, error) { var repos []Repo diff --git a/appview/pages/pages.go b/appview/pages/pages.go index f737319..a9738f2 100644 --- a/appview/pages/pages.go +++ b/appview/pages/pages.go @@ -317,6 +317,7 @@ type ProfilePageParams struct { CollaboratingRepos []db.Repo ProfileStats ProfileStats FollowStatus db.FollowStatus + Profile *db.Profile AvatarUri string ProfileTimeline *db.ProfileTimeline @@ -341,6 +342,31 @@ func (p *Pages) FollowFragment(w io.Writer, params FollowFragmentParams) error { return p.executePlain("user/fragments/follow", w, params) } +type EditBioParams struct { + LoggedInUser *auth.User + Profile *db.Profile +} + +func (p *Pages) EditBioFragment(w io.Writer, params EditBioParams) error { + return p.executePlain("user/fragments/editBio", w, params) +} + +type EditPinsParams struct { + LoggedInUser *auth.User + Profile *db.Profile + AllRepos []PinnedRepo + DidHandleMap map[string]string +} + +type PinnedRepo struct { + IsPinned bool + db.Repo +} + +func (p *Pages) EditPinsFragment(w io.Writer, params EditPinsParams) error { + return p.executePlain("user/fragments/editPins", w, params) +} + type RepoActionsFragmentParams struct { IsStarred bool RepoAt syntax.ATURI diff --git a/appview/pages/templates/user/fragments/editBio.html b/appview/pages/templates/user/fragments/editBio.html new file mode 100644 index 0000000..8cc182e --- /dev/null +++ b/appview/pages/templates/user/fragments/editBio.html @@ -0,0 +1,107 @@ +{{ define "user/fragments/editBio" }} +
+{{ end }} + +{{ define "stat" }} + {{ $id := index . 0 }} + {{ $stat := index . 1 }} + +{{ end }} diff --git a/appview/pages/templates/user/fragments/editPins.html b/appview/pages/templates/user/fragments/editPins.html new file mode 100644 index 0000000..49a64b9 --- /dev/null +++ b/appview/pages/templates/user/fragments/editPins.html @@ -0,0 +1,38 @@ +{{ define "user/fragments/editPins" }} + {{ $profile := .Profile }} + +{{ end }} diff --git a/appview/pages/templates/user/profile.html b/appview/pages/templates/user/profile.html index 0529e58..f56bd4e 100644 --- a/appview/pages/templates/user/profile.html +++ b/appview/pages/templates/user/profile.html @@ -1,22 +1,22 @@ {{ define "title" }}{{ or .UserHandle .UserDid }}{{ end }} {{ define "content" }} -ACTIVITY
+ACTIVITY
- {{ didOrHandle .UserDid .UserHandle }} +
+ {{ didOrHandle .UserDid .UserHandle }}
-{{ .Profile.Description }}
+ {{ end }} - {{ if ne .FollowStatus.String "IsSelf" }} - {{ template "user/fragments/follow" . }} - {{ end }} +REPOS
-This user does not have any repos yet.
- {{ end }} -This user does not have any repos yet.
+ {{ end }} +COLLABORATING ON
+{{ define "collaboratingRepos" }} + {{ if gt (len .CollaboratingRepos) 0 }} +COLLABORATING ON
This user is not collaborating.
- {{ end }} +This user is not collaborating.
+ {{ end }}