Merge frontend repository into app/ subdirectory

This commit is contained in:
2025-07-25 19:12:51 -03:00
parent bb2e4f2a42
commit cedf771f16
54 changed files with 1513 additions and 0 deletions

View File

@@ -0,0 +1,25 @@
<ion-header>
<ion-toolbar>
<ion-buttons left>
<button ion-button icon-only (tap)="closeChat($event)">
<ion-icon name="arrow-back"></ion-icon>
</button>
</ion-buttons>
<ion-title><img class="title-profile-avatar" [src]="'https://appsby.fitz.guru/urge/' + this.profile.details.pic.thumb" height="24" width="24"> {{this.profile.details.name}}</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-list>
<ion-item class="message-bubble" *ngFor="let message of this.profile.messages" [ngClass]="{ 'is-user': (message.isUser == true) }">
<img *ngIf="message.image" [src]="'https://appsby.fitz.guru/urge/' + message.image" (press)="showLightbox($event, message.image)">
<p *ngIf="message.text != ''">{{message.text}}</p>
</ion-item>
</ion-list>
</ion-content>
<ion-footer>
<ion-toolbar>
</ion-toolbar>
</ion-footer>

View File

@@ -0,0 +1,48 @@
page-chat {
.toolbar-title {
.title-profile-avatar {
vertical-align: bottom;
}
}
.list {
.message-bubble {
background-color: #fdb315;
border-radius: 0.5rem;
font-family: 'Helvetica Neue', HelveticaNeue, Helvetica, Arial, sans-serif;
line-height: 1.5;
margin: 1rem 1rem 2.5rem auto;
max-width: 75%;
overflow: visible;
padding: 0.75rem;
position: relative;
&.is-user {
background-color: #6fbedf;
margin: 1rem auto 2.5rem 1rem;
}
.item-inner {
border-bottom: none;
}
ion-label {
overflow: visible;
text-overflow: unset;
margin: 0 0.75rem;
}
p {
color: #1d1e1f;
white-space: normal;
&.timestamp {
color: #acacac;
}
}
}
}
}

View File

@@ -0,0 +1,34 @@
import { Component } from '@angular/core';
import { NavController, NavParams } from 'ionic-angular';
import { LightboxPage } from '../lightbox/lightbox';
@Component({
selector: 'page-chat',
templateUrl: 'chat.html'
})
export class ChatPage {
profile: any;
tabNavEl: any;
constructor(public navCtrl: NavController, private _params: NavParams) {
this.profile = this._params.get('profile');
this.tabNavEl = document.querySelector('#tab-nav .tabbar');
}
ionViewWillEnter() {
this.tabNavEl.style.display = 'none';
}
closeChat(event) {
this.navCtrl.pop();
}
showLightbox(event, image) {
this.navCtrl.push(LightboxPage, {
image: image
});
}
}

View File

@@ -0,0 +1,15 @@
<ion-header>
<ion-toolbar>
<ion-title>Urnings</ion-title>
</ion-toolbar>
</ion-header>
<ion-content no-padding>
<ion-grid no-padding>
<ion-row align-items-stretch>
<ion-col col-4 class="profile" *ngFor="let current of profiles" (tap)="profileTapped($event, current)" (press)="profilePressed($event, current)" [style.backgroundImage]="getBackgroundThumbnail(current.details.pic)">
<span class="username" [ngClass]="{ 'online': (current.messages?.length > 0) }">{{current.details.name}}</span>
</ion-col>
</ion-row>
</ion-grid>
</ion-content>

View File

@@ -0,0 +1,66 @@
page-grid {
ion-toolbar {
.toolbar-title {
color: #ffffff;
font-size: 2.42em;
font-weight: 700;
line-height: 1.29;
text-decoration: underline;
}
}
.grid {
.row {
.col {
&.profile {
background-size: cover;
border: 1px solid #000000;
box-sizing: border-box;
padding: 0 0 33% !important;
position: relative;
.username {
background-size: cover;
bottom: 0.25rem;
box-sizing: border-box;
color: #ffffff;
display: inline-block;
left: 0.5rem;
overflow: hidden;
position: absolute;
right: 0.25rem;
text-overflow: ellipsis;
text-shadow: 0 0 3px rgba(0, 0, 0, 1);
white-space: nowrap;
&::before {
border: 0.125rem solid #acacac;
border-radius: 1rem;
bottom: 0.125rem;
content: '';
display: inline-block;
height: 0.8rem;
margin-right: 0.5rem;
position: relative;
vertical-align: middle;
width: 0.8rem;
}
&.online {
&::before {
background-color: #00ff00;
border-color: #00ff00;
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,52 @@
import { Component } from '@angular/core';
import { DomSanitizer } from '@angular/platform-browser';
import { NavController } from 'ionic-angular';
import { ChatPage } from '../chat/chat';
import { ProfileService } from '../../services/profiles';
import { ProfilePage } from '../profile/profile';
@Component({
selector: 'page-grid',
templateUrl: 'grid.html',
providers: [ ProfileService ]
})
export class GridPage {
profiles: any;
tabNavEl: any;
constructor(public navCtrl: NavController, public profileService: ProfileService, private _sanitizer: DomSanitizer) {
profileService.loadVerified().then((data) => {
this.profiles = data;
console.debug('profiles: ', this.profiles);
});
this.tabNavEl = document.querySelector('#tab-nav .tabbar');
}
ionViewWillEnter() {
this.tabNavEl.style.display = 'flex';
}
doTellStory() {
this.navCtrl.push(TellYourStoryPage);
}
getBackgroundThumbnail(pics) {
return this._sanitizer.bypassSecurityTrustStyle('url(https://appsby.fitz.guru/urge/' + pics.thumb + ')');
}
profilePressed(event, profile) {
if (profile.messages && profile.messages.length) {
this.navCtrl.push(ChatPage, {
profile: profile
});
}
}
profileTapped(event, profile) {
this.navCtrl.push(ProfilePage, {
profile: profile,
});
}
}

View File

@@ -0,0 +1,20 @@
<ion-header>
<ion-toolbar>
<ion-buttons right>
<button ion-button icon-only (tap)="close($event)">
<ion-icon name="close"></ion-icon>
</button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content padding>
<div class="content-box" padding margin>
<h3>About this Project</h3>
<div class="info-blurb">
<p>The app was designed by Nicholas Pfosi and developed by Michael Fitzpatrick, modeled after the popular gay dating app Grindr.</p>
<p>Presenting these stories in this form, which is the conduit through which much participation in dating occurs, served multiple purposes. First, it educated the viewer who may not have used Grindr before, how it functions and how it is different from other apps such as Tinder, whereby matching with a person is a prerequisite for conversation. Second, it makes the scope of the project flexible, allowing for the submission of stories from the audience to be slotted into an expandable presentation.</p>
<p>Please direct any questions or concerns to Nicholas Pfosi at npfosi@gmail.com</p>
</div>
</div>
</ion-content>

View File

@@ -0,0 +1,9 @@
page-information {
.content-box {
background-color: #ffffff;
color: #000000;
font-family: 'Helvetica Neue', HelveticaNeue, Helvetica, Arial, sans-serif;
line-height: 1.5;
}
}

View File

@@ -0,0 +1,23 @@
import { Component } from '@angular/core';
import { NavController } from 'ionic-angular';
@Component({
selector: 'page-information',
templateUrl: 'information.html',
})
export class InformationPage {
tabNavEl: any;
constructor(public navCtrl: NavController) {
this.tabNavEl = document.querySelector('#tab-nav .tabbar');
}
ionViewWillEnter() {
this.tabNavEl.style.display = 'none';
}
close(event) {
this.navCtrl.pop();
}
}

View File

@@ -0,0 +1,13 @@
<ion-header>
<ion-toolbar>
<ion-buttons right>
<button ion-button icon-only (tap)="close($event)">
<ion-icon name="close"></ion-icon>
</button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content (click)="close($event)">
<img class="image-detail" [src]="'https://appsby.fitz.guru/urge/' + this.image">
</ion-content>

View File

@@ -0,0 +1,11 @@
page-lightbox {
.image-detail {
display: block;
height: auto;
position: relative;
top: 50%;
transform: translate3d(0, -50%, 0);
width: 100%;
}
}

View File

@@ -0,0 +1,26 @@
import { Component } from '@angular/core';
import { NavController, NavParams } from 'ionic-angular';
@Component({
selector: 'page-lightbox',
templateUrl: 'lightbox.html'
})
export class LightboxPage {
image: string;
tabNavEl: any;
constructor(public navCtrl: NavController, private _params: NavParams) {
this.image = this._params.get('image');
this.tabNavEl = document.querySelector('#tab-nav .tabbar');
}
ionViewWillEnter() {
this.tabNavEl.style.display = 'none';
}
close(event) {
this.navCtrl.pop();
}
}

View File

@@ -0,0 +1,28 @@
<ion-header>
<ion-toolbar>
<ion-title>Urnings</ion-title>
</ion-toolbar>
</ion-header>
<ion-content no-padding>
<ion-list>
<ng-container *ngFor="let profile of profiles">
<ion-item no-padding *ngIf="profile.messages?.length > 0">
<ion-thumbnail padding-left item-start (tap)="profilePictureTapped($event, profile)">
<img [src]="'https://appsby.fitz.guru/urge/' + profile.details.pic.thumb">
</ion-thumbnail>
<ion-grid (tap)="interviewTapped($event, profile)">
<ion-row nowrap justify-content-between>
<ion-col class="username">
{{profile.details.name}}
</ion-col>
<ion-col class="timestamp" [innerHTML]="getLatestMessageTimestamp(profile.messages)"></ion-col>
</ion-row>
<ion-row class="latest-message" nowrap>
<ion-col [innerHTML]="getLatestMessage(profile.messages)"></ion-col>
</ion-row>
</ion-grid>
</ion-item>
</ng-container>
</ion-list>
</ion-content>

View File

@@ -0,0 +1,47 @@
page-messages {
ion-toolbar {
.toolbar-title {
color: #ffffff;
font-size: 2.42em;
font-weight: 700;
line-height: 1.29;
text-decoration: underline;
}
}
ion-header {
.button {
color: #9e9ea8;
}
}
.col {
color: #ffffff;
font-family: 'Helvetica Neue', HelveticaNeue, Helvetica, Arial, sans-serif;
line-height: 1.5;
&.username {
font-weight: 700;
}
&.timestamp {
font-size: 0.7em;
font-style: italic;
text-align: right;
}
.latest-message {
font-size: 0.8em;
}
}
.list {
> .item-block:last-child {
border-bottom: none;
}
}
}

View File

@@ -0,0 +1,52 @@
import { Component } from '@angular/core';
import { NavController } from 'ionic-angular';
import { ProfileService } from '../../services/profiles';
import { ProfilePage } from '../profile/profile';
import { ChatPage } from '../chat/chat';
import moment from 'moment';
@Component({
selector: 'page-messages',
templateUrl: 'messages.html',
providers: [ ProfileService ]
})
export class MessagesPage {
profiles: any;
tabNavEl: any;
constructor(public navCtrl: NavController, public profileService: ProfileService) {
profileService.load().then((data) => {
this.profiles = data;
});
this.tabNavEl = document.querySelector('#tab-nav .tabbar');
}
ionViewWillEnter() {
this.tabNavEl.style.display = 'flex';
}
getLatestMessage(messages) {
var latest = messages[(messages.length - 1)];
var isUser = latest.isUser;
return latest.text ? latest.text : '<em>' + (!isUser ? 'Sent ' : '') + 'Photo' + (isUser ? ' Recieved' : '') + '</em>';
}
getLatestMessageTimestamp(messages) {
return moment(messages[(messages.length - 1)].timestamp).fromNow();
}
interviewTapped(event, profile) {
this.navCtrl.push(ChatPage, {
profile: profile
});
}
profilePictureTapped(event, profile) {
this.navCtrl.push(ProfilePage, {
profile: profile
});
}
}

View File

@@ -0,0 +1,27 @@
<ion-content no-padding [style.backgroundImage]="getBackground(profile.details.pic)" (press)="showLightbox($event, profile.details.pic.detail)" on-swipe-left="nextProfile($event)" on-swipe-right="previousProfile($event)">
<ion-toolbar class="profile-toolbar">
<ion-buttons left>
<button ion-button icon-only (tap)="closeProfile($event)">
<ion-icon name="arrow-back"></ion-icon>
</button>
</ion-buttons>
<ion-title>{{this.profile.details.name}}</ion-title>
</ion-toolbar>
<button ion-button icon-only clear large (tap)="openChat($event, this.profile)" class="button-chat">
<ion-icon name="ios-chatboxes"></ion-icon>
</button>
<div id="detail-overlay" class="details">
<ion-grid>
<ion-row nowrap align-items-center justify-content-between>
<ion-col col-12 text-center (click)="toggleProfileDetails($event)" class="detail-toggle">
<ion-icon name="arrow-down"></ion-icon>
</ion-col>
</ion-row>
<ion-row class="about" *ngIf="this.profile.details.about">
<ion-col col-12 [innerHTML]="this.profile.details.about"></ion-col>
</ion-row>
</ion-grid>
</div>
</ion-content>

View File

@@ -0,0 +1,72 @@
page-profile {
ion-content {
background-position: center;
background-repeat: no-repeat;
background-size: cover;
}
.scroll-content {
overflow-y: hidden;
}
ion-toolbar {
border-bottom: 1px solid #ffffff;
transition: opacity 250ms 125ms ease-in-out;
&.hidden {
opacity: 0;
z-index: -1;
}
.toolbar-background {
background-color: rgba(0, 0, 0, 1);
}
.bar-button,
.toolbar-title {
color: #ffffff;
}
}
.button-chat {
bottom: 3rem;
color: #fdb315;
position: absolute;
right: 1.5rem;
z-index: 100;
}
.detail-toggle {
font-size: 2.5em;
z-index: 100;
}
.details {
bottom: 0;
height: 60px;
left: 0;
position: absolute;
right: 0;
transition: all 250ms 125ms ease-in-out;
&.open {
background: rgba(0, 0, 0, 0.8);
height: 100%;
overflow-y: scroll;
}
.about {
font-family: 'Helvetica Neue', HelveticaNeue, Helvetica, Arial, sans-serif;
line-height: 1.5;
}
.actions {
text-align: right;
.button-clear {
color: #fdb315;
}
}
}
}

View File

@@ -0,0 +1,89 @@
import { Component } from '@angular/core';
import { DomSanitizer } from '@angular/platform-browser';
import { NavController, NavParams } from 'ionic-angular';
import { ChatPage } from '../chat/chat';
import { LightboxPage } from '../lightbox/lightbox';
import { ProfileService } from '../../services/profiles';
@Component({
selector: 'page-profile',
templateUrl: 'profile.html',
providers: [ ProfileService ]
})
export class ProfilePage {
detailsOpen: boolean = false;
profile: any;
tabNavEl: any;
constructor(public navCtrl: NavController, public navParams: NavParams, public profileService: ProfileService, private _sanitizer: DomSanitizer) {
this.profile = navParams.get('profile');
this.tabNavEl = document.querySelector('#tab-nav .tabbar');
}
ionViewWillEnter() {
this.tabNavEl.style.display = 'none';
}
closeProfile(event) {
this.navCtrl.pop();
}
closeProfileDetails(event) {
if (this.detailsOpen) {
this.detailsOpen = false;
document.querySelector('.profile-toolbar').classList.remove('hidden');
document.getElementById('detail-overlay').classList.remove('open');
}
}
getBackground(pics) {
return this._sanitizer.bypassSecurityTrustStyle('url(https://appsby.fitz.guru/urge/' + pics.detail + ')');
}
markFavorite(event, profile) {
console.debug('favorite profile', { event: event, profile: profile });
}
nextProfile(event) {
this.profile = this.profileService.getNextProfile(this.profile._id);
this.navCtrl.setRoot(this.navCtrl.getActive().component);
}
openChat(event, profile) {
this.navCtrl.push(ChatPage, {
profile: profile
});
}
openProfileDetails(event) {
if (!this.detailsOpen) {
this.detailsOpen = true;
document.querySelector('.profile-toolbar').classList.add('hidden');
document.getElementById('detail-overlay').classList.add('open');
}
}
previousProfile(event) {
this.profile = this.profileService.getPreviousProfile(this.profile._id);
this.navCtrl.setRoot(this.navCtrl.getActive().component);
}
showLightbox(event, image) {
if (event.target.classList.contains('scroll-content')) {
this.navCtrl.push(LightboxPage, {
image: image
});
}
}
toggleProfileDetails(event) {
if (!this.detailsOpen) {
this.openProfileDetails(event);
} else {
this.closeProfileDetails(event);
}
}
}

View File

@@ -0,0 +1,6 @@
<ion-tabs id="tab-nav" selectedIndex="0">
<ion-tab [root]="tab1Root" tabIcon="contacts"></ion-tab>
<ion-tab [root]="tab2Root" tabIcon="compass"></ion-tab>
<ion-tab [root]="tab3Root" tabIcon="chatboxes"></ion-tab>
<ion-tab [root]="tab4Root" tabIcon="information-circle"></ion-tab>
</ion-tabs>

View File

@@ -0,0 +1,21 @@
import { Component } from '@angular/core';
import { GridPage } from '../grid/grid';
import { InformationPage } from '../information/information';
import { MessagesPage } from '../messages/messages';
import { UsersPage } from '../users/users';
@Component({
templateUrl: 'tabs.html'
})
export class TabsPage {
tab1Root = GridPage;
tab2Root = UsersPage;
tab3Root = MessagesPage;
tab4Root = InformationPage;
constructor() {
}
}

View File

@@ -0,0 +1,12 @@
<ion-header>
<ion-toolbar>
<ion-buttons right>
<button ion-button icon-only (tap)="close($event)">
<ion-icon name="close"></ion-icon>
</button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
</ion-content>

View File

@@ -0,0 +1,27 @@
page-tell-your-story {
ion-col {
&.cruise {
background-size: cover;
border: 1px solid #000000;
box-sizing: border-box;
padding: 0 0 37.5% !important;
position: relative;
.placename {
bottom: 0.25rem;
box-sizing: border-box;
color: #acacac;
display: inline-block;
left: 0.5rem;
overflow: hidden;
position: absolute;
right: 0.25rem;
text-overflow: ellipsis;
text-shadow: rgba(0, 0, 0, 1);
white-space: nowrap;
}
}
}
}

View File

@@ -0,0 +1,26 @@
import { Component } from '@angular/core';
import { NavController } from 'ionic-angular';
import { ProfileService } from '../../services/profiles';
@Component({
selector: 'page-tell-your-story',
templateUrl: 'tell.html',
providers: [ ProfileService ]
})
export class TellYourStoryPage {
tabNavEl: any;
constructor(public navCtrl: NavController) {
this.tabNavEl = document.querySelector('#tab-nav .tabbar');
}
ionViewWillEnter() {
this.tabNavEl.style.display = 'none';
}
close(event) {
this.navCtrl.pop();
}
}

View File

@@ -0,0 +1,20 @@
<ion-header>
<ion-toolbar>
<ion-title>Urnings</ion-title>
</ion-toolbar>
</ion-header>
<ion-content no-padding>
<ion-grid no-padding>
<ion-row align-items-stretch>
<ion-col col-4 class="profile tell-your-story">
<button ion-button clear large icon-only (tap)="doTellStory()">
<ion-icon name="md-person-add"></ion-icon>
</button>
</ion-col>
<ion-col col-4 class="profile" *ngFor="let current of profiles" (tap)="profileTapped($event, current)" (press)="profilePressed($event, current)" [style.backgroundImage]="getBackgroundThumbnail(current.details.pic)">
<span class="username" [ngClass]="{ 'online': (current.messages?.length > 0) }">{{current.details.name}}</span>
</ion-col>
</ion-row>
</ion-grid>
</ion-content>

View File

@@ -0,0 +1,78 @@
page-users {
ion-toolbar {
.toolbar-title {
color: #ffffff;
font-size: 2.42em;
font-weight: 700;
line-height: 1.29;
text-decoration: underline;
}
}
.grid {
.row {
.col {
&.profile {
background-size: cover;
border: 1px solid #000000;
box-sizing: border-box;
padding: 0 0 33% !important;
position: relative;
&.tell-your-story {
position: relative;
button {
color: #acacac;
position: absolute;
top: 50%;
left: 50%;
transform: translate3d(-50%, -50%, 0);
}
}
.username {
background-size: cover;
bottom: 0.25rem;
box-sizing: border-box;
color: #ffffff;
display: inline-block;
left: 0.5rem;
overflow: hidden;
position: absolute;
right: 0.25rem;
text-overflow: ellipsis;
text-shadow: 0 0 3px rgba(0, 0, 0, 1);
white-space: nowrap;
&::before {
border: 0.125rem solid #acacac;
border-radius: 1rem;
bottom: 0.125rem;
content: '';
display: inline-block;
height: 0.8rem;
margin-right: 0.5rem;
position: relative;
vertical-align: middle;
width: 0.8rem;
}
&.online {
&::before {
background-color: #00ff00;
border-color: #00ff00;
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,52 @@
import { Component } from '@angular/core';
import { DomSanitizer } from '@angular/platform-browser';
import { NavController } from 'ionic-angular';
import { ChatPage } from '../chat/chat';
import { ProfileService } from '../../services/profiles';
import { ProfilePage } from '../profile/profile';
import { TellYourStoryPage } from '../tell/tell';
@Component({
selector: 'page-users',
templateUrl: 'users.html',
providers: [ ProfileService ]
})
export class UsersPage {
profiles: any;
tabNavEl: any;
constructor(public navCtrl: NavController, public profileService: ProfileService, private _sanitizer: DomSanitizer) {
profileService.loadSubmitted().then((data) => {
this.profiles = data;
});
this.tabNavEl = document.querySelector('#tab-nav .tabbar');
}
ionViewWillEnter() {
this.tabNavEl.style.display = 'flex';
}
doTellStory() {
this.navCtrl.push(TellYourStoryPage);
}
getBackgroundThumbnail(pics) {
return this._sanitizer.bypassSecurityTrustStyle('url(https://appsby.fitz.guru/urge/' + pics.thumb + ')');
}
profilePressed(event, profile) {
if (profile.messages && profile.messages.length) {
this.navCtrl.push(ChatPage, {
profile: profile
});
}
}
profileTapped(event, profile) {
this.navCtrl.push(ProfilePage, {
profile: profile,
});
}
}